CreateWorkflow 工具

1. 定位

CreateWorkflow 做三件事:类型检查 一段对着 dynamic-workflow facade 写的 TypeScript 脚本 → 干净编译后请用户确认 (弹窗展示真正的脚本与因果图)→ 确认后经 DynamicWorkflowRunPort 后台启动 run,结果以完成通知回来。handler 头注释概括了两条路径:

  1. 端口在场(生产接线):clean compile + Allow → port.submit → 输出 {status:"backgrounded", backgroundTaskId: runId},结果经后台通知管线回到主 agent。
  2. 端口缺席(未接线的宿主、单测):保持接线前的占位行为------只回诊断与因果图,什么都不执行。刻意不降级成"假装启动了"------模型据此会去等一个永不到来的通知。

2. 三个来源与"恰好一个"

执行体有三个来源,恰好给一个 (contracts/tools/create-workflow.ts):

来源 字段 语义
内联脚本 script 一次性工作流;无论编译结果如何都会落盘成草稿文件,结果点名它
已保存定义 saved: { name, args?, scope? } 跑项目/全局档里的定义;实参按定义声明校验
磁盘文件 path(+ args) 内联提交的回程------上一次结果点名的草稿文件,改一行再交路径

XOR 约束刻意不写在 zod schema 上,契约注释给出了完整论证:

resolveInput 把保存的脚本(或 path 文件)解析出来之后,输入同时带着 script(要跑的字节)与 saved / path(它的来龙去脉)。一条 superRefine 会在两个地方炸掉这个合法形状------handler 自己的 parse,以及 hook 改写输入后 call-runner 的二次校验------而且只在非内联路径上炸 ,属于那种测不到就上线的错误。XOR 因此由 entry.validateInput 在模型入参上强制,那里正是它唯一为真的地方。

配套的错误文案是常量("模型是它唯一的读者,三条路径同一句话"):CREATE_WORKFLOW_SOURCE_ERROR("Provide exactly one workflow source...")与 CREATE_WORKFLOW_ARGS_WITHOUT_PATH_ERROR(args 只属于 path 来源)。

三个"回填事实"字段与模型面分开 :saved.path/saved.scope(解析后变成命中的那根)、saved.draft(归一化时写下的工作副本落点)、script_line_offset(正文行 → 文件行偏移,诊断行号能直接粘进一次 Edit)。它们不在投给模型的 JSON schema 里------"事实由工具算,模型若硬填,归一化会无条件覆盖"(与 AmendWorkflow.predecessor 同一姿态)。saved.draft 的存在还有一个安全含义:保存的定义永远不因为一次 run 被改动,模型要改的是这份拷贝 ;handler 注释补充:"一个 hook 若在归一化之后改写 saved,是刻意无效的,改不了将要跑的东西。"

3. 生命周期:唯一读盘点与四道门

