Agent协作:工具面

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

实现边界:图中的自动结果通知和异步自动归档是规范目标。现有 helper 未被检查的 canonical 与借用 claim 完成路径调用,不能凭函数存在认定功能已接入。当前历史读取也缺少目标级 owner/project ACL。详见实现状态与完整合同。
01 四域词汇表 02 一个agent能做的四件事 03 消息和任务的区别 04 两个预算 05 归档 06 典型场景 07 参考命名 08 Attach 定位 09 实现状态

01四域词汇表

四个域区分资源,不要求每个工具只操作一个域。agent 工具选择或创建运行分支并提交工作;job 查询读取已接纳的执行;execution 控制入口管理执行生命周期。

计划

todo

写下来的计划

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

todo_createtodo_updatetodo_list
执行

job

已接纳的执行记录

Job 包含 execution_id、状态和结果,可以排队、运行或结束。list_jobs 与 job_output 查询执行;取消通过 execution 命令协议完成,不是独立的模型工具。

list_jobsjob_outputexecution.cancel(命令)
实体

agent

执行任务的实体

本页的 agent 指运行中的对话分支。agent() 创建或继续分支,list_agents 列出分支,archive_agent 归档分支。保存的 AgentSpec 是独立的可复用配置,由 agent_id 选择。

agentlist_agentsarchive_agent
通讯

message

发消息,和读历史

send_message 继续已有分支,不创建新分支;实际轮次仍须 Job 准入。read_conversation 读取本地历史而不执行,但尚未实现历史读取的目标级 owner/project ACL。

send_messageread_conversation
资源分工:todo_* 修改计划;agent 创建或继续对话分支并提交执行;list_jobs/job_output 查询已经接纳的 Job;execution.cancel 通过 execution 命令协议控制已有执行。它不是可调用的 job_stop 模型工具。取消执行不归档分支,也不删除保存的配置。
标识符兼容:后台派发返回 execution_id;当前 job_output(job_id=...) 参数、details.job_id 和 attach.job_id 保留兼容名称。将返回的 execution ID 传给该参数,不引入 task_id 别名。

02一个agent能做的四件事

新建分支、向已有分支派发、发送消息或查询。前台新建、后台新建和向已有分支派发均持久化接纳一个 Job。后台新建与已有分支派发立即返回 execution_id;前台新建等待该 Job 完成并返回最终结果。同步返回不表示没有内部执行身份,也不绕过接纳。

一个agent能做的四件事 新建、派发和消息触发的实际轮次均须 Job 准入;读取历史不触发执行。 我 当前agent 一轮之内 新建 · 创建一个新agent agent(prompt, description, …) 新分支 = 新agent description成为它的名字 派任务 · 给已有agent派受管任务 agent(to="名字", prompt=…) 已有agent,不新建 它忙就排队,空闲就跑 发消息 · 给已有agent发一条消息 send_message(to="名字", message=…) 已有agent 读到之后自己决定 不新建分支;实际执行仍须 Job 准入 查询 · 只读,四个来源 list_agents agent列表 · list_jobs 派出去的任务 read_conversation 任意分支全文 todo_list 自己的计划清单 返回一段文本,什么也不改 任务记录 execution ID · 状态 · 结果 list_jobs() 列出任务 job_output(job_id=…) 取结果 execution.cancel 命令请求取消 结果读取:按执行 session 关系检查 取消:检查执行权限和状态版本 知道 execution ID 不等于获得控制权限 目标:完成结果写入调用方会话;canonical 完成路径仍待接入 目标:回送非空完成结果;额外显式 send_message 回复可选
怎么读:四条横道从左边的"我"出发。新建和派任务都走agent这一个工具,区别只在有没有to=: 没有 to= 就新建分支并立即执行 prompt;有 to= 就向已有分支提交工作。图中的 Job 记录适用于前台新建、后台新建和已有分支派发。前台新建等待完成;异步形式返回执行标识。 发消息使用 send_message,不创建新分支。直接投递启动已准入 Job,并返回 delivery_id=job_id;目标忙时先返回收件箱回执,此时尚不代表执行准入。查询使用四个只读工具。
颜色:绿=创建实体 · 橙=受管任务和任务记录 · 蓝=消息 · 青=只读。虚线=异步回流。

03消息与任务派发:均须 Job 准入,并采用相同的完成回送

两者都通过 Job 准入继续已有分支。目标合同对两者均自动回送非空完成结果;额外显式调用 send_message 回复是可选的。当前 canonical 完成路径尚未接入自动完成回送。

