Agent协作:工具面

一个agent与别的agent交互的全部工具,按四个域摆开:计划、执行、实体、通讯。 这页说明每个工具做什么、哪个会话能操作哪些任务、两个预算怎么限制一条链的规模。 设计文档:docs/reference/design/runtime/agent-collaboration.md。 八个参考实现在同样八个维度上怎么做,见Agent协作:八家实现对照

01 四域词汇表 02 一个agent能做的四件事 03 消息和任务的区别 04 两个预算 05 归档 06 典型场景 07 和Claude Code的对应

01四域词汇表

四个域各有一个名词。名词分清楚了,工具名就不用记:每个域的工具都在操作它自己那个名词。

计划

todo

写下来的计划

手写的计划清单。只记录打算做什么,不对应任何在跑的东西,改它不会让任何agent运行。

todo_createtodo_updatetodo_list
执行

task

正在运行的任务

派出去正在跑的任务:一个task_id、一个状态、一份结果。它是任务列表里的条目,不是执行任务的那个实体。

list_taskstask_outputtask_stop
实体

agent

执行任务的实体

新建一个、给已有的派任务、列出所有agent、把干完的归档。一个agent就是会话DAG里的一条分支。

agentlist_agentsarchive_agent
通讯

message

发消息,和读历史

发一条消息过去,不产生任务记录;或者直接读任意一条分支的全文,不触发对方运行。

send_messageread_conversation
最容易混的两组:todo和task同是"任务"两个字,但一个是写下来的计划、一个是正在运行的执行记录,所以计划清单用todo_*前缀, 执行侧用list_tasks。task和agent也不是一回事:停掉一个task只是停那一轮运行, agent本体还在agent列表里;把agent归档是archive_agent

02一个agent能做的四件事

新建agent、派任务、发消息、查询。前两件产生task_id并记入任务列表,第三件不产生,第四件只读。

一个agent能做的四件事 新建和派任务都记一条任务,发消息只是把内容送过去,查询不改动任何状态。 当前agent 一轮之内 新建 · 创建一个新agent agent(prompt, description, …) 新分支 = 新agent description成为它的名字 派任务 · 给已有agent派受管任务 agent(to="名字", prompt=…) 已有agent,不新建 它忙就排队,空闲就跑 发消息 · 给已有agent发一条消息 send_message(to="名字", message=…) 已有agent 读到之后自己决定 不记任务:没有task_id,也就没有必回的结果 查询 · 只读,四个来源 list_agents agent列表 · list_tasks 派出去的任务 read_conversation 任意分支全文 todo_list 自己的计划清单 返回一段文本,什么也不改 任务记录 task_id · 状态 · 结果 list_tasks() 列出任务 task_output(task_id) 取结果 task_stop(task_id) 取消,级联 后两个带归属检查: 只有派它的会话及其祖先能操作 别的会话拿到task_id也不行 任务结果自动回流:目标跑完,结果接到我这条会话后面,我下一轮读到 目标如果回复,回复也自动送回来,回不回由它决定
怎么读:四条横道从左边的"我"出发。新建派任务都走agent这一个工具,区别只在有没有to=: 没有to=就新建一条分支,有to=就把任务交给已经存在的那条。两者都记一条任务,拿回一个task_id发消息send_message,消息送达即结束,不产生task_id查询四个工具全是只读的。
颜色:绿=创建实体 · 橙=受管任务和任务记录 · 蓝=消息 · 青=只读。虚线=异步回流。

03消息和任务的区别:消息不产生任务记录,任务可追踪可取消

同样是"把内容送到另一条分支上跑一轮",消息和任务给出的承诺完全不同。差别就写在收件方第一眼看到的那行回执头里。