#mermaid-svg-xVjeXEbMlzpTnbfx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xVjeXEbMlzpTnbfx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xVjeXEbMlzpTnbfx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xVjeXEbMlzpTnbfx .error-icon{fill:#552222;}#mermaid-svg-xVjeXEbMlzpTnbfx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xVjeXEbMlzpTnbfx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xVjeXEbMlzpTnbfx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xVjeXEbMlzpTnbfx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xVjeXEbMlzpTnbfx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xVjeXEbMlzpTnbfx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xVjeXEbMlzpTnbfx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xVjeXEbMlzpTnbfx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xVjeXEbMlzpTnbfx .marker.cross{stroke:#333333;}#mermaid-svg-xVjeXEbMlzpTnbfx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xVjeXEbMlzpTnbfx p{margin:0;}#mermaid-svg-xVjeXEbMlzpTnbfx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xVjeXEbMlzpTnbfx .cluster-label text{fill:#333;}#mermaid-svg-xVjeXEbMlzpTnbfx .cluster-label span{color:#333;}#mermaid-svg-xVjeXEbMlzpTnbfx .cluster-label span p{background-color:transparent;}#mermaid-svg-xVjeXEbMlzpTnbfx .label text,#mermaid-svg-xVjeXEbMlzpTnbfx span{fill:#333;color:#333;}#mermaid-svg-xVjeXEbMlzpTnbfx .node rect,#mermaid-svg-xVjeXEbMlzpTnbfx .node circle,#mermaid-svg-xVjeXEbMlzpTnbfx .node ellipse,#mermaid-svg-xVjeXEbMlzpTnbfx .node polygon,#mermaid-svg-xVjeXEbMlzpTnbfx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xVjeXEbMlzpTnbfx .rough-node .label text,#mermaid-svg-xVjeXEbMlzpTnbfx .node .label text,#mermaid-svg-xVjeXEbMlzpTnbfx .image-shape .label,#mermaid-svg-xVjeXEbMlzpTnbfx .icon-shape .label{text-anchor:middle;}#mermaid-svg-xVjeXEbMlzpTnbfx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xVjeXEbMlzpTnbfx .rough-node .label,#mermaid-svg-xVjeXEbMlzpTnbfx .node .label,#mermaid-svg-xVjeXEbMlzpTnbfx .image-shape .label,#mermaid-svg-xVjeXEbMlzpTnbfx .icon-shape .label{text-align:center;}#mermaid-svg-xVjeXEbMlzpTnbfx .node.clickable{cursor:pointer;}#mermaid-svg-xVjeXEbMlzpTnbfx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xVjeXEbMlzpTnbfx .arrowheadPath{fill:#333333;}#mermaid-svg-xVjeXEbMlzpTnbfx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xVjeXEbMlzpTnbfx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xVjeXEbMlzpTnbfx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xVjeXEbMlzpTnbfx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xVjeXEbMlzpTnbfx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xVjeXEbMlzpTnbfx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xVjeXEbMlzpTnbfx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xVjeXEbMlzpTnbfx .cluster text{fill:#333;}#mermaid-svg-xVjeXEbMlzpTnbfx .cluster span{color:#333;}#mermaid-svg-xVjeXEbMlzpTnbfx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xVjeXEbMlzpTnbfx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xVjeXEbMlzpTnbfx rect.text{fill:none;stroke-width:0;}#mermaid-svg-xVjeXEbMlzpTnbfx .icon-shape,#mermaid-svg-xVjeXEbMlzpTnbfx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xVjeXEbMlzpTnbfx .icon-shape p,#mermaid-svg-xVjeXEbMlzpTnbfx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xVjeXEbMlzpTnbfx .icon-shape .label rect,#mermaid-svg-xVjeXEbMlzpTnbfx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xVjeXEbMlzpTnbfx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xVjeXEbMlzpTnbfx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xVjeXEbMlzpTnbfx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 解析失败/来源非法
编译不过
干净
拒绝
Allow
模型调用 CreateWorkflow
resolveInput

全流程唯一一次读盘
saved/path → script + args 校验

max_concurrency 钳进 1, 天花板

subagent_model 解析成规范形
业务失败退回

(不到确认窗)
prepareApproval

analyzeScript
不弹窗,直接放行

handler 回诊断(不建 run)
gate: ask + 因果图 display

用户确认(alwaysAsk)
无效果,无 run
handler: port.submit

(失败向上冒泡成工具失败)
backgrounded

runId = backgroundTaskId

ToolEntry 上三个生命周期钩子各有一条"唯一性"论证(create-workflow.ts:321-334):

  • resolveInput 是全流程唯一一次读盘 :"此后 hook、权限规则、确认窗与 handler 看到的都是同一份字节。" 并在此完成两件必须在开窗前 做的事------并发天花板钳制("钳制必须发生在确认窗之前,否则用户批准的是一个不会生效的数")与 subagent_model 规范化("解不出来的调用会在批准之后才失败"------所以解不出来在开窗之前就退回)。
  • prepareApproval 对两条来源是同一段代码 :"走到这里输入已被 resolveInput 归一化,这里既不知道也不需要知道脚本是内联写的还是从磁盘读的。saved run 的确认窗因此与内联逐字节同形 "。两种失败(输入不合 schema、编译不过)直接放行给 handler 而不弹窗------"让用户去批准一段编不过的代码,只会用一个无效果的决策打断 agent 自己的改错重试回路。"
  • handler 的 inline 草稿无论如何落盘:"编不过的脚本走不到确认窗,handler 是它唯一必经的地方,不写就等于「唯一需要被编辑的那份脚本反而没有文件」。"

提交失败(编译产物损坏、journal 不可用)向上冒泡成工具调用失败:"绝不吞成一个带 backgroundTaskId 的成功输出,那会让后台追踪器去轮询一个不存在的 run。"

4. 确认窗与 alwaysAsk:持久确认永不减免

ts 复制代码
alwaysAsk: true,   // workflow 是一整块执行 + 模型调用,任何权限模式(含 yolo / plan)都要先问。
askOptions: { allowAlways: "session" },

这段注释是权限设计的范文(create-workflow.ts:351-362):

「每次调用都是不同的脚本」对保存的 workflow 不再成立:按名字调用时,同一个名字每次都是"同一个"工作流。但「同一个」工作流也不足以支撑免确认------每次仍要确认,理由有两条更硬的 :运行一次要花钱且有真实副作用;而保存的文件在批准之后随时可能被改动(手改、git pull、别人提交),所以"上次批准过这个名字"根本不能推出"这次要跑的还是那段代码"。持久确认永不减免。

放开的是会话作用域 :用户看过并批准了本会话第一个脚本之后,可以选「Always allow in this session」。授权只活在 PermissionService 实例的内存里,重启 / 冷恢复 / /new 都从零开始。