消息 · 已有分支,Job 准入 任意agent 谁都能发 send_message [message from SID:HEAD] 额外显式消息回复是可选的 目标agent 自己决定做不做 实际轮次仍须 Job 准入 目标:自动回送非空完成结果 收件箱回执不表示执行已准入 任务 · 可追踪可取消 派发方 按作用域访问执行 agent(to=…) [task from SID:HEAD] 目标:向派发者回送最终结果 目标agent 这一轮归任务 execution_id:可查询、请求取消 取消会级联,子任务一起停 控制由 canonical 授权决定 归属检查: 对话记录可能包含其他执行的 ID;知道 ID 不代表获得控制权限。 结果按执行 session 关系读取;控制由 canonical 入口检查权限和状态版本。
承诺消息 send_message任务 agent() / agent(to=)
谁能发起已启用的发送方,向可解析且未归档的分支投递启用的调用方可提交;产生的 execution_id 受查询归属和执行控制权限约束
有没有execution_id直接投递返回 delivery_id=job_id;目标忙时先返回收件箱回执,尚未创建执行 Job,实际轮次仍须准入三种形式均有内部执行身份。后台新建和已有分支派发返回 execution_id;前台新建等待并返回最终结果
结果合同目标自动回送非空完成结果;额外显式调用 send_message 回复可选。完成路径仍待接入这一轮的终态形成结果、错误或取消;同样适用非空完成结果回送,仍待接入
能不能取消取消已准入执行不会撤回已保存的消息;该执行受 canonical 取消权限约束。收件箱回执不是执行 IDexecution 命令 execution.cancel 使用 execution_id 和 expected_version 请求取消;排队工作撤回,运行工作进入取消流程
取消会不会级联准入后,Job 按其执行父子关系处理取消级联:停一个任务,它派生的任务全停
能操作别人的任务吗知道投递或执行 ID 不获得控制权限;准入后的执行查询和控制仍受各自授权检查job_output 允许目标 execution session、caller session 或满足祖先关系的调用方读取;canonical 控制独立检查 owner/project/session/action 权限及预期状态版本
多方并发安不安全回执表示已投递或进入收件箱,不保证执行准入或成功完成;仍可能发生错误安全,且有记录:目标忙就排队,不会被打断
当前 archive_agent 允许具备该工具能力的本地 session 归档其他分支,没有 creator-only 检查,这是现有共享本地信任范围。目标生命周期操作必须复用其他分支操作的作用域授权;保留数据不构成免授权理由。

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

新建、send_message 和 agent(to=...) 每次向外投递增加消息计数。目标要求自动结果通知继承已完成 Job 的计数、不再次加一;helper 定义了该行为,但尚未接入 canonical 完成路径。只有新建分支增加派生深度;Job 接纳累计、排队容量和活动容量分别限制。

一条链的两个计数器 消息数:每次向外的 spawn、send_message 或 agent(to=...) 加一。目标自动结果通知不增加计数,完成路径仍待接入。 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 代数用完时 越界的那次创建被拒,并给出理由("自己完成"),别的工具全留着。 消息预算仍有余额时,可继续向已有分支派发和发送消息。 消息数用完时 当前:agent 和 job_output 使用消息预算工具门控。 目标:拒绝新增投递,保留结果读取与取消。

两个参数均支持设为 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.json的agent段。 将这三个限制设为 0 不会关闭资源接纳限制。max_jobs_per_session 统计已接纳 Job 累计数,max_queued_per_session 限制排队数,max_live_per_session 限制活动容量;见资源治理设计。前台新建、后台新建和已有分支派发均持久化接纳一个 Job。前台路径调用 run_agent_turn,其内部调用 runner.spawn_job(wait=True) 并等待该 Job;同步等待不豁免接纳或资源限制。
扇出预算不在上面那两个链上计数器里。派生把计数交给孩子、自己那份不动, 所以链上计数器数不到兄弟。扇出按(会话,轮次)单独计,只拒绝不摘工具: 工具清单在轮次开始时就冻结了,这个数在轮次里才花掉。
目标回送计数:现有 helper 继承已完成 Job 的消息计数,自动结果通知不额外增加计数; 派生深度恢复派发方的值,因为读取结果不创建分支。canonical 与借用 claim 完成路径目前未调用该 helper,因此仍须实现并验收回送行为。
有一条规则不受预算影响:自己发给自己一律拒。to=指向发起方当前这条分支是直接的环,立刻拒掉。

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

归档保留分支历史,并在目标分支的共享元数据里写入 archived。该状态在当前本地会话库中全局生效,不是每个调用方单独隐藏;所有发送方读取同一归档状态。

图中的手动归档已经实现。异步 archive_when_done 是目标完成行为,其 helper 尚未接入当前 canonical 或借用 claim 完成路径;同步工具的本地归档路径已经调用。

