Imported from openJiuwen-ai/agent-core (
openjiuwen/agent_teams/AGENTS.md). Install upstream withnpx skills add openJiuwen-ai/agent-core --skill agent_teams. Copyright stays with the author.
Agent Teams
多智能体团队协作子系统。一个 Leader + 若干 Teammate,通过消息总线与任务数据库协同完成复杂任务。
本文件是本模块的入口索引。深入细节时跳转到:
tools/AGENTS.md— 团队工具设计与描述契约prompts/AGENTS.md— Prompt 模板与语言切换runtime/AGENTS.md— 对象池 / 派发 / 并发门禁,spec 公共入口 + leader-only 不变量interaction/AGENTS.md— 三视角交互 inbox + HITT runtime 表面agent/AGENTS.md— TeamAgent 四象限分解 + spawn / coordination / stream 等 managercli/AGENTS.md— 交互式 TUI / 斜杠命令子模块(prompt_toolkit + rich)
统一术语
新 API、事件、trace 和文档统一采用以下层级:
Session
└── Turn 一次外部输入 -> 一次稳定外部输出
└── Iteration 一次 Agent Loop 控制循环
└── Step 一次可观测原子执行动作
Round 只用于 multi-agent 协作/协议阶段,一个 Round 可以包含多个 Agent Turn。不要把单 Agent
Turn、Agent Loop Iteration 或原子 Step 命名为 Round。历史 NativeHarness/TaskLoop 中已有的
round_id、on_round 等属于待独立迁移的 legacy 名称;新增接口不得继续复制这些命名,也不要在
无兼容方案的普通变更中顺手批量重命名历史表面。三方 Harness 协议独立采用
Session > Turn > Step,以 openjiuwen/harness_protocol/AGENTS.md 为准;其中 Step 表示一次 Agent
Loop 控制循环,原子动作不进入该协议层级。
公开入口(public API)
公开符号仅限 __init__.py 导出。没有 factory wrapper——create_agent_team / resume_persistent_team / recover_agent_team 这套已删除,所有 lifecycle 都走 TeamAgentSpec.build() + Runner facade。
| 入口 | 用途 |
|---|---|
TeamAgentSpec(...).build() |
从零组装 TeamAgent 的唯一公共路径。agents["leader"] 必填,agents["teammate"] 可选 |
Runner.run_agent_team[_streaming](agent_team=spec, session=..., ...) |
运行入口;冷启动 / 热恢复 / 切 session 由 runtime 子系统的 dispatch 表自动选 |
cli.run_team_cli(*, specs=None, yaml_paths=None) |
交互式 TUI 公共入口。/team /session /spec 子命令覆盖 lifecycle 全 facade,普通文本透传 Runner.interact_agent_team。详见 cli/AGENTS.md |
新增配置项一律走 TeamAgentSpec——曾经的"在 factory 函数上堆参数"路径已物理消失。扩参数列表永远是 hack,扩 Spec 才是设计。
Cold-recover 的低层入口:TeamAgent.recover_from_session(session, team_name, runtime_spec=spec) + await agent.recover_team()。仅供运维脚本绕过 Runner 直接拿 leader 实例时使用,常规应用走 Runner.run_agent_team_streaming(agent_team=spec, session=...) 即可。
Runner 入口签名收紧:Runner.run_agent_team / run_agent_team_streaming 默认接 str | TeamAgentSpec——str 是 team_name,要求该 team 已被 spec 路径激活过、pool 里有 entry。已 build 的 TeamAgent 实例不再是合法入参。要跑 multi_agent 体系的 BaseTeam(str | BaseTeam),传同一个方法、加 base=True 切到 multi_agent 路径——Runner 公共表面只有这一对方法,由 base 参数分流。
模块地图
agent_teams/
├── __init__.py # 公开 API 聚合导出
├── constants.py # 保留名(user/team_leader/human_agent)集中定义
├── context.py # session_id 跨成员/跨模式共享 contextvars
├── i18n.py # 运行时中/英文字符串(仅装运行时 hard-coded 串)+ `reply_hint_for(sender)`:按发件人选 reply-hint 文案(user 走无条件强制版,其余走通用条件版)——文案归它管,选哪条文案也归它管,两个消费点(coordination `MessageHandler` / external `format`)不各写一遍
├── timefmt.py # 毫秒 epoch → "绝对本地时间 + 相对差" 渲染(喂 LLM/观测,文案走 i18n)
├── inbound_render.py # 入站消息/框架事件/团队状态 → <team-inbound>/<team-event>/<team-note>/<team-context> XML 渲染(纯函数,喂 LLM;文案由 handler 从 i18n 取;`<team-note>` 嵌在它所修饰的 inbound / event 块内部,不平级)+ `render_team_policy`:外部 CLI 成员的系统提示词包成 `<team-policy tools="<mcp server>">`,首个子元素是 `<team-note kind="tool-namespace">`,一次声明裸工具名归属哪个 MCP server(正文是团队自己的 markdown,不做转义)+ `render_controller_input`:HITT 控制者指令渲染成 `<team-inbound from="controller">`,让 avatar 分得清控制者与团队侧的 `user`(见 interaction/AGENTS.md 运行约束 6)+ `SNAPSHOT_EVENT_KINDS`/`snapshot_kind_of`/`drop_superseded_snapshots`:判定哪些 event 是全量幂等快照、并从一批排队输入里整条剔除被覆盖的那几条(当前只有 task-board)。见 F_46 / F_70 / F_71 / F_72
├── team_context.py # TeamContextTracker:判定该告诉这个成员哪些团队状态(自身身份 / 团队元数据 / 成员名册),并把投递进度基线持久化到成员自己的 child AgentSession。两个调用方:TeamPolicyRail(进程内)与 CliRuntimeBase(外部 CLI)。见 F_70
├── message_template.py # 框架模板消息的两阶段渲染:发送存意图(消息行 content 空 + meta={template,refs,params}),投递时按收件人语言加载 prompts/<lang>/<key>.md、用 {{task.*}}/{{member.*}}/{{param.*}} 填当前行(单遍替换不二次扫描、字段白名单、失败降级为 meta 合成的 fallback 行)。见 F_63
├── tiny_agent.py # Tiny Agent:随时唤起的极简 NativeHarness(system_prompt + model + 仅结构化输出工具);run 单轮 / chat 多轮;ephemeral(含 title/summary 预定义)+ team-scoped(TeamAgentSpec.tiny_agents 多实例,TeamInfra 持有)。见 F_45
├── paths.py # 文件系统布局单一真相源
├── schema/ # 全部数据模型(Spec / Context / Event / Status / Task)
├── models/ # 多模型部署原语(ModelPoolEntry / 池继承 / Allocator)
├── agent/ # 核心运行时(TeamAgent 本体 + 装配链)
├── prompts/ # 系统提示词模板、加载器、PromptSection 构造与缓存
├── rails/ # 团队 Rail + manifest 元素声明(team rail / 内置 rail·tool / context handle / team confirm payload)
├── security/ # Team 权限安全辅助(permission narrowing 等)
├── runtime/ # Runner 进程内 TeamAgent 对象池 + 派发决策 + Run/Interact 并发门禁
├── interaction/ # 外部交互入口(UserInbox / HumanAgentInbox / @ 路由)
├── tools/ # 团队工具(Leader / Teammate / Human Agent 可调用的原子操作)
├── messager/ # 消息传输层(inprocess / pyzmq)
├── spawn/ # 成员启动(process / inprocess)
├── monitor/ # 团队运行态监控(TeamMonitor 只读视图 + TeamStreamLogger 流式诊断日志)
├── observability/ # 团队 OpenTelemetry 观测。三方 harness 成员不在这里:其模型请求由 provider 以 ModelRequestEvent 交付,`harness_providers/trajectory.py` 记录(F_112)。agent 层 span 不在这里——`TeamObservabilityRail` 只贡献 `agentteam.*` 增量,span 本身由 `harness/observability/` 的 `AgentObservabilityRail` 开关(成对挂载,不继承);两边共用 `extensions/observability/`(含 demand.py 的 provider 需求协调,进程内只允许一个 TracerProvider) **team 根 span(`team.{name}`)按 session 注册**(`get_or_create_team_span(session_id=)` → `set_root_span(session_id=)`,session 由 runner 传入而非只靠 ContextVar):进程内 teammate 在自己的 task 里跑,只按 session id 查根,注册不上就整轮不落记录。**interact 路径(用户 `@` 直呼成员)也要开根**——它不属于任何 streaming run,上一条 trace finalize 后就没有根可挂了。
├── reliability/ # 主动可靠性框架(健康信号采集 rail + 检测器 + 分级处置;opt-in)
├── team_workspace/ # 团队共享工作空间(跨成员的文件/锁/版本)
├── cli/ # 交互式 TUI / 斜杠命令子模块(prompt_toolkit + rich)
├── external/ # 外部 agent 接入核心(ExternalTeamClient;member_runtime.py 投影根级 harness_protocol;dsh/ 首个 SDK adapter)
├── skill/ # 外部 agent 的非交互 CLI + SKILL_member.md / SKILL_operator.md(按 scope 分化)
├── mcp/ # 外部 agent 的 stdio MCP server(低层 mcp.server.lowlevel.Server,按 scope 分化)
├── workflow/ # Swarmflow 多 agent 工作流编排(dw 引擎移植 + worker backend + 4 层表示)
└── worktree_remote.py # 跨机器 worktree 后端(团队专属,generic 实现见 harness/tools/worktree)
agent/ — 运行时主骨架
TeamAgent 单一实现同时承担 Leader / Teammate 角色(按 TeamRole 切),内部组合一个 DeepAgent。字段按"四象限"拆(blueprint 静态 / state 跨 operator 可变 / resources 每实例 / infra 每进程),spawn / recovery / session / stream / coordination 各有独立 manager。
详见 agent/AGENTS.md。
prompts/ — 系统提示词
| 文件 | 职责 |
|---|---|
loader.py |
load_template,按语言加载 .md |
sections.py |
TeamSectionName + build_team_*_section 构造 PromptSection(系统提示词的唯一装配路径);build_team_role_section 读 leader_policy / teammate_policy。leader 侧另有 build_leader_bootstrap_section(它在系统提示词里唯一的 section,协作机制选择按 swarmflow_enabled 填槽——gate 与 swarmflow 工具同源)与 build_leader_policy_disclosure(其余 section 拼成 build_team 的 ToolResult),见 F_76 |
messages.py |
团队状态的消息正文(身份 / 团队信息 / 名册快照与增量)+ diff_roster。纯渲染,不管投递 |
section_cache.py |
MtimeSectionCache:通用 mtime 缓存原语(团队侧当前无使用者) |
cn/ · en/ |
角色 / 工作流 / 生命周期模板 |
唯一装配路径是 sections.build_team_*_section(由 TeamPolicyRail / build_team_member_system_prompt 消费,各 builder 直接 load_template 读对应 .md)。改正文即时生效。详见 prompts/AGENTS.md。
模板里的工具一律写裸名(send_message / view_task / ...)。进程内成员本来就这么调;外部 CLI
成员的工具走 MCP、带命名空间,且 CLI 自带工具可能重名(Claude Code 的 SendMessage、Codex 的
collaboration.send_message),所以 build_team_member_system_prompt(mcp_server_name=...) 把提示词包进
<team-policy tools="...">,用 tool_namespace.md 声明一次:本区域与所有 team-* 消息块里的裸名都归这个
MCP server。Codex 例外:团队工具以裸名注册为顶层 Dynamic Tools,没有 server 可声明,spawn 时不传
mcp_server_name、提示词不包 <team-policy>。不要逐条把模板里的工具名改成全限定名——两家 CLI 的全限定形式不同,声明作用域是一处改动,
改名是全量改动,而且会把厂商细节焊进模板。裸名到真实调用名的翻译由 provider 声明(见
harness_providers/AGENTS.md 不变式 12)。
rails/ — 团队 Rail 注入 + manifest 声明
team rail 全部经 manifest 声明式装配(@harness_element provider),不再手搓 new —— 见
docs/features/F_32_declarative-harness-assembly.md。AgentConfigurator 把 team rail 作为
RailSpec 注入 build_spec.rails,live handle 经 BuildContext.extras 传给工厂,rail 随
spec.build + ensure_initialized 自动挂载 / init(不再有 _MountedRails / 手动 set/init /
customizer 后处理)。
| 文件 | 职责 |
|---|---|
team_policy_rail.py |
TeamPolicyRail:所有进 builder 的团队 section 均静态且成员间逐字一致,init 建一次注入 SystemPromptBuilder(前缀 KV cache 稳定)。按角色分两条装配道(F_76):非 leader 拿全量静态集(role / HITT 协作契约 [gate hitt_enabled] / bridge avatar 自契约 [仅 BRIDGE_AGENT] / workflow / dispatch [gate dispatch_mode] / lifecycle / extra + inbound 说明);leader 只拿 team_bootstrap(身份 + 协作机制选择,gate swarmflow_enabled)+ team_extra,其余协同准则改由 build_team 的 ToolResult 披露——那才是 dispatch_mode / enable_hitt 等模式变量落定的时刻。团队状态(自身身份 / team_info / 成员名册)不进 builder 也不进 attachment,由 TeamContextTracker 在数据出现或变化的那次调用写进成员的对话历史。另外两件事都是「一次喂给模型多少排队输入」,按队列性质分手段:on_user_message 把非 leader 成员一批排队输入里被后来者覆盖的任务看板整条剔除(看板是全量幂等快照,丢旧的不损失信息);before_steering_drain 处理丢不掉的那类——信箱消息每条各说各的,只能少拿,非 leader 每次 drain 限 steer_batch_size 条(默认 2),剩下的留在队列里由后续模型调用取走。两处的角色门是同一道:leader 都不参与,它读的是快照之间的差异。见 F_46 / F_50 / F_52 / F_68 / F_70 / F_71 / F_76 / F_78 |
confirm_payload.py |
TeamConfirmPayload + TeamPermissionConfirmResponse:team-specific confirmation payload/response models(extend harness base classes with decided_by tracking) |
team_permission_rail.py |
TeamPermissionRail + TeamApprovalOrchestrator:team-mode permission guardrail;继承 PermissionInterruptRail,leader-mediated ASK resolution + session-scoped auto-confirm(_persist_allow_always=False)。enable_permissions=True 时替代 TeamToolApprovalRail |
tool_approval_rail.py |
TeamToolApprovalRail:teammate 调工具时通过消息向 leader 申请审批的中断 rail(enable_permissions=False 时使用) |
team_tool_rail.py / team_plan_mode_rail.py |
TeamToolRail(协同工具注册)/ TeamPlanModeRail(plan mode 提示叠加) |
team_skill_use_rail.py |
TeamSkillUseRail(SkillUseRail) + create_team_skill_use_rail:Skill 实体唯一存放于 paths.global_skills_dir(),成员/团队各自只有一份 skills-visibility.json。只覆写两个方法——_filter_skills(先按声明重算 allow/deny 再调 super())与 _build_skills_snapshot_signature(把合成后的授权本身并进签名,否则授权变了库里没动、提示词不刷新),另加 get_skills_for_session 复查(session 基线是持久化状态)。单 agent 的 harness/rails/skills/skill_use_rail.py 一字未改:team 行为靠继承 + agent_configurator 把 skills=[]/enable_skill_discovery=False 写进 build_spec 关掉通用 rail。见 F_79 |
elements.py |
7 个 team rail 的 @harness_element 工厂 + ConstructionInput(core.team.tool/core.team.policy/core.team.workspace/core.team.tool_approval/core.team.plan_mode/core.team.reliability/core.team.skill_use/core.team.observability)。agent span 的 core.observability 是 harness 内置,不在这里声明。没有 core.team.permission——TeamPermissionRail 由平台(jiuwenswarm)自己挂,enable_permissions=True 时替代 TeamToolApprovalRail,agent_teams 下不声明它 |
team_context.py |
TeamHandleKey + accessor + inject_team_handles:team live handle 经 BuildContext.extras 的 key 常量 + 类型化读取。rail 不缓存——需跨重建存活的状态(如 reliability_components)作为复用对象注入,由每轮新建的 rail 包装 |
builtin_elements.py |
openjiuwen 内置 core.* rail/tool(core.task_planning/core.skill_use/core.web_search 等)名字常量的薄再导出——声明的真身已上移到 harness/manifest/builtin_elements.py,本文件只为保持既有 import 路径(对象 is-一致) |
registration.py |
ensure_harness_elements_registered():import elements → register_from_catalog(),spec build 路径的统一注册入口 |
schema/ — 数据模型分层
blueprint.py # TeamAgentSpec / LeaderSpec / TransportSpec / StorageSpec —— 顶层装配蓝图
deep_agent_spec.py # DeepAgentSpec / SubAgentSpec / RailSpec 等 —— DeepAgent 侧的 Spec。RailSpec/BuiltinToolSpec 只走 provider(class registry _RAIL_TYPE_REGISTRY/_TOOL_TYPE_REGISTRY 已删,见 F_32);DeepAgentSpec.resolve_parts/build 分离
team.py # TeamSpec / TeamRole / TeamLifecycle / TeamRuntimeContext / TeamMemberSpec
events.py # EventMessage / TeamTopic —— 跨进程事件
status.py # MemberStatus / ExecutionStatus / TaskStatus —— 状态机枚举 + 三组成员状态子集(departed / settled / quiescent)+ 任务状态子集 TASK_REASSIGNABLE_STATUSES(归属可原地转移的状态,见 F_82)
stream.py # TeamOutputSchema —— OutputSchema 子类,带 source_member / role 成员归属字段;`is_team_event_marker` 判定框架标记 chunk
task.py # TaskSummary / TaskDetail —— 任务返回模型
Spec统一含义:可 JSON 序列化的装配蓝图。用model_dump()可跨进程;不放运行时资源引用。TeamRuntimeContext:运行时上下文,携带 role / messager_config / db_config 等资源配置,是Spec → Runtime的边界。- 新增 spec 字段要想清楚:属于装配数据(放 Spec)还是运行时资源(放 Config/Manager/Runtime)。不要让 Spec 持有
Runner、Session、文件句柄。 - Session checkpoint 状态结构按 team 分桶:
session.update_state的全局状态根上有一个teamsnamespace ——state["teams"][team_name] = {spec, context, model_allocator_state, lifecycle, db_state, pending_resume}。同一 session 可以承载多个 team 的状态;读写一律走runtime/metadata.py的read_team_namespace / merge_team_namespace / read_team_db_state / merge_team_db_state / read_pending_resume / merge_pending_resume / clear_pending_resume,不要直接在 root 上update_state({"spec": ...})。db_state用pending_create / created / cleaned标记 team DB row 生命周期;pending_resume({"query": ...},leader-only)由kernel.pause写、kernel.start尾部消费,使pause → stop → start等价于pause → resume(见 [[F_61]])。 MemberStatus状态流转:UNSTARTED(DB 记录已创建,agent 进程未启动)→STARTING(CAS guard 占位,正在 spawn)→READY(agent 进程已就绪)→BUSY/PAUSED/STOPPED/SHUTDOWN/ERROR。STARTING是过渡态——只有第一个 startup 路径能 CAS 成功UNSTARTED→STARTING,第二个并发路径查到 STARTING/READY 直接跳过。spawn 失败时 rollbackSTARTING→UNSTARTED保证可重试。PAUSED是自然 round-end idle(persistent team);STOPPED是外部stop_team拆掉 runtime、但 team 仍 live;ERROR保留真实失败并等待显式消息/调度或冷恢复;SHUTDOWN是显式退场,冷恢复不得自动复活(状态表保留SHUTDOWN→RESTARTING仅供显式复活能力)。schema.team.TeamLifecycle(temporary / persistent)描述静态团队类型,runtime.pool.RuntimeState(running / paused)描述对象池中 team 的运行时状态——和 MemberStatus 是不同层次的枚举,不要混用。- 成员状态的三组子集回答三个不同问题,任何两组都不要合并:
MEMBER_DEPARTED_STATUSES/MEMBER_UNREACHABLE_STATUSES(退场的两道门槛,见status.py头部注释);MEMBER_SETTLED_STATUSES("干完了吗",喂团队完成判定,故排除UNSTARTED/ERROR);MEMBER_QUIESCENT_STATUSES("现在动没动",喂 leader 的 team-idle 信号,故包含UNSTARTED/ERROR,活跃补集是STARTING/BUSY/RESTARTING/SHUTDOWN_REQUESTED)。见 [[F_74_leader-member-activity-and-team-idle]]。 - leader 的流上有三种框架标记 chunk(都是
TeamOutputSchema,payload.event_type以team.开头):team.completed(完成,随后关流)、team.idle(全员静止持续 2s、且其后复查任务板无非终态任务(空板也算)才发,不关流;窗口内任一成员再动就取消,见 [[F_77_team-idle-requires-a-settled-task-board]])、team.interact.failed(首轮路由失败)。is_team_event_marker是它们的统一判定,TeamAgent.invoke用它把标记排除在返回值之外——非流式调用方要的是 agent 产出的内容,不是框架记账。 TeamOutputSchema是core.session.stream.OutputSchema的子类(不污染 core 层),扩出source_member: str | None与role: TeamRole | None。Runner.run_agent_team_streaming的所有输出 chunk 在 team 路径下都会被StreamController自动升级为TeamOutputSchema并打上(agent_name, role)标签。inprocess 模式下,SpawnManager在 spawn teammate 时通过StreamController.add_chunk_observer把 teammate chunk fan-out 到 leader 的stream_queue,让 leader 的 streaming 流出全成员 chunk;subprocess 模式不做转发(chunk 留在 teammate 进程内),扩展点已留好(messager-driven observer)。详见agent/AGENTS.md的 StreamController 段。
models/ — 多模型部署原语
pool.py # ModelPoolEntry / ModelRouterConfig / IntelliRouterConfig
# / IntelliRouterDeployment / inherit_pool_ids
# —— 池条目 + 两种 router 便利配置 + 池刷新 model_id 继承
allocator.py # Allocation / ModelAllocator(Protocol)
# / RoundRobinModelAllocator / ByModelNameAllocator
# / RouterAllocator / IntelliRouterAllocator
# build_model_allocator / resolve_member_model
ModelPoolEntry是TeamSpec.model_pool的元素,描述一个 LLM 端点 + 凭证 + provider;通过to_team_model_config()物化为TeamModelConfig。- 持久化身份用
(model_name, group_index),运行时 client 身份用自动 uuidmodel_id;inherit_pool_ids在池刷新时只对 bit-exact 旧条目继承model_id,避免基础设施层缓存到旧凭证的 client。 - 四条分配策略:
RoundRobinModelAllocator(线性轮转,无视model_name)/ByModelNameAllocator(按model_name分组、组内轮转)/RouterAllocator(单端点路由,model_name 唯一映射,无 hint 时返回首项)/IntelliRouterAllocator(RouterAllocator子类,客户端可靠路由)。新策略实现ModelAllocator协议即可;build_model_allocator读team_spec.model_pool_strategy派发。 - 可靠性只归一层(S_11 不变量 14):前三条策略把成员摊到多个端点上,可靠性归 allocator;
intelli_router把多端点整个下沉给客户端 router(请求级重试 / failover / 限流感知),可靠性归 client。两者二选一,叠加即两层都做负载均衡。 ModelRouterConfig是用户面向的便利输入:一份(api_key, api_base_url, api_provider)+model_names: list[str]。在TeamAgentSpec.build()时通过to_pool_entries()展开成model_pool并把model_pool_strategy设为"router",下游resolve_member_model/inherit_pool_ids/update_model_pool全部复用 pool 路径,没有特殊分支。IntelliRouterConfig同理展开(strategy 设为"intelli_router"):每个逻辑 name 一条 entry,每条都带全量 deployment 列表、api_provider="intelli_router"、entry 自身api_key/api_base_url为空(凭证 per-deployment)。"*"(统一路由)默认排首位,故 leader 不配model_name即取到可用性最高的一档。IntelliRouterDeployment.api_base不含/v1——与ModelClientConfig.api_base约定相反,intelli_router 的 adapter 自己拼/v1/chat/completions;配错的报错是ResponseNotRead而非 404。详见 [[F_67]]。model_pool/model_router/model_intelli_router在TeamAgentSpec上三者互斥:配置超过一个直接ValueError。strategy"router"/"intelli_router"也可以由用户手动配 pool + 设置 strategy 触发,但对应 allocator 在构造时强制约束(router要求 name 唯一;intelli_router额外要求 provider 正确 + deployments 非空)。- 空池 →
build_model_allocator返回None→ 走TeamAgentSpec.agentsper-agent 模型配置兜底。 - 新增模型相关原语优先放本目录,避免渗回
schema/team.py(schema 层只声明字段引用,实现在 models/)。
基础设施插件化:TransportSpec / StorageSpec
两者通过 注册表 + type 字符串 解析具体实现:
register_transport("custom", MyTransportConfig)
register_storage("mysql", MyStorageConfig)
- 内置类型:transport =
inprocess/pyzmq;storage =sqlite/memory。 TransportSpec.build()/StorageSpec.build()是 spec → 实例的唯一桥。不要绕过注册表直接 import 具体 Config 类。- 新后端实现前先查
_ensure_builtin_infra_registered,避免重复登记或依赖环。
messager/ — 消息传输
| 文件 | 作用 |
|---|---|
base.py |
MessagerTransportConfig、MessagerPeerConfig、create_messager 工厂 |
messager.py |
Messager 抽象接口 + MessagerHandler |
inprocess.py |
进程内内存 pub/sub |
pyzmq_backend.py |
基于 ZeroMQ 的跨进程传输 |
Messager 是点对点 + broadcast 的统一抽象,任何直接新建 socket / 操作 asyncio queue 的代码都是错的。经由 create_messager(config)。
spawn/ — 成员启动
注册与拉起是两件事(细则见 docs/specs/S_05 不变量 1):
- 注册:四个
spawn_*工具与build_team的 predefined 成员都只落到TeamBackend.spawn_member——只写 DB 行(UNSTARTED),不启动任何东西(该方法 docstring 即契约:"does NOT start the member")。 - 拉起:只有一条链 ——
TeamAgent.auto_start_member/auto_start_all→TeamBackend.startup_member/startup→MemberStatus.UNSTARTED→STARTING的 CAS guard(try_transition_member_status)→_spawn_and_publish→_on_teammate_created→SpawnManager.spawn_teammate。
拉起漏斗有五个触发点,全是"活要交给一个还没起来的成员":leader send_message 的 _auto_start_members、leader create_task 落库后拉起(仅 autonomous,F_84)、interact dispatch(@member / @all / operator 消息)、调度器投递(F_62),外加 leader round-idle 对账兜底(TeamAgent._reconcile_member_startup,看板有非终态任务时才动,F_84)。前四个共用入口 TeamBackend.autostart_unstarted()——它持有构造期注入的 on_member_started 回调、自带 leader 门与回调门,调用方不各自捎带 spawn 回调。CAS 是这条链上唯一的并发闸——不要在 spawn 工具里顺手把 agent 拉起来,那会绕过注册与拉起的分离(走 autostart_unstarted 不算,它经同一条 CAS 链)。模式由 TeamAgentSpec.spawn_mode 决定。
启动模式:
spawn_mode="process"→Runner.spawn_agent走子进程(跨平台,默认)。spawn_mode="inprocess"→inprocess_spawn在同 event loop 启动协程(适合测试或轻量场景)。- 顶层
context.py的session_idcontextvars 是两种模式共享的上下文载体。
tools/ — 团队工具集合
参见 tools/AGENTS.md。要点:
create_team_tools(role=..., teammate_mode=..., exclude_tools=..., lang=...)是唯一入口。- 工具描述文本是行为契约,不是 feature 摘要。长文案放
tools/locales/descs/<lang>/<domain>/<tool>.md。 - ToolCard ID 统一
team.{name}前缀。
runtime/ — TeamAgent 对象池 + 派发 + 并发门禁
TeamRuntimePool + TeamRuntimeManager + 7 路 dispatch truth table + InteractGate + finalize / finalize_member lifecycle hooks,是 Runner.run_agent_team* / interact_agent_team / register_human_agent_inbound / pause_agent_team / stop_team / release_session / delete_team 这一组 SDK facade 的实现层。Runner.run_agent_team* 公共入口接 str | TeamAgentSpec(默认):spec 走 manager.activate(dispatch 决策 + pool 写入),str 是 team_name、复用已激活的 pool entry。跨 session 切换由 activate 在 dispatch 前 stop_team + pool.remove stale entry,再走 cold rebuild;不再保留"warm 跨 session 复用同一 TeamAgent 实例"的路径。run cycle 退出时由 Runner finally 调 manager.finalize / finalize_member 决定 pause vs stop(coordination kernel 的 finalize_round 不再决策)。spawn 出来的 teammate / human-agent 实例走 Runner.run_agent_team*(member=True) 入口、跳过 activate/dispatch、不入 pool;退出 finally 调 finalize_member。
team_workspace/ — 共享工作空间
跨成员的文件共享区,支持锁 / 版本 / 冲突策略。独立于 worktree:worktree 管代码隔离,workspace 管产物协同。
interaction/ — 外部交互入口 + HITT + Bridge Agent
三种交互视角(GodViewMessage / OperatorMessage / HumanAgentMessage)+ UserInbox / HumanAgentInbox 的实现层,所有 HITT runtime 表面(enable_hitt 分层开关、人类成员来源、一致性约束、运行约束)也落在这里。
Bridge Agent 把外部独立 agent(claudecode / codex / openclaw / hermes 等)以"团队成员"形式接入:bridge_protocol.py 定义纯文本 BridgeProtocolAdapter(connect / relay / close),BridgeMemberSpec(TeamMemberSpec) 子类 + enable_bridge 是 capability ceiling,dynamic spawn 走 spawn_bridge_agent 工具;bridge avatar 本地是完整 teammate,远程做实际工作,框架在 mailbox 路径自动转发原消息给远程并把回复组合进 avatar context(参见 agent/coordination/handlers/message.py:_bridge_deliverable_for)。详见 docs/features/F_07_bridge-agent.md。
cli/ — 交互式 TUI / 斜杠命令
prompt_toolkit + rich 驱动的交互式 CLI。run_team_cli(*, specs, yaml_paths) 是公共入口,把 Runner 暴露的 team lifecycle facade(run_agent_team_streaming / interact_agent_team / pause_agent_team / stop_agent_team / delete_agent_team / release / list_active_teams / register_human_agent_inbound / get_agent_team_monitor)映射成 /team /session /spec 子命令。普通文本透传 Runner.interact_agent_team,由 runtime 的 parse_interact_str 一处解析 # / $ / @member 前缀;CLI 不做二次解析。详见 cli/AGENTS.md。
external/ · skill/ · mcp/ — 外部 agent 直连接入
让团队进程之外的 agent(第三方 CLI claudecode / codex / openclaw / hermes, 或独立运行的 agent 服务进程)以一等成员身份直接调用协同工具——直连共享 DB + zmq messager,不经本地 avatar 代理。与 F_07 bridge(本地完整 DeepAgent + relay 纯文本给 无工具远程执行者)是正交互补的两条接入路径:bridge 管"被动文本执行者",本路径管 "自主一等成员"。
external/ 同时包含两组正交表面:descriptor/client/skill/MCP 让已经在团队进程之外运行的 agent
直连协同基础设施;根级 harness_protocol + member_runtime.py 让由宿主拥有生命周期的三方 Python Harness
适配成内部成员行为。不要把 ExternalTeamClient 的协同工具协议与 HarnessProtocol 的
provider session/Turn 协议合并。
-
openjiuwen/harness_protocol/:公共三方 Harness Python SPI 1.0,使用Session > Turn > Step、单消费者持续/单 Turn 事件视图以及独立 observation / interaction / hook 三平面。协议包保持无厂商 SDK 依赖。 -
openjiuwen/harness_providers/:协议的内置实现(nativeDeepAgent /claudecode/codex/dsh),共用SerializedTurnHarness骨架;HarnessIOAdapter把协议投影成 DeepAgent 风格的OutputSchema/InteractiveInput契约(含 ask-user 中断),create_harness(manifest, provider=...)按 AgentTemplate manifest 建 harness。DSH adapter 从external/dsh/迁到harness_providers/dsh/(external/dsh/__init__.py仅保留 re-export)。见harness_providers/AGENTS.md。 -
external/member_runtime.py:ExternalHarnessMemberRuntime,组合HarnessIOAdapter并叠加团队 行为:成员 child AgentSession(provider checkpoint sink +TeamContextTracker投递基线)、harness.state/harness.round(legacy 兼容名)回调、外部 runtime 可靠性上下文 (bind_reliability_context:FAILED terminal / 启动失败 → leader 邮箱失败消息,retrying 诊断 → 进度事件)、轨迹记录(bind_trajectory_recorder:协议事件 →HarnessTrajectoryRecorder,STARTED 前注入持久化成员 turn 身份,见 [[F_112_harness-protocol-trajectory-observation]])、认证 fallback 持久化(bind_fallback_promotion:以auth_fallbackprovider interaction 先持久化再放行,持久化失败 provider 回退原生端点)与 MCP server 挂载(bind_mcp_servers)。resume_external_backend=True时要求 checkpoint 存在并以REQUIRE_RESUME启动。Claude Code / Codex 成员都走这一条路径(build_cli_runtime),不再有ClaudeSdkRuntime/CodexSdkRuntime。详见 [[F_96_protocol-harness-providers-and-member-migration]]。 -
external/cli_agent/claude/:只剩团队侧接线——sdk_mcp.py(进程内 SDK MCP 团队工具集,作为McpServerConfig(IN_PROCESS)挂到 runtime)、ssh_transport.py(Claude SDK ssh transport,经ClaudeCodeHarness(transport_factory=...)注入)、options.py(team 命名的 session id 助手)。external/cli_agent/codex/:只有options.py(team MCP overrides 助手);Codex 观测在harness_providers/codex/observation.py。DSH 的 Turn 边界与限制见 [[F_95_dsh-external-harness-adapter]]。 -
external/descriptor.py:TeamJoinDescriptor(session/team/member + role + language + dispatch_mode + teammate_mode + db_config + transport_config)+TEAM_JOIN_ENV环境变量(OPENJIUWEN_TEAM_JOIN)。 团队拉起外部 agent 时注入,或运维下发给独立服务。 两个场景,按descriptor.scope首次连接时分化(F_26)——scope与 teamrole正交: -
member(cli-agent 三方团队成员):
ExternalTeamClient.connect建最小TeamBackend+create_team_tools(role="teammate"),对外暴露真实 teammateTeamTool(view_task/claim_task[claimed|completed]/send_message,结果即render_for_llm()文本,与进程内成员逐字一致)。入站消息与原生成员同路——父进程 coordination push 进 CLI, 不暴露 pull 工具(operator 专有的read_inbox对 member 不可见)。complete_task折进claim_task(status=completed)、list/get/claimable 折进view_task。MCP instructions 空(系统提示词已在 spawn 时直接注入 CLI,见 [[F_25]])。 -
operator(团队外非成员控制接口,默认 scope):
ExternalTeamClient的 per-op 方法 (send/broadcast/list/get/claimable/claim/complete/update/list_members +create_task)+fetch_inbox/watch,全团队控制面;operator 没有自己的 coordination 层,MCP 工具集 含 operator 专有的read_inboxpull 工具;MCP instructions = 控制工作流。
公共件:client.tools(member 真实工具字典)、client.read_inbox()(operator 侧 pull,<team-inbound>/<team-event> XML)、
client.bind_session_context()(每调用重绑 session/language contextvar)。
external/format.py:纯函数把消息 / 任务板渲染成与进程内 dispatcher 一致的<team-inbound>/<team-event>XML(复用inbound_render结构 +i18n.tnote 文案);read_inbox用之。见 F_51。external/runtime.py的CliRuntimeBase:团队状态的第二条投递通道(F_70)。外部 CLI 成员没有 rail、拿不到对端上下文,所以send在真正投递前把TeamContextTracker的待发 文本拼到 user message 正文最前面(搭车),成员变更事件另经announce_team_context()单独 发一条公告(补偿)。两个入口共用下沉后的_send_raw——唯一真正投递的地方,都是"投递成功 再 commit"。start(team_session=...)统一开成员自己的 child AgentSession(基线 + codex thread id 都存在那里)。skill/cli.py:非交互脚本式 CLI(team-member入口),两段解析后按 scope 分化子命令 (member 驱动真实工具 / operator 控制面)。两份 skill 文档:skill/SKILL_member.md/skill/SKILL_operator.md。与cli/的交互式 TUI 不同——后者给人用,本 CLI 给外部 agent 脚本化调用。mcp/server.py:低层mcp.server.lowlevel.Server(openjiuwen-team-mcp入口;不用 FastMCP, 因 FastMCP 从函数签名推断 schema,无法暴露真实工具的card.input_params)。list_tools/call_tool按client.scope在请求期分化;member 工具的inputSchema即card.input_params、 结果即str(tool.invoke())。这是仓库首个 MCP server(其余 MCP 代码都是 client)。
team 拉起外部 CLI 成员(F_22):CLI 启动知识静态预置在
TeamAgentSpec.external_cli_agents(ExternalCliAgentSpec 列表:cli_agent 种类标识 +
command/cwd/inject_mcp/mcp_server_command/env/ssh_transport),非空集即外部 CLI 成员的能力上限。
leader 用 spawn_external_cli(cli_agent=<name>) 按名引用,不在 spawn
调用里传启动细节。claude / codex 条目可声明 builtin_models(订阅等 CLI 自身登录提供的模型与
effort 目录):声明后 leader 可在 spawn 时挑内置模型,并用 set_member_model 在运行中切换模型 /
effort(落库到 options.builtin_model,经 HarnessModelControl.set_model 下一 turn 生效);
未声明则行为不变。见 [[F_113_external-harness-builtin-model-selection]]。当前内置 backend:claude / codex(harness_providers 协议 provider +
ExternalHarnessMemberRuntime)与 adapter 型 gemini / openclaw / hermes / generic
(CliRuntimeBase 子进程 runtime)。spawn 路径(external_cli_spawn → build_cli_runtime)按 backend
注入进程内团队工具——claude 走 SDK 进程内 MCP(_bind_protocol_member_team_tools 在 configure
后把 build_claude_sdk_mcp_tool_set 的 server 作为 McpServerConfig(IN_PROCESS) 挂上)、codex 在同一
时点把真实 teammate TeamBackend 构造的 ExternalTeamToolGateway 绑定为 Dynamic Tools;Codex
gateway 在每次调用期间显式绑定父团队 session(恢复时重新构造),两者都不经过外部传输。未设置该 flag 的 gemini / hermes 由 spawn 路径跑一次 <cli> mcp add ... 带外注册(mcp_register_command),
openclaw 无已知注册方式则 mcp_inject=none + 大声告警。MCP server 是 CLI 子进程,继承
OPENJIUWEN_TEAM_JOIN env,自动绑定成员身份。ssh_transport 配置后 CLI 进程在远程 SSH 端点
启动,command / cwd / mcp_server_command 均按远程主机解释;DB / messager 可达性由部署保证。
外部 CLI 成员的系统提示词复用 team-rail 的
build_team_static_sections(role/workflow/lifecycle/private-prompt,排除其它 DeepAgent rail),经
claude --append-system-prompt / codex -c developer_instructions / 其余 prepend 下发;其
stdout 叙述经 outputs() surface 为 TeamOutputSchema chunk、与进程内成员同路 fan-out。
详见 [[F_22]] 与 [[F_25_external-cli-hardening-and-gemini]]。
设计文档见 docs/features/F_21_external-agent-access.md(接入面 + spawn 接线)与
docs/features/F_22_external-cli-spawn-member-and-mcp-injection.md(external_cli spawn 工具 +
静态 spec 配置 + MCP 自动注入)。
worktree — Git worktree 隔离
通用实现已下沉到 openjiuwen.harness.tools.worktree,由 deepagent 与 team 共用。team 侧只保留三件事:
- 通过
TeamAgentSpec.worktree(WorktreeConfig)描述配置;team 下 worktree 隔离只由 leader / 宿主在SpawnManager.build_context_from_db里按TeamMember.options.worktree.isolation == "worktree"调用create_owner_worktree(slug)创建,不向 leader 或 teammate 暴露enter_worktree/exit_worktree作为手动兜底。 - workspace 视图软链由 team 侧自管:
create_worktree_manager给WorktreeManager注入一个翻译适配器,把WorktreeCreatedEvent/WorktreeRemovedEvent路由到TeamWorkspaceManager.mount_worktree/unmount_worktree,在共享 team workspace 下维护.worktree/{slug}软链。这一层是"本 team 当前活跃 worktree 一览"的导航视图,单 agent 不订阅事件,软链物理上不存在——WorktreeManager本身不知道软链。team 侧同时把这些事件桥到TeamEvent.WORKTREE_*总线。 - Team teammate 隔离 worktree 命名固定为
agent-{team_name}-{agent_name}-{hash8}。TeamMember.options.worktree持久化isolation/path;旧库迁移会把model_ref_json回填到options.model_ref后删除旧列,不匹配isolation/worktree_path物理列。worktree_name/worktree_branch/head_commit留在 leader 宿主内存。cleanup_teammate停掉成员后检查变更:干净则git worktree remove并清空路径字段;有变更、宿主 metadata 丢失或无法确认状态则保留worktree_path给 leader 合并与解冲突。 worktree_remote.py:RemoteWorktreeBackend/WorktreeRemoteHandler跨机器 worktree 后端,依赖paths.get_agent_teams_home。需要时由调用方直接WorktreeManager(backend=RemoteWorktreeBackend(...))注入,不走 backend registry(构造参数不止 config)。
harness/tools/worktree 暴露的 WorktreeManager 接受可选 event_handler: Callable[[WorktreeEvent], Awaitable[None]];team 端如需进一步把生命周期事件桥接到 TeamEvent.WORKTREE_* 总线,让上面的 mount/unmount 适配器和总线发布共享同一个 handler 即可(当前总线投递未启用)。
架构铁律
-
Card / Config 分层不可破坏
Card(AgentCard、ToolCard)是可序列化标识;Config / Manager / Runtime 持运行时状态。不要把runner/session塞进 Card;不要在 Config 上定义 static description 字段。详见.claude/rules/architecture.md。 -
TeamAgent 是单一实现
Leader / Teammate 通过TeamRole切换行为,不是两个类。新增角色前先想:真的需要新类,还是一个新的 policy 分支? -
coordinator 不做决策
CoordinatorLoop只管 wake-up,所有业务行为由内部 DeepAgent + team tools 驱动。不要把业务逻辑塞到 loop 里。 -
i18n 的两条路径
- 运行时 hard-coded 字符串(dispatcher 通知、default desc 等)走
agent_teams/i18n.py的t(key); - Prompt / 工具描述的长文本走各自模块的
locales/或prompts/(按lang参数入参传递)。
新增字符串前先判断归属:运行时提示进i18n.py,模板正文进prompts/或tools/locales/descs/。
调度器交接消息(F_63)属后者且只有模板一份:文案在prompts/<lang>/scheduler_*.md, 投递时渲染;i18n.py里不再留成员侧短句变体——同一条消息两处文案必然漂移。scheduler.*键只剩 leader 直投的摘要/升级(不经邮箱,无模板通道)。
- 运行时 hard-coded 字符串(dispatcher 通知、default desc 等)走
-
paths.py 是文件系统布局的单一真相源
get_agent_teams_home()、team_home(team_name)、independent_member_workspace(agent_name)。创建和清理都走这里,不要散落Path("…")硬编码。 -
Spec → build() → Runtime 是单向流
Spec 不保留运行时引用;build() 产出运行时对象;运行时对象不回写 Spec。想支持热更新?通过 session + resume 路径,而不是反向污染 Spec。
测试
- 单测路径镜像源码:
openjiuwen/agent_teams/tools/team_tools.py→tests/unit_tests/agent_teams/tools/test_team_tools.py - 内存后端:
MemoryDatabaseConfig+InProcessMessager适合单测,不依赖 sqlite / zmq。 - 多成员场景优先用
spawn_mode="inprocess",避免子进程拉起开销。 pytest纯函数风格;禁止print,改用team_logger或 test_logger。
代码风格
- 类型注解:使用 PEP 585 内置泛型(
list[X]、dict[K, V]、set[X])和 PEP 604 联合类型(X | Y、X | None);禁止从typing导入List/Dict/Set/Tuple/Optional/Union等已被内置语法替代的别名。Callable、Awaitable、AsyncIterator、TYPE_CHECKING、Any等无内置等价物的仍从typing导入。
设计文档归档与双向同步(仅本模块强制)
本模块的设计文档全部落在 docs/ 下,命名与结构规约见 docs/AGENTS.md。
把 **openjiuwen/agent_teams/ 下的代码 + 本目录 docs/specs/ + docs/features/
- 各级
<subdir>/AGENTS.md** 当作一个一致性单元来维护——agent_teams子系统 对契约一致性的要求高于仓库其它模块(多角色 + 多进程 + 持久化状态 + 公共 SDK 表面), 所以本节的强制约束只限于openjiuwen/agent_teams/子树,不上推到仓库其它模块。
前置铁律——文档先是设计的输入,才是提交的产出。 动手改任何代码设计之前,先
grep + 读相关的 docs/specs/S_NN_*(现有契约 / 不变量 / 边界)与 docs/features/F_NN_*
(为什么长这样、当初拒绝了哪些方案),拿它们校准方案——spec 说"系统是什么样"、feature 说
"为什么这么改",先读才不会撞坏既有契约、也不会重蹈已被拒绝的方案。方案定了当场同步改掉
对应 S / F,让文档和方案一起成形。先码后档、拖到提交前才补文档是把流程做反了:那样文档
不再参与设计,退化成走过场的归档义务。下面三条是"提交时必查"的产出侧同步;本条是它更靠前的
输入侧——两半合起来才叫双向同步,缺了前半,第 3 条的"读到不一致当场改"永远慢半拍。
三条强制约束,提交时必查:
- 每次特性更新必须归档 feature 文档,且特性代码、测试代码、文档拆成三个连续提交:在
docs/features/下新增一份F_NN_<slug>.md,记录决策、拒绝的方案、验证基线、已知遗留。commit message 只写 what,feature 文档负责写 why / why-not。落地顺序固定为三个紧邻的提交——提交 1 落特性代码 (feat(swarm): ...或对应 type),提交 2 落本次新增/改动的单测(test(swarm): ...),提交 3 落本次涉及的全部文档:docs/features/F_NN_*.md新增 + 下面 #2 要求的docs/specs/S_NN_*.md修订(docs(swarm): ...)。特性代码、测试、文档不再混进同一次 commit——既不让大段文档 diff 淹没 代码评审,也让测试改动独立可审,还避免文档归档拖延导致设计上下文随时间漂移。 - 所有模块设计规约变动必须更新 specs 文档:模块契约、跨子模块的公共协议、不变量、
公共 API 形态发生变化时,同步修订对应
docs/specs/S_NN_<slug>.md;新规约 = 新 spec 文件。 规约变了但 specs 没改,下次读 spec 的人就被误导——这是设计债,不是文档懒。 - 双向同步:读到与代码不一致的描述必须当场修文档。在本模块任何一份
AGENTS.md/docs/specs/S_*/docs/features/F_*里读到的接口名、枚举值、 truth table 行数、文件路径、不变量等只要与当前代码不符,不要把过时表述当作新约束去执行、 也不要原样转述给用户;先grep代码、以代码为准刷新文档,在同一次改动里落地。AGENTS.md里每条点名了"X 个分支 / Y 路 dispatch / Z 方法"的句子都是契约的一部分。 更新目标是AGENTS.md,不是CLAUDE.md——CLAUDE.md现在只是@AGENTS.md的单行壳, 编辑它没有任何意义;所有内容变更一律落到对应目录的AGENTS.md。
收尾规范:
- spec 头部"最近一次修订日期"字段在每次修订该 spec 时填当天日期(
YYYY-MM-DD);feature 文档用 头部"日期"字段记录归档当天,不设独立修订字段。不要在元信息里写 commit hash——避免"提交后 回填"的来回反复。 - 子模块自身的本地约定继续放各
<subdir>/AGENTS.md;跨子模块的设计规约一律落到docs/specs/, 不要塞进单一子目录的 AGENTS.md。 CLAUDE.md是只读壳,不要编辑它:本模块每个子目录的CLAUDE.md仅含@AGENTS.md一行, 编辑CLAUDE.md的修改不会被保留在任何有效文档里。需要更新文档时,直接编辑AGENTS.md。- 拿不准某次改动算 feature-grade(要
F_NN_*.md)还是普通修复(不必归档)时,先问用户。 歧义情况默认归档——多一份 markdown 的成本远低于丢失设计上下文。
提交约定
本模块改动的 commit message scope 固定用 swarm(如 feat(swarm): ...)。
footer 用 Refs: #<issue> 格式关联 issue。issue 号若无法从当前上下文明确,必须先询问用户,不要臆造或留空。
涉及 docs/features/F_* / docs/specs/S_* 文档更新的特性改动,特性代码、测试代码、文档拆成三个连续提交(提交 1 特性代码 feat(swarm),提交 2 单测 test(swarm),提交 3 文档 docs(swarm)),细则见上文「设计文档归档与双向同步」约束 #1。
