AmendWorkflow 工具

1. 定位:修订一个 run,而不是重造一个工作流

AmendWorkflow 的能力声明一句话说完:

Revise an existing dynamic-workflow run with a new script or revised settings: stop it if it is still running, import its finished work, and start the revision in the background.

工具描述里的核心承诺是省 token:

Starts a NEW run that supersedes the old one and imports its finished work as a cache, so only what you changed is paid for again.

与 CreateWorkflow 的差别刻意做到最小(amend-workflow.ts 头注释):只有两处 ------resolveInput 解析的是前驱 run 而不是保存的文件(省略的字段从前驱沿用),handler 调 port.amend(service 负责停在飞前驱、等结算、导入、启动)。编译、诊断路径、确认窗 display、backgrounded 输出契约与 CreateWorkflow 逐字共用。

为什么是独立工具而不是 CreateWorkflow 的参数 (contracts 头注释):修订是另一种动作、另一张卡------模型不是在启动什么,而是在改一个用户正看着的东西,而且前驱可能仍在跑。两者共用同一个输出形状,所以 executor 的后台追踪与 display 载荷只有一份实现。

2. 适用场景与家族分工

工具描述给的四条判据:

场景 动作
run errored(脚本本身失败) 修脚本后 amend------已成功的步骤全部从缓存导入,不为同一件事付两次钱。绝不用 CreateWorkflow 从头重写
run completed 但要加一个阶段/精化 amend------新增或改动的步骤实跑,没动的分文不花
run 仍在跑且已看出脚本有问题 立即 amend,一次调用完成。不要先 TaskStop、不要等它跑完:本工具会停前驱、等结算、导入停止前已完成的一切。改得越早,重付的越少
只想改并发/子代理模型/名字 amend 且只传那一个字段、path 与 script 都不给

与 ResumeWorkflowRun 的分工:resume 是"字节不变的续跑"(继续一个 stopped run,脚本一个字不改);amend 是"脚本或设定变了"。两条入口、两种语义,描述里互相指路。

3. 输入契约:只有 run_id 必填

ts 复制代码
run_id: string,                        // 必填:任何本项目的 run------completed/errored/stopped/running
script?: string,                       // 整份修订脚本(与 path 至多给一个)
path?: string,                         // 修订脚本的文件(常态:前驱自己的脚本文件就地改完交回)
name?: string,                         // 新 run 显示名;缺省沿用前驱名
max_concurrency?: number | null,       // 三态
subagent_model?: string | null,        // 三态

每个可省略字段守同一条规则:省略即沿用前驱(description 注释解释了为什么三条 bullet 并排且脚本在最前------模型若以为脚本必填,就会为改一个数把几千 token 的脚本再抄一遍)。

三态字段的统一语义(只在 resolveInput 存活,之后归一成"一个数/一条规范形或没有"):

传入 max_concurrency subagent_model
省略 沿用前驱上界(再钳一次------前驱可能在另一台机器上起) 沿用前驱选型,重新解析一遍(前驱可能几天前起的,模型可能已删------现在失败,而不是拖到子代理第一次开口时看起来像运行时故障)
null 解除(跑在天花板上) 回到会话模型
值 设定(钳到 [1, 天花板]) 设定(解析失败整次调用失败,什么都没停没建)

沿用值解析失败时的文案必须说清它是继承来的 (resolveAmendSubagentModel 注释)------模型这次调用压根没提模型名,直接丢解析诊断,它会以为自己传错了参数然后原样重试。目录缺席时两种来源也分开处理:模型自己给的字符串照 CreateWorkflow 拒掉,沿用的原样带过(为一个这次没提到的字段让整次修订失败,是把宿主接线缺口记到用户头上)。

predecessor 不在模型的 JSON schema 里------它是 resolveInput 无条件覆盖回填 的事实块(name/status/stop_reason/owned_by_this_session/script_inherited),"伪造它是无效的"。

4. resolveInput:确认之前的一切

resolveAmendWorkflowInput(amend-workflow-resolve.ts)是全流程唯一一次读端口,在 hook、权限、确认窗之前同步完成------三处都要读"将要发生的执行事实",而它们都在 handler 之前。

