opencode 怎么把活派给子 agent:task 工具与探索型子代理

2026-08-04

本文基于 opencode 仓库 commit 7fe9938(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/anomalyco/opencode 最新代码与文档为准。

opencode 的多 agent 能力全部收口在一个叫 task 的工具上:主 agent 想让别人干活,只有这一个口子;派出去的活跑在一个独立子会话里,父会话拿到的只是最后一段文本。 你如果只记一句话,记这句就够——它决定了上下文怎么隔离、权限怎么继承、以及什么时候派活是净亏损。

opencode 是一个跑在终端里的开源编码 Agent 项目,采用 MIT 许可证(LICENSE 里写的是 Copyright 2025 opencode)。这类工具会在你的机器上执行 shell 命令、直接改你仓库里的文件、把读到的代码内容发给模型服务商,所以它的权限设计不是锦上添花,是必须先看懂的部分。

站内已有几篇讲多 agent 的文章:Claude Code 的 subagent 机制讲的是另一套产品的子代理约定,Hermes 的派单与并行讲常驻服务形态下怎么排队,Agent 并发编排讲的是脱离具体实现的通用编排取舍;本篇只钉在 opencode 这一个仓库上,逐行对着它的源码讲 task 工具的参数、子会话权限推导和探索型子代理的提示词。

一、task 工具解决的是上下文问题,以及一次派活实际发生了什么

先说它为什么存在。主会话跑久了,历史消息里塞满了 grep 命中的几百行、读文件读进来的整屏内容,这些东西对当前这一步几乎没用,但每一轮都要重新发一遍。task 的价值在于:把一段”需要翻很多文件但结论只有三行”的工作扔到别的会话里跑,跑完只把那三行拿回来。

packages/opencode/src/tool/task.txt 是这个工具给模型看的描述文本,开头一句就是 Launch a new agent to handle complex, multistep tasks autonomously。紧接着写的却是四条不要用的场景:知道具体路径就直接用 Read 或 Glob;要找 class Foo 这种明确定义就直接用 Grep;范围只在两三个文件里就直接读;没有合适的 agent 就别派、直接用工具。一个工具的说明书里反对使用的篇幅和推荐使用差不多长,这个取向本身就是信息。

描述里还有几条值得你留意的约定:派出去的活,结果对用户是不可见的(The result returned by the agent is not visible to the user),主 agent 必须自己转述一遍;每次调用默认是全新上下文;以及”明确告诉子 agent 你要它写代码还是只做调研,因为它不知道用户的意图”。最后这条是实践里踩得最多的——子 agent 拿到的只有你给的那段 prompt,你没写的它一概不知道。

一次派活,源码里实际发生了什么

packages/opencode/src/tool/task.ts 是主线。参数定义在文件上半部分,一共五个基础字段:description(三到五个词的短描述)、prompt(要它干的活)、subagent_type(用哪个 agent)、task_id(可选,传上一次的 id 就接着同一个子会话往下聊,而不是开新的)、command(可选,触发这次任务的命令)。另外还有一个 background

执行过程按顺序做了这么几件事:

第一步查深度。它顺着当前会话的 parentID 一路往上数,数出来的层数如果达到配置里的 subagent_depth(默认取 1),直接失败并提示你调大这个值。文档 packages/web/src/content/docs/config.mdx 里写得更直白:默认 1 表示主 agent 可以派子 agent、但子 agent 不能再往下派;设成 2 多放一层;设成 0 则完全禁止派活。

第二步问权限。除非上下文里带了 bypassAgentCheck,否则它会以 task 为权限名、以 subagent_type 为匹配模式发起一次询问。这个旁路标志在 packages/opencode/src/session/prompt.ts 里被设置——判断依据是用户最后那条消息里是否含有 agent 类型的片段,也就是你自己 @ 了某个子代理,这时不再多问一次。

第三步建子会话。标题是你给的 description 再拼上 (@名字 subagent),父会话 id 写进 parentID。真正讲究的是权限:packages/opencode/src/agent/subagent-permissions.ts 里的 deriveSubagentSessionPermission 只从父会话继承两类规则——external_directory 相关的,以及所有 action 为 deny 的。文件顶部的注释解释了为什么:父 agent 的限制只约束父 agent 自己,子 agent 的能力由它自己的权限集决定;但”禁止”要往下传。在此之上还会补两条默认拒绝:子 agent 没有明确开 todowrite 就禁掉,没有明确开 task 就禁掉——后者意味着默认情况下子 agent 不能再派活,和深度限制是双保险。配置里 experimental.primary_tools 列出的工具,也会在这一步对子会话统统置为 deny。

第四步定模型。子 agent 自己配了模型就用自己的,没配就沿用父消息用的那个 provider 和 model。各家服务商的计费与限流规则不同且会调整,以官方最新说明为准,这里只讲机制:派活默认不会帮你换到更便宜的模型,模型分层得你自己在 agent 配置里写。

最后把结果包成一段结构化文本还给父会话,形如 <task id="…" state="completed"> 里裹一个 <task_result>;出错时标签换成 <task_error>。父 agent 看到的就是这段。

二、探索型子代理为什么是另一份提示词

packages/opencode/src/agent/agent.ts 里内置了几个 agent。buildplan 是主 agent,generalexplore 是子 agent,另外还有三个隐藏的系统 agent 用于压缩上下文、生成标题和摘要。

generalexplore 的差别,是本篇最想让你看清的一处设计。

general 在源码里没有 prompt 字段。而 packages/opencode/src/session/llm/request.ts 里组装系统提示词的那一行是这样写的:

...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),

