vl-workflow-spec-3.19.md 128 KB

VL Workflow Spec 3.19

版本说明

  • Spec 版本:3.19(文档版本)
  • Workflow JSON 格式版本:过渡期兼容 3.163.173.18;当前已发布的 VLCode-Lite JS 引擎仍以 "version": "3.16" 为主
  • 兼容性规则(3.19 延续):规范升级先于所有运行时升级。只要 workflow 未使用更高版本独占能力,现有 3.16 workflow 应继续可运行;运行时应优先按 capability 判断,而不是只按版本字符串拒绝加载
  • 3.19 相比 3.18 的主要差异
    • 明确 IDE 输入值语义:界面可以展示 Doc Ref 或 viewer URL 方便复制和打开,但真正落盘与传给运行时的配置值仍然是 Doc ID
    • 明确 DocCenter 引用优先级:workflow 中 registry.docs 与 step in.docs 仍以保留 path 号表达语义,但运行时必须优先使用用户配置的 Doc ID 覆盖,再退回 path→docId 目录解析
    • 明确 官方文档与用户文档解耦:官方核心 spec / workflow docs 通过稳定 Doc ID 固定;path 仅作为逻辑别名与默认槽位
    • 补充 IDE Settings 配置位约束VL SyntaxTheme 与 workflow docs 可由用户替换其 Doc ID;Meta SpecWorkflow Spec 可由 IDE/平台锁定
    • 新增 Tool_* 标准扩展剖面:workflow 可直接调用本地/宿主工具,支持 tool / toolNameinput / intimeoutallowError 等字段
    • 新增 Subflow_* 显示/运行别名:在 editor 与运行时中可作为 WorkflowRun 的语义化别名使用,用于突出“子工作流”而不引入新的底层执行原语
    • 新增 工具运行时事件tool_start / tool_message / tool_done / tool_error,要求向外层窗口、SSE 消费方、日志系统完整暴露
    • 新增 稳定运行标识 clientRunToken:在真实 runID 尚未分配前,即可对并行 run、pause/resume、rerun 做稳定归属
    • 明确 checkpoint 续跑 / rerunFromStep 的 fork、loop、overrides 语义,允许从复杂分支中的指定节点向下继续执行
    • 补充 workflow-of-workflows 编排模式:父 workflow 可通过工具或宿主能力同步/异步调度子 workflow,并透传结构化事件

0.3 DocCenter 文档解析优先级(3.19 延续)

registry.docs、step in.docs、以及任何 workflow 内部文档引用,运行时必须按以下顺序解析:

  1. 当前项目或 IDE settings 中显式配置的 Doc ID
  2. 平台内置官方默认 Doc ID
  3. DocCenter 目录中 path -> latest docId 的回退解析

约束:

  • workflow JSON 内保留的 "1", "2", "141" 等值,语义上是 保留 path 槽位
  • 真正读取文档内容时,必须优先落到具体 Doc ID
  • 当某个 path 配置了显式 Doc ID 时,运行时不得再用本地旧文档或 path fallback 覆盖
  • UI 中允许展示 vl://doc/<id>/doc-center.html?docId=<id> 作为辅助引用,但运行时配置序列化结果必须仍然是数值型 Doc ID

第一章:Workflow 总纲

1.1 定位与目标

Workflow 是 VL 平台中的一种通用流程编排机制,用于以结构化、可执行的方式描述和运行多步骤流程。它既可以用于开发阶段的流程约束,也可以作为应用运行时的一部分,对外提供能力。

Workflow 本身是一种程序化定义的流程,可被前端、后端服务或工具环境触发执行,并根据使用场景的不同,呈现为不同的承载形式与生命周期。

在 VL 平台中,Workflow 主要覆盖以下四类用途:

场景 承载位置 使用对象 是否项目资产 是否参与编译 调用方式 主要作用
IDE 内 agent flow(无服务调用) Process/ 开发者自用 IDE 内调用 约束 IDE 内的开发流程,支持基于项目上下文的 Agent 执行
业务 workflow(可以调用服务) Workflows/ 应用用户 前端调用 / 服务内调用 编排后端流程,作为应用能力对外提供 API 或自动化服务
审批流(可以调用服务) Workflows/ 应用用户 前端调用 / 服务内调用 支持长流程执行,允许 pause / resume 与人工介入
本地自用 workflow LocalWorkflows/(建议) 开发者自用 本地工具调用 作为用户本地或个人使用的自动化工具

IDE 场景强约束(3.9 新增,3.10 延续)

当 workflow 用于 IDE 内 agent flow(典型承载路径 Process/)时,必须满足:

  • steps 中不得出现 Service_* 节点;出现即编译/加载报错
  • registry.services 必须为空数组(或在实现允许时省略)
  • 该约束仅适用于 IDE workflow;业务 workflow(如 Workflows/)不受此限制

Workflow 的整体设计目标是

  • 用统一的流程编排模型覆盖开发流程、业务流程与自动化工具等不同场景
  • 通过承载位置与编译参与度,明确 Workflow 的生命周期与责任边界
  • 同时支持前端触发与服务内调用,避免将 Workflow 限定为单一调用入口
  • 区分"开发者自用流程"与"作为应用能力对外提供的流程",避免语义混淆
  • 保持流程定义的结构化、可执行性与长期可维护性

1.2 Workflow 的状态

Workflow 在 VL 平台中的运行模型遵循 "执行无状态,状态外置" 的原则。

从定义层面看,Workflow 本身不包含持久化状态,也不描述状态如何存储或演进。

同一个 Workflow 定义可以被多次、并行地执行,而不会因历史运行而产生语义差异。

在运行层面,Workflow 可以在执行时选择性地挂载一个外部状态空间,用于承载执行过程中所需的上下文或产物,例如文件、运行中间结果或流程进度信息。

该状态空间具有以下特征:

  • 是否挂载是可选的
  • 状态空间在 Workflow 运行时通过参数指定
  • 状态空间不属于 Workflow 定义的一部分
  • 不同运行实例可以挂载不同的状态空间

当未挂载状态空间时,Workflow 仍可正常执行,其行为仅依赖运行参数与即时计算结果。

当挂载状态空间时,Workflow 可以在该空间中读写数据,但仍保持执行引擎本身的无状态特性。

通过将状态与执行解耦,VL Workflow 既能够支持短时、一次性的流程执行,也能够支撑需要持续上下文的复杂场景,而无需引入重量级的内置状态机机制。

1.3 多步输出

Workflow 支持在执行过程中产生多步输出(Multi-step Output),用于对外暴露流程的中间进度、阶段性结果或执行事件。

多步输出的核心目标是: 在不阻塞流程执行的前提下,使外部系统能够感知并响应 Workflow 的执行过程。

Workflow 的多步输出具有以下特性:

  • 输出以事件流的形式产生,而不是一次性返回最终结果
  • 每一步输出均对应 Workflow 执行过程中的某个阶段或节点
  • 对于串行执行的节点,输出顺序与 Workflow 的实际执行顺序保持一致
  • 对于并行执行的节点(如 children 并行分支或 Loop_* mode:"parallel"),输出事件的顺序取决于各节点的实际完成时间,不做确定性顺序保证

多步输出的消费方式不限定于前端场景,可根据运行环境选择不同的承载方式:

  • 前端消费 多步输出可以通过流式接口推送至前端,用于展示实时进度、阶段结果或交互提示。
  • 后端消费 多步输出也可以发送至消息队列(MQ)或类似的异步通道,由后端系统监听并据此触发后续处理逻辑。

Workflow 本身不关心多步输出的最终消费方,也不依赖具体的传输机制。

无论输出被前端直接展示,还是被后端服务订阅处理,其语义均保持一致。

通过多步输出机制,Workflow 可以自然支持:

  • 长时间执行的流程
  • 需要外部系统按阶段响应的任务
  • 前后端解耦的异步处理模式

1.4 控制流模型(Control Flow)

控制流由两部分组成:

A 节点属性(Node Properties)

用于描述"节点之间如何连接、并发以及是否执行":

  • next:串行后继节点(显式继续信号)
  • children:并行子分支入口列表(并行扇出 + 汇合)
  • if:节点执行条件(条件不满足时跳过节点本体及其 children 子树)

规则取向:

  • next 必须显式写出,支持特殊语义如"RETURN"和"BREAK"
  • Stop_*节点,其他节点必须有 next ,否则spec编译报错
  • if 为 false 时:
    • 不执行该节点的实际动作(如不调用 service/llm/component、不写 out)
    • 同时跳过该节点的所有 children 子树
    • 直接进入 next

说明:

  • next / children / if 都是所有节点可拥有的通用属性,不属于某个特定节点类型。
  • next / children 在任何节点上都表示控制流出口,而不是数据端口

B 控制节点类型(Control Node Types)

用于表达结构化控制语义:

  • branch:条件分支(选择性执行某一条分支)
  • loop:循环(对集合数据或条件执行重复步骤,支持数组遍历模式与 while 条件模式)

说明:branch / loop 是"节点类型(kind)",它们本身也是 steps 中的一种节点。

C 入口与终止(Entry & Termination)

不设置显式的"开始节点"。入口规则与终止语义如下:

入口节点(Entry Nodes)

入口节点 = steps 中不被任何其他节点的 nextchildrencases 引用的节点。

  • 引擎在启动 workflow 时,自动识别所有入口节点
  • 若入口节点有且仅有一个:workflow 从该节点开始执行
  • 若入口节点有多个:这些入口节点被视为并行起点,引擎同时启动它们(等同于隐含一个虚拟的 children 扇出)
  • 若入口节点为零(所有节点都被其他节点引用):workflow 定义非法,引擎应报错(存在环路或结构错误)

不设显式 Start 节点的原因:入口点可由结构自动推断,额外的 Start 节点只会增加图上的冗余方块,不携带任何执行语义。

终止、暂停与失败语义

workflow 存在三种互斥的结束状态,其语义明确且不可混用:

结束方式 触发条件 状态 是否可恢复 调用方含义
完成(stopped) 执行到达 Stop_*节点 stopped 流程正常完成,可获取最终结果
暂停(paused) 执行到达 Pause_* 节点 paused 流程等待外部输入,需要 resume
失败(failed) 节点执行出错 failed 否(需重跑) 流程异常中断

补充说明:

  • Stop_* 用于表示 workflow 的正常完成,到达即终止,不可恢复。
  • Pause_* 节点用于表示 可恢复的执行中断,不等价于流程结束。
  • failed 表示执行异常,不属于正常控制流的一部分。

约束规则:

  • 每个 workflow 至少应存在一个 Stop_* 节点,以保证存在明确的正常完成路径。
  • 一个 workflow 可以包含多个 Stop_* 节点,用于表示不同分支或条件下的提前完成路径。

1.5 变量

变量用于承载轻量级、结构化的数据,例如参数、标识、状态标志或小规模结果。

变量具有以下特征:

  • 以键值形式存在
  • 生命周期限定在单次 Workflow 运行过程中
  • 适合传递控制信息与小体量数据
  • 不适合承载大文本或二进制内容

变量主要用于节点之间的数据传递与执行控制,而不是作为持久化产物存在。

全局变量(Workflow Globals)

  • 使用 $xxx 表示工作流全局变量
  • $vars 在 registry 中声明
  • 支持在步骤间传递数据,并作为最终输出的一部分

局部变量(Locals)

  • 使用 _xxx 表示局部/循环变量
  • 不需要声明,用于循环体与临时计算

系统变量(User Space Config)

  • 使用 SYSVAR.xxx 引用用户预先配置在系统空间里的通用变量
  • SYSVAR 为只读,常用于密钥、默认配置、偏好参数等

1.6 文件与工作空间

Workflow 在运行过程中可以通过 out 将结果写入文件空间。文件用于承载中间产物或结果性输出,例如代码文件、配置文件、文档等,其生命周期和管理方式由**工作空间(Workspace)**决定。

工作空间的基本概念

工作空间是 Workflow 运行时可选挂载的外部文件与状态承载空间。当 Workflow 运行时指定了 workspaceId,其所有文件写入行为将发生在该工作空间中;未指定时,系统会为本次运行创建临时文件空间。

工作空间的存在与否、以及具体使用哪个工作空间,均由运行时参数决定,而不是 Workflow 定义的一部分。

工作空间的组成

一个工作空间在逻辑上分为两个部分:

  1. 系统空间(System Space)
  2. 产物空间(Artifacts Space)

系统空间(System Space)

系统空间用于存储与工作空间自身相关的系统级与流程级元数据,例如:

  • Workflow 的运行状态
  • 暂停 / 恢复相关的信息
  • 中间状态标识、执行记录等

系统空间由平台自动管理:

  • 不提供给用户直接写入接口
  • 不作为业务产物参与编译或交付
  • 类似于数据库系统中自动维护的系统表

Workflow 在运行过程中可以使用系统空间承载必要的运行状态,但不直接暴露其结构与内容。


产物空间(Artifacts Space)

产物空间用于存储 Workflow 运行过程中产生或修改的业务文件与结果文件,例如:

  • 代码文件
  • 配置文件
  • 文档与资源文件

通过 out 写入的文件路径(以 / 开头),最终都会落入产物空间。

产物空间具有以下特征:

  • 文件内容对用户可见
  • 文件可以参与项目编译或交付
  • 文件的管理策略独立于 Workflow 本身

产物空间的版本管理能力

针对产物空间,工作空间本身提供版本管理能力,包括但不限于:

  • 提交(commit)
  • 回滚(rollback)

这些能力属于工作空间自身的能力,而不是 Workflow 的职责:

  • Workflow 不自动触发 commit
  • Workflow 不负责回滚策略
  • 是否提交、何时回滚由用户或外部系统决定

Workflow 的职责仅限于"在当前工作空间状态下生成或修改文件"。


工作空间的锁定机制

工作空间支持加锁机制,用于控制并发访问与写入冲突。

  • 在 Workflow 运行期间,可对工作空间加锁
  • 锁的粒度与策略由平台统一管理
  • 加锁期间,其他写入操作需等待或被拒绝

该机制用于保证:

  • Workflow 执行期间文件状态的一致性
  • 多次运行或多用户操作之间的安全隔离

设计原则

  • Workflow 关注的是文件如何生成或修改
  • 工作空间负责文件的存储、版本与并发控制
  • 系统空间与产物空间职责清晰,避免混用
  • 文件状态管理与 Workflow 执行解耦

通过引入工作空间机制,VL Workflow 能够在保持执行模型无状态的前提下,支持复杂的文件生成、修改与长期演进场景。

1.7 运行时参数

Workflow 在定义完成后,可以在每一次运行(run)时通过运行时参数对本次执行行为进行控制。运行时参数不属于 Workflow 定义本身,而是每次 run 的附加上下文,用于让同一份 Workflow 在不同场景下以不同方式执行。

运行时参数主要包含以下几类:


1) params(可选:业务入参)

用于向 workflow 传入业务数据

  • 入参名称与类型在 registry.params 中声明
  • 无默认值的入参为必填,缺失时引擎报错
  • 有默认值的入参可省略(使用声明中的默认值)
  • 入参在 workflow 内部以只读变量形式存在