顺序不能换 (注释给出了反例论证):前驱查找(不存在即 run_not_found,与脚本无关)→ 落定脚本 → 比字节。倒过来,一个指向不存在 run 的调用会先因文件读不出报错,模型就会去修一个根本不是问题的东西。

4.1 脚本的三条来路(amend-workflow-source.ts)

来源 处理
path(常态) 读文件;frontmatter 元数据块剥掉且声明被忽略------修订不带实参(实参是前驱那次 run 的事实,随 journal 走)
script 内联整份,原样
都省略 经端口读前驱存档的那份字节(resume 重放的同一份);存档缺失(脚本落库前的老 run / 老宿主)当场失败

两个来源都给 → 入参级违规(code 400)。都不给在 CreateWorkflow 是违规,在这里合法------那是"沿用前驱的脚本"。

沿用的脚本要"认家" (resolveKeptScriptFile):前驱记下的脚本文件若此刻 读出来仍是这份字节,新 run 继续记这个文件(模型下一次修订仍去编辑它);文件没了或被改过就回 undefined,handler 照"不来自文件的脚本"写一份新草稿。铁律(handler 注释):run 记的永远是装着这一次脚本的文件------把一个内容已经不是这份脚本的文件记到新 run 上,会让诊断行号与"去编辑那个文件"都指向另一段代码。

4.2 script_unchanged 预检

对模型给的 path 且本次调用什么都没改(没同时设两个设定字段;null 算"传了",它是显式解除)时,与 port.getScript 的存档做字节比对------字节相同直接拒绝:

workflow_script_unchanged: ...is byte-for-byte the script run X was started with, so amending it would repeat that run exactly.

这道网抓的是忘了编辑。把脚本贴一遍、或明说"脚本不动"(两个来源都省略)都不是那个错误。文案同时给出三条出路:先编辑文件再交同一个 path;只改设定就不传 path;同脚本续跑走 ResumeWorkflowRun。

5. 确认与权限

  • permission 与 CreateWorkflow 同一道门 :alwaysAsk: true、askOptions: { allowAlways: "session" }------但本会话自己起的 run 免确认 (权限服务的 owner 规则在 always-ask 分支放行;判据就是回填的 predecessor.owned_by_this_session)。用户停掉的、别的会话起的 run,先问用户。
  • 确认窗预览与 CreateWorkflow 同一段代码 (prepareAmendWorkflowApproval):先编译,编不过直接 proceed 放行给 handler 回诊断("窗开了也无物可批");编得过就画将要跑的那份脚本的图。display 的 kind 仍是 create_workflow------图、草稿笔与诊断卡在 UI 侧只有一份实现,"这是修订"由工具名与入参的 run_id/predecessor 说出来。

6. 执行编排:port.amend 的时序