也就是说,agent 自带提示词就整份替换掉按模型挑选的默认提示词,没有才回落到默认。general 走的是后者——它拿的就是按模型挑出来的那套完整默认提示词;而它自己的权限配置在默认集之上只多写了一条:todowrite 置为拒绝。你可以把它理解成”一个不写待办清单的自己”。

explore 则挂了 packages/opencode/src/agent/prompt/explore.txt。这份文件很短,第一句是 You are a file search specialist,然后列了三条长处(用 glob 找文件、用正则搜内容、读文件),六条守则(宽泛匹配用 Glob、搜内容用 Grep、路径明确用 Read、文件操作用 Bash、按调用方指定的彻底程度调整搜索策略、返回绝对路径),外加两条硬约束:不要用 emoji,以及不要创建任何文件、不要跑任何会改动用户系统状态的 bash 命令。

配套的权限是先把 * 全部 deny,再逐个放行 grepgloblistbashwebfetchwebsearchread,外部目录访问则套用一份只读策略(默认 ask,白名单目录 allow)。

这里有个你必须自己心里有数的落差:bash 在权限上是 allow 的,“不许改系统状态”这条只写在提示词里。 提示词是软约束,模型偶尔会越界。如果你打算让探索型子代理在生产仓库上跑,别只依赖这份文本,该在配置里收紧的还是要收紧,具体写法见 Agent 最小权限设计里的通用思路。

还有一处容易被忽略:explore 的 description 里明确要求调用方指定彻底程度,给了 quick、medium、very thorough 三档措辞。这个参数不在 schema 里,它就是让你写进 prompt 的自然语言。你不说,它自己挑。

组成部分它负责什么对应仓库位置你什么时候会碰到它
task 工具实现建子会话、查深度、问权限、等结果、包装输出packages/opencode/src/tool/task.ts每次主 agent 派活
工具描述文本告诉模型什么时候该派、什么时候别派packages/opencode/src/tool/task.txt觉得模型该派活却不派时
子会话权限推导从父会话继承 deny 与外部目录规则,补默认拒绝packages/opencode/src/agent/subagent-permissions.ts子 agent 报权限不足时
agent 注册与内置定义定义 build/plan/general/explore 等及其权限packages/opencode/src/agent/agent.ts想加自定义子代理时
探索型提示词把子代理限定成只读的检索角色packages/opencode/src/agent/prompt/explore.txt嫌探索结果太散或越权时
agent 清单注入把可用 agent 名字与描述拼进 task 工具描述packages/opencode/src/tool/registry.ts模型点名了一个不存在的 agent 时

最后一行值得展开。registry.ts 里有个 describeTask,它把所有非主 agent 的名字和描述拼成一段清单,追加在 task 工具描述后面;而且会先按权限过滤——task 权限被判为 deny 的 agent 根本不会出现在清单里。文档 packages/web/src/content/docs/agents.mdx 对此的说法是:设成 deny 后子代理会被整个从 task 工具描述里移除,模型不会尝试调它。这是一种比”调了再拒绝”更省 token 的做法。