两种归档方式,一个标记,五种后果 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上就查这个标记,所以没有哪条调用能绕开。 当前:具备工具能力的本地 session 可归档其他分支。目标:复用作用域生命周期授权。 没有反归档。归档的意思是"这段对话结束了";还想接着用,就用 agent(start_from="SID:MSG_ID") fork一条新的出来,新名字新生命周期。

06典型场景走一遍

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

场景一 · 派一批任务出去

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

01 计划
todo_create(subject=…)
五件事写成五条。blocked_by标出谁等谁。
02 派任务
agent(prompt, description, run_in_background=true)
逐条派。后台形式立刻返回execution_id,不占我这一轮。
03 盯
list_jobs()
五个 execution ID 和五个状态。使用 job_output(job_id=...) 读取结果,通过 UI 或 execution 命令入口取消;没有注册 job_stop 模型工具。
04 收
目标:自动结果通知
目标是在调用方会话追加终态结果。当前 canonical 完成路径未调用 follow-up helper;接入并验收前,通过获授权的 job_output 读取结果。
05 更新计划
todo_update(todo_id, status="completed")
计划状态和实际执行一致。

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

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

01 查agent列表
list_agents(scope="all")
按会话分组。每条分支给名字、一个能直接用的to="SID:HEAD"、轮数和大致字数、还有末尾预览。
02a 发消息
send_message(to="名字", message=…)
提问、通知或共享结论,不创建新分支。直接投递准入 Job;目标忙时先返回收件箱回执。额外显式 send_message 回复可选;目标仍回送非空完成结果。
02b 派任务
agent(to="名字", prompt=…)
这件事确实由它做,而且我要结果。产生 execution_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参考命名与当前接口

参考实现对比记录设计背景,不定义当前 OpenProgram API。外部产品的工具名不是本框架的兼容别名。

OpenProgram 接口合同
agent创建分支并执行,或用 to 向已有分支派发。保存 AgentSpec 配置是另一项操作。
list_jobs列出当前会话的 Job、状态和资源视图。
job_output(job_id, block, timeout)读取或等待执行结果。block=true,timeout 单位为毫秒,默认 30000,上限 600000。
execution.cancelexecution 命令,不是模型工具。当前 Web UI 通过统一控制入口提交 execution ID、预期状态版本及 command ID。
todo_*维护计划,修改 todo 不触发执行。
list_agents、archive_agent列出和归档对话分支,不管理保存的 AgentSpec 注册表。
send_message、read_conversation消息投递继续已有分支,实际轮次进入 Job 准入;读取历史不触发执行。send_message 与 agent(to=...) 采用相同的非空完成结果回送目标;额外显式回复可选。

08Attach 卡片:所属会话与目标会话分别定位

源会话 S 的消息 A 发起调用,在 T 创建分支时,attach 节点保存于 S,predecessor=A;其 extra.attach.session_id=T 与 extra.attach.head_id=H 引用目标。流事件外层 data.session_id=S 决定接收更新的会话,card_id 在运行和终态更新之间保持不变。当前没有持久化 attach.caller_session_id 字段。

操作使用哪个会话定位字段
加载或更新卡片S,由所属消息库或流事件外层确定Attach 节点 ID / card_id
在调用位置显示卡片S持久化 predecessor,对话映射解析调用方;实时路径使用 tool_call_id 关联调用,旧数据按顺序匹配。
查看所引用的分支T = attach.session_idattach.head_id;后台分支仍运行时可为空
查询执行状态或取消执行权限独立判断,不由卡片位置决定兼容字段 job_id;取消使用 execution_id 和 status_version

当前对话映射将卡片关联到 S 的调用消息。AssistantBubble 区分同会话卡片与外部卡片,AttachCard 使用 T 导航。跨会话目标不是 S 中的本地 DAG 顺序节点:应显示指向 T 的引用并在 T 中解析 H,不能在 S 中查找 H,也不能因 attach.session_id 是 T 而将源卡片移动到 T。

当前限制:完整 AttachCard 的跨会话 Open 操作只打开会话 T,没有显式选中 H;execution strip 会提交 T 与 H 的 checkout。目标要求两个入口都打开引用的同一分支,目标不可用时显示明确状态并保留源卡片。这项导航一致性仍待实现,本次文档修改没有实现它。

09实现状态与源码依据

已核实当前工具清单、标识符兼容、同步与异步 Job 准入、消息投递与收件箱回执的区别、attach 字段和共享手动归档状态。完成 helper 定义了自动回送与异步自动归档,但检查的 canonical 与借用 claim 完成路径未调用它们。完成行为接入、作用域生命周期授权、历史读取目标级 owner/project ACL、预算耗尽后结果读取,以及跨会话导航一致性均待实现。