动态工作流(三):从工具调用到落库
动态工作流的产品层:功能开关与入口,模型可用的十个工作流工具,一次“创建并运行”从草稿文件、编译诊断、确认窗到后台 run 的全过程,每个 actor 怎样成为一个子会话,进度怎样回到主会话,日志存进 SQLite 的哪四张表,以及升级问答、保存、修订、恢复与无头模式。
前两篇讲的是纯函数库与沙箱:编译器把脚本变成带站点 ID 的 JavaScript,引擎在子进程里调度它。本篇把它们接到产品上:模型通过哪些工具使用动态工作流,run service 怎样启动与观察一个 run,生产 driver 怎样把每个 actor 落成真实的子会话,日志存在哪里。
代码分布在几处:工具在 apps/zcode-cli/packages/core/src/tool/handlers(*-workflow*.ts 与 saved-workflows/),run service 与 driver 在 apps/zcode-cli/packages/bootstrap/src/app(dynamic-workflow-run-*.ts、workflow-driver*.ts),SQLite 日志在 apps/zcode-cli/packages/adapters/src/storage/session-store,界面在 packages/ui。bootstrap 里的 script-workflow-* 是旧脚本工作流的底座,动态工作流沿用了它的子运行时工厂;旧的 Workflow 工具在内置工具表里已被注释掉(apps/zcode-cli/packages/core/src/tool/handlers/index.ts:70)。adapters/src/workflow 与 contracts/src/workflow 属于专家工作流和旧脚本工作流,动态工作流的契约在 contracts/src/tools/*workflow*.ts 与 contracts/src/interfaces/dynamic-workflow-run.port.ts。
开关与入口
功能开关定义在 packages/shared/src/dynamic-workflow-feature.ts。服务端 /api/v1/client/configs 下发 dynamicWorkflow.mode,取值 disabled、onDemand、alwaysOn(packages/shared/src/dynamic-workflow-feature.ts:9),消费侧折成布尔值,只有 disabled 算关(dynamic-workflow-feature.ts:37)。缺省、格式非法或请求失败都按 disabled 处理,即 fail-closed(dynamic-workflow-feature.ts:23)。本地覆盖用环境变量 ZCODE_DYNAMIC_WORKFLOW_MODE,优先级是覆盖、远端、缺省(dynamic-workflow-feature.ts:20、dynamic-workflow-feature.ts:63)。桌面端的 Main 进程在拉起 Host 前改写它:未打包的开发版透传 shell 里的合法值,打包的 preview 版固定写 alwaysOn,打包的正式版删掉继承值(packages/desktop/src/main/desktopRuntimeEnv.ts:441)。
开关只管桌面与 Web 那一侧。Host 判定后经 workspace/updateDynamicWorkflowPolicy 告诉 CLI 的协议服务端,服务端的缺省值是关(apps/zcode-cli/packages/bootstrap/src/zcode-protocol/server.ts:253、apps/zcode-cli/packages/bootstrap/src/zcode-protocol/dynamic-workflow-policy.ts:4)。运行时配置里这个字段“缺席即开启”:终端 TUI、无头 -p 和工作流子会话都不设置它,保留完整的工具面,只有受信 Host 创建的协议会话才会显式写 false(apps/zcode-cli/packages/core/src/runtime/types.ts:207)。所以在终端里直接用 Agent CLI,动态工作流默认可用;桌面端正式版则取决于服务端下发。
开关关闭时撤掉三样东西:十个工作流工具(index.ts:146);/workflow 命令,面板里剔除、手打也不展开(apps/zcode-cli/packages/bootstrap/src/zcode-protocol/slash-commands.ts:16、apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:804);以及教模型写脚本的 dynamic-workflows 技能(create-app.ts:753)。后两者来自内置插件 zcode-guide(apps/zcode-cli/packages/bootstrap/src/app/official-plugin-definitions.ts:82),这个插件的源码不在开源仓库里。
即使工具在场,模型也不该自作主张:工具说明规定只有用户明确要求“用工作流”时才调用,否则用 Agent 工具委托或自己做(apps/zcode-cli/packages/core/src/tool/handlers/create-workflow-description.ts:40)。
工具一览
| 工具 | 作用 | 确认 |
|---|---|---|
CreateWorkflow | 编译脚本,确认后在后台启动一个新 run;script、saved、path 三种来源恰好给一个 | 任何权限模式都询问(含 yolo、plan),可选“本会话始终允许”(apps/zcode-cli/packages/core/src/tool/handlers/create-workflow.ts:351) |
AmendWorkflow | 修订任意状态的 run(包括还在跑的),新 run 取代旧 run,并把已完成的结果作为缓存导入 | 同上;本会话发起、且不是用户亲手停下的 run 免确认 |
EvalWorkflowSnippet | 同步编译并运行一段试验台片段,不落库、不建后台任务 | 片段含 world.run 时才询问 |
SaveWorkflow | 把脚本连同元数据存成可复用的定义 | 总是询问,不提供“始终允许”(apps/zcode-cli/packages/core/src/tool/handlers/save-workflow.ts:286) |
ListSavedWorkflows | 列出项目与全局保存的定义 | 只读 |
ListWorkflowRuns | 列出本项目的 run,包括其他会话发起的 | 只读 |
GetWorkflowRun | 一个 run 的即时快照:进度、用量、日志尾、结果或失败、产物 | 只读 |
ResumeWorkflowRun | 用同一个 runId 继续一个 stopped 的 run | 不询问 |
ResolveWorkflowQuestion | 回答子 Agent 升级上来的阻塞问题 | 不询问 |
ListModels | 列出可填给 subagent_model 的模型 | 只读 |
这十个就是开关关闭时一起下架的那一组(index.ts:146)。actor 子会话里另有两个控制工具:交结果的 submit_result 与提问用的 escalate。
ResumeWorkflowRun 不弹窗,理由是恢复被 scriptHash 钉死在提交时已批准的同一段脚本上(apps/zcode-cli/packages/core/src/tool/handlers/resume-workflow-run.ts:12)。注册表处的注释还写着它“alwaysAsk 非 yolo 不可”(index.ts:122),与实际的 needsApproval: false(resume-workflow-run.ts:190)不一致,以后者为准。
创建并运行:一次调用的全过程
输入归一。执行器先跑 validateInput,再跑 resolveInput(apps/zcode-cli/packages/core/src/tool/executor/call-runner.ts:187、call-runner.ts:207)。CreateWorkflow 在后者里读保存的定义或 path 指向的文件,这是全流程唯一一次读盘,此后钩子、权限规则、确认窗与处理函数看到的是同一份字节;并发上界钳进 [1, 天花板],subagent_model 解析成规范形式,都在弹窗之前完成,用户批准的就是将要生效的值(handlers/create-workflow.ts:324)。
草稿与诊断回路。内联脚本无论编译成败都写一份草稿(handlers/create-workflow.ts:80),落在 <cwd>/.zcode/workflow-drafts/,目录自带一份内容为 * 的 .gitignore(apps/zcode-cli/packages/core/src/tool/handlers/workflow-drafts.ts:9)。文件名取 run 名,没有就取第一个阶段名,扩展名 .dwf.ts;每次内联提交都新建文件,重名依次加 -2、-3,用独占方式创建,绝不在模型背后覆盖(workflow-drafts.ts:70、workflow-drafts.ts:102)。编不过时,诊断按文件行号写成 {path}:L{line}:C{column} {message},行号能直接粘进一次 Edit(apps/zcode-cli/packages/core/src/tool/handlers/workflow-script-notes.ts:28),末尾再附一条 NOTE:没有执行,就地编辑这个文件、用 path 重新提交,不要把脚本再内联贴一遍(workflow-script-notes.ts:53)。工具说明给的理由是:两万 token 的脚本重流一遍既慢,有的 provider 还会卡住(create-workflow-description.ts:27)。为了让这个回路顺畅,权限服务对草稿目录里的 Edit 与 Write 免确认,它排在 plan 模式与项目 deny、ask 规则之后(apps/zcode-cli/packages/core/src/permission/service.ts:198、apps/zcode-cli/packages/core/src/permission/workflow-draft-path.ts:33)。这类诊断不弹确认窗:让用户批准一段编不过的代码,只会打断模型自己的改错回路(handlers/create-workflow.ts:288)。
确认与提交。分析通过才弹窗,窗里画的是因果图、控制流图和交接图拼成的阶段图(handlers/create-workflow.ts:292、apps/zcode-cli/packages/core/src/tool/handlers/workflow-analysis-display.ts:5)。放行后处理函数调用 DynamicWorkflowRunPort.submit,带上脚本、工作目录、名字、实参、阶段表、并发上界、子代理模型与脚本文件的绝对路径(handlers/create-workflow.ts:131),拿到 runId 就返回 status: "backgrounded",回复里明确劝模型不要用 TaskOutput 干等(handlers/create-workflow.ts:185)。run service 这一侧(apps/zcode-cli/packages/bootstrap/src/app/dynamic-workflow-run-submit.ts:244)做四件事:compileOnce 在一个 ts.Program 上产出降级代码、ask 规格、world.run 命令集、submit profile 与 sha256 脚本哈希;按天花板钳好并发;在启动之前先把 run 登记进注册表,因为取消随时可能到来,后台追踪器也会立即开始查快照;然后不等待地启动(dynamic-workflow-run-submit.ts:285、dynamic-workflow-run-submit.ts:316)。runId 形如 dwfrun-<uuid>(dynamic-workflow-run-submit.ts:582)。
有一个缝隙:处理函数与确认窗用的 analyzeWorkflowScript 不做 schema 合成,ask<T> 的结果类型不能序列化(诊断 9002)要到 compileOnce 才发现,此时它直接抛错,用户已经确认过,这次工具调用以失败告终(dynamic-workflow-run-submit.ts:540、handlers/create-workflow.ts:129)。
run service 的文件头列了七条不变式(apps/zcode-cli/packages/bootstrap/src/app/dynamic-workflow-run-service.ts:9),其中几条决定了可见行为:dwf_run 行只由引擎在构造时创建;没有日志存储就不构造本服务,CreateWorkflow 退回“只做类型检查”的占位回复(create-app.ts:581、handlers/create-workflow.ts:114);每次启动都登记为会话的常驻阻塞工作,免得跑着 run 的会话被当成空闲会话在 10 分钟后回收。
actor:每个都是一个子会话
引擎第一次向某个 actor 派活时,driver 用 runId 与 actor 引用(如 actor#1@1)拼出会话 ID,形如 sess_dwf-<runId>-<actor>,非安全字符折成下划线,同一 run、同一 actor 每次都铸出同一个(apps/zcode-cli/packages/bootstrap/src/app/workflow-driver-helpers.ts:53、apps/zcode-cli/packages/contracts/src/interfaces/shared.ts:32、apps/zcode-cli/packages/bootstrap/src/app/workflow-driver.ts:172)。子运行时由 createScriptWorkflowAgentRuntime 造出(create-app.ts:601),它强制 mode: "yolo"、taskType: "workflow_child",并关掉子 Agent(apps/zcode-cli/packages/bootstrap/src/app/script-workflow-child-runtime.ts:118)。也就是说,一次 run 获批之后,actor 在里面执行 Bash、编辑文件都不再逐个询问,模式的含义见权限模式与规则。会话随后落成一行普通会话,并以角色 workflow_actor、路径 dwf/<runId>/<actor> 挂到父会话的任务链接上;恢复时直接从存储重新水化(apps/zcode-cli/packages/bootstrap/src/app/dynamic-workflow-run-launch.ts:428、dynamic-workflow-run-launch.ts:379)。
actor 的工具面是全集减去一张减法表(apps/zcode-cli/packages/bootstrap/src/app/workflow-actor-tools.ts:33):
const ACTOR_DISALLOWED_TOOLS: readonly string[] = [
ASK_USER_QUESTION_TOOL_NAME,
ENTER_PLAN_MODE_TOOL_NAME,
EXIT_PLAN_MODE_TOOL_NAME,
"CreateWorkflow",
// 修订入口与 CreateWorkflow 同一种嵌套编排,同一个根因入列。
"AmendWorkflow",
READ_SESSION_CONTEXT_TOOL_NAME,
// 子代理不许替主代理回答升级问题。
// 与上面几条的根因不同:这不是悬挂也不是越权读,而是**身份**——升级的整个意义是把判断权
// 交给创建这条工作流的那一方;让另一个 actor 顺手作答,等于把它悄悄退化成 actor 之间的
// 互相说服。actor 提问用 `escalate`(恒注册),作答只属于主会话。
RESOLVE_WORKFLOW_QUESTION_TOOL_NAME,
];前三个在无人应答的子会话里会一直挂着;嵌套编排与越界读父会话是越权;最后一个守的是身份,升级问题只能由创建这条工作流的主会话回答。core 按 taskType 再兜一层结构性禁用,另外去掉 SaveWorkflow 与 ResumeWorkflowRun(apps/zcode-cli/packages/core/src/runtime/helpers/tool-allowlist.ts:26)。
系统提示词里,交互式的身份段被换成工作流子 Agent 的身份段(apps/zcode-cli/packages/core/src/context/sections/workflow-actor.ts:48):开场一句说明它是动态工作流 run 里的子 Agent,读它输出的是脚本而不是人,作者写的 persona 叠在其后,安全提示与 Harness 块照旧复用,最后是一段契约(workflow-actor.ts:33):
function buildWorkflowContract(): string {
return [
"# Working inside a workflow",
`- ${TOOL_SURFACE}`,
"- Each ask states what to do. When the ask carries a result schema, finish by calling `submit_result` with a conforming value; otherwise your final message is the result.",
`- ${EVIDENCE_RULE}`,
"- Report outcomes faithfully. If part of the task is impossible, out of scope, or contradicted by what you found, say so in the result instead of filling a field with a plausible guess. Never fake a passing result to satisfy an instruction.",
"- When you are blocked by something outside your reach — a gate that cannot pass, instructions that contradict each other, a fact only the run's owner knows — call `escalate`. Questions written in prose reach nobody.",
// 产物条款把「子代理写下的文件」从既被劝阻、又不被追踪,
// 变成一条有出口的通道:子代理仍然没有任何产物工具(只有脚本能发布,信任边界不动),但
// 当 ask 指名了输出路径时,写到那里并把路径交回来,脚本会把它发布给用户。
"- Do not write report or summary files on your own initiative; findings go in the result. When the ask names an output path, write exactly there and return that path in the result — the script publishes it to the user.",
].join("\n");
}每个 ask 在下发时还会追加尾注:一段质量标准(引用读过或跑过的东西、跑过才算通过、做不到就照实说、被堵住就 escalate)(apps/zcode-cli/packages/bootstrap/src/app/workflow-ask-epilogue.ts:20),typed ask 再加一段 submit_result 说明。submit profile 为 mono 的 actor,schema 已写进工具声明,尾注只剩一句;generic 的则把整份 JSON Schema 附在尾注里(workflow-driver.ts:311、workflow-driver-helpers.ts:135)。尾注在计算输入哈希之后才追加,不影响缓存身份。
driver 把引擎的三种裁决落到对话上(workflow-driver.ts:13):accept 时 submit_result 的处理函数返回成功并停掉本轮;reject 时它抛错,模型在同一轮里拿到一条错误结果去修;nudge 时本轮已经结束,driver 在同一个持久运行时上起一个新轮次,提示词固定以“You ended your turn without submitting a result.”开头(workflow-driver-helpers.ts:127)。子会话默认跑在父会话当前的模型上;subagent_model 在场时覆盖,恢复时还会按日志里记下的模型“钉住”,免得父会话换了模型,同一个 run 的后半段悄悄换人(create-app.ts:613)。
进度怎样回到主会话
引擎每记一条事件,driver 的 emit 都把它包成会话事件载荷,附上刚分配的日志序号(dynamic-workflow-run-launch.ts:168)。进度汇先核对 run 的父会话就是本 App 的会话,不是就丢弃并记日志,宁可少一份投影也不污染别人的对话(apps/zcode-cli/packages/bootstrap/src/app/dynamic-workflow-run-progress-sink.ts:23)。然后在父会话里以根 trace 追加一条 dynamic_workflow_run_progress 事件:run 在后台跑,事件不能冒领任何一轮,否则冷恢复后它会出现在一轮已完结对话的末尾(apps/zcode-cli/packages/core/src/runtime/methods/dynamic-workflow-run-progress.ts:12、apps/zcode-cli/packages/contracts/src/events/session.events.ts:143)。v4 协议把它归约进 workflowRuns 状态键(apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/product-projection.ts:4305、packages/shared/src/zcode-protocol-v4/workflow-runs-reducer.ts:89),桌面与 Web 的时间线由此点亮;投影里最多保留最近 8 个 run,完整事实仍在日志里(workflow-runs-reducer.ts:123、packages/shared/src/zcode-protocol-v4/workflow-runs.ts:16)。
模型这一侧,CreateWorkflow、AmendWorkflow、ResumeWorkflowRun 三个工具的输出被执行器当成后台任务追踪,快照、等待与取消都走同一个端口(apps/zcode-cli/packages/core/src/tool/executor/background-task-registry.ts:39、apps/zcode-cli/packages/core/src/tool/executor/background-tasks.ts:752)。run 结算后,主 Agent 收到一条完成通知,里面带着脚本的顶层返回值、report 过的条目和产物,外加一段按终态定制的“交付指引”,比如用户亲手停下的 run 不要主动恢复(apps/zcode-cli/packages/core/src/runtime-task/notification.ts:20、notification.ts:235)。运行途中只有两类事件会主动通知:子 Agent 的升级提问与 run 停滞(dynamic-workflow-run-progress.ts:38)。取消只有一条路径,界面按钮与模型的 TaskStop 都汇到 DynamicWorkflowRunPort.cancel,run 结算成 stopped(user) 或 stopped(model)(apps/zcode-cli/packages/core/src/runtime/methods/background-stop-dynamic-workflow.ts:5)。通知与后台任务的通用机制见后台任务与通知。
日志落在哪几张表
日志与会话共用一个 SQLite 库,默认在 ~/.zcode/cli/db/db.sqlite(apps/zcode-cli/packages/adapters/src/storage/session-store/paths.ts:7)。四张表由迁移 0019_dwf_journal 一次建齐,它是 beta 之前把开发期十二条迁移压成的单一基线(apps/zcode-cli/packages/adapters/src/storage/session-store/migrations.ts:799):
| 表 | 一行是什么 | 要点 |
|---|---|---|
dwf_run | 一个 run | 脚本原文与哈希、实参、工具调用 ID、resumed_from、并发上界、已花 token、状态、结果与失败 |
dwf_actor | 一个 actor 实例 | (run_id, site_id, ordinal) 唯一,persona、会话 ID、实际使用的模型 |
dwf_node | 一次 ask、世界读取、命令、report 或产物 | 输入哈希、有界输入、状态、结果或错误、用量、消息边界、产物 id |
dwf_event | 一条运行事件 | 每个 run 内单调的 sequence,载荷是事件 JSON |
dwf_node 的定义(migrations.ts:874):
create table if not exists dwf_node (
id integer primary key autoincrement,
run_id text not null references dwf_run(id) on delete cascade,
site_id text not null,
ordinal integer not null,
kind text not null check(kind in ('ask', 'world-read', 'world-run', 'report', 'artifact')),
actor_site_id text,
actor_ordinal integer,
actor_seq integer,
input_hash text not null,
input_json text,
status text not null check(status in ('running', 'completed', 'failed')),
result_json text,
error_json text,
stats_json text,
message_boundary integer,
artifact_id text,
time_created integer not null,
time_updated integer not null,
unique(run_id, site_id, ordinal)
);几处设计:会话 ID 两列都是纯文本、不加外键;run 表没有节点上限或 token 预算列,token 只是观察面;可空列一律“NULL 即缺席”(migrations.ts:812)。产物的字节不进日志,dwf_node 里只存指向 tool-artifact store 的 URI(apps/zcode-cli/packages/dynamic-workflow/src/engine/types.ts:301)。追加事件时,序号的分配与插入写在同一条 SQL 里,因为 WAL 模式下多个 Agent 进程可能共用这个库,先读 MAX(sequence) 再插入会竞争出重复序号(apps/zcode-cli/packages/adapters/src/storage/session-store/repositories/dwf-journal.ts:367)。
逻辑状态与物理列不同名:dwf_run.status 的 CHECK 集仍是早先的五个值,没有迁移,映射只活在编解码里(apps/zcode-cli/packages/adapters/src/storage/session-store/repositories/dwf-journal-codecs.ts:29):
| 逻辑状态 | 物理 status | failure_json |
|---|---|---|
stopped,带原因与可选错误 | cancelled | 信封 {"stopReason": …, "error"?: …} |
errored | failed | 错误 JSON 原样 |
completed、pending、running | 同名 | 不变 |
读回时,没有信封的老 cancelled 行一律解成 stopped(user),失败码是 Interrupted 的老 failed 行解成 stopped(interrupted)。旧脚本工作流另有自己的 workflow_* 表,两套互不引用(migrations.ts:801)。
App 启动构造 run service 时,会把本会话留在日志里、还停在非终态的 run 收敛成 stopped(interrupted),它们只能是死进程的遗物;只收敛本会话,免得误伤兄弟会话正在跑的 run(apps/zcode-cli/packages/bootstrap/src/app/dynamic-workflow-run-reconcile.ts:43)。create-app.ts 里一处注释还说“收敛成 failed”(create-app.ts:691),已经过时。
运行中途的提问:升级问答
actor 不能直接问用户(AskUserQuestion 在减法表里),被堵住时调用 escalate:处理函数阻塞在会话级的升级端口上,driver 铸一个问题 ID dwfq-<runId 片段>-<序号>,把问题登进注册表,双轨发出 escalation-raised,actor 就停在这次 ask 里等(workflow-driver.ts:29、apps/zcode-cli/packages/bootstrap/src/app/workflow-driver-escalation.ts:194)。主会话随即收到一条通知(dynamic-workflow-run-progress.ts:113)。主 Agent 可以先看 GetWorkflowRun,或者用 AskUserQuestion 去问用户,再调用 ResolveWorkflowQuestion 作答;答案原样成为 escalate 的工具结果,那个 actor 从原处继续,其余 actor 与脚本一直在跑(apps/zcode-cli/packages/core/src/tool/handlers/resolve-workflow-question.ts:46)。
规则有三条:每个 ask 最多升级 3 次,用完返回一条普通结果而不是错误,免得模型反复撞墙(workflow-driver-helpers.ts:84、apps/zcode-cli/packages/core/src/tool/handlers/escalate.ts:9);没人作答就一直等,不设超时,逃生口是取消,停驻的问题随 ask 一起撤下(workflow-driver.ts:359);升级不写 dwf_node 行,等待不算工作量(apps/zcode-cli/packages/dynamic-workflow/src/engine/types.ts:753)。ResolveWorkflowQuestion 的说明里还建议“用 CreateWorkflow 的 resume_from 接着跑修订后的脚本”(resolve-workflow-question.ts:52),但这个参数已经从 CreateWorkflow 删掉,修订改由 AmendWorkflow 负责(apps/zcode-cli/packages/contracts/src/tools/create-workflow.ts:146)。
保存、修订与恢复
保存。SaveWorkflow 的 scope 必填:项目档写到 .zcode/workflows/<name>.dwf.ts,随仓库提交、只在本项目可见;全局档写到 ~/.zcode/workflows/<name>.dwf.ts,本机所有项目可见(apps/zcode-cli/packages/core/src/tool/handlers/save-workflow-description.ts:8)。名字只许 [A-Za-z0-9_.-]、最长 64 个字符,这条正则就是防路径穿越的那道线(apps/zcode-cli/packages/contracts/src/tools/saved-workflow.ts:38)。文件是合法的 TypeScript,开头一段块注释装 YAML 元数据(apps/zcode-cli/packages/core/src/tool/handlers/saved-workflows/frontmatter.ts:7):
// /* zcode-workflow
// description: ...
// args:
// pr: { type: string, required: true }
// */
// <plain dwf script>description 必填,whenToUse 可选,args 的类型只有 string、number、boolean 与什么都收的 json(saved-workflow.ts:68)。脚本逐字节放在注释之后,保存再读回必须拿到同一份,否则 run 的脚本哈希对不上用户看到的文件(frontmatter.ts:50)。查找时项目档在前、全局档在后,先到者胜,同名时项目档遮蔽全局档(apps/zcode-cli/packages/core/src/tool/handlers/saved-workflows/store.ts:50)。工具说明对保存的要求最严:绝不主动调用,先用一句话建议,等用户同意(save-workflow-description.ts:20)。运行时用 CreateWorkflow 的 saved 来源,实参按声明校验,未知的键、缺少的必填值、类型不符都在运行前拒绝;实际执行的是写到草稿目录的一份拷贝,定义本身不会被一次 run 改动(apps/zcode-cli/packages/contracts/src/tools/create-workflow.ts:53)。桌面端的工作流中枢也能直接启动保存的定义,这条路不经工具执行器,用户在中枢里的点击就算同意(apps/zcode-cli/packages/core/src/runtime/methods/dynamic-workflow-run-start.ts:49)。
修订。AmendWorkflow 对任何状态的 run 都可用。run 还在跑时,service 先以“被取代”为由取消它并等它结算,再从已结算的前驱构建导入缓存,然后启动新 run;两个 run 之间只有 resumed_from 这一个指针(dynamic-workflow-run-submit.ts:103)。缓存按名字匹配具名 actor,每个 actor 的 ask 按顺序比对指令哈希,第一次不一致之后这个 actor 就不再查缓存(apps/zcode-cli/packages/dynamic-workflow/src/engine/imported-cache.ts:37)。第一个子 Agent 即将执行改写工作区的工具,或者第一条 world.run 真正执行,缓存就关门,此后只有没碰过外部世界的“纯” ask 还能命中(imported-cache.ts:55)。省略脚本、并发上界或子代理模型,都沿用前驱的值;并发上界与子代理模型显式传 null 才解除(apps/zcode-cli/packages/core/src/tool/handlers/amend-workflow-description.ts:21)。确认规则见上表:免确认的依据是回填的前驱事实,即 run 属于本会话且不是用户停下的,重启之后依然成立(service.ts:373)。界面上的“配置”按钮是 port.amend 的第二个调用方:同一份脚本、新的子代理模型或并发上界,不经模型、不开确认窗,实参沿用前驱(apps/zcode-cli/packages/core/src/runtime/methods/dynamic-workflow-run-settings.ts:65)。
恢复。ResumeWorkflowRun 只接受 stopped 的 run,无论原因,superseded 除外;errored 的 run 重放只会以同样方式失败,要走修订(resume-workflow-run.ts:60)。已完成的步骤从日志回放、不花 token,未完成的重新派发。拒绝原因各有错误码,包括存下的脚本已编不过当前门面、哈希与原文不符(日志被外力改过)、老 run 没有存脚本(resume-workflow-run.ts:50)。进程亡故留下的 stopped(interrupted) 行正是它的主要对象。界面上的恢复按钮走同一个 port.resume,再合成一个工具描述子交给执行器,重新接上后台追踪与完成通知(apps/zcode-cli/packages/core/src/runtime/methods/dynamic-workflow-run-track.ts:5)。
无头模式
无头模式 -p 从不构造权限代理,core 退回“一律拒绝”,而 CreateWorkflow 的 alwaysAsk 在任何模式下都要过代理,于是工作流在 -p 下会立即被拒(apps/zcode-cli/packages/cli/src/headless-workflow.ts:13)。CLI 为此包了一层只放行两个工具的代理,在 -p 的入口里装上(apps/zcode-cli/packages/cli/src/prompt-command.ts:219、headless-workflow.ts:36):
requestPermission: async (request, options) => {
// AmendWorkflow 与 CreateWorkflow 同一道门、同一条例外。
if (
request.toolName !== CREATE_WORKFLOW_TOOL_NAME &&
request.toolName !== AMEND_WORKFLOW_TOOL_NAME
) {
return await denyBroker.requestPermission(request, options);
}
return {
decision: "allow",
reason: `Headless CLI auto-approves ${request.toolName}: no interactive gate exists in -p mode.`,
resolvedAt: new Date(),
};
},注释强调这只是绕过确认门,不是绕过权限:PermissionRequest 钩子仍先于代理应答(headless-workflow.ts:27)。进度输出按格式分工:text 模式在 stderr 打简短进度,run 的开始、结算与脚本的 log 不节流,节点状态变化至少间隔 400 毫秒(headless-workflow.ts:163、headless-workflow.ts:176);stream-json 把进度定型成 type 为 workflow.run.progress 的 NDJSON 行(headless-workflow.ts:61);json 什么都不打,只输出最终那一个对象。只要观察到工作流活动,进程就每 100 毫秒轮询一次,等在飞的 run 结算、完成通知驱动的回合跑完再退出,不设超时;按 Ctrl-C 中断时,run 最终落成可恢复的 stopped(interrupted)(headless-workflow.ts:315、headless-workflow.ts:317、prompt-command.ts:315)。命令行的其余部分见命令行入口。
界面
终端 TUI 用与 v4 投影同一份 reducer 维护一份 workflowRuns 镜像,另外自己保留每个 run 最近 10 条 log(apps/zcode-cli/packages/tui/src/app-workflow-mirror.ts:4、app-workflow-mirror.ts:26);内置命令 /dwf 可以列出本会话的 run、取消或恢复(apps/zcode-cli/packages/cli/src/command-center/handlers/dwf.ts:9)。桌面与 Web 的界面在 packages/ui 里:src/components/workflow-timeline/ 画 run 的阶段时间线、子代理与产物,src/settings/saved-workflows/ 管理保存的定义,src/app-shell/WorkflowRunSidePane.tsx 是 run 详情侧栏。它们读的都是上面那条 workflowRuns 投影与 run 端口的查询面,本书在Web 与服务端一篇概述 UI 的整体结构。工具结果卡的 display 在 core 的执行器里构造(apps/zcode-cli/packages/core/src/tool/executor/create-workflow-display.ts:1、apps/zcode-cli/packages/core/src/tool/executor/workflow-observation-display.ts:1);产物在模型面的那一行格式只写一遍,完成通知与 GetWorkflowRun 共用(apps/zcode-cli/packages/core/src/tool/executor/workflow-published-artifacts.ts:1)。
下一篇:技能与自定义命令——技能从哪些目录被发现、怎样进入上下文,自定义命令的格式与 shell 展开,以及内置的斜杠命令。