amendDynamicWorkflowRun(bootstrap/src/app/dynamic-workflow-run-submit.ts):
前驱引擎(在飞?) journal(持久事实) run service(bootstrap) handler(工具层) 前驱引擎(在飞?) journal(持久事实) run service(bootstrap) handler(工具层) #mermaid-svg-fcdmlyFptsg3VKdv{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-fcdmlyFptsg3VKdv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fcdmlyFptsg3VKdv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fcdmlyFptsg3VKdv .error-icon{fill:#552222;}#mermaid-svg-fcdmlyFptsg3VKdv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fcdmlyFptsg3VKdv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fcdmlyFptsg3VKdv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fcdmlyFptsg3VKdv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fcdmlyFptsg3VKdv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fcdmlyFptsg3VKdv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fcdmlyFptsg3VKdv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fcdmlyFptsg3VKdv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fcdmlyFptsg3VKdv .marker.cross{stroke:#333333;}#mermaid-svg-fcdmlyFptsg3VKdv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fcdmlyFptsg3VKdv p{margin:0;}#mermaid-svg-fcdmlyFptsg3VKdv .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fcdmlyFptsg3VKdv text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-fcdmlyFptsg3VKdv .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fcdmlyFptsg3VKdv .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-fcdmlyFptsg3VKdv .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-fcdmlyFptsg3VKdv .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-fcdmlyFptsg3VKdv #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-fcdmlyFptsg3VKdv .sequenceNumber{fill:white;}#mermaid-svg-fcdmlyFptsg3VKdv #sequencenumber{fill:#333;}#mermaid-svg-fcdmlyFptsg3VKdv #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-fcdmlyFptsg3VKdv .messageText{fill:#333;stroke:none;}#mermaid-svg-fcdmlyFptsg3VKdv .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fcdmlyFptsg3VKdv .labelText,#mermaid-svg-fcdmlyFptsg3VKdv .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-fcdmlyFptsg3VKdv .loopText,#mermaid-svg-fcdmlyFptsg3VKdv .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-fcdmlyFptsg3VKdv .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fcdmlyFptsg3VKdv .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-fcdmlyFptsg3VKdv .noteText,#mermaid-svg-fcdmlyFptsg3VKdv .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-fcdmlyFptsg3VKdv .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fcdmlyFptsg3VKdv .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fcdmlyFptsg3VKdv .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fcdmlyFptsg3VKdv .actorPopupMenu{position:absolute;}#mermaid-svg-fcdmlyFptsg3VKdv .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-fcdmlyFptsg3VKdv .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fcdmlyFptsg3VKdv .actor-man circle,#mermaid-svg-fcdmlyFptsg3VKdv line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-fcdmlyFptsg3VKdv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 被拒(run_not_found/missing_boundaries)→ 不停任何东西,前驱照跑 alt 注册表里前驱在飞 amend({scriptText, predecessorRunId, ...}) preflightAmendImport(存在 ∧ 已完结 ask 边界齐全;不看状态) abort({ superseded: 新 runId }) await settlement(与 waitForTask 同一 promise,永不 reject) buildImportedCache(终态门 + 边界门 → ImportedRunCache) 结算后重读 spentTokens(用量起点,避免漏掉最后几轮花费) startNewRun:编译一次 → 注册表条目 → fire-and-forget launch { ok, runId, supersededRunId? }

设计要点(均有注释背书):

  • 预检在停止之前:两条拒绝理由(run 不存在、边界记账不可信)都是前驱 journal 的性质,停止不改变它们------先判定再停,被拒的 amend 才不会留下一个被白白停掉的 run。
  • 在飞判定读本进程注册表而不是 journal 状态:journal 说 running 而注册表没有,是别的(或死)进程的 run------本 service 停不了也等不到,按已结算处理,让导入构建的终态门说话。
  • 确定性是正确性前提 (dynamic-workflow-import.ts 头注释):buildImportedCache 是纯函数------同一份 journal 状态必须给出同一张表(无时间戳、无随机、遍历序来自 journal 插入序)。因为 resume 崩溃恢复时要用 rebuildImportedCacheForResume 从 journal 重建同一张表:修订 run 的 journal 只对"已到达的执行前缀"自含。
  • 端口层绝不二次继承 :并发上界与子代理模型不从前驱继承------"省略即沿用"是工具面 的三态,resolveInput 已归一;端口再继承一次,"null 解除"就永远到不了这里。脚本文件同理绝不沿用前驱路径。
  • 显示名沿用前驱:修订是同一件工作的下一版,卡片与通知里换个名字只会让用户以为是另一条工作流。
  • 编译恰好一次 :startNewRun 内 compileOnce,脚本哈希由 service 算------resume 的哈希校验、AmendWorkflow 的沿用与 script_unchanged 预检对"这个 run 在跑什么"不能有两个答案(getScript 注释)。

7. 缓存语义:什么被导入、什么重跑

buildImportedCache 读前驱 journal 构建 ImportedRunCache(actors 表 + world 队列):

  • actor 按名字匹配 :名字是缓存身份键。匿名不收 (没有可比对的坐标);重名两个都不收------"挑一个不如都不挑:代价是这两个 actor 全新重跑,而错挑的代价是把另一个 actor 的会话前缀当成本 actor 的上文"。
  • actor 缓存 = 最长全 completed ask 前缀 (按 actorSeq 连续,停在第一个非 completed 处)。失败的 ask 对新 run 无约束力 (修订常常就是为了越过一次失败),它自己不导入;但跳过它去导入其后的条目会走私上下文------被跳过那轮的问答仍在源会话转录里,缓存却声称它没发生过。
  • 源诚实性检查 (honorsBoundary):转录读面在场时核对源会话至少有边界那么长,短了整个候选弃掉(该 actor 全新重跑)。降级而不失败是构建的总基调------缺前缀/缺 persona/无转录都只是"这个 actor 没有缓存";唯一整体拒绝的是边界记账不可信 (missing_boundaries,严格到全表),因为那会让已收下的候选截断到错误位置。
  • 缓存何时关闭 (工具描述):缓存保持开启,直到一个实跑的子代理第一次写工作区 (或一次实跑的 world.run 执行)------从那一刻起,缓存的世界读取与"读过东西的 actor"的缓存 ask 描述的已是一个不存在的工作区,它们也转实跑;只回答过问题的 ask 继续从缓存结算。
  • 怎么强制重跑 :编辑一条 ask 的文本(字节比对就不再命中)。配套写作纪律:名字保持稳定、可调常量别写进 ask 文本。