消息 · 不产生任务记录 任意agent 谁都能发 send_message [message from SID:HEAD] 回复是可选的,没什么要补充就别回 目标agent 自己决定做不做 没有task_id,没有归属检查 对方看到后自己决定回不回 所以多方同时发也是安全的 任务 · 可追踪可取消 派发方 只能操作自己派的 agent(to=…) [task from SID:HEAD] 这一轮就是任务,最终回复自动回给派发者 目标agent 这一轮归任务 有task_id:结果必回、可取消 取消会级联,子任务一起停 所以只能操作自己派出的任务 归属检查: read_conversation能读任何分支,所以任何agent都可能看到别人的task_id。 task_output和task_stop因此先验归属:当前会话必须是这个任务的派发者,或者任务链上的祖先,否则直接拒绝。用户和界面不受这条限制。
承诺消息 send_message任务 agent() / agent(to=)
谁能发起任何agent,对任何未归档的分支同样任何agent,但产生的task_id只有自己能操作
有没有task_id没有,只回一个delivery_id告诉你送到了task_id,记入任务列表,list_tasks能查
对方必须回吗不必。回执头明说"没什么要补充就别回"必须。这一轮的最终回复就是任务结果
能不能取消不能。消息已经送达,无法撤回task_stop,排队中的直接撤回,在跑的停那一轮
取消会不会级联不涉及级联:停一个任务,它派生的任务全停
能操作别人的任务吗不涉及,消息发出后没有后续操作不能。task_output/task_stop有归属检查
多方并发安不安全安全。不产生任务记录,最坏的结果是对方不回复安全,且有记录:目标忙就排队,不会被打断
归档一个agent本体是另一件事,任何会话都能做:归档不中断在跑的工作、也不删数据, 所以archive_agent不做task_stop那样的归属检查。见下一节。

04两个预算:两个计数器,各管一件事

一条链是一次用户轮次产生的全部调用。消息数每跳加一,回复回流也算一跳;代数只有创建新agent时加一。读回结果花的是消息,不花代数,所以派一批活、看结果、再派一批走得通。

一条链的两个计数器 下面这条轴是消息数:spawn、send_message、agent(to=)、回流每跳都+1。代数是另一个计数器,只有创建新agent才+1。 agent.max_spawn_depth = 1(代数) 只有左边这一跳涨代数,后面的消息跳都不涨;worker不能再创建新agent agent.max_messages = 8 整条链传满8跳就不再投递,A和B来回也停 新建 发消息 回复 派任务 0 1 2 3 4 5 6 7 8 主agent worker 代数用完时 越界的那次创建被拒,并给出理由("自己完成"),别的工具全留着。 worker还能用agent(to=)和send_message,工具也不摘。 消息数用完时 agent、task_output、task_stop直接从工具列表里消失:每种派发都要 交出一条消息,留在列表里模型只会调用一次再被拒,白费一轮。

两个都支持设0,0就是这条线不存在,什么也不拦。

# 命令行改,即时生效
openprogram config set agent.max_spawn_depth 1   # 默认:主agent创建worker,worker自己完成工作
openprogram config set agent.max_spawn_depth 2   # worker可以再创建一层,第三层被拒
openprogram config set agent.max_spawn_depth 0   # 不限层数

openprogram config set agent.max_messages 8      # 默认:一条链最多传8跳
openprogram config set agent.max_messages 0      # 不限条数,agent之间可以一直互发

openprogram config set agent.max_spawn_fanout 8  # 默认:一轮最多创建8个agent
openprogram config set agent.max_spawn_fanout 0  # 一轮想创建多少个都行
设置页的Agent分组里是同样的数字框:Max spawn depth、Max messages per chain、Max spawn fan-out, 改完即时生效,落盘在~/.openprogram/config.jsonagent段。 全设0之后,防失控就只剩并发上限(OPENPROGRAM_TASK_WORKERS,默认4)、 每轮50次工具调用的上限和用户自己按停。
扇出预算不在上面那两个链上计数器里。派生把计数交给孩子、自己那份不动, 所以链上计数器数不到兄弟。扇出按(会话,轮次)单独计,只拒绝不摘工具: 工具清单在轮次开始时就冻结了,这个数在轮次里才花掉。
回流那一跳两个计数走不同方向。消息数从孩子那边接着走,A↔B来回才会停; 代数退回派发方的计数,读结果不算创建,协调者读完第一波结果还能派第二波。
有一条规则不受预算影响:自己发给自己一律拒to=指向发起方当前这条分支是直接的环,立刻拒掉。

05归档:停止接收投递,历史保留

分支在会话DAG里永远留着,fork、回放、读全文都要用它。所以归档不是删除,只是在分支上写一个archived标记。