2) workspaceId(可选:挂载文件空间)

用于指定本次运行所使用的外部文件空间。

  • 未指定:系统为本次 run 创建临时文件空间;节点通过 out 写入的文件路径(以 / 开头)将落到该临时空间;运行结束后按 TTL 自动清理。
  • 指定:节点通过 out 写入的文件路径将落到该 workspaceId 对应的文件空间中,文件持续保留,供后续运行或人工操作使用。

workspaceId 只影响本次 run 的文件落点,不改变 Workflow 定义。


3) nodes(可选:部分执行 / 重跑)

用于指定本次运行中实际参与执行的节点列表

  • 未指定:按 Workflow 定义从起始节点执行完整流程。
  • 指定:仅执行给定列表中的节点(引擎会按 Workflow 定义的顺序与依赖关系进行调度)。

该参数用于支持:

  • 增量执行(只跑某些生成/修复节点)
  • 失败后的局部重试
  • 规划阶段生成"执行指令"后按指令选择性运行

4) mode(可选:运行模式)

用于指定本次运行的整体执行模式。该模式为引擎提供统一的运行策略,用于影响节点的行为(尤其是文件写入相关行为)。

常见模式包括:

  • create:用于初始化或新增产物
  • patch:用于增量修改
  • regenerate:用于重建/重写
  • validate:仅校验,不产生文件写入

运行模式是 run 级别的策略参数,不属于 out 的一部分。


设计原则

  • 运行时参数是执行级控制,与 Workflow 定义解耦
  • 不同 run 的参数相互独立,可并行运行
  • params 传入业务数据,workspaceIdnodesmode 控制执行行为与文件落点
  • Workflow 通过 out 的一层映射同时支持变量与文件输出:
    • $... 写入变量
    • /... 写入文件空间(临时或 workspace)

通过运行时参数机制,Workflow 能在保持结构稳定的同时,支持新项目生成、增量修改、局部重跑与校验等多种运行方式。

示例

// 示例:Workflow 的运行时参数(runParams)
// 说明:
// - 该示例展示一次"增量修改"的运行方式
// - 同一份 Workflow 定义,在不同 runParams 下可以表现出完全不同的执行行为

{
  // 要执行的 workflow 定义
  "workflowId": "codegen_apply_v1",

  // 本次 workflow run 的运行时参数
  "runParams": {
    // 可选:业务入参(对应 registry.params 中声明的入参)
    "params": {
      "userRequest": "帮我生成一个登录页面",
      "targetLang": "zh-CN"
    },

    // 可选:指定外部文件空间
    // - 不指定:使用临时文件空间(有 TTL)
    // - 指定:文件写入落到该 workspace 中
    "workspaceId": "project_workspace_123",

    // 可选:仅执行部分节点
    // 用于增量执行、失败后重跑、或按规划指令执行
    "nodes": [
      "blueprint",
      "contract",
      "file_backend"
    ],

    // 可选:运行模式(run 级别策略)
    // - create      初始化 / 新建
    // - patch       增量修改
    // - regenerate 重建 / 重写
    // - validate    仅校验,不写文件
    "mode": "patch"
  }
}

1.8 节点就绪即执行与并行调度(Eager Execution)

Workflow 的执行引擎采用 "就绪即执行"(Eager Execution) 的调度策略:

核心规则:当一个节点的所有输入条件(前置依赖)已经全部满足时,该节点即可立即开始执行,无需等待与其无依赖关系的其他节点完成。

这意味着 Workflow 的实际执行顺序不是简单的线性序列,而是由 数据依赖关系 决定的偏序执行图(Partial Order)。多个彼此无依赖的节点可以被引擎同时调度和执行。


对 workflow.json 生成的指导意义