8. 结构化失败:Nothing was stopped or created

失败码表(amend-workflow-source.ts:25;数值只是日志位,判别键在 message 前缀):

code 键 场景
21 workflow_amend_unavailable 端口不带 amend 能力("capability gap, not a status problem")
22 workflow_amend_missing_boundaries 前驱 journal 早于边界记账/边界未记------无回退,指引改走全新 CreateWorkflow
23 workflow_subagent_model_unresolved 模型引用解析失败(含"沿用的那一个已不可用"的特殊文案)
24 workflow_script_unchanged 字节级未改动预检命中
25 workflow_script_file_unreadable path 读不出来
26 workflow_amend_script_unavailable 无存档可沿用(record/host 两种成因同一出路:把脚本交上来)
400 (来源违规) script 与 path 同时给出

统一纪律:每条失败都以 "Nothing was stopped or created." 收尾------模型据此知道世界没有副作用,可以安全重试或改道。端口契约外的拒绝理由也回结构化失败(throw 是接线故障的通道),文案带原词供日志排查。

编译失败响应 的文案有两处精细设计(compileFailureResponse):沿用来的脚本编不过时先说清它是继承来的 ("模型这次调用没写这些行,光给诊断它会以为自己传错了参数");两种情形都补一句"Run X was not touched"("否则模型会以为它刚把一个在跑的 run 停掉了")。内联修订无论编译结果如何都落盘 成草稿------一段编不过的修订也有文件可改,结果点名草稿路径,下次修订就回到 path 轨道。

9. 输出与后续

输出形状是 CreateWorkflowOutput(同一 schema),status: "backgrounded"、backgroundTaskId = 新 runId。给模型的 response 按事实拼接:

  • 哪条被替代:在飞前驱 → "Run X was still running: it has been stopped and superseded, and everything it finished before the stop is imported as cache. It will not send a notification of its own.";已结算前驱 → "Run X had already settled; its finished work is imported as cache."
  • 脚本有没有变 :沿用时点明 "The script of run X started unchanged"------模型据此知道这次改的只是设定,不去翻一段自己没写过的新脚本。
  • backgrounded 引导 (照 CreateWorkflow 的样子):给出新 id、说明仍在跑、结果以通知形式回来、显式劝阻轮询("Do not wait for it or poll it with TaskOutput; continue with other work unless the user asked you to wait.")。
  • 附并发上界说明与脚本文件句子(内联修订点名新草稿,path 修订点名同一文件)。

10. 灰度门与家族

AmendWorkflow 属于动态工作流十工具族,受同一道灰度门控制(handlers/index.ts 的 DYNAMIC_WORKFLOW_TOOL_NAMES):只有显式 false 才整体下架,门同时管工具描述(灰度关时 Agent 描述里指向 CreateWorkflow 的那行一并抹掉)。配套的 dynamic-workflows 技能也随门禁用(见 skill-tool-guide 第 8 节)。

家族工具 职责
CreateWorkflow 启动新 run(alwaysAsk,无免确认)
AmendWorkflow 修订既有 run:停飞前驱 + 导入缓存 + 启动修订(本会话 run 免确认)
EvalWorkflowSnippet 同步、只读、瞬态的脚本实验通道
SaveWorkflow / ListSavedWorkflows 定义清单的写与读
ListWorkflowRuns / GetWorkflowRun 历史内省(run_id 的发现面)
ResumeWorkflowRun 字节不变续跑(stopped run 的复活,alwaysAsk 非 yolo 不可)
ListModels 给 subagent_model 供模型目录
ResolveWorkflowQuestion 子代理升级问答的主代理侧