三、后台开关、任务续接与自定义子代理

background 参数默认关闭,而且关闭时不只是运行时报错——task.ts 末尾在没开实验开关时,给模型的 JSON schema 用的是不含 background 的那份基础参数结构。模型压根看不到这个参数。开关名字叫 OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS,在 packages/opencode/src/effect/runtime-flags.ts 里注册,同时也受总开关 OPENCODE_EXPERIMENTAL 影响。真开了以后,工具描述会多拼一段文字,讲的是另一层意思:前台才是默认,后台只留给”能一边跑一边让主 agent 干别的”的独立工作,跑完会自动通知。至于”别 sleep、别轮询、别问它进度、别去动它正在动的文件”这几句,位置在 background 参数自己的说明里,以及后台任务启动(和追加上下文)之后回给模型的那段输出文本里。也就是说,这套约束不是提前教的,是等模型真派了后台活、拿到”已启动”回执的那一刻,当面念给它听的——顺手把”去做不重叠的活,或者简单跟用户说一句你启动了什么就结束这一轮”也一并交代了。

完成后的通知走的是把结果作为一条合成文本消息注入回父会话。如果对同一个子会话再发一次 task,走的是”追加上下文给正在跑的任务”这条路,返回的提示语是”已更新”而不是”已启动”。

另一个开关是 task_id。默认每次派活都是全新上下文,传了 task_id 就接着上次那个子会话往下走,带着它之前的消息和工具输出。这在”第一轮让它摸清楚模块结构、第二轮让它基于同样的理解去改”这类场景里能省下重新摸索的成本,代价是子会话的上下文也会跟着涨——它不再是一次性的廉价探针了。上下文预算怎么分配可以参考 Agent 上下文预算

自定义子代理有两种写法:在配置里写 agent 段,或者在 ~/.config/opencode/agents/ 与项目里的 .opencode/agents/ 放 markdown 文件,frontmatter 写 descriptionmodemodeltemperaturepermission,正文就是系统提示词。注意——按前面 request.ts 那行逻辑,你在这里写的正文会替换默认提示词,不是追加。很多人自定义子代理后觉得它”变笨了”,原因往往就在这儿:默认提示词里那些关于工具用法、输出规范的内容,被你几十个字的角色描述整份顶掉了。

四、什么时候派活反而更慢

派活不是免费的。一次 task 至少要付出这些成本:新会话的系统提示词要重新发一遍;工具定义要重新发一遍;你交代任务的那段 prompt 里,凡是主会话里已经有的背景信息都得重写一遍;子 agent 摸索的过程你看不见,它绕远路你也不知道;结果回来还是一段文本,主 agent 得再花一轮把它消化掉。

几类明确的负收益场景:

目标文件已经定位到了。 这正是 task.txt 反对的第一类情况。你知道路径就直接读,派个 agent 去读同一个文件,多的全是开销。

任务描述比任务本身还长。 如果你要写三百字才能把上下文交代清楚,那这三百字本身已经是主会话里现成的信息,派出去等于把它重新序列化一遍。

需要来回确认的活。 子会话是单程的:一个 prompt 进去,一段文本出来。中途它遇到歧义只能自己拍板,拍错了你到最后才知道。这类活留在主会话里,你还能中途叫停。

结果需要保真。 回来的是子 agent 自己总结的文本,不是原始工具输出。一旦你后续要基于具体行号、具体代码片段做修改,二手总结就不够用了,往往还得自己再读一遍。

反过来,真正划算的是”输入短、过程长、输出短”这一类:搜遍全仓找某个约定的所有出现位置、把一个陌生模块的调用链摸清楚、在几十个候选文件里筛出真正相关的三个。explore 被设计成只读、被塞进一份专门的检索提示词,针对的就是这一类。

五、边界与代价:这个设计明确不管的事

它不做任务编排。 task 就是一个工具调用,没有 DAG、没有依赖声明、没有重试策略。并发靠的是模型在一条消息里发多个工具调用——task.txt 里那句”尽可能并发启动多个 agent”就是在教模型这么干。真出现”A 的产物是 B 的输入”这种依赖,得靠主 agent 自己按顺序调,中间没有任何机制帮你保证顺序。

