跳转到内容

工作流

Beta

子代理是让模型临时按需扇出,而 workflow 工具是针对一支子代理队伍 运行一段确定性的编排脚本——扇出/汇总的结构写死在脚本里,不是每次调用临时决定的。

工作流脚本是 Ruby(跑在内嵌的 mruby 解释器上,编译成 WASM——不需要系统装 Ruby)。没有 JavaScript 这个选项。

调用 返回值 作用
agent(prompt, opts = {}) String 跑一个子代理直到完成。optsmodel:tools:(限制只能用这些工具)、read_only: trueschema:(一段 JSON Schema 字符串——回复会是匹配它的 JSON 字符串)
skill(name, params = {}, opts = {}) Hash / Array / 标量 运行一个具名 skill 或浏览器录制。同名的录制和 SKILL.md 冲突时用 "browser:""md:" 前缀区分。和 agent 不同,带 schema 的结果会直接解析好返回
parallel(items) { |it| ... } Array 对每个 item 并发跑这段代码块;结果保持输入顺序
pipeline(items, *stages) Array 每个 item 独立走完所有阶段——阶段之间没有屏障,item A 可能已经领先 item B 好几个阶段
log(msg) 在这次运行的日志里打一行进度
phase(title) 进度流里一个纯展示性的命名标记(不影响调度)
args Hash / Array / 标量 / nil 这次运行启动时传入的 args(见下面的具名工作流
budget_remaining Integer 这次运行还剩多少输出 token 预算
JSON.parse / JSON.generate 用来处理 schema: 结果和 args 的序列化/反序列化
# @description 按三个维度并行 review 一次 diff
findings = parallel(["correctness", "security", "perf"]) do |dimension|
agent("Review the current diff for #{dimension} issues", read_only: true)
end
findings.each { |f| log(f) }

并发在底层是协作式调度(mruby Fiber 加一个 Go 侧的事件循环),但真正干活是在 goroutine 上—— parallel/pipeline 会真的让子代理调用重叠执行,但每次运行里同时在飞的 agent/skill 调用 最多 8 个,不管你给 parallel 传了多少个 item。

解释器是 IO-free 的:没有 FileDirTimeProcess,也没有 shell 反引号——碰到其中任何 一个都会在脚本产出结果之前直接抛错。任何要接触外部世界的事情(读写文件、跑 git/gh 命令、取当前 日期)都通过 agent(...) 调用去做,它会委派给一个带真实工具的子代理;要落地一份报告或状态文件,就 让 agent 去写。

纯进程内的 Ruby 是可用的:Array/Hash/String/IntegerJSON.parse/JSON.generate,以及 Regexp——字面量 /.../ =~,还有 String#match/scan/gsub/sub/split。正则引擎是 RE2(线性时间,所以模型写的模式不会把整个运行卡死),因此和 Ruby 默认引擎有几处差异:

  • 模式内不支持反向引用和 lookaround(用到不支持的构造会抛错,而不是静默匹配错)。但 gsub/sub替换串里的反向引用——\1\&\k<name>——是可以用的。
  • 命名分组用 (?P<name>)
  • 要让 . 跨行,用 /m 后缀,不要用内联 (?m)(内联 (?m) 在这里是 multiline,不是 dot-all); /x 扩展模式不可用。

要从子代理里取结构化数据,优先用 agent(prompt, schema: ...) + JSON.parse,别用正则去抠它的 散文回复——schema 让回复天生就是合法 JSON,比在自由文本上做模式匹配既简单又可靠得多。

在交互式入口(TUI、Web、IM)里,每一次 workflow(...) 调用都是异步的——它立刻在后台启动, 返回一个运行 id(wf_1wf_2……)而不等脚本跑完;运行结束时系统会自动把结果通知给代理。

headless one-shotocto "prompt")里则改为阻塞执行、直接返回最终结果:进程在回合结束时 就会退出,后台运行永远来不及交付结果。运行期间进度行会实时输出到 stderr。