两种归档方式,一个标记,五种后果 agent(archive_when_done=true) 派任务时就声明:做完自动归档 archive_agent(to="名字") 事后指定:干完的逐个归档 已归档的分支 archived: true 写在它自己的分支meta上 拒收 list_agents 默认视图里不列 send_message(to=…) 拒收 agent(to=…) 拒收 照常 read_conversation(…) 照读全文 agent(start_from="SID:MSG_ID") 照fork 另外 list_agents(scope="archived") 单独列已归档的 拒收只写在一个地方:两条投递路径共用的地址解析器。地址一旦落到分支当前的tip上就查这个标记,所以没有哪条调用能绕开。 任何会话都能归档任何agent:归档不中断在跑的工作、也不删数据,所以不做归属检查。 没有反归档。归档的意思是"这段对话结束了";还想接着用,就用 agent(start_from="SID:MSG_ID") fork一条新的出来,新名字新生命周期。

06典型场景走一遍

三条最常走的路。同一批工具,组合方式不同。

场景一 · 派一批任务出去

手上有五件互不依赖的事。先写成计划,再一件件派出去,最后回来更新计划。

01 计划
todo_create(subject=…)
五件事写成五条。blocked_by标出谁等谁。
02 派任务
agent(prompt, description, run_in_background=true)
逐条派。后台形式立刻返回task_id,不占我这一轮。
03 盯
list_tasks()
五个task_id五个状态。要结果就task_output,要取消就task_stop
04 收
结果自动回流
不用轮询也行。哪个跑完,它的结果就接到我这条会话后面,我下一轮读到。
05 更新计划
todo_update(todo_id, status="completed")
计划状态和实际执行一致。

场景二 · 找一个已经存在的agent

这件事已经有agent做过一半,不需要再创建一个从零开始的。先查agent列表,再决定是发消息还是正式派任务。

01 查agent列表
list_agents(scope="all")
按会话分组。每条分支给名字、一个能直接用的to="SID:HEAD"、轮数和大致字数、还有末尾预览。
02a 发消息
send_message(to="名字", message=…)
问一句、通知一声、把结论同步过去。不产生task_id,对方可以不回复。
02b 派任务
agent(to="名字", prompt=…)
这件事确实由它做,而且我要结果。产生task_id,可取消,结果必回。
03 忙就排队
目标在跑就进收件箱
不打断也不丢弃。它这一轮结束后就接着跑我这条。

场景三 · 回看一段旧会话

想知道上次那件事到底怎么做成的,但不想触发任何agent运行,也不想把整段历史全部读进上下文。

01 拿地址
list_agents(scope="all")
同一份agent列表。它标出的轮数和字数可以先判断体量。
02 只看结尾
read_conversation(session_id="SID:HEAD", start_turn=-10)
负数从末尾数,-10就是最后十轮。list_agents给的SID:HEAD整串粘进去也认。
03 要不要工具细节
include_function_calls=false
默认连每轮调了什么工具、参数、结果都给。只想看对话就关掉。
04 控体量
max_chars=60000
超了就丢后面的轮次。归档的分支也照读,读取不触发任何agent运行。

07和Claude Code的对应

同名的地方是有意对齐的,差别只有一处命名冲突和三个我们额外提供的工具。

这边Claude Code关系
agent同名同义。创建新agent的唯一入口;to=派任务是我们这边的扩展
list_agents同名同义。看得见对方是通信的前提
send_message同名同义。跨分支发消息
task_outputTaskOutput参数形状照抄:block默认true,timeout毫秒,默认30000,上限600000
task_stop同名同义。我们额外加了归属检查和级联取消
todo_create / todo_update / todo_listTaskList 同物不同名。Claude Code的TaskList是计划清单,和我们的list_tasks撞名不撞义, 所以我们的计划清单一律走todo_*前缀,把list_tasks这个名字留给真正在运行的任务
list_tasks没有我们多出来的:让模型自己查后台正在跑的任务,不必一直保存派生时返回的task_id
archive_agent没有我们多出来的:agent不归档,agent列表就会一直堆积早就做完的worker
read_conversation没有我们多出来的:一条分支渲染成可读的文字稿,带轮次范围和体量上限,不用直接读原始记录文件
一句话收尾:四个名词分四个域,agent一个工具管新建和派任务,消息不产生任务记录所以任何agent都能发, 任务可追踪可取消所以只能操作自己派出的,两个预算限制一条链的规模,归档只停止后续投递。