UI 侧(tool-identity.ts)AmendWorkflow 归 workflow 家族,与 CreateWorkflow、submit_result 同族渲染。

11. 使用指引

给模型:

  • run 报错时先想到 amend 而不是重写------每一个已成功的 ask 都在替你省钱;
  • 修订的常态是 path:错误通知和 GetWorkflowRun 都已给出脚本文件路径,就地 Edit 再把同一个 path 交回来,一次修订只花一次 Edit;
  • 只改设定就两个脚本来源都不给;改并发/模型只在用户开口时做;
  • 收到 script_unchanged 说明你忘了编辑,或你应该改用 ResumeWorkflowRun;
  • steer 不适用于本工具:它同步编译 + 后台启动,15s 超时且不可取消(cancellation.supported: false------"AmendWorkflow typechecks synchronously and cannot be cancelled")。

给运维/调试:

  • "前驱还在跑却说我没停它":检查注册表与 journal 的归属------别的进程的 run 不归本 service 停,走终态门;
  • 缓存没命中:核对子代理名字是否稳定、ask 文本是否字节一致、是否已有实跑写入工作区;
  • missing_boundaries:前驱 journal 早于 marker 列引入,无回退,走全新 run。

12. 相关文件索引

文件 职责
packages/core/src/tool/handlers/amend-workflow.ts handler、确认窗预览、输出拼接、工具元数据(头注释:与 CreateWorkflow 的两处差别)
packages/core/src/tool/handlers/amend-workflow-resolve.ts resolveInput:前驱回填、三态归一、子代理模型解析(工具与 GUI 共用判定本体)
packages/core/src/tool/handlers/amend-workflow-source.ts 脚本三来路、沿用认家、script_unchanged 预检、错误码表
packages/core/src/tool/handlers/amend-workflow-description.ts 工具描述:缓存机制、省略即沿用、确认规则
packages/contracts/src/tools/amend-workflow.ts 输入契约(三态字段)、predecessor 事实块
packages/bootstrap/src/app/dynamic-workflow-run-submit.ts amendDynamicWorkflowRun:预检 → 停飞 → 导入 → 启动;startNewRun 共用启动尾
packages/bootstrap/src/app/dynamic-workflow-import.ts preflightAmendImport / buildImportedCache:纯确定缓存构建、三道门、actor 候选降级
packages/bootstrap/src/app/dynamic-workflow-run-service.ts 端口实现、getScript(沿用/预检/resume 的同一字节来源)、七条不变式
packages/core/src/tool/handlers/create-workflow.ts 共用的编译诊断、display、backgrounded 文案
packages/core/src/tool/handlers/workflow-drafts.ts 内联修订的草稿落盘
packages/core/src/tool/handlers/index.ts 灰度门(DYNAMIC_WORKFLOW_TOOL_NAMES)
packages/shared/src/tool-identity.ts AmendWorkflow → 家族 "workflow"
相关推荐
小果冻。。1 小时前
大模型生成原理与幻觉控制:从原理到实战
人工智能
沐风___1 小时前
REA AI 逆向工程:没有源码,怎样查清软件的一个功能?
人工智能·机器学习
蒲公英eric1 小时前
线性回归与正则化:从最小二乘到岭回归、LASSO
人工智能·算法·数学建模·线性回归·岭回归·最小二乘·lasso
ElectroComp1 小时前
高功率 PoE 与 AI 算力电源双轮驱动,磁性元器件迎来技术升级新契机
人工智能
renhongxia11 小时前
中美AI竞争下人工智能赋能大众生活
大数据·人工智能·生活
AI增长技术研究院1 小时前
AI心智指数(绿雪智能)怎么用?AI可见性检测与GEO优化步骤
人工智能
老唐7771 小时前
Agent 开发入门教程:从零搭建一个 Python AI Agent
人工智能·python·深度学习·langchain·大模型·agent·智能体
宿州派大星4 小时前
Python实战:PyQt5 + OpenCV + Dlib 打造AI桌面应用
人工智能·python·qt
风娜5 小时前
IT 疑难杂症诊疗室:从现象到根因的系统排查方法论
人工智能·github