另外 world.run 的命令集必须在确认时可见------描述里的 authoring rule 因此要求命令首参是编译期字符串字面量("the script's command set is shown to the user at confirmation",运行时值只能插进 args 数组)。

5. 失败路径的文案纪律:说谎的代价

诊断输出的 NOTE 按路径 分成三句(create-workflow.ts:52-59 注释):

「没执行」的 NOTE 按路径分成两句,各自只说该路径为真的事。合并成一句常量的旧写法(PLACEHOLDER_HINT)在引擎接线后两个断言都成了谎言:它说执行模型仍在开发中、说本工具只做类型检查,而干净脚本 + 端口在场早已真启动一个后台 run。文案是模型唯一的读者,说谎的代价是它据此放弃提交或重复提交。

三分支(diagnosticsNote):saved 来源编不过 → "the saved file needs fixing (edit it, or save a corrected version)"------"保存的定义编不过是它的 问题,不是这次调用的输入问题;不点名文件的话,模型会以为是自己刚才写错了什么,然后原样重试";有文件的按文件行 报(行号能直接粘进一次 Edit,"输出里的 diagnostics 数组保持正文行不变------转录面画的是正文");没文件的保留老话。

并发上界的一句话同理 (describeWorkflowConcurrencyLimit):"只在设了上界时出现------跑在天花板上的 run 没有可说的,多一句「至多 N 个」只会让模型以为自己设过什么";且正好等于天花板时点明 "(this machine's maximum)"------"不说破的话它会把「至多 32」当成已生效,并在用户追问时复述一个假数。"

6. 启动后的引导:劝阻轮询

启动成功的 response(create-workflow.ts:180-185 注释)明确劝阻默认轮询:

模型拿到 backgrounded 输出后立刻用 TaskOutput 阻塞等待,把异步 run 变成了同步等待------文案必须显式劝阻默认轮询(用户显式要求等待时 TaskOutput 仍然可用,这里只改默认引导,不改工具语义)。

文案原文:"It is still running --- you will be notified with the final output when it completes. Do not wait for it or poll it with TaskOutput; continue with other work unless the user asked you to wait." 随后拼接并发上界一句话、子代理模型说明、脚本文件位置。

描述里还有一条修订路由 (create-workflow-description.ts:32):改动已有 run 是 AmendWorkflow 的工作------"This tool starts a NEW workflow. To change a run that already exists... call AmendWorkflow with the run's ID and the revised script. It stops a running predecessor for you, re-uses every finished result you left untouched at no token cost..."。契约里也有对应的硬约束:.strict() 让旧写法 resume_from 成为可见的 schema 错误 ,"而不是被静默忽略后变成一次全价重跑"(create-workflow.ts:146-147)。

7. 工具描述的构造:从 facade 现挂

描述在模块加载期从 FACADE_DTS 合成(create-workflow-description.ts:1-2)------"the model always sees the current API surface":facade 声明(ask<T>、world.run、artifact.*、phase、agent)整段嵌进描述,API 演进不需要手同步文档。结构顺序有讲究:

  • SOURCES 放最前(注释:"「先看看有没有现成的」是模型在写第一行脚本之前就该做的判断------放在后面它已经开始写了"),紧跟两段"回路"引导("模型默认会把改过的脚本整段重贴,而那正是这个特性要消灭的三笔代价------两万 token 重流一遍、被 compaction 丢掉的脚本、以及为了改一行而重新生成整段")。
  • WHEN_TO_USE 是一道硬门 (create-workflow-description.ts:36-44 注释):"工作流只能由 /workflow 或明确请求发起,模型不得自行决定开一条。不能把「多子代理 + 确定性控制流」也列为适用场景------那等于给了模型一条自选入口:一次编排需求就可能在用户没开口时变成一条要确认、要跑很久的 run。" 用户点名 workflow 时是强制:"Do not substitute the Agent/Task subagent tools, do not do the work inline yourself, and do not judge the task too small for a workflow --- the user chose the tool, and that choice is theirs. Size only decides how many subagents the script gets, never whether it is written."
  • RULES 的几条关键纪律 :无 declare/export/import/Node API(脚本编译在函数体内);world.run 非零退出是值不是异常(gate 分支用 exitCode);门从仓库选不从习惯选 ("A check that exists and was skipped is not notCovered; it is unverified work");验证与代价成比例;Promise.all 只在下一步需要全部结果处 join("so confirmation starts as each result lands instead of after the slowest item");固定逻辑先用 EvalWorkflowSnippet 测("a passing snippet pastes into the workflow verbatim");provider 错误永不进脚本 ("Rate limits... are retried by the runtime without limit... a deterministic one stops the whole run as stopped... So do not write retry loops or try/catch for provider errors");return 是报告形状、artifact 是用户可见交付物,"Never put the same content in both."
  • PHASES 强制 (create-workflow-description.ts:78-84):"required in every script, not optional"------"The confirmation graph the user approves draws one node per phase... That graph is the user's entire experience of a workflow --- supplying phases is part of writing one." 阶段名用用户语言("Orchestration vocabulary the user never chose --- 'fan-out', 'gate', 'aggregate' --- is not a phase name; the user approves stages by what they do");每个阶段必须含至少一个 ask 或 world.run("plain script logic between two asks runs in a flash and shows no progress, so it is not a stage")。
  • NAMES :子代理卡片的文字就是 agent("...") 的名字------"A variable-style token ('w1', 'planner_2') says nothing to the user";名字同时是身份------"a revised re-run matches its cached results by it. Keep names stable across revisions."