在设计和生成 workflow.json 时,应 尽量利用并行性,具体原则如下:

  1. 能并行就并行:如果多个节点之间不存在数据依赖关系(即节点 B 不需要读取节点 A 的输出),则应将它们设计为可并行执行的结构(如使用 children 扇出,或在 Loop_* 中使用 mode:"parallel")。
  2. 避免不必要的串行:不要仅因为"书写顺序"而将无依赖关系的节点串联为 next 链。只有当节点 B 确实依赖节点 A 的输出(如 B 的 in 引用了 A 写入的 $vars)时,才应将 A → B 设为串行。
  3. 依赖判定标准:节点 B 依赖节点 A,当且仅当:
    • B 的 in 中引用了 A 通过 outSet_* 写入的全局变量($xxx
    • B 需要读取 A 通过 Write_*out 写入的文件
    • B 在控制流上显式依赖 A(如 A 的 next 指向 B)
  4. 推荐结构模式
    • 对于多个独立的 LLM 调用或 Service 调用,优先使用 children 并行扇出
    • 对于集合数据的处理,优先使用 Loop_* mode:"parallel"
    • 仅在确有顺序依赖时使用 next 串行连接

示例

// ❌ 不推荐:无依赖关系却串行排列
{
  "id": "LLM_GenFrontend",
  "in": { "messages": [{ "role": "user", "content": "=$prd" }] },
  "out": { "$frontendCode": "=_result" },
  "next": "LLM_GenBackend"   // GenBackend 并不依赖 GenFrontend 的输出
}

// ✅ 推荐:使用 children 并行执行无依赖节点
{
  "id": "Set_StartGen",
  "target": "$status",
  "value": "\"generating\"",
  "children": ["LLM_GenFrontend", "LLM_GenBackend"],
  "next": "Service_Merge"
}

引擎行为约束

  • 引擎在调度时,应根据节点的实际依赖关系构建 DAG(有向无环图),并尽可能并行执行就绪节点
  • children 中列出的节点天然并行;但即使是通过 next 串联的节点,如果引擎检测到无实际数据依赖,也 允许 提前调度(但这属于引擎优化,不改变语义正确性)
  • steps 数组中的顺序不影响执行顺序,执行顺序完全由 next / children / branch / loop 等控制流结构以及数据依赖关系决定

通过就绪即执行策略,Workflow 能在保持语义正确性的同时,最大化执行并行度,显著提升整体吞吐效率。

1.9 错误处理

Workflow 在执行过程中可能遇到各类异常情况,包括但不限于:

  • 调用型节点执行失败(如 Service 调用超时、LLM 返回异常、Component 执行错误)
  • 表达式求值失败(如引用了不存在的变量或字段)
  • 文件写入失败(如路径越界、IO 错误)
  • 类型错误(如 Loop source 求值结果不是数组)

1.9.1 默认错误行为

当节点执行发生不可恢复的错误时,workflow 进入 failed 状态:

  • failed 不同于 paused(由 Pause_* 节点触发的可恢复挂起)和 stopped(正常终止)
  • 引擎停止后续节点的执行
  • 已启动的并行分支(children / parallel loop 迭代)由引擎决定是否等待其完成或立即中止

1.9.2 Failed 返回结构

当 workflow 进入 failed 状态时,引擎返回:

  • status: "failed"
  • failedAt: "<stepId>"(失败节点的 id)
  • error: { code: "<errorCode>", message: "<errorMessage>" }
  • vars: {...}(失败时的全局变量快照,用于调试与重跑)

1.9.3 与运行时参数的关系

failed 状态下,可通过运行时参数 nodes 指定失败节点及其后续节点进行局部重跑,实现失败恢复。


1.9.4 设计原则

  • 错误处理采用 fail-fast 策略:节点失败即终止整个 workflow,不做隐式重试
  • 重试与容错由外部系统(调用方)决定,不内建于 workflow 定义中
  • 支持节点级 onError:节点失败时,若配置了 onError,则转入指定失败处理节点;未配置时保持 fail-fast

1.10 流程图简洁性规则(Graph Simplicity)

Workflow 在可视化时表现为流程图(Graph),每个节点对应图上的一个方块,每条 next / children 对应一条连线。为了保持流程图的 可读性与稳定性,在设计和生成 workflow.json 时应遵循以下简洁性原则:


1.10.1 核心原则:能内联就不外拆

调用型节点(Service_* / API_* / Component_* / LLM_*)自身已具备通过 out 写入变量和文件的能力。 当变量写入或文件写入的内容来源于当前节点的 _result 时,应直接在该节点的 out 中完成,不应 额外创建独立的 Set_*Write_* 节点。

// ❌ 不推荐:多余的 Set 节点,增加了图上的方块和连线
{ "id": "LLM_GenCode", "in": {...}, "out": "$rawResult", "next": "Set_ExtractStatus" }
{ "id": "Set_ExtractStatus", "target": "$status", "value": "=$rawResult.status", "next": "Stop_End" }

// ✅ 推荐:在 out 中直接完成映射,图上只有一个节点
{ "id": "LLM_GenCode", "in": {...}, "out": { "$rawResult": "=_result", "$status": "=_result.status" }, "next": "Stop_End" }

1.10.2 何时使用独立的 Set_* / Write_* 节点

独立状态写入节点仅在以下场景中使用:

  1. 写入内容不来自任何节点的 _result — 例如需要拼接多个变量、写入常量、或进行纯计算
  2. 需要在控制流的特定位置写入 — 例如在 Branch_* 之前设置分支条件变量,或在 Loop_* 之前初始化计数器
  3. 需要特殊写入策略 — 例如 Write_*mode:"append"mode:"failIfExists",这些是 out 文件写入不支持的
  4. 需要在并行扇出前设置共享状态 — 例如 Set_* 后接 children 并行启动

1.10.3 控制节点的合并原则

  • 避免空壳节点:如果一个 Set_* 节点的唯一作用是设置变量然后立刻 next 到下一个节点,且该变量可以在前一个调用型节点的 out 中完成,则该 Set_* 节点应被合并
  • Branch / Loop 保持独立:控制节点(Branch_* / Loop_*)本身代表流程结构语义,应始终作为独立节点存在
  • Stop 保持独立Stop_* 代表明确的终止语义,应始终作为独立节点存在

1.10.4 并行结构的简化

  • 如果两个节点可以并行执行且无依赖关系,优先使用 children 扇出,而不是串行排列后依赖引擎优化
  • 如果并行扇出的父节点不需要做任何计算或写入,可以使用一个轻量的 Set_* 节点作为扇出锚点(设置状态标志),避免为此创建一个空的 Service_* 调用

1.10.5 节点数量参考基准

  • 一个典型的业务流程应以 5–15 个节点 为目标
  • 如果节点数超过 20 个,应检查是否存在可合并的 Set/Write 节点
  • 嵌套的 Loop / Branch 内部节点不计入上层计数

1.10.6 总结

场景 推荐做法 避免做法
调用节点输出写变量 out中直接映射 创建额外 Set_*
调用节点输出写文件 out中使用 /path写入 创建额外 Write_*
纯计算 / 拼接写入 使用独立 Set_* 无法内联,独立节点是正确选择
特殊写入策略 (append) 使用独立 Write_* out不支持 mode,独立节点是正确选择
多个无依赖调用 children并行扇出 next串行排列
条件判断 独立 Branch_*节点 不应合并到其他节点

第二章:Spec JSON 顶层结构

工作流以一个 JSON 对象表示,顶层结构固定如下:

{
  "version": "当前版本号",
  "name": "string",
  "registry": { },
  "steps": [ ]
}

2.1 字段定义

2.1.1 version(必填)

  • 固定值:"当前版本号"

2.1.2 name(必填)

  • 工作流名称(用于展示、检索、日志定位)
  • 类型:string

2.1.3 registry(必填)

  • 外部资源与全局边界声明区(先注册后使用)
  • 类型:object
  • 结构在第三章定义

2.1.4 steps(必填)

  • 节点列表(workflow 的主体)
  • 类型:array<Step>
  • Step 的结构在后续章节定义

2.2 顶层约束

  • registrysteps 必须同时存在
  • steps 必须非空
  • 节点的执行与连接关系不依赖数组顺序,只依赖节点的 next / children / branch / loop 等显式结构(因此数组顺序仅用于阅读与 diff)

2.3 图结构校验规则(Graph Validation)

workflow 的 steps 在结构上构成一个有向图(Directed Graph)。为确保所有节点都有意义且可执行,引擎在加载 workflow 时应执行以下校验:


2.3.1 入口节点校验

  • 入口节点 = 不被任何其他节点的 nextchildrencases 引用的节点(见 1.4 C 节)
  • 入口节点数量必须 ≥ 1,否则 workflow 非法(存在环路或结构错误)

2.3.2 可达性校验(Reachability)

  • 每个节点必须从至少一个入口节点可达
  • 可达的定义:从入口节点出发,沿 nextchildrencases(含 ELSE)的引用链能够到达该节点
  • 不可达的节点视为死代码,引擎应报错或发出警告

2.3.3 引用完整性校验

  • 所有 nextchildrencases 中引用的 step id 必须在 steps 中存在
  • 不允许引用不存在的节点
  • 不允许节点 id 重复

2.3.4 Stop_* 校验

  • 推荐至少存在一个 Stop_* 节点(缺失时引擎发出警告)
  • Stop_* 不允许有 nextchildren(若存在应报错)

2.3.5 校验级别建议

校验项 级别 说明
入口节点 ≥ 1 ERROR 无入口则无法启动
所有节点可达 ERROR 不可达节点是死代码
引用 id 存在 ERROR 断链导致运行时崩溃
id 不重复 ERROR 控制流歧义
至少一个 Stop_* WARNING 纯审批流可能无 Stop
Stop_* 无 next/children ERROR 语义冲突

2.4 3.16 增量编译检查

检查项 规则 级别
while 与 source 互斥 同一 Loop_* 同时声明 whilesource ERROR
while 必填项 while 模式缺少 maxIterations ERROR
maxIterations 范围 maxIterations < 1 ERROR
while mode 约束 while + mode:"parallel" ERROR
BREAK 作用域 BREAK 出现在非 Loop_* children 子树 ERROR
BREAK 保留字 BREAK 被用作 stepId ERROR
LLM model 空值 LLM_* 节点 model 为空字符串 ERROR
LLM model 分段 model/ 但任一侧为空 ERROR

第三章:Registry

registry 用于声明 workflow 运行所需的外部依赖全局边界,并强制执行"先注册后使用"。workflow 只负责执行逻辑,不内置任何特定业务闭环(如 git commit、审批提交、触发下一个流程等),这些均由 workflow 外部系统完成。


3.1 Registry 顶层结构

{
  "params": [ ... ],
  "services": [ ... ],
  "apis": [ ... ],
  "components": [ ... ],
  "vars": [ ... ],
  "files": {
    "inputs": [ ... ],
    "artifacts": [ ... ]
  },
  "docs": { ... },
  "schemas": { ... }
}

3.2 params(选填)

声明 workflow 接受的运行时入参。入参由调用方在 runParams.params 中传入,workflow 内部以只读变量形式访问。

3.2.1 结构

params 是字符串数组,每个元素声明一个入参名称与类型,可带默认值:

"params": [
  "userRequest(STRING)",
  "targetLang(STRING)",
  "maxRetries(INT) = 3"
]

3.2.2 规则

  • 入参声明不带 $ 前缀,与 VL 语法中服务/方法的入参风格对齐
  • 有默认值的入参在 runParams.params 中可省略;无默认值的入参为必填,缺失时引擎报错
  • 入参在 workflow 内部以只读变量形式存在,步骤中直接用参数名引用(如 =userRequest),不可出现在 out 左侧或 Set_*target
  • 入参名称不得与 registry.vars 中的全局变量名冲突

3.3 services(必填)

声明 workflow 会调用的项目内服务,并在 registry 中明确其入参/出参契约(用于静态校验与 AI 生成约束)。

3.3.1 结构(VL 单行签名)

services 是字符串数组,每个元素是一条服务签名:

"services": [
  "PlannerService(prd(STRING), rulesFile(FILE_REF)) RETURN plan(OBJECT)",
  "WriteFileService(path(STRING), content(STRING)) RETURN ok(BOOL)",
  "ApprovalService(form(OBJECT), policy(STRING)) RETURN decision(STRING), comment(STRING)"
]

3.3.2 规则

  • ServiceName(...) RETURN ... 为固定格式
  • 入参用 paramName(Type) 表示,可多个
  • 出参用 resultName(Type) 表示,可多个
  • ServiceName 必须唯一
  • Service_* 节点的 id 中 Service_ 后缀部分必须匹配 registry.services 中的某个 ServiceName
  • service 节点传入参数必须满足签名约束;返回结果应满足 RETURN 声明

3.4 apis(选填)

声明 workflow 会调用的第三方外部 API,并在 registry 中明确其端点、方法与认证方式

API_* 节点与 Service_* 的区别在于:Service_* 调用的是 VL 项目内部的服务(由项目自身定义和部署),而 API_* 调用的是项目外部的第三方 HTTP 接口(如支付网关、地图服务、天气 API、SaaS 平台 API 等)。

3.4.1 结构

apis 是对象数组,每个元素声明一个第三方 API 端点:

"apis": [
  {
    "id": "StripeCreateCharge",
    "method": "POST",
    "url": "https://api.stripe.com/v1/charges",
    "auth": "SYSVAR.stripeApiKey",
    "headers": { "Content-Type": "application/x-www-form-urlencoded" },
    "desc": "Stripe 创建收费"
  },
  {
    "id": "WeatherQuery",
    "method": "GET",
    "url": "https://api.weatherapi.com/v1/current.json",
    "auth": "SYSVAR.weatherKey",
    "desc": "查询当前天气"
  },
  {
    "id": "SlackPostMessage",
    "method": "POST",
    "url": "https://slack.com/api/chat.postMessage",
    "auth": "SYSVAR.slackBotToken",
    "headers": { "Content-Type": "application/json" },
    "desc": "发送 Slack 消息"
  }
]

3.4.2 字段说明

字段 必填 类型 说明
id string API 唯一标识,API_*节点 id 的 API_后缀部分必须匹配此 id
method string HTTP 方法:GET / POST / PUT / PATCH / DELETE
url string API 端点 URL(可包含路径参数占位符,如 {orderId}
auth string 认证凭据来源(通常引用 SYSVAR.xxx),引擎负责注入到请求头
headers object 固定请求头(键值对),每次调用自动携带
desc string 人类可读的用途说明

3.4.3 规则

  • id 必须唯一
  • API_* 节点的 id 后缀必须匹配 registry.apis 中的某个 id
  • auth 引用的凭据建议存放在 SYSVAR 中(系统级密钥管理),不应明文写入 workflow 定义
  • url 中的路径参数(如 {orderId})在运行时由节点 in.pathParams 替换
  • API 的实际调用由引擎层执行,workflow 节点只声明请求参数,不直接发起 HTTP 请求

3.5 components(必填)

声明 workflow 会调用的系统内置组件能力(例如 MCP、文件能力等)。

"components": ["FileOps", "MCP_Search", "MCP_VectorDB"]

规则:

  • componentId 必须唯一
  • Component_* 节点引用的 componentId 必须在该列表中
  • component 的入参/出参由系统内置定义,不要求在 registry 中重复声明

3.6 vars(必填)

声明 workflow 可读写的全局变量集合(VL 风格)。

"vars": ["$keyword(STRING)", "$items([OBJECT])", "$result(OBJECT)", "$count(INT)"]

规则:

  • 变量名必须以 $ 开头
  • 变量名必须唯一
  • workflow 中出现的任何 $xxx 必须在此声明
  • set.target 与节点 out 只能写入已声明的 $xxx

3.7 files(必填)

声明 workflow 的文件读写边界(只读输入 + 临时产物)。

"files": {
  "inputs": ["Process/PRD.json", "Process/Rules/*"],
  "artifacts": ["Process/Artifacts/*"]
}

3.7.1 inputs(只读)

规则:

  • workflow 中所有读取文件地址的行为必须落在 inputs 范围内
  • inputs 在 workflow 运行期间视为只读,不允许修改

3.7.2 artifacts(临时可写)

规则:

  • workflow 中所有写入文件地址的行为必须落在 artifacts 范围内
  • artifacts 是 run scope 的临时文件空间,不承诺长期可见性或稳定读取入口

3.8 docs(语义文档引用)

registry.docs 用于声明 workflow 可能引用的语义文档标识。这些文档不以"文件路径"形式暴露给 workflow,而是通过 稳定的 docId 引用。


3.8.1 定位与原则

  • docs语义级引用表,不是文件系统
  • workflow 只感知 docId,不感知路径
  • 文档 只读
  • 文档的真实存储位置、加载方式由系统管理,不属于 workflow 规范
  • docs 的作用是:为 LLM / service 提供稳定、可复用的背景知识引用

3.8.2 结构

docs 是一个对象,key 为 docId(字符串或数字),value 为该文档的语义说明:

"docs": {
  "11": "VL 语法与表达式规则",
  "12": "Workflow v2.x 设计约束说明",
  "20": "前端组件生成规范"
}

说明:

  • docId 必须在 workflow 内唯一
  • value 是人类可读的用途描述,用于:
    • 理解 workflow
    • 辅助 AI 生成
    • 审计与维护

3.8.3 使用方式

在 workflow 中,节点只引用 docId,不引用路径或内容。

常见用法(示意):

{
  "id": "LLM_Generate",
  "in": {
    "docs": ["11", "20"],
    "messages": [
      { "role": "system", "content": "请严格遵守相关规范。" }
    ]
  }
}

语义:

  • "11" 表示「VL 语法与表达式规则」
  • "20" 表示「前端组件生成规范」
  • 文档的实际内容如何注入 prompt,由系统实现决定,不属于workflow规范

3.8.4 约束规则

  • workflow 中引用的 docId 必须已在 registry.docs 中声明
  • workflow 不能修改 docs
  • docs 不参与表达式计算
  • docs 不影响控制流

3.8.5 与 files.inputs 的关系

对比项 files.inputs docs
暴露形式 文件路径 docId
是否 IO
是否参与计算
主要使用者 Service / Component LLM(为主)
关注点 数据 语义 / 规则 / 规范

3.9 Registry 通用约束(强制)

  1. 先注册后使用
    1. service / api / component / 全局变量 / 入参 / 文件地址引用均必须先在 registry 声明
    2. LLM_* 节点是例外:不需要在 registry 中注册。其模型通过节点 model 字段与运行环境默认规则解析
  2. 唯一性
    1. services.id、apis.id、components、params、vars 均不允许重复
    2. params 与 vars 之间不允许同名
  3. 只读与可写边界明确
    1. params 只读(入参不可被 workflow 修改)
    2. inputs 只读
    3. docs 只读
    4. artifacts 可写(临时产物)
  4. 保持逻辑纯净
    1. registry 只描述运行所需依赖与边界,不表达业务闭环语义(如 commit、发布、审批提交等)

3.10 schemas(JSON Schema 复用,选填)

registry.schemas 用于集中声明可复用的 JSON Schema,供 LLM_* 节点在 output_config.format.schemaRef 中引用。

"schemas": {
  "SpecSchema": { "type": "object", "additionalProperties": false, "properties": { ... } },
  "PlanSchema": { "type": "object", "additionalProperties": false, "properties": { ... } }
}

规则:

  • schemaId(key)在 workflow 内唯一
  • schemaRef 必须能在 registry.schemas 中找到
  • 若同时声明 schemaschemaRef,建议视为错误(避免歧义)
  • 执行前引擎需将 schemaRef 归一化展开为 format.schema

3.11 模型配置边界

本规范采用“本地 SDK 内置”模式:LLM provider 由引擎内置 SDK 调用,不经由统一 LLM_URL 网关。

规则:

  • 不在 registry 中声明模型列表或 API key
  • LLM_* 节点通过 model 字段声明 provider/model;model 允许以下三种写法:
    • 不写 model:使用运行环境全局默认 LLM_MODEL(格式为 <provider>/<modelId>
    • model: "<provider>":仅声明 provider,modelId 取运行环境 <PROVIDER>_MODEL
    • model: "<provider>/<modelId>":节点内完全指定
  • 运行环境仅负责提供 provider 级凭据(如 OPENAI_API_KEYANTHROPIC_API_KEY)与默认模型值
  • workflow 只声明流程逻辑与输出约束(如 output_configschemaRef),不声明密钥

最小 .env 示例:

LLM_MODEL=anthropic/claude-sonnet-4-5-20250929
ANTHROPIC_API_KEY=sk-ant-xxx
OPENAI_API_KEY=sk-openai-xxx
ANTHROPIC_MODEL=claude-sonnet-4-5-20250929
OPENAI_MODEL=gpt-4.1

第四章:节点类型(Step Types)

steps 中所有节点分为三大类:调用型节点状态写入节点控制节点


4.1 调用型节点(Call Nodes)

用于调用外部能力并产出结果(可流式输出)。

  • Service_*:调用项目内服务(需在 registry.services 注册并声明签名)
  • API_*:调用第三方外部 HTTP API(需在 registry.apis 注册端点与认证信息)
  • Component_*:调用系统内置组件能力(需在 registry.components 注册)
  • LLM_*:调用大模型推理能力(可流式输出;不需要在 registry 中注册,模型通过节点 model 字段选择)
  • Download_*:从外部来源下载单文件并落盘到 artifacts(流式,不经过 $vars 承载正文)
  • Unzip_*:读取 zip 并逐 entry 解压到 artifacts(流式分发,支持后缀路由)

4.2 状态写入节点(State Nodes)

用于把数据写入 workflow 内部状态空间(变量 / 临时文件),作为步骤间数据传递与过程记录手段。

  • Set_*:写入/更新全局变量($vars
  • Write_*:写入 workflow 空间内的临时文件(artifacts)

4.3 控制节点(Control Nodes)

用于表达流程结构与执行策略。

  • Branch_*:条件分支(选择性执行某一条分支)
  • Loop_*:循环节点(对集合执行重复步骤或按条件循环;支持 sourcewhile 两种互斥模式)
  • Stop_*:终止节点(明确结束 workflow)
  • Pause_*:暂停节点(挂起 workflow 等待外部 resume,可恢复)

第五章:节点属性(Step Properties)

5.1 属性概览(总表)

属性 一句话简介 适用节点类型 Service_* API_* Component_* LLM_* Download_* Unzip_* Set_* Write_* Branch_* Loop_* Stop_* Pause_*
id 节点唯一标识(同时编码类型,如 Service_xxx) 全部 必填 必填 必填 必填 必填 必填 必填 必填 必填 必填 必填 必填
if 条件表达式;为 false 时跳过该节点及其 children 子树 除Stop_* 选填 选填 选填 选填 选填 选填 选填 选填 选填 选填 不适用 选填
in 调用入参对象(由服务/组件/LLM/API 定义) 调用型节点 必填 必填 必填 必填 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用
out 将本节点输出_result 映射写入 \$vars 或文件 调用型节点 选填 选填 选填 选填 选填 选填 不适用 不适用 不适用 不适用 不适用 不适用
model LLM 选择字段(支持 <provider>/<modelId> / <provider> / 省略) LLM_* 不适用 不适用 不适用 选填 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用
source 下载来源 / zip 来源 / loop 数据源(与 while 互斥) Download/Unzip/Loop 不适用 不适用 不适用 不适用 必填 必填 不适用 不适用 不适用 条件必填 不适用 不适用
while 条件循环表达式(仅 Loop_*,与 source 互斥) Loop_* 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 条件必填 不适用 不适用
maxIterations 迭代上限(while 时必填,source 时选填) Loop_* 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 选填 不适用 不适用
routeByExt 按后缀路由目录映射 Download/Unzip 不适用 不适用 不适用 不适用 选填 必填 不适用 不适用 不适用 不适用 不适用 不适用
defaultDir 路由未命中时的兜底目录 Download/Unzip 不适用 不适用 不适用 不适用 选填 选填 不适用 不适用 不适用 不适用 不适用 不适用
overwrite 解压覆盖策略(默认 true) Unzip_* 不适用 不适用 不适用 不适用 不适用 选填 不适用 不适用 不适用 不适用 不适用 不适用
target 状态写入目标(变量路径或文件路径) 状态写入节点 不适用 不适用 不适用 不适用 选填 不适用 必填 必填 不适用 不适用 不适用 不适用
value 写入内容/值(表达式) 状态写入节点 不适用 不适用 不适用 不适用 不适用 不适用 必填 必填 不适用 不适用 不适用 不适用
children 并行子分支入口列表(join 后再走 next) 除Stop_* 选填 选填 选填 选填 选填 选填 选填 选填 选填 必填 不适用 不适用
next 串行后继节点, 除Stop_*外必填 除Stop_* 必填 必填 必填 必填 必填 必填 必填 必填 必填 必填 不适用 必填
cases 分支规则列表 Branch_* 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 必填 不适用 不适用 不适用
mode 执行/写入模式 Loop / Write 不适用 不适用 不适用 不适用 不适用 不适用 不适用 选填 不适用 必填 不适用 不适用
meta 元信息(展示/追踪,不参与执行语义) 全部 选填 选填 选填 选填 选填 选填 选填 选填 选填 选填 选填 选填
print 节点结束后输出自定义消息(流式事件) 除Stop_* 选填 选填 选填 选填 选填 选填 选填 选填 选填 选填 不适用 不适用
reason 暂停等待原因文案(前端/通知/日志展示) Pause_* 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 选填
resumeResultTarget resume payload 写入的 \$vars 路径 Pause_* 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 必填
timeout 超时配置(sec: 秒数, on: 超时后继节点) Pause_* 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 不适用 选填

* inService_* / API_* / Component_* / LLM_*:语法上可选,但调用型节点通常应填写(建议实现侧强制)。

Stop_*:不需要 next/children,到达即终止;若出现也应视为非法或忽略(建议非法)。

5.2 属性详解

本节逐个定义step 的属性语义、约束与推荐写法。

5.2.1 id

  • 类型string
  • 必填:是(每个 step 必须有)
  • 作用:节点唯一标识,用于 next / children 等引用;同时用前缀表达节点类型,因此 不再需要 kind

前缀约束(强制):

id 前缀是引擎识别节点类型的 唯一依据。不符合以下已定义前缀的 id 视为非法,引擎应报错:

  • Service_xxx
  • API_xxx
  • Component_xxx
  • LLM_xxx
  • Download_xxx
  • Unzip_xxx
  • Set_xxx
  • Write_xxx
  • Branch_xxx
  • Loop_xxx
  • Stop_xxx
  • Pause_xxx

示例:

{ "id": "Service_Approval", "next": "Branch_CheckAmount" }

5.2.2 if

  • 类型string(表达式)
  • 必填:否
  • 作用:控制"是否执行该节点及其子树(children)"

规则:

  • if 不写 ⇒ 默认执行
  • if 求值为 true ⇒ 执行该节点本体,并按正常规则执行 children,最后进入 next
  • if 求值为 false跳过该节点本体 + 跳过该节点的所有 children 子树,然后直接进入next

示例:

{
  "id": "Service_SendNotify",
  "if": "=$needNotify == true",
  "in": { "msg": "=$text" },
  "next": "Stop_End"
}

5.2.3 in

  • 类型object
  • 必填:对 Loop_* 为必填;对 Service_* / API_* / Component_* / LLM_* 通常必填
  • 作用:节点输入参数。不同节点类型的 in 结构不同。

规则(通用)

  • in 是一个键值对象:{ key: value }
  • value 可以是:
    • 常量(字符串/数字/布尔/对象/数组)
    • 全局变量引用:$xxx
    • 系统变量引用:SYSVAR.xxx
    • 局部变量引用:_item / _index / _result / _meta / _error
    • 表达式(按表达式规则求值)
  • 若节点被 if=false 跳过,则该节点的 in 不会被执行/求值

Service_*in

  • in 的字段必须匹配 registry.services 中该服务的入参签名
  • 允许少字段(若服务入参允许缺省),但建议引擎做静态校验

示例:

{
  "id": "Service_PlannerService",
  "in": { "prd": "=$prdText", "rulesFile": "Process/Rules/rules.txt" },
  "out": "$plan",
  "next": "LLM_Generate"
}

Component_*in

  • in 结构由系统组件定义(不在 registry 重复声明)
  • 同样允许引用 $vars / SYSVAR / _item / _index

示例:

{
  "id": "Component_MCP_Search",
  "in": { "query": "=$keyword", "topK": 5 },
  "out": "$hits"
}

LLM_*model(选填,3.16 更新)

LLM_* 节点通过 model 字段选择 provider/model,支持三种写法:

  • 不写 model:使用环境变量 LLM_MODEL(格式 <provider>/<modelId>
  • model: "<provider>":仅指定 provider,modelId 从环境变量 <PROVIDER>_MODEL 获取
  • model: "<provider>/<modelId>":节点内完全指定

校验规则:

  • model 写了但为空字符串 → 编译错误
  • model 包含 / 但左右任一侧为空(如 /gpt-4.1openai/)→ 编译错误
  • 解析出的 provider 不受支持 → 运行时错误
  • 对应 provider 的 API key 缺失 → 运行时错误
  • 仅写 provider 但无 <PROVIDER>_MODEL → 运行时错误
  • 不写 model 且无 LLM_MODEL → 运行时错误

LLM_*in

  • in 结构由 LLM 调用协议定义(如 messages / stream / output_config
  • 支持结构化输出(output_config):通过 output_config.format.type = "json_schema" 配合 format.schema,可强制 LLM 输出严格符合指定 JSON Schema 的内容。引擎自动将返回的 JSON parse 为对象绑定到 _result
  • output_config 为可选字段。workflow 统一写 in.output_config,引擎按 provider 做参数映射,不要求作者写各厂商原生字段
  • 支持流式输出(in.stream: true
output_config 字段定义

in.output_config 为可选字段,仅适用于 LLM_* 节点。该字段用于声明输出约束;其中 format 是规范定义字段。

跨 provider 兼容规则:

  • workflow 只写统一 in.output_config
  • openai/*:引擎映射到 OpenAI 的 response_format
  • anthropic/*:引擎映射到 Anthropic 对应输入结构(保持 output_config.format 语义)
  • 若目标 provider 不支持对应能力或无法映射,运行时错误(不得静默降级)

统一字段结构:

字段 类型 必填 说明
output_config object 指定 LLM 输出格式约束。不填则为自由文本输出
output_config.format object 是(当 output_config 存在时) 输出格式配置对象
output_config.format.type string 支持 "text""json_object""json_schema" 三个值。"text" 为默认行为(自由文本),无需显式声明
output_config.format.schema object 是(当 type"json_schema" 时) 标准 JSON Schema 对象,定义输出的结构约束。必须包含 "additionalProperties": false
output_config.format.schemaRef string 是(当 type"json_schema" 且未提供 schema 时) 引用 registry.schemas 中已声明的 schemaId。执行前引擎必须展开为 format.schema;若找不到引用应报错

json_object 模式新增约束:

  • type = "json_object" 时,不得提供 schemaschemaRef
  • type = "json_object" 且同时提供 schema / schemaRef,引擎应报 bad_request
  • json_objectjson_schema 语义互斥(由 type 单选表达)
LLM 节点运行时上下文(3.10)

LLM_* 节点执行时,引擎统一提供三元组上下文:

字段 语义 成功时 失败时
_result 业务内容本体 必有 不参与正常流程
_meta 调用元信息(usage/model/latency/request_id 等) 必有 可有
_error 失败信息(标准化错误对象) 必有

其中 _result 规则固定如下:

场景 _result 类型 说明
output_config(或 format.type: "text" string 自由文本输出,正文直接存于 _result
output_config.format.type = "json_object" 已解析的 JSON 对象/数组 引擎 parse 后直接绑定,不做 schema 校验。out 可路径访问,但不保证字段存在性/类型
output_config.format.type = "json_schema" 已解析且通过校验的 JSON 对象/数组 out 中可直接路径访问(如 =_result.score

引擎保证:

  • type = "json_object" 时,_result 一定是可解析 JSON;parse 失败时 _result = null_error.type = "json_parse_error"
  • json_object 模式不保证字段存在性;下游路径访问前应做防御性判断,或使用 onError 兜底
  • type = "json_schema" 时,_result 一定是合法且符合 schema 的对象(或数组),无需下游再次 parse 或校验
stream 的兼容性

output_configin.stream: true 可同时使用:

  • 流式传输期间,引擎逐步接收 JSON 片段
  • 流式完成后,引擎将完整 JSON 文本 parse 为对象,再绑定到 _result
  • out 映射始终在流式完成后执行,因此 _result/_meta/_errorout 求值时均为最终态
引擎映射规则

引擎对 in.output_config 执行统一抽象映射:

  • workflow 层统一写 in.output_config
  • OpenAI provider:映射到 response_format
  • Anthropic provider:映射到对应输入结构(保持 output_config.format 语义)
  • 无法映射时,运行时错误(不得静默降级)

示例(结构化输出 — Anthropic Claude):

{
  "id": "LLM_Generate",
  "in": {
    "stream": true,
    "messages": [
      { "role": "system", "content": "Generate a project plan based on the PRD." },
      { "role": "user", "content": "=$prdText" }
    ],
    "output_config": {
      "format": {
        "type": "json_schema",
        "schema": {
          "type": "object",
          "properties": {
            "title": { "type": "string" },
            "phases": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "tasks": { "type": "array", "items": { "type": "string" } }
                },
                "required": ["name", "tasks"],
                "additionalProperties": false
              }
            }
          },
          "required": ["title", "phases"],
          "additionalProperties": false
        }
      }
    }
  },
  "out": {
    "$planTitle": "=_result.title",
    "$planPhases": "=_result.phases"
  },
  "next": "Service_Validate"
}

示例(Loop 中使用结构化输出):

{
  "id": "LLM_ExtractEntity",
  "in": {
    "messages": [
      { "role": "system", "content": "Extract structured entity information from the given text." },
      { "role": "user", "content": "=_item.rawText" }
    ],
    "output_config": {
      "format": {
        "type": "json_schema",
        "schema": {
          "type": "object",
          "properties": {
            "name": { "type": "string" },
            "type": { "type": "string", "enum": ["person", "org", "location", "other"] },
            "confidence": { "type": "number" }
          },
          "required": ["name", "type", "confidence"],
          "additionalProperties": false
        }
      }
    }
  },
  "out": {
    "$entities[_index]": "=_result"
  },
  "next": "Write_SaveEntity"
}

示例(自由文本输出,与 3.6 行为一致):

{
  "id": "LLM_FreeChat",
  "in": {
    "stream": true,
    "messages": [
      { "role": "user", "content": "=$userQuestion" }
    ]
  },
  "out": "$answer",
  "next": "Stop_End"
}

示例(json_object:要求合法 JSON,不约束结构):

{
  "id": "LLM_Analyze",
  "in": {
    "messages": [
      { "role": "system", "content": "Analyze and return JSON with your own structure." },
      { "role": "user", "content": "=$inputText" }
    ],
    "output_config": {
      "format": { "type": "json_object" }
    }
  },
  "out": { "$analysis": "=_result" },
  "next": "Stop_End"
}

不写 output_config 即表示自由文本输出,_result 为文本正文字符串。

三种模式对比:

// 自由文本(默认,不写 output_config)
{ "id": "LLM_Chat", "in": { "messages": [...] } }

// JSON Object 模式(有结构但不约束 schema)
{ "id": "LLM_Analyze", "in": { "messages": [...], "output_config": { "format": { "type": "json_object" } } } }

// JSON Schema 模式(强约束结构)
{ "id": "LLM_Extract", "in": { "messages": [...], "output_config": { "format": { "type": "json_schema", "schema": { ... } } } } }

选型指南:

场景 推荐模式
对话、摘要、生成类任务,下游直接展示 text(默认)
需要 JSON 输出,但结构灵活或由 prompt 约定 json_object
需要 JSON 输出,且下游强依赖特定字段(如 =_result.score json_schema
下游需要类型安全的路径访问,且字段稳定 json_schema

示例(onError + _meta + _error 联合使用):

{
  "id": "LLM_GenSpec",
  "in": {
    "messages": [{ "role": "user", "content": "..." }],
    "output_config": { "format": { "type": "json_schema", "schemaRef": "SpecSchema" } }
  },
  "out": {
    "$spec": "=_result",
    "$title": "=_result.title",
    "$tokens": "=_meta.usage.total_tokens"
  },
  "onError": "Set_LogError",
  "next": "Stop_End"
}
{ "id": "Set_LogError", "target": "$runState.lastError", "value": "=_error", "next": "Stop_End" }

说明:onError 指向的节点在当前节点失败时执行,此时 _error 可在该节点的 value 表达式中读取。成功路径走 next,失败路径走 onError,两者互斥。


API_*in

API_* 节点的 in 用于描述 HTTP 请求的参数。引擎根据 registry.apis 中声明的端点信息与节点 in 的内容,构造并发起 HTTP 请求。

in 支持以下字段:

字段 类型 说明
body object / string 请求体(POST/PUT/PATCH 时使用)
query object URL 查询参数(键值对)
pathParams object 路径参数(替换 URL 中的 {xxx}占位符)
headers object 本次请求额外的请求头(与 registry 中声明的 headers 合并,节点级优先)
timeout number 本次请求超时时间(毫秒),可选

示例:

{
  "id": "API_StripeCreateCharge",
  "in": {
    "body": {
      "amount": "=$orderAmount",
      "currency": "usd",
      "source": "=$paymentToken"
    }
  },
  "out": { "$chargeId": "=_result.id", "$chargeStatus": "=_result.status" },
  "next": "Branch_CheckPayment"
}
{
  "id": "API_WeatherQuery",
  "in": {
    "query": { "q": "=$cityName", "key": "=SYSVAR.weatherKey" }
  },
  "out": "$weather",
  "next": "LLM_AnalyzeWeather"
}
{
  "id": "API_SlackPostMessage",
  "in": {
    "body": {
      "channel": "=$slackChannel",
      "text": "=$notifyMessage"
    }
  },
  "out": "$slackResult",
  "next": "Stop_End"
}

执行语义:

  • 引擎根据 registry.apis 中对应 id 的 method / url / auth / headers 构造请求
  • in.pathParams 用于替换 URL 中的路径占位符(如 https://api.example.com/orders/{orderId}https://api.example.com/orders/12345
  • in.query 拼接为 URL 查询字符串
  • in.body 作为请求体发送(JSON 序列化或按 Content-Type 处理)
  • in.headers 与 registry 声明的固定 headers 合并(节点级优先)
  • auth 凭据由引擎自动注入到请求头(如 Authorization: Bearer xxx),节点无需手动处理
  • 请求完成后,HTTP 响应体(JSON 解析后)作为 _resultout 映射使用
  • HTTP 状态码非 2xx 时,节点视为执行失败,workflow 进入 failed 状态(引擎可按实现提供更细粒度的错误码判断)

5.2.4 out

out 用于定义节点执行完成后的输出映射


两种写法

out 支持两种写法:

A) 简写形式(string)

"out": "$plan" — 等价于 { "$plan": "=_result" },即将完整 _result 写入指定变量。

适用于节点只需要把完整输出存入一个变量的简单场景。

B) 完整形式(object)

"out": { "$plan.status": "=_result.ok", "/path/file": "=_result" } — 逐字段映射。

每一项都是一个映射规则:key 表示写入目标value 表示取值来源(通常来自 _result_item 等运行时上下文)。


变量写入(以 $ 开头)

out 的 key 以 $ 开头时,表示将值写入变量空间。

  • 变量名支持层级路径(如 $draft.content
  • 支持数组/索引写入(如 $generated[_index].name
  • 常用于节点间传参、控制信息、统计信息等

文件写入(以 / 开头)

out 的 key 以 / 开头时,表示将值写入文件空间中的某个文件路径。

  • 文件路径可使用运行时上下文变量进行插值(例如包含 _item_index 等)
  • 适用于 loop 场景下按条目生成/修改多份文件
  • 文件最终写入位置取决于本次 run 是否指定了 workspace:
    • 未指定:写入系统创建的临时文件空间(有 TTL)
    • 指定:写入该 workspace 文件空间
  • 文件写入路径必须落在 registry.files.artifacts 范围内

重要:文件写入由引擎层执行。节点只通过 out 声明"写入结果",不直接对文件系统进行写操作。


out 文件写入与 Write_* 节点的使用边界

两者功能有重叠,但适用场景不同:

  • out 文件写入:适用于调用型节点直接将 _result 中的某部分落盘,路径与内容在同一步完成
  • Write_*:适用于需要独立控制写入时机、写入策略(append / failIfExists)或写入内容来自非 _result 的场景(如从 $vars 中取值写入文件)

value 取值来源(右值表达式)

out 的 value 采用路径引用方式,从运行时上下文中取值,常见来源包括:

  • _result.*:当前节点的执行结果
  • _item.*:loop 场景下当前迭代项
  • _index:loop 场景下当前迭代下标
  • 以及其他已存在的运行时上下文对象(按平台约定)

执行语义与约束

  • out 只描述"输出映射规则",不描述执行顺序
  • 当 key 为文件路径时,引擎负责:
    • 在写入前做必要的安全与一致性校验
    • 执行实际落盘(临时空间或 workspace)
    • 记录必要的变更信息用于调试与审计
  • 若同一次节点输出对同一变量或同一路径发生冲突,按平台的冲突处理规则执行(建议默认以本节点输出为准或直接报错,具体策略由引擎统一定义)

示例:

{
  "id": "LLM_GenFilesInLoop",
  "in": {
    "messages": [
      { "role": "system", "content": "Generate file content for the given spec." },
      { "role": "user", "content": "=_item.specText" }
    ]
  },
  "out": {
    // ===== 变量写入,以$开始 =====
    "$generated[_index].name": "=_item.name",
    "$generated[_index].tokens": "=_meta.usage.total_tokens",

    // ===== 文件写入,以/开始(key 用文件路径,支持 _index / _item 变量插值)=====
    // 表示:把本次 _item 对应的文件内容写入到 /vsc 目录下
    "/vsc/{_item.path}": "=_result"

    // 例:若 _item.path = "files/fileA.ts",
    // 则引擎会把 _result 写入 /vsc/files/fileA.ts
  },
  "next": "API_CustomLogic"
}

5.2.5 Loop_*sourcewhile

Loop_* 支持两种互斥模式:

  • source 模式:数组遍历
  • while 模式:条件循环

互斥与约束:

  • sourcewhile 不得同时出现(编译错误)
  • while 模式下 maxIterations 必填且 mode 必须为 "serial"
  • source 模式下 maxIterations 选填(若填写,迭代上限 min(len(source), maxIterations)

A) source 模式

  • source:循环数据源表达式,求值必须是数组
{
  "id": "Loop_ForItems",
  "mode": "parallel",
  "source": "=$items",
  "children": ["LLM_GenOne", "Write_One"],
  "next": "Stop_End"
}

B) while 模式(3.16 新增)

  • while:每轮 children 执行前求值;true 继续,false 退出
  • maxIterations:循环上限(integer,>=1)
  • while 模式局部变量:_index_iterDir 可用;_item 不可用
  • if 先于 while 求值:if=false 时整个 Loop 跳过,不执行 while 判定
{
  "id": "Loop_review",
  "while": "=$review.approved != true",
  "maxIterations": 5,
  "mode": "serial",
  "children": ["LLM_Generate"],
  "next": "Write_final"
}

5.2.5.C Download_*/Unzip_* 路径与来源约束

Download_*

  • source 必填,可为 URL 字符串或对象
  • targetrouteByExt 二选一
  • routeByExt 未命中时可用 defaultDir 兜底
  • source 为对象时建议包含:url(必填)/headers/auth/timeout/checksum

Unzip_*

  • source 必填,必须是 zip 文件路径(表达式字符串)
  • routeByExt 必填,按后缀分流到目标目录
  • defaultDir 选填,overwrite 选填(默认 true

通用约束:

  • 必须流式处理(下载流式、解压逐 entry)
  • 必须防止 zip-slip(../、绝对路径、盘符跳转)
  • 文件正文不得整体落入 $vars,变量仅保存路径或摘要
  • 路径必须落在 registry.files.artifacts 范围

Write_* 的边界与协作:

  • Write_* 仅负责将 value 写入 target,不负责外部下载与解压
  • 单文件下载场景:Download_* 可直接按后缀分流落盘;如需补丁写入,再接 Write_*
  • zip 场景:先 Download_* 到 artifacts(建议 .tmp/...),再 Unzip_* 分流;若需二次加工,再接 Write_*

5.2.6 target & value

targetvalue 是**状态写入节点(State Nodes)**的核心属性,适用于:

  • Set_*(写全局变量)
  • Write_*(写临时文件)

target

  • 类型string
  • 必填:是(Set_* / Write_* 必填)
  • 作用:写入目标
Set_*
  • target 必须是全局变量路径:
    • $var
    • $var.field
    • $arr[_index]
    • $arr[_index].field

示例:

{ "id": "Set_Status", "target": "$plan.status", "value": "\"draft\"" }

Write_*
  • target 必须是临时文件路径(字符串或表达式字符串)
  • 写入目标必须落在 registry.files.artifacts 范围内

示例:

{
  "id": "Write_ComponentFile",
  "target": "=\"Process/Artifacts/components/\" + _item.name + \".tsx\"",
  "value": "=$componentCode"
}

value

  • 类型string(表达式)
  • 必填:是
  • 作用:写入内容/值
Set_*
  • value 求值后写入 target 指向的变量路径

示例:

{
  "id": "Set_Count",
  "target": "$count",
  "value": "=$items.length"
}
Write_*
  • value 求值后作为文件内容写入 target 指向的临时文件

示例:

{
  "id": "Write_PlanJson",
  "target": "\"Process/Artifacts/plan.json\"",
  "value": "=$plan"
}

说明:若写入 JSON 文件,推荐 value 为对象/数组(由引擎序列化),或由调用方显式 toJson(...) 后写入文本(取决于表达式系统是否内置该函数)。

5.2.7 children

  • 类型string[](step id 列表)
  • 必填:否(但对 Loop_* 为必填)
  • 作用:声明当前节点的并行子分支入口(并行扇出)。

规则

1) 并行执行(fan-out)
  • 当执行到包含 children 的节点时,引擎会并行启动 children 中列出的所有入口节点。
  • children 仅表达控制流并行,不隐含数据传递语义。
2) 子分支完成与 RETURN
  • 每个 children 分支从其入口节点开始,沿自身的 next / children / Branch_* / Loop_* 执行完整控制流链。
  • 当某个分支执行到 next: "RETURN" 时:
    • 表示该分支 正常完成
    • 控制流返回到父节点的 children join
3) join 汇合规则
  • 父节点会等待 所有 children 分支均完成(RETURN) 后,才进入父节点自身的 next
  • children 的完成顺序不做保证。
4) 异常与终止传播

在任一 children 分支中发生以下情况时,行为如下:

  • 到达 Stop_* → 整个 workflow 立即终止(stopped
  • 节点执行失败 → 整个 workflow 进入 failed
  • 执行到 Pause_* 节点 → 整个 workflow 进入 paused,等待 resume
5) 与 if 的关系
  • 若父节点 if=false
    • 父节点及其整个 children 子树均被跳过
    • 直接进入父节点的 next
  • children 内部节点的 if
    • 仅影响该分支内部执行
    • 不影响其他分支

示例(并行分支完成后返回 join)

{
  "id": "Service_GenerateParts",
  "children": ["LLM_GenA", "LLM_GenB"],
  "next": "Service_Merge"
}

{
  "id": "LLM_GenA",
  "next": "RETURN"
}

{
  "id": "LLM_GenB",
  "next": "RETURN"
}

5.2.8 next

  • 类型:string
  • 必填性:必填(除 Stop_* 节点外)
  • 作用:显式声明该节点执行完成后的控制流行为。

规则

单后继
  • next 只能有一个,表示唯一的后继控制流。
  • 多路径控制需使用 Branch_*children
合法取值

next 的取值必须显式表达控制流语义,合法取值包括:

  • "<stepId>":线性推进到指定后继节点。
  • "RETURN":结束当前执行分支,返回最近一层父执行上下文(join)。
  • "BREAK":结束当前迭代并退出整个 Loop_*,跳转到 Loop_*.next(仅在 Loop_* children 子树内有效)。

"RETURN""BREAK" 为保留关键字,不得作为 stepId 使用。

BREAK 规则:

  • 仅可用于 Loop_* 的 children 子树;在 Loop 外使用为编译错误
  • 适用于 sourcewhile 两种模式
  • mode:"parallel" 下,触发 BREAK 后未启动迭代不再启动,已启动迭代继续到自然结束,然后进入 Loop_*.next

可恢复的暂停不通过 next 的魔法值表达,而是通过节点类型 Pause_* 显式声明(见第十一章)。

控制流完整性
  • Stop_* 节点外,所有节点必须显式声明 next
  • 不允许通过缺失 next 推断 pause、return 或 stop 语义。
  • Stop_* 节点缺失 next,属于 Spec 校验错误。

示例

{
  "id": "Service_PlannerService",
  "next": "LLM_Generate"
}

5.2.9 cases(仅 Branch_*

  • 类型array
  • 必填:是(仅适用于 Branch_*
  • 作用:定义条件分支入口;运行时只会选择其中一个入口节点执行。

格式

cases 是一个二维数组,每一项是:

["<whenExpr>", "<stepId>"]

  • <whenExpr>:条件表达式字符串,或固定字符串 "ELSE"
  • <stepId>:该分支的入口节点 id

示例:

"cases": [
  ["=$amount < 500", "Service_ManagerApprove"],
  ["ELSE", "Service_ManagerAndDirectorApprove"]
]

规则

  1. 单入口规则 每个规则只能指定 一个入口节点(一个 stepId)。
  2. 选择规则 按顺序判断:
  • 第一个 <whenExpr> 求值为 true 的分支被选中
  • "ELSE" 必须放最后,作为兜底分支
  1. 执行语义(重要) 命中某个 case 后:
  • 从该 case 的入口节点开始执行一整条分支链(沿该入口节点自己的 next/children/branch/loop 继续执行)
  • 只有当该分支链执行结束(到达 Stop_* 或遇到 next: "RETURN")才算该 case 完成
  • case 完成后,才进入 Branch_* 节点自身的 next
  1. 未命中的 case 不执行 其它 case 完全不执行。

5.2.10 mode

  • 类型string
  • 必填:对 Loop_* 必填;对 Write_* 选填;其他节点不适用
  • 作用:声明该节点的执行/写入策略(不同节点含义不同)。

Loop_*mode(必填)

用于指定循环的执行方式:

  • "parallel":各次迭代可并行执行
  • "serial":按迭代顺序串行执行
  • 若使用 while 模式,则 mode 必须为 "serial"

示例:

{
  "id": "Loop_ForItems",
  "mode": "parallel",
  "source": "=$items",
  "children": ["Service_HandleOne"],
  "next": "Stop_End"
}

Write_*mode(选填)

用于指定写文件时的写入策略:

  • "overwrite":覆盖写(默认)
  • "failIfExists":若已存在则报错
  • "append":追加写(新内容拼接到末尾)
  • "prepend":前置写(新内容拼接到开头)

约束:

  • append/prepend 属于读改写(read-modify-write)语义,适用于文本内容(或实现明确支持的字节拼接)
  • 目标文件不存在时,append/prepend 均应按新建文件处理
  • 同一路径并发 append/prepend 不保证顺序;需由上层流程避免并发冲突

示例:

{
  "id": "Write_Log",
  "mode": "append",
  "target": "\"Process/Artifacts/log.txt\"",
  "value": "=$line"
}

5.2.11 meta

  • 类型object
  • 必填:否
  • 作用:用于展示/调试/追踪的元信息,不参与执行语义。

示例:

{
  "id": "LLM_GenerateUI",
  "meta": { "title": "Generate UI Map", "tags": ["gen", "ui"], "owner": "agent-ui" },
  "in": { "messages": [ { "role": "user", "content": "=$prdText" } ] },
  "out": { "$uiMap": "=_result" }
}

5.2.12 print

  • 类型string(表达式或字面字符串)
  • 必填:否
  • 作用:在节点执行完成后输出一条自定义消息,并由引擎发出 run_events.type = "step_print" 事件。

规则:

  • print 仅在节点实际执行且成功完成时触发
  • 若节点因 if=false 被跳过,不触发 print
  • 若节点执行失败,不触发 print(仅发 step_error
  • Stop_* 不允许 print
  • print 的求值结果应为字符串;若结果非字符串,引擎应按 JSON 序列化为字符串后输出

建议:

  • 用于进度提示、阶段摘要、关键指标展示
  • 不用于传递最终业务结果(最终结果应通过 out / workflow 输出返回)

示例:

{
  "id": "LLM_CheckVersion",
  "in": {
    "messages": [
      { "role": "user", "content": "请返回文档版本号" }
    ]
  },
  "out": { "$version": "=_result" },
  "print": "=\"版本检查完成: \" + $version",
  "next": "Stop_End"
}

第六章:变量与作用域(Variables & Scope)

6.1 变量类型总览

workflow 中可使用的"数据引用"分为 7 类:

写法 类型 可写 声明位置
paramName 入参(Params) registry.params
$xxx 全局变量(Global Vars) registry.vars
SYSVAR.xxx 系统变量(System Vars) 系统预配置
_item / _index 循环局部变量(Loop Locals) 自动注入
_result 节点输出变量(Step Local Result) 自动注入
_meta 节点元信息变量(Step Local Meta) 自动注入(LLM 节点)
_error 节点错误变量(Step Local Error) 自动注入(节点失败时)

6.2 入参 params

6.2.1 定义方式

入参在 registry.params 中声明,不带 $ 前缀:

"params": ["userRequest(STRING)", "targetLang(STRING)", "maxRetries(INT) = 3"]

6.2.2 读写规则

  • 读取:任何表达式位置都可以直接引用参数名(如 =userRequest
  • 只读:入参不可被 workflow 修改。不可出现在 out 左侧或 Set_*target
  • 入参名不带 $,与 VL 语法中服务/方法入参的风格对齐

6.3 全局变量 $vars

6.3.1 定义方式

全局变量必须在 registry.vars 中声明(VL 风格):

"vars": ["$plans([OBJECT])", "$generated([OBJECT])", "$summary(STRING)"]

6.3.2 读写规则

  • 读取:任何表达式位置都可以读 $xxx
  • 写入
    • 调用型节点通过 out 写入(字段映射)
    • 状态节点通过 Set_* 写入(target/value
  • $vars 是 workflow 的主要数据传递方式(节点连线只表示控制流)

6.3.3 并发注意事项(Loop parallel)

Loop_* 使用 mode:"parallel" 时:

  • 允许写 $vars,但应避免多个迭代写入同一位置导致竞态
  • 推荐写法:按 _index 分槽写入,例如:
    • $generated[_index] = ...
    • $files[_index].path = ...

6.4 系统变量 SYSVAR.xxx

6.4.1 定义

SYSVAR.xxx 表示用户在系统空间预先配置的通用变量。

6.4.2 规则

  • 只读
  • 不需要在 registry.vars 声明
  • 可在任何表达式位置引用

6.5 Loop 局部变量 _item / _index / _iterDir

当进入 Loop_* 的循环体(其 children 子树)时,引擎会为每次迭代提供:

  • _item:当前迭代元素
  • _index:当前迭代索引(从 0 开始)
  • _iterDir:当前迭代临时目录名,格式 {loopNodeId}_{_index}

规则:

  • 仅在 loop 的子树范围内可用
  • 不需要声明
  • _iterDir 建议用于并行迭代下的临时路径隔离(例如 .tmp/{_iterDir}/bundle.zip
  • mode:"serial"mode:"parallel" 下语义一致

6.6 _result(调用型节点输出)

6.6.1 定义

调用型节点(Service_* / API_* / Component_* / LLM_*)执行完成后,引擎将内容输出绑定为:

  • _result

6.6.2 可用范围

  • _result 只在该节点的 out 映射求值阶段可用
  • 仅表示"当前节点的输出",不会跨节点存在

6.6.3 LLM_* 结构化输出时的 _result

LLM_* 节点中,_result 永远表示业务内容本体:

  • 文本输出:_result 为字符串正文
  • json_schema 输出:_result 为引擎自动解析并校验后的 JSON 对象/数组

即使是文本场景,也不再要求通过 =_result.content 取正文。

示例:

{
  "id": "Service_PlannerService",
  "in": { "prd": "=$prdText" },
  "out": { "$plan": "=_result", "$planId": "=_result.id" }
}

6.7 _meta(LLM 节点元信息)

6.7.1 定义

_metaLLM_* 节点的元信息对象,承载调用层信息,不承载业务正文。

6.7.2 可用范围

  • _meta 只在当前节点 out 求值阶段可用
  • 仅表示当前节点执行元信息,不跨节点存在

6.7.3 LiteLLM 推荐结构

建议至少包含:

  • providermodelmodel_resolved
  • request_idresponse_id
  • latency_msfinish_reason
  • usage.input_tokensusage.output_tokensusage.total_tokens
  • cost(若启用成本计算)

约束:

  • usage 统一命名为 input_tokens / output_tokens / total_tokens
  • 厂商特有 usage 字段统一放在 usage.raw

取值示例:

  • =_meta.usage.total_tokens
  • =_meta.model_resolved

6.8 _error(节点失败信息)

6.8.1 定义

节点失败时,引擎将标准错误对象绑定到 _error

6.8.2 可用范围

  • _error 在失败路径中可读(如 onError 分支或失败处理节点)
  • 成功执行时 _error 不存在

6.8.3 LiteLLM 推荐结构

建议至少包含:

  • type(标准分类)
  • codemessageretryablestatus_code
  • providermodelrequest_id
  • litellm_exception
  • detailsraw

type 建议使用以下标准枚举:

  • auth_error
  • bad_request
  • rate_limit
  • timeout
  • context_length_exceeded
  • content_policy_violation
  • service_unavailable
  • connection_error
  • json_parse_error
  • schema_validation_error
  • internal_error
  • unknown_error

第七章:表达式与路径语法(Expressions & Paths)

7.1 表达式前缀规则(= 前缀)

Workflow JSON 中的所有字符串值遵循统一规则:

  • = 开头:表达式,引擎对 = 后的内容进行求值
  • 不以 = 开头:字面字符串,原样使用
  • == 开头:字面字符串,内容为 = 后的部分(用于极少数需要以 = 开头的字面字符串)

示例:

"msg": "hello"              → 字面字符串 "hello"
"msg": "=$userRequest"      → 表达式,求值 userRequest 入参
"msg": "=$text + ' world'"  → 表达式,字符串拼接
"msg": "==formula"          → 字面字符串 "=formula"

该规则适用于所有 JSON 字符串值位置。引擎不再需要区分"哪些字段是表达式位置"——统一由 = 前缀决定。


7.2 表达式出现的位置

以下属性中的字符串值通常需要使用 = 前缀:

  • if:条件表达式(如 "=$count > 0"
  • 调用型节点 in 的字段值(引用变量时需 =,字面值不需要)
  • 调用型节点 out 的右侧取值表达式(如 "=_result.id"
  • Set_*value(如 "=$plan.status"
  • Write_*target(当为动态路径时,如 "=\"Process/\" + _item.name"
  • Write_*value(如 "=$componentCode"
  • Loop_*source(如 "=$items"
  • Branch_*cases[*][0](when 表达式,如 "=$amount < 500"
  • print(如 "=\"当前阶段: \" + $phase"

以下位置不使用 = 前缀(它们是目标路径或标识符,非求值表达式):

  • out 的左侧(写入目标路径,如 "$plan""$generated[_index].name"
  • out 简写形式(如 "out": "$plan",表示目标变量)
  • Set_*target(写入目标路径,如 "$plan.status"
  • next / children / cases[*][1](步骤 ID)
  • modemodel 等配置值

7.3 表达式的最小能力集合

表达式建议至少支持:

  • 入参引用:userRequesttargetLang(params,无 $ 前缀,只读)
  • 全局变量引用:$xxx(vars,可读写)
  • 系统变量引用:SYSVAR.xxx(只读)
  • 上下文变量:_item_index_result_meta_error
  • 布尔逻辑:&&||!
  • 比较:==!=>>=<<=
  • 字符串拼接:+
  • 数字运算:+ - * /
  • 括号:(...)
  • 基础属性访问:.field
  • 数组/字典索引:[expr]

说明:表达式语言不需要复杂到脚本语言,保持可静态分析与可预测即可。


7.3 路径语法(关键规则)

里"路径"既用于读(表达式),也用于写(out 左侧、Set.target)。

7.3.1 点号 .:永远是字面字段名

例如:

  • $plan.status 表示字段名就是 "status"
  • _meta.usage.total_tokens 表示逐级字段访问

点号后面的内容不会被当作变量解析


7.3.2 方括号 [...]:永远是表达式索引

方括号内部必须按表达式解析。

  • $generated[_index]_index 是变量(表达式求值)
  • $arr[0]:数字常量索引
  • $obj[_item.name]:表达式求值为 key

7.3.3 字面 key 的写法:["literal"]

如果确实要访问 key 名为 "_index" 的字段(而不是变量),必须写成:

  • $obj["_index"]

这样解析器完全不需要猜测。


7.4 out 的路径写入语义(deep-set)

out 左侧是路径(如 $plan.status)时:

  • 引擎执行 deep-set 写入
  • 若中间对象不存在,可自动创建(实现可选,但建议支持)

示例:

"out": { "$plan.status": "=_result.success" }


7.5 Loop 中路径写入推荐

Loop 并行场景推荐按 _index 分槽写入,避免覆盖:

"out": { "$generated[_index]": "=_result" }

或:

"out": { "$generated[_index].name": "=_item.name" }

第八章:临时文件 Artifacts(读写规则)

本章定义workflow 空间内的**临时文件(artifacts)**的用途、路径约束、读写方式与生命周期规则。

Artifacts 仅用于 workflow 内部的数据临时存储与步骤间传递,不等同于业务最终产出文件。


8.1 基本定义

  • Artifacts:workflow 运行时产生的临时文件,属于 workflow 当前运行实例的隔离空间。
  • Artifacts 的存在目的是:
    • 存放大文本/大对象(避免全局变量膨胀)
    • 存放多文件中间产物(如 components/ 目录下 N 个文件)
    • 支持 pause/resume 时仅保存"文件地址引用",而不是保存完整内容

8.2 路径声明与边界(Registry 约束)

8.2.1 registry.files.artifacts

workflow 允许写入的临时文件路径范围必须在 registry.files.artifacts 中声明:

"files": {
  "inputs": ["Process/Rules/*", "Process/PRD.json"],
  "artifacts": ["Process/Artifacts/*"]
}

8.2.2 先注册后使用

  • 所有 artifacts 写入路径必须落在声明的范围内
  • 超出范围的写入属于非法(运行时报错)

8.3 写入规则(Write_*)

8.3.1 写入节点

写入 artifacts 必须使用状态写入节点:

  • Write_*

8.3.2 写入目标

Write_*target 表示写入路径(可为表达式字符串):

{
  "id": "Write_ComponentFile",
  "target": "=\"Process/Artifacts/components/\" + _item.name + \".tsx\"",
  "value": "=$componentCode"
}

8.3.3 写入内容

value 表示文件内容(表达式求值结果):

  • 文本(STRING)
  • JSON 对象/数组(由引擎序列化为 JSON 文本,或按实现约定保存为结构化对象)

8.4 写入策略(Write.mode)

Write_* 支持 mode(可选):

  • "overwrite":覆盖写(默认)
  • "failIfExists":若文件已存在则报错
  • "append":追加写(适用于日志类文件)
  • "prepend":前置写(将新内容写在文件开头)

补充约束:

  • append/prepend 属于读改写语义(read-modify-write)
  • 目标文件不存在时按新建文件处理
  • 并发写入同一路径时不保证顺序,建议避免并发冲突

示例:

{
  "id": "Write_Log",
  "mode": "append",
  "target": "\"Process/Artifacts/run.log\"",
  "value": "=$line + \"\\n\""
}

8.5 读取规则(Artifacts Read)

当前版本不强制提供"专门的读文件节点"。Artifacts 的读取建议通过以下方式完成:

8.5.1 作为路径引用传递给调用型节点

Artifacts 文件路径可以作为普通字符串存入 $vars,并作为调用型节点 in 的参数传递:

{ "id": "Service_UseArtifact", "in": { "filePath": "=$artifactPath" } }

8.5.2 读取能力的来源

Artifacts 的实际读取由以下之一承担:

  • Component_*(系统内置能力,例如 FileOps.Read)
  • 或项目内 Service_*(自定义的读取服务)

当前版本的核心原则是:workflow 只定义编排,不绑定具体 IO 实现细节;因此"读文件"属于能力节点的职责。


8.6 Artifacts 与变量的关系

  • Artifacts 文件地址(path/uri)可以作为普通数据写入 $vars
  • $vars 可存"文件引用",而 artifacts 存"文件内容"
  • 推荐模式:
    • 大内容写入 artifacts
    • 变量只保存 artifacts 路径、摘要、hash、索引信息

8.7 临时目录(.tmp)与生命周期

8.7.1 .tmp/ 语义

  • 路径以 .tmp/ 开头时,视为临时文件路径(例如 .tmp/bundle.zip
  • workflow 定义中不感知 runId,只写相对路径

8.7.2 引擎解析与隔离

  • 引擎将 .tmp/xxx 解析为 <artifactsRoot>/.tmp/{runId}/xxx
  • 隔离维度:workspaceId + runId
  • 不同 run 的临时文件互不可见
  • 懒创建:仅在本次 run 首次写入 .tmp/ 路径时创建 .tmp/{runId}/
  • 若本次 run 未使用 .tmp/,则不创建 .tmp/{runId}/ 目录

8.7.3 迭代级隔离(_iterDir

  • Loop_* 并行场景下,建议使用 .tmp/{_iterDir}/... 防止迭代间冲突
  • 完整解析路径:<artifactsRoot>/.tmp/{runId}/{_iterDir}/...

8.7.4 生命周期

  • run 进入终态(stopped / failed / cancelled)后,引擎应清理 .tmp/{runId}/
  • 需提供 TTL 兜底清理,处理异常中断后的遗留目录

8.7.5 可见性约束

  • .tmp/ 仅作为中间态目录,不作为业务可读目录
  • 对外可见目录应仅暴露已发布完成的正式文件

8.8 Download/Unzip 执行约束

  • Download_* 必须流式下载,避免将完整正文加载到内存
  • Unzip_* 必须逐 entry 解压,并做 zip-slip 防护
  • routeByExt 路由目录建议与组件工厂目录约定一致(如 ExtComponents/Sections/Services
  • 大文件正文不落 $vars,仅将路径、摘要、统计信息写入变量

8.9 并发与冲突约束(重要)

当 workflow 存在并行执行(children 并行、Loop_* mode:"parallel")时:

  • 多个节点写入同一路径会导致冲突
  • 建议规则:
    • 引擎检测到同路径并发写入时应报错(或按最后写入覆盖,但不推荐)
  • 推荐写法:
    • 路径按 _index_item.name 分片,保证每轮写不同文件

示例:

{
  "id": "Write_ComponentFile",
  "target": "=\"Process/Artifacts/components/\" + _item.name + \".tsx\"",
  "value": "=$code"
}

第九章:Loop 完整执行语义(Loop_*

本章定义 Loop_* 节点的完整执行规则,包括数据源求值、并行/串行模式、循环体执行、join 汇合、局部变量作用域,以及与 if/children/next 的交互。


9.1 节点结构

Loop_* 是控制节点,支持 sourcewhile 两种互斥模式。

source 模式:

{
  "id": "Loop_xxx",
  "mode": "parallel | serial",
  "source": "<expr>",
  "children": ["<stepId>"],
  "next": "<stepId>"
}

while 模式:

{
  "id": "Loop_xxx",
  "mode": "serial",
  "while": "<expr>",
  "maxIterations": 5,
  "children": ["<stepId>"],
  "next": "<stepId>"
}

约束:

  • mode 必填;while 模式必须为 "serial"
  • sourcewhile 二选一
  • source 模式:source 必填且求值为数组,maxIterations 选填
  • while 模式:whilemaxIterations 必填
  • children 必填:循环体入口节点列表(推荐仅 1 个入口;若允许多个则表示并行体)

9.2 数据源求值(source

执行到 Loop_* 时,引擎首先求值:

  • items = eval(source)

规则:

  • items 必须是数组
  • 若求值结果为 null/undefined
    • 建议视为 [](空循环)
  • 若不是数组:
    • 运行时报错(Loop 输入类型错误),workflow 进入 failed 状态

9.2.A while 模式求值(3.16 新增)

Loop_* 使用 while 模式时:

  • 每轮执行 children 前先求值 while
  • 第 0 轮若为 false,children 零次执行并直接进入 next
  • 每轮后 _index++,达到 maxIterations 后强制退出
  • _item 不存在(while 无数据源),_index_iterDir 可用

9.3 空循环行为

items.length == 0

  • loop 不执行任何循环体
  • 直接进入 Loop_*next
  • Loop_* 必须声明 next;空循环时直接进入该 next

9.4 循环体(Loop Body)

9.4.1 循环体入口

children 定义循环体入口节点(stepId 列表)。

推荐约束(更清晰):

  • Loop_*children 建议限制为 1 个入口节点
  • 如果循环体需要并行分叉,在该入口节点上再写 children

(如果允许 Loop_* children 多入口,则它们在每次迭代中并行启动)


9.4.2 局部变量绑定

每次迭代 i(从 0 开始)都会在循环体子树范围内注入:

  • _item = items[i]
  • _index = i

作用域规则:

  • _item/_index 仅在该次迭代的循环体子树内可用
  • 不会泄露到 loop 外部
  • 不同迭代的 _item/_index 相互隔离

9.5 执行模式(mode

9.5.1 mode: "serial"

语义:按顺序逐个迭代执行。

执行流程:

  1. 执行迭代 0 的循环体(从 children 入口开始跑完整链)
  2. 迭代 0 完成后,执行迭代 1
  3. …直到最后一个迭代完成
  4. loop 完成后进入 Loop_* next

特点:

  • 迭代间严格顺序
  • 适用于有依赖关系、写同一份状态、或需要限流的场景

9.5.2 mode: "parallel"

语义:所有迭代可并行执行。

执行流程:

  1. 引擎为每个 i 启动一个迭代任务(并行)
  2. 每个迭代从 children 入口开始执行完整链
  3. 所有迭代都完成后,loop 才算完成(join)
  4. loop 完成后进入 Loop_* next

特点:

  • 高吞吐
  • 必须注意共享状态冲突(尤其是 $vars 与 artifacts 同路径写入)

9.5.3 while 模式的 mode 约束(3.16 新增)

  • while 模式下 mode 必须为 "serial"
  • while + mode:"parallel" 属于编译错误

9.6 迭代的“完成”定义

一次迭代从其入口节点开始执行,沿自身的 next / children / Branch_* / Loop_* 推进,并在以下任一情况发生时结束:

  • 迭代中的某个节点执行到 next: "RETURN",表示该迭代正常完成,控制流返回到 Loop_* 的 join。
  • 迭代中的某个节点执行到 next: "BREAK",表示该迭代完成并退出整个 Loop,跳转到 Loop_*.next
  • 迭代中的某个节点为 Pause_*,workflow 进入 paused 状态,等待 resume;loop 未完成。
  • 迭代中的某个节点执行到 Stop_*,workflow 立即终止(stopped)。
  • 迭代中的某个节点执行失败,workflow 进入 failed 状态。

9.7 Loop 的 join 汇合规则

无论 modeserial 还是 parallelLoop_* 的汇合规则一致:

  • Loop_* 仅在 所有迭代均正常完成(RETURN 后,才会进入自身的 next
  • 若任一迭代触发 BREAK:退出循环并进入 Loop_*.next;在 parallel 模式下已启动迭代继续执行至自然结束。
  • 若任一迭代遇到 Pause_* 节点进入 paused 状态,整个 workflow 进入 paused,loop 不再继续。
  • 若任一迭代到达 Stop_*,整个 workflow 立即终止(stopped)。
  • 若任一迭代执行失败,整个 workflow 进入 failed 状态。

9.8 与 if 的交互

9.8.1 Loop 自身的 if

  • Loop_* if=false
    • 整个 loop 节点及其循环体子树被跳过
    • 直接进入 Loop_* next

9.8.2 循环体内部节点的 if

  • 每个迭代中节点 if 独立求值
  • if=false 会跳过该节点及其子树,但不影响其他迭代

9.9 与 $vars / artifacts 的交互建议

9.9.1 $vars 写入建议

  • serial:可安全写共享变量(仍建议结构化)
  • parallel:推荐按 _index 分槽写入,例如:
    • $generated[_index] = ...

9.9.2 artifacts 写入建议

  • parallel 模式下必须保证不同迭代写入不同路径,例如:
    • Process/Artifacts/components/<n>.tsx

9.10 Loop 的输出与数据汇总(推荐方式)

Loop 本身不直接产出 _result。循环结果通常通过:

  • 循环体内调用型节点 out 写入 $vars[_index]
  • 循环体内 Write_* 写入 artifacts,并把路径写入 $vars[_index]

示例(概念):

  • $files[_index].path = "Process/Artifacts/components/xxx.tsx"

第十章:Branch 完整执行语义(Branch_*

本章定义 Branch_* 节点的完整执行规则,包括 case 选择、分支链执行、汇合(join)与 next 的关系,以及与 if/children 的交互。


10.1 节点结构

Branch_* 是控制节点,基本结构如下:

{
  "id": "Branch_xxx",
  "cases": [
    ["<whenExpr1>", "<stepId1>"],
    ["<whenExpr2>", "<stepId2>"],
    ["ELSE", "<stepIdElse>"]
  ],
  "next": "<stepId>"
}

约束:

  • cases 必填
  • 每个 case 必须是二元组:[whenExpr, stepId]
  • whenExpr 为表达式字符串,或固定字符串 "ELSE"
  • "ELSE" 必须存在且必须放最后(推荐强约束)

10.2 分支选择规则(Case Selection)

执行到 Branch_* 时,引擎按顺序对 cases 进行判断:

  1. 对每个 ["<whenExpr>", "<stepId>"]
    1. <whenExpr> 不是 "ELSE"
      • 计算 eval(<whenExpr>)
      • 若结果为 true,该 case 命中,停止继续判断后续 case
  2. 若前面都未命中,则命中 "ELSE" case

10.3 单入口规则

每个 case 只能指定 一个入口节点(一个 stepId)。该入口节点代表"该分支路径的起点"。


10.4 分支执行语义(执行完整链)

命中某个 case 后,引擎将从该 case 的入口节点开始执行:

  • 沿入口节点自己的 next / children / Branch_* / Loop_* 继续推进
  • 这条链路可能包含嵌套分支、嵌套循环、并行 children 等

重要:Branch 并不是只执行入口节点一次就返回,而是执行"从入口节点开始的一整条分支链"。

10.5 分支的“完成”定义

分支链从被选中的 case 入口节点开始执行,沿其自身的控制流推进,并在以下任一情况发生时结束:

  1. 分支链中的某个节点执行到 next: "RETURN",表示该分支正常完成,控制流返回到 Branch_* 的 join。
  2. 分支链中的某个节点为 Pause_*,workflow 进入 paused 状态,等待 resume。
  3. 分支链中的某个节点执行到 Stop_*,workflow 立即终止(stopped)。
  4. 分支链中的某个节点执行失败,workflow 进入 failed 状态。

10.6 Branch 的汇合(Join)与 next

当且仅当:

  • 命中的分支链 正常完成(即执行到 next: "RETURN"

Branch_* 节点才会进入其自身的 next

执行规则如下:

  • Branch_*next必须显式声明
  • 当分支链完成后,控制流从 Branch_* next 指向的节点继续执行。

异常情况:

  • 若分支链执行过程中遇到 Pause_* 节点,workflow 进入 paused 状态,Branch_* 不再继续推进。
  • 若分支链执行过程中到达 Stop_*,workflow 立即终止(stopped)。
  • 若分支链执行失败,workflow 进入 failed 状态。

10.7 未命中的分支不执行

所有未命中的 case 对应的入口节点及其子树完全不执行。

10.8 与 if 的交互

10.8.1 Branch 自身的 if

Branch_* if=false

  • 整个 Branch 节点被跳过(不会判断 cases)
  • 直接进入 Branch_* next

10.8.2 分支链内部节点的 if

  • 仅影响该分支链内部执行
  • 不影响 Branch 的 case 选择逻辑(case 选择发生在进入分支链之前)

10.9 Branch 与并行(children)的关系

Branch 本身是"互斥选择",一次只会选择一个 case。如果某个 case 的入口节点内部包含 children,则该分支链内部仍可产生并行执行,并遵循 children 的 join 规则。


10.10 ELSE 缺失时的行为(不推荐)

如果实现允许 "ELSE" 缺失(不推荐):

  • 所有 when 都不命中时:
    • Branch 视为"不执行任何分支"
    • 直接进入 Branch_* next

推荐强制要求 ELSE 存在,以保证分支行为可预测。

第十一章:Pause_* 节点与暂停/恢复协议

本章定义 Pause_* 节点的结构、执行语义与配套的 resume 恢复协议,用于在 workflow 中显式表达"等待外部结果再继续"的语义,适配用户输入与审批流等场景。引擎保持通用,不将审批领域规则内置到引擎。


11.1 定位

Pause_* 用于在流程中间等待外部系统输入结果。

引擎执行到该节点时进入 paused 状态,仅在收到合法 resume 请求后继续执行 next

Pause_*Stop_*Stop_* 是终止(不可恢复),Pause_* 是可恢复的挂起。
Pause_*failedfailed 是异常中断,Pause_* 是设计意图内的等待。


11.2 节点结构

11.2.1 必填与选填字段

字段 类型 必填 说明
id string 节点标识,须以 Pause_ 开头
reason string 给前端/通知/日志展示的等待原因文案
resumeResultTarget string resume(payload) 写入的 $vars 目标路径
timeout object 超时配置(见下)
next string 恢复成功后的后继节点

11.2.2 timeout 结构

字段 类型 必填 说明
sec number 超时秒数(必须 > 0)
on string 超时后跳转的节点(通常为 Stop_* 或异常处理链路)

11.2.3 示例

{
  "id": "Pause_UserInput_Address",
  "reason": "请补充收货地址",
  "resumeResultTarget": "$vars.userInput.address",
  "timeout": {
    "sec": 86400,
    "on": "Stop_Timeout"
  },
  "next": "Service_CreateOrder"
}

11.3 执行语义

11.3.1 进入暂停

执行到 Pause_* 时,引擎必须:

  1. 将 run 状态置为 paused
  2. 持久化等待上下文(至少包含 runIdpauseNodeIdwaitToken(hash)、expireAtresumeResultTarget
  3. 发出 pause_start 事件

11.3.2 恢复执行

收到 resume 请求后,引擎必须:

  1. 校验 run 当前为 paused
  2. 校验 waitTokenpauseNodeId 对应关系
  3. payload 写入 resumeResultTarget
  4. 发出 pause_resumed 事件
  5. 从该 Pause_* 节点的 next 继续执行

11.3.3 超时处理

若到达 timeout.sec 仍未恢复:

  1. 发出 pause_timeout 事件
  2. 跳转到 timeout.on 执行
  3. 若未配置 timeout,实现可按平台策略保持 paused 或转 failed(需实现侧明确文档化)

11.4 恢复接口协议

11.4.1 请求体最小字段

{
  "runId": "run_xxx",
  "token": "wait_token_xxx",
  "payload": {
    "approved": true,
    "comment": "ok"
  },
  "requestId": "req_xxx"
}

11.4.2 约束

  1. resume 必须幂等(建议基于 requestId 去重)
  2. 同一个等待 token 只能成功恢复一次
  3. run 非 paused 时应拒绝恢复并返回当前状态
  4. 不允许外部指定 nextStep,恢复路径固定为 Pause_*next

11.5 事件定义

11.5.1 pause_start

触发:进入 Pause_* 时。

关键字段:runIdnodeIdreasonwaitTokenexpireAtresumeResultTarget

11.5.2 pause_resumed

触发:resume 成功且已写入 resumeResultTarget 后。

关键字段:runIdnodeIdrequestIdresumedAt

11.5.3 pause_timeout

触发:等待超时。

关键字段:runIdnodeIdexpiredAttimeoutAction

11.5.4 pause_rejected

触发:恢复请求被拒绝(token 无效、状态错误、schema 不通过)。

关键字段:runIdnodeIdrequestIdreasonCode


11.6 与 Service_* 的职责边界

  1. workflow 负责"何时等待/何时继续"
  2. 业务 Service 负责"谁来处理/规则如何计算/何时形成最终结果"
  3. 审批会签/或签/回退等复杂逻辑不内置到 workflow 引擎
  4. 业务 Service 在形成"可推进最终结果"时调用 resume

11.7 典型编排示例(三层审批)

推荐结构(每层 2 节点):

  1. Service_StartL1ApprovalPause_L1Branch_L1Result
  2. Service_StartL2ApprovalPause_L2Branch_L2Result
  3. Service_StartL3ApprovalPause_L3Branch_L3Result

说明:

  1. 每层 Pause_* 使用独立 resumeResultTarget(如 $vars.approval.l1$vars.approval.l2$vars.approval.l3
  2. 每层审批发起可复用同一个通用 service(如 approval.create),通过 stage 参数区分

11.8 约束与非目标

  1. 本章不要求引擎实现审批规则引擎能力
  2. 不引入 next: "PAUSE" 魔法值;暂停由 Pause_* 节点显式表达
  3. 不要求外部回传完整引擎状态快照;推荐由引擎服务端持久化 run/pause 状态

第十二章:IDE 内 Workflow 交互说明

说明

  • Workflow 是项目的一部分,不需要用户选择,存储在项目内 /config/workflow.json 中
  • 新建项目时自动生成默认 workflow
  • IDE 始终运行在已知 tenantId + projectId 上下文中
  • workflow 的执行策略由 Agent 决定,前端不提供配置面板

12.1 前端核心交互窗口

窗口区域 主要内容 用户操作 说明
对话窗口 用户自然语言输入 输入需求 所有 workflow 执行的唯一入口
Workflow 运行视图 当前项目 workflow 的执行过程 自动展示当前 run 状态
输出控制台 日志 / 节点状态 / 错误 查看 由后台 run 自动推送
工作空间视图 当前 workspace 文件状态 commit / rollback 与 workflow 解耦
状态提示区 run 状态 查看 running / finished / failed

12.2 后台接口(IDE 调用)

接口名称 接口用途(使用方) 入参 出参 重点说明
LoadProjectContext 加载项目上下文(IDE) tenantId, projectId 项目信息、默认 workspaceId IDE 初始化
LoadWorkspace 加载工作空间 tenantId, projectId, workspaceId 文件树摘要、workspace 状态 判断是否初始状态
AnalyzeIntent 生成运行入参(Agent) tenantId, projectId, workspaceId, userInput runMaps(mode / nodes),notes(用来在会话窗口展示当前修改计划供用户查看,但不会中断流程) 仅非初始 workspace 调用
RunProjectWorkflow 运行项目 workflow tenantId, projectId, workspaceId, userInput, runMaps? runId + SSE 连接 自动建立 SSE,返回执行状态
GetRunStatus 查询运行状态 tenantId, projectId, runId run 状态、错误信息 用于断线恢复
CommitWorkspace 提交产物 tenantId, projectId, workspaceId commitId 与 workflow 无关
RollbackWorkspace 回滚产物 tenantId, projectId, workspaceId 回滚结果 与 workflow 无关

12.3 前端功能实现核心要点(含对话驱动主流程)

功能点 实现逻辑 设计约束
对话驱动执行 用户输入即触发 workflow 无显式按钮
初始 workspace 判断 LoadWorkspace 后判断是否初始状态 前端只判断,不决策
新项目执行 初始 workspace → 直接 run 不生成 runMaps
修改型执行 非初始 workspace → 先 AnalyzeIntent 由 Agent 决定 mode / nodes
运行参数生成 AnalyzeIntent 输出 runMaps 前端不理解语义
执行启动 RunProjectWorkflow 自动 SSE
执行反馈 后台推送运行状态 前端被动展示
产物管理 commit / rollback workspace workflow 不参与
错误处理 run 失败即终止 支持后续再次对话

对话驱动执行主逻辑(摘要)

workspace 状态 执行流程
初始状态 userInput → RunProjectWorkflow
非初始状态 userInput → AnalyzeIntent → RunProjectWorkflow

第十三章:运行时消息规范(run_events

13.1 概述

Workflow 执行过程中,引擎向 run_events 事件流写入结构化消息,用于向调用方(前端、MQ 消费者、日志系统)传递执行进度和阶段结果。

设计原则:

  • run_events 只描述过程,最终结论以 workflow_runs 为准
  • 每条事件独立可解析,不依赖前序事件状态
  • 事件流为尽力而为输出,消费侧异常不影响 workflow 执行

13.2 事件基础结构

{
  "run_id": "run_xxx",
  "seq": 1,
  "ts": "2026-02-21T10:00:00.123Z",
  "type": "step_start",
  "step_id": "LLM_Answer",
  "payload": {}
}
字段 类型 说明
run_id string 本次 workflow 执行唯一 ID
seq integer run 内单调递增序号,从 1 开始
ts string ISO 8601 时间戳(毫秒精度)
type string 事件类型
step_id `string \ null`
payload object 事件负载

13.3 事件类型

Workflow 级

  • workflow_start
  • workflow_done
  • workflow_failed
  • workflow_cancelled

Step 级

  • step_start
  • step_print
  • step_done
  • step_error
  • step_skipped

LLM 专属

  • llm_token(仅当节点 in.stream: true
  • llm_done

文件状态专属

  • file_start(步骤开始时声明将写入某路径的文件)
  • file_done(文件内容已完整写入对应路径)

Pause 专属

  • pause_start(进入 Pause_* 节点,workflow 进入 paused 状态)
  • pause_resumed(resume 成功,已写入 resumeResultTarget,即将继续执行)
  • pause_timeout(等待超时,即将跳转到 timeout.on
  • pause_rejected(resume 请求被拒绝,token 无效或状态不符)

顺序约束:

  • step_startllm_token(xN) → llm_donestep_print(0..1) → step_done
  • llm_done 必在所有 llm_token 之后
  • 同一步骤的所有 file_startstep_start 之后立即批量发出,早于任何 llm_token
  • 所有 file_donellm_done 之后、step_print/step_done 之前发出

13.4 事件 payload 规范(最小集)

workflow_start

{ "params": {} }

workflow_done

{ "stop_id": "Stop_End", "duration_ms": 4821 }

workflow_failed

{
  "failed_step_id": "LLM_Analyze",
  "error": { "type": "json_parse_error", "message": "Unexpected token", "retryable": true },
  "duration_ms": 1203
}

workflow_cancelled

{
  "reason": "user_request",
  "duration_ms": 800
}

step_start

{ "step_type": "LLM_*" }

step_done

{ "step_type": "LLM_*", "duration_ms": 2341 }

step_done 的 payload 不包含 _result

step_print

节点执行完成后发出,用于承载节点级自定义打印内容(来自该节点 print 属性)。

{ "message": "版本检查完成: 3.13" }
字段 类型 说明
message string print 求值后的输出文本

step_error

{
  "step_type": "LLM_*",
  "error": { "type": "rate_limit", "message": "Rate limit exceeded", "retryable": true, "status_code": 429 },
  "duration_ms": 312
}

step_skipped

{ "step_type": "Set_*", "reason": "if_false" }

file_start

步骤开始时批量发出,声明本步骤将写入的文件路径。此时文件内容尚未生成。

{
  "path": "src/components/Header.tsx"
}
字段 类型 说明
path string 文件路径(相对于 workspace 根目录)

file_done

文件内容已完整写入对应路径后发出,在 llm_done 之后、step_done 之前。

{
  "path": "src/components/Header.tsx",
  "size_bytes": 2048
}
字段 类型 说明
path string 与对应 file_start 事件中的 path 一致
size_bytes integer 写入后文件的字节数

llm_token

{ "delta": "这是" }

delta 为原始文本增量,不做 JSON parse。

llm_done

{
  "finish_reason": "stop",
  "usage": { "input_tokens": 312, "output_tokens": 428, "total_tokens": 740 },
  "model": "claude-sonnet-4-5",
  "latency_ms": 2180
}

pause_start

{
  "nodeId": "Pause_UserInput_Address",
  "reason": "请补充收货地址",
  "waitToken": "tok_abc123",
  "expireAt": "2026-03-03T10:00:00.000Z",
  "resumeResultTarget": "$vars.userInput.address"
}

pause_resumed

{
  "nodeId": "Pause_UserInput_Address",
  "requestId": "req_xyz",
  "resumedAt": "2026-03-02T11:30:00.000Z"
}

pause_timeout

{
  "nodeId": "Pause_UserInput_Address",
  "expiredAt": "2026-03-03T10:00:00.000Z",
  "timeoutAction": "Stop_Timeout"
}

pause_rejected

{
  "nodeId": "Pause_UserInput_Address",
  "requestId": "req_bad",
  "reasonCode": "INVALID_TOKEN"
}

13.5 并行与顺序保证

  • 串行节点:事件顺序与执行顺序一致
  • 并行节点:同一节点内事件有序;不同节点间不保证顺序
  • 消费方应使用 step_id + seq 进行重组

13.6 完整事件流示例

示例 A:包含单个 LLM_* 节点(in.stream: true,无文件写入)

{ "run_id": "run_abc", "seq": 1,  "ts": "...", "type": "workflow_start",  "step_id": null,         "payload": { "params": {} } }
{ "run_id": "run_abc", "seq": 2,  "ts": "...", "type": "step_start",      "step_id": "LLM_Answer", "payload": { "step_type": "LLM_*" } }
{ "run_id": "run_abc", "seq": 3,  "ts": "...", "type": "llm_token",       "step_id": "LLM_Answer", "payload": { "delta": "这是" } }
{ "run_id": "run_abc", "seq": 4,  "ts": "...", "type": "llm_token",       "step_id": "LLM_Answer", "payload": { "delta": "一个" } }
{ "run_id": "run_abc", "seq": 5,  "ts": "...", "type": "llm_token",       "step_id": "LLM_Answer", "payload": { "delta": "回答。" } }
{ "run_id": "run_abc", "seq": 6,  "ts": "...", "type": "llm_done",        "step_id": "LLM_Answer", "payload": { "finish_reason": "stop", "usage": { "input_tokens": 80, "output_tokens": 12, "total_tokens": 92 }, "model": "claude-sonnet-4-5", "latency_ms": 1340 } }
{ "run_id": "run_abc", "seq": 7,  "ts": "...", "type": "step_print",      "step_id": "LLM_Answer", "payload": { "message": "版本检查完成: 3.13" } }
{ "run_id": "run_abc", "seq": 8,  "ts": "...", "type": "step_done",       "step_id": "LLM_Answer", "payload": { "step_type": "LLM_*", "duration_ms": 1355 } }
{ "run_id": "run_abc", "seq": 9,  "ts": "...", "type": "workflow_done",   "step_id": null,         "payload": { "stop_id": "Stop_End", "duration_ms": 1400 } }

示例 B:包含文件写入的 workflow(一个步骤生成多个文件)

以一个代码生成 workflow 为例,LLM_GenCode 节点生成两个文件。两个 file_start 在步骤开始时批量发出,两个 file_donellm_done 之后发出:

{ "run_id": "run_xyz", "seq": 1,  "ts": "...", "type": "workflow_start",  "step_id": null,           "payload": { "params": {} } }
{ "run_id": "run_xyz", "seq": 2,  "ts": "...", "type": "step_start",      "step_id": "LLM_GenCode",  "payload": { "step_type": "LLM_*" } }
{ "run_id": "run_xyz", "seq": 3,  "ts": "...", "type": "file_start",      "step_id": "LLM_GenCode",  "payload": { "path": "src/components/Header.tsx" } }
{ "run_id": "run_xyz", "seq": 4,  "ts": "...", "type": "file_start",      "step_id": "LLM_GenCode",  "payload": { "path": "src/components/Header.module.css" } }
{ "run_id": "run_xyz", "seq": 5,  "ts": "...", "type": "llm_token",       "step_id": "LLM_GenCode",  "payload": { "delta": "import React" } }
{ "run_id": "run_xyz", "seq": 6,  "ts": "...", "type": "llm_token",       "step_id": "LLM_GenCode",  "payload": { "delta": " from 'react'..." } }
{ "run_id": "run_xyz", "seq": 7,  "ts": "...", "type": "llm_done",        "step_id": "LLM_GenCode",  "payload": { "finish_reason": "stop", "usage": { "input_tokens": 210, "output_tokens": 560, "total_tokens": 770 }, "model": "claude-sonnet-4-5", "latency_ms": 3100 } }
{ "run_id": "run_xyz", "seq": 8,  "ts": "...", "type": "file_done",       "step_id": "LLM_GenCode",  "payload": { "path": "src/components/Header.tsx", "size_bytes": 1872 } }
{ "run_id": "run_xyz", "seq": 9,  "ts": "...", "type": "file_done",       "step_id": "LLM_GenCode",  "payload": { "path": "src/components/Header.module.css", "size_bytes": 634 } }
{ "run_id": "run_xyz", "seq": 10, "ts": "...", "type": "step_print",      "step_id": "LLM_GenCode",  "payload": { "message": "代码文件生成完成: 2 个文件" } }
{ "run_id": "run_xyz", "seq": 11, "ts": "...", "type": "step_done",       "step_id": "LLM_GenCode",  "payload": { "step_type": "LLM_*", "duration_ms": 3280 } }
{ "run_id": "run_xyz", "seq": 12, "ts": "...", "type": "workflow_done",   "step_id": null,            "payload": { "stop_id": "Stop_End", "duration_ms": 3350 } }

第十四章:3.17 扩展与兼容性

14.1 目标

3.17 的目标不是推翻 3.16,而是把过去分散在实现层里的高价值能力收敛成一套可公开引用的规范:

  • workflow 调用宿主 tools
  • 外层窗口可感知完整工具事件
  • 并行运行从第一步起具备稳定归属
  • 从 checkpoint 在复杂 fork / loop 中继续执行

本章允许这些能力以“标准扩展剖面”的形式存在。实现方可以分阶段支持,但一旦声明支持 3.17 扩展剖面,就应满足本章的字段、事件与兼容性要求。

14.2 Tool_* 节点

14.2.1 定位

Tool_* 用于调用 workflow 宿主环境提供的工具能力,例如文件、检索、编译、浏览器控制、DocCenter、子 workflow 调度等。

Tool_* 属于 3.17 标准扩展剖面。不支持该剖面的运行时,应在加载期或执行期显式报错,而不是静默跳过。

14.2.2 推荐结构

{
  "id": "Tool_ReadBlueprint",
  "tool": "ReadFile",
  "input": { "file_path": "docs/blueprint.md" },
  "timeout": 15000,
  "allowError": false,
  "out": {
    "$blueprint": "_result",
    "$readMeta": "_toolResult"
  },
  "next": "LLM_AnalyzeBlueprint"
}

字段约定:

字段 类型 说明
tool / toolName string 要调用的宿主工具名;两者二选一,推荐 tool
input / in object 传给工具的输入对象;两者二选一,推荐 input
timeout integer 可选,毫秒级超时
allowError boolean true 时,工具失败不会直接使 workflow failed,而是将错误信息作为结果暴露给后续步骤
out object 输出映射,语义与其他节点保持一致

14.2.3 输出映射

Tool_* 执行完成后,至少应向 out 提供以下两个临时值:

  • _result:工具的主结果;若工具返回 envelope 且存在 result 字段,可优先解包为主结果
  • _toolResult:工具的完整原始返回对象,保留元数据、diff、undoId、usage 等附加字段

14.2.4 错误语义

  • allowError = false:工具异常应导致当前节点报错,并使 workflow 进入 failed
  • allowError = true:运行时应发出 tool_error,同时允许后续节点读取错误 envelope 并自行决策

14.2.5 Subflow_* 语义化别名

当 workflow 需要显式表达“此节点会调度一个子 workflow”时,可以使用 Subflow_* 作为 WorkflowRun 的语义化别名。

约束:

  • Subflow_* 不引入新的底层执行原语
  • 运行时可以将其降级为对宿主 WorkflowRun 能力的调用
  • editor 应将 Subflow_* 与普通 Tool_* 在视觉上区分开,便于识别子流程边界
  • 推荐字段包括:workflow_pathparamsmodework_diremit_events

14.3 工具运行时事件

3.17 在第十三章基础上新增以下事件类型:

  • tool_start
  • tool_message
  • tool_done
  • tool_error

14.3.1 最小 payload

tool_start

{
  "nodeId": "Tool_ReadBlueprint",
  "name": "ReadFile",
  "input": { "file_path": "docs/blueprint.md" }
}

tool_message

{
  "nodeId": "Tool_ReadBlueprint",
  "name": "ReadFile",
  "level": "info",
  "message": "Reading file",
  "data": { "file_path": "docs/blueprint.md" }
}

tool_done

{
  "nodeId": "Tool_ReadBlueprint",
  "name": "ReadFile",
  "output": { "result": "...file content..." }
}

tool_error

{
  "nodeId": "Tool_ReadBlueprint",
  "name": "ReadFile",
  "error": "ENOENT: file not found"
}

14.3.2 抛出要求

工具相关事件必须满足以下要求:

  • 必须进入 workflow 的标准事件流
  • 必须可被外层窗口、SSE 客户端、日志系统、嵌套 workflow 调用方感知
  • 不得只在节点内部吞掉 message/progress/warn/error
  • 并行 run 下,必须带上足以归属 run 的标识(见 14.4)

14.4 运行标识与并行归属

14.4.1 runIDclientRunToken

runID 是运行时正式分配的执行实例标识;但在部分实现中,它可能直到 workflow_start 之后或首次 checkpoint 之后才稳定。

3.17 引入 clientRunToken 作为前置、稳定、由调用方或前端先生成的运行归属标识。

要求:

  • clientRunToken 应从 workflow_start 开始就随事件一起抛出
  • 一次新的 RunrerunFromStepexecuteFrom,都应拥有新的 clientRunToken
  • 即使底层 runID 被复用,外层 UI 也不应因此把不同 session 混成同一条 run

14.4.2 并行场景

对于同一个 workflow 图上的多次并行运行:

  • pause / resume / cancel 应按选定 run 生效
  • tool_*llm_*checkpointnode_* 事件都应能按 runIDclientRunToken 正确归属
  • “从第一步高亮到结束”的 run 轨迹应保持独立

14.5 Checkpoint、断点续跑与复杂分支

3.17 明确要求:基于 checkpoint 的继续执行不仅适用于简单串行流,也应覆盖 fork / loop 等复杂结构。

14.5.1 rerunFromStep / executeFrom

当调用方指定某个节点作为继续执行起点时,运行时应:

  • 保留本次继续执行仍然依赖的 artifacts 与变量
  • 对已完成分支信息、loop 进度、当前 step 指针进行归一化
  • 允许调用方提供 overrides 覆盖输入变量
  • 防止旧 checkpoint 中的已完成 sibling branch 错误地阻止新的下游执行

14.5.2 Fork / Loop 最低要求

  • fork 续跑:允许从某个 fork 后继节点向下继续,且不会错误复用无关 sibling branch 的完成态
  • loop 续跑:允许从中途 iteration 之后继续,必要时重建 loop progress
  • parallel loop 续跑:允许在并行 loop 中基于 checkpoint 继续剩余迭代

14.6 Workflow-of-Workflows

3.17 认可“workflow 调度 workflow”作为推荐模式之一。

典型形式包括:

  • 父 workflow 通过 Tool_* 调用宿主 WorkflowRun 能力
  • 父 workflow 并行调度多个子 workflow
  • 子 workflow 的 node_* / tool_* / checkpoint 摘要通过 tool_message 或宿主桥接向父级外抛

该模式特别适合:

  • 多 worker 软件工程流水线
  • 数学/研究问题的分而治之探索
  • 带人工 gate 的审查与返工链路

14.7 迁移建议

从 3.16 迁移到 3.17 时,建议按以下顺序进行:

  1. 先发布文档与 capability 说明
  2. 保持现有 workflow JSON 继续使用 "version": "3.16",避免一次性打断旧运行时
  3. 对支持 Tool_*tool_messageclientRunToken、checkpoint 续跑的实现,显式标注支持 3.17 扩展剖面
  4. 等所有关键运行时完成升级后,再逐步切换到 "version": "3.17"

本规范的 canonical copy 发布于 DocCenter Path 3。若本地镜像与文档中心不一致,以文档中心最新版本为准。