工具 作用
workflow_status 不带 id:列出当前会话里的每个运行及其状态/耗时/最近活跃时间。带 id:完整结果或错误,外加最多 500 行捕获到的日志
workflow_kill 取消一个运行;会传导给所有正在执行的子代理

同时最多能有 4 个后台工作流运行(CLI 进程内,或 Web/IM 按会话计);第 5 次 workflow(...) 调用会直接报错,而不是排队等待。

agent(...)isolation: "worktree",这次调用就会在一个从当前 HEAD 新建的 git worktree add -b octo-wf/<label>-<随机串> 里跑——一个隔离的 checkout,让并发分支碰文件时 不会互相冲突。这是按调用手动开启的,parallel/pipeline 本身不会自动套上。

那个 agent 结束时:

  • 没有改动 → worktree 和它的分支会被自动删掉。
  • 有改动 → 改动会被提交到那个分支上,worktree 保留在磁盘上,分支名、路径和一份 diffstat 会追加进这个 agent 的回复文本里。

没有任何东西会自动帮你把分支合并回去——review 和合并是你自己(或者你另外写的一次 agent() 调用)的事。

先保存一次脚本,之后按名字跑:

workflow_save(name: "my-check", script: "...") # 写入 .octo/workflows/my-check.rb
workflow(name: "my-check", args: { target: "src/" })
  • name 必须匹配 ^[a-z0-9][a-z0-9-]*$scope: "project"(默认)写入 .octo/workflows/—— 这需要在一个 git 仓库里,否则保存失败;scope: "user" 则写入 ~/.octo/workflows/
  • 注册表按内置预置 < 用户工作流 < 项目工作流的顺序合并同名条目,每次调用都重新扫描——不缓存, 手改保存过的脚本后也不需要重启。项目级工作流按当前 turn 的工作目录解析(即你运行 octo 时所在的目录、定时任务的 directory, 或者 web/IM 会话在被改到偏离服务器默认目录后的工作目录),不一定是服务器进程的启动目录。正在运行的工作流脚本自身的 agent()/skill() 调用也会一直按同一个目录解析,即便是在后台模式下。
  • 一个工作流的描述来自脚本开头 # @description ... 这一行注释。
  • args(任意 JSON 值)会一路传给脚本里的 args 调用;不传的话 args 返回 nil

二进制里内置了三个预置工作流,就算你一个都没保存过也始终可用:

名字 作用
parallel-understand 并行梳理一个代码库——每个子系统配一个 reader,最后合成一张架构图
batch-migrate 在很多文件上应用同一个机械式改动,每个文件各自跑在独立的 git worktree 里,并发编辑不会冲突
daily-triage 跑一次每日巡检循环——发现未处理的 issue 和 CI 失败,在隔离的 worktree 里起草安全修复,用第二个 agent 验证,最后写出状态报告

每完成一次 agent/skill 调用,都会实时追加写入 ~/.octo/workflow-journals/<run-id>.jsonl 这个 journal(运行记录)文件,所以中途崩溃最多丢掉 正在进行的那一次调用。结果或错误文本里会带上这个 journal id——格式类似 wf-20260703-143000-a1b2c3d4

workflow(script: "...", resume_from: "wf-20260703-143000-a1b2c3d4") 会按位置从 journal 里回放已完成的调用,直接跳到上次运行停下的地方——但前提是脚本和 args 的哈希必须和原来那次 运行完全一致;改了任何一个都会被当成重新开始,而不是续跑(并且会报错提示你想真的重新开始就 不要传 resume_from)。

如果你在同一轮里手动调用了两个及以上不同的 skill 或浏览器录制,octo 会给出一个提醒(模型可见、 UI 上不显示),建议你把这个串起来存成一个工作流——指向 workflow-creator 这个 skill,或者直接 用 workflow_save。如果这一轮里已经调用过 workflowworkflow_save,就不会再提醒了。

下一步:浏览器录制可以通过 skill("browser:<name>") 直接接进工作流脚本——见 浏览器自动化