8. 因果图契约:三层有界投影

causalityGraph 是确认窗渲染的图的有界投影 (契约 create-workflow.ts:182-213 注释):"诊断可能很多,必须在协议边界限长,避免类型检查结果把 continuous/replayable 消息扩成无界载荷。" 三层结构:

层 内容 读者
steps 站点(一次 ask / world.read,含 lane 展开),运行状态的键 不再画(没有读者)
participants / handoffs 每阶段每车道的参与者卡与交接边("归约后 happens-before 按卡取商") 折叠/展开视图
phases / phaseEdges / exits 控制流事实(阶段商、指向 sink 的边) 阶段时间轴

关键不变式:阶段词汇表与 phaseEdges/exits/Step.phase 全有或全无 ------"零标记脚本四者全缺席,UI 的视图切换条件就是「词汇表在场与否」。超界时也是整体缺席(+ truncated)------裁一半的阶段图会说谎。 " 边只表示一件事:runs after("back 标记循环回边------画法与其他边完全相同、不标注,只有布局排秩与帧头 cycle 计数读它")。phase.alongside 是节点事实不是边 ("控制并没有从那些阶段转移过来......归约会把它当成 runs after,砍掉真正的边")。声明阶段表(createWorkflowPhaseNames/createWorkflowPhaseAlongside)随提交进 run-launched,"两条提交路径(CreateWorkflow 工具、中枢直接启动)共用本函数,于是同一份脚本在两条路上画出同一条轨道"。

9. 相关文件索引

文件 职责
packages/core/src/tool/handlers/create-workflow.ts handler(两路径)、三钩子、alwaysAsk 论证、并发/NOTE 文案
.../handlers/create-workflow-description.ts 从 FACADE_DTS 合成的工具描述(SOURCES/WHEN_TO_USE/RULES/PHASES/NAMES)
.../handlers/create-workflow-source.ts resolveInput(唯一读盘、钳制、模型解析)与来源校验
.../handlers/workflow-drafts.ts / workflow-script-notes.ts / workflow-script-analysis.ts / workflow-analysis-display.ts 草稿落盘、诊断行号、静态分析、display 投影
packages/contracts/src/tools/create-workflow.ts 三来源契约、XOR 论证、因果图三层 schema 与有界常量
packages/core/src/tool/handlers/saved-workflows/store.ts saved 来源的解析(见 ListSavedWorkflows 篇)
apps/zcode-cli/packages/bootstrap/src/app/workflow-driver.ts 引擎桥:四条时序(见 submit_result / escalate 篇)
apps/zcode-cli/packages/bootstrap/src/app/dynamic-workflow-run-service.ts run 生命周期:并发钳制、resume/amend
packages/core/src/tool/handlers/amend-workflow.ts 修订入口(改动已有 run 的正路)
packages/core/src/tool/handlers/eval-workflow-snippet.ts 提交前的片段试验台
相关推荐
武子康1 小时前
ESP32 唤醒词标称 98%,为什么还要自己做验收?
人工智能·后端·agent
墨心@2 小时前
AmendWorkflow 工具
人工智能·agent·harness
老唐7773 小时前
Agent 开发入门教程:从零搭建一个 Python AI Agent
人工智能·python·深度学习·langchain·大模型·agent·智能体
九卷12 小时前
企业级Agent系统整体技术架构介绍
ai·架构·大模型·llm·agent·智能体
Csvn14 小时前
提示词即代码:有版本、能评审、可回滚(R01)
人工智能·aigc·agent
浮生望14 小时前
Agentic RAG(下):ES + Milvus 多路召回,再用 Rerank 去噪
llm·agent
杨杨杨大侠15 小时前
从一次请求讲透 A2A:Agent 怎么选、怎么协作、记忆放在哪里
人工智能·agent
后端小肥肠15 小时前
做公众号漫画还在纯手搓?这套工具从选题到复刻任意热门漫画一键搞定
人工智能·aigc·agent