它不做产物落盘。 子会话与父会话之间只有那一段文本。子 agent 写进文件系统的东西当然还在磁盘上,但工具协议层面不存在”结构化产物”这回事,父 agent 只能从文本里读。

它不隔离文件系统。 子会话继承的是权限规则,不是沙箱。子 agent 和主 agent 操作的是同一个工作目录。两个并发子 agent 同时改同一个文件,谁后写谁赢,没有任何冲突检测。这也是后台模式的提示词里反复念叨”避开它正在动的文件”的原因——那是一句请求,不是一道锁。

它不替你控制外泄面。 子 agent 读到的代码同样会发给模型服务商。内置的默认权限对 .env 类文件是”询问”而不是”允许”,外部目录默认也是询问;但你一旦在配置里图省事把这些放宽,子 agent 会一并继承这份宽松。密钥和私有代码的外泄面不会因为”它只是个子 agent”而变小。

它不保证子 agent 的判断力。 task.txt 里那句”子 agent 的输出通常应当被信任”是给模型的行为指引,不是质量承诺。子 agent 误判了,主 agent 默认不会去复核。

六、上手与避坑清单

自定义子代理后感觉能力下降。 会踩是因为 agent 自带提示词会整份替换默认提示词,而不是叠加。避法:写自定义提示词时,把工具使用规范、输出格式要求这些原本由默认提示词承担的内容自己补齐,或者先只改 permissionmodel、暂时不写 prompt,确认行为符合预期再逐步加。

子 agent 报”不能派活”。 会踩是因为默认权限推导会给子会话补一条 task 的拒绝规则,同时 subagent_depth 默认也是 1。避法:这两处得一起改——既要在子 agent 自己的权限里显式允许 task,也要把 subagent_depth 调到 2 以上。只改一个不生效。

模型点名了一个不存在的 agent。 会踩是因为 agent 清单是动态拼进工具描述的,被 task 权限 deny 的不会出现,而模型可能凭印象叫别的名字。避法:出现”不是有效 agent 类型”的报错时,先去看当前主 agent 的 task 权限规则——规则是按顺序求值、最后匹配的那条生效,很容易被前面一条 "*": "deny" 之后忘了再放行。

探索型子代理动了不该动的东西。 会踩是因为它的 bash 权限是放开的,“不改系统状态”只写在提示词里。避法:在敏感仓库上跑之前,自己给它加一层权限约束,别把提示词当围栏。

用户看不到子 agent 干了什么。 会踩是因为子 agent 的返回对用户不可见,得由主 agent 转述。避法:在 prompt 里明确写出”你最后一条消息必须包含哪些内容”,把你要的结构提前指定死,否则拿回来的可能是一段泛泛的总结。

后台任务和你自己撞车。 会踩是因为后台模式只用提示词请求主 agent 避开重叠文件,没有实际的锁。避法:派后台任务时,在 prompt 里把它的作用域写成明确的目录或文件清单,你自己也守住这条边界。

收尾

把这套机制压成一句判断:opencode 的子 agent 是上下文隔离手段,不是并行加速器。它省的是主会话的上下文预算,付的是重建上下文的固定成本;只有当”过程很长、输入输出都很短”时这笔账才划算。

想继续往下看的话,按这个顺序读三个文件收益最高:packages/opencode/src/tool/task.txt 看它教模型什么时候别派活,packages/opencode/src/agent/agent.ts 看内置 agent 的权限是怎么一层层叠出来的,packages/opencode/src/agent/subagent-permissions.ts 只有二十几行,但把”父的禁止往下传、父的允许不往下传”这条规则讲得最清楚——这条规则连同它的理由,直接写在文件顶部的注释里,不用你猜。三个文件的体量也友好:task.txt 不到二十行,subagent-permissions.ts 不到三十行,最长的 agent.ts 四百多行且大半是内置 agent 的字面定义,一眼能扫完。读源码这件事在这个项目上成本很低,比任何二手讲解都准。

本篇属于一个把开源AI 编程 Agent 项目 opencode逐层拆开讲的系列,整体地图见 opencode 是什么:终端里的开源 AI 编程 Agent 全景地图;沿着这条线往下,还可以看 开源项目 opencode 的 shell 工具:命令怎么解析、哪些会被权限层拦下opencode 的 plan 模式在拦什么:模式切换本质是改工具可用面

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。