Workflow 模型

workflow-code 的业务代码是自包含的 TypeScript workflow。平台、CLI、server 和 App 会注入一个全局命名空间 workflow,业务 workflow 不应该从 "workflow-code" 导入 runtime API,也不应该在 workflow package.json 里声明仓库内部依赖。

Runtime API

当前 workflow 全局对象来自 core/runtime-api.ts,包含:
workflow.defineWorkflow
workflow.defineExecutor
workflow.createInputNode
workflow.createFileInputNode
workflow.createUserInputNode
workflow.createLLMNode
workflow.createLLMStreamNode
workflow.createLLMProviderFromEnv
workflow.createLLMProviderRef
workflow.createOutputNode
workflow.createDetailsOutputNode
workflow.createOutputItem
workflow.createOutputPayload
workflow.addOutputItem
workflow.createOutputItemChunk
workflow.createTimerNode
workflow.endTimer
workflow.createQuestionClassifierNode
workflow.runNode
workflow.runStreamNode
workflow.runIf
workflow.runFor
workflow.runWhile
workflow.runWorkflow
workflow.getEnv
workflow.getRuntimePlatform
workflow.getLLMProvider
workflow.WorkflowError
完整方法、参数和运行对象请看 Core API

Workflow

Workflow 是业务流程本体,由 workflow.defineWorkflow<Input, Output>() 定义:
export const exampleWorkflow = workflow.defineWorkflow<Input, OutputPayload>({
  name: "example",
  async run(input, context) {
    const parsed = await workflow.runNode(inputNode, input, context);
    const result = await workflow.runStreamNode(outputNode, parsed, context);
    return result.output;
  },
});
Workflow 的 run 接收 inputcontext,并返回最终 payload。执行期间如果抛出异常,executor 会把它序列化到执行报告中。

Node

Node 是最小可执行单元,分为普通节点、流式节点、分支节点和循环节点:
const inputNode = workflow.createInputNode<Input, Parsed>({
  name: "input",
  parse(input) {
    return { message: input.message.trim() };
  },
});

const outputNode = workflow.createOutputNode<Parsed, OutputPayload>({
  name: "output",
  async *stream(input) {
    yield input.message;
  },
  format(stream) {
    return workflow.createOutputPayload({
      items: stream.items,
    });
  },
});
输出 payload 的当前协议是 WorkflowOutputPayload.items: WorkflowOutputItem[]。每个 item 都是一个可独立展示和折叠的消息块,包含 idtitlecontent、可选 icon / iconPresetcontentTypecollapsedmetadatacontentType 只决定渲染方式:markdowntextjsonaudio;对象和数组应直接放进 content,不要提前 stringify 成 Markdown code block。audio content 使用 { src, mimeType, name, size, durationMs?, format? }src 可为安全 data:audio/...;base64,...http(s) URL。 workflow.createDetailsOutputNode 是创建独立可折叠 output item 的便捷节点,会把 { title, content, icon } 转成 items: [{ title, content, icon, contentType, collapsed: true }],并仍按 output 标准节点被 CLI、App 和 server 收集。workflow.createTimerNode 会创建标准名为 timer 的普通节点,用于启动业务计时;workflow.endTimer(timer, context, { status, message }) 显式结束计时并写入 report.timers,未结束的 running timer 会在 workflow 收尾前自动标记为 cleared,避免 UI 残留运行态。workflow.runNode 返回节点输出;workflow.runStreamNode 返回 { chunks, output },其中 chunks 是原始流式 chunk 数组,outputfinalizeformat 生成的最终 payload。流式 createOutputNode({ stream, format })format 接收累积后的 { text, items, chunks };字符串 chunk 会自动写入一个默认 Markdown item,结构化输出可使用 workflow.createOutputItemChunk(...) 发送 output_item_startoutput_item_deltaoutput_item_endoutput_item_delta 除了合并 content,还可以携带 item patch 更新 titleiconcollapsedcontentTypemetadata.status,用于展示工具调用从 running 到 success / failed 的状态变化。workflow.runIf 会按顺序检查 if / else-if / else 分支,命中后执行对应 run,并用 branch-node 运行事件记录所选分支。workflow.runForworkflow.runWhile 会把循环本身记录为可追踪节点,让循环体在 App Diagram 中保持可见。 workflow.createUserInputNode 会在 workflow 中插入一个可持久恢复的人工输入点。节点的 params 使用与 executor.params 相同的字段定义,App、server embed 和 server API 会用同一套 Run workspace 表单展示。首次执行时如果没有提交值,runtime 发出 user_input_requested 事件,report 写入 pendingUserInput,run 状态变为 waiting_for_input;提交答案后,同一个 runId 会带着 resolvedUserInputs 恢复执行,并发出 user_input_resolved。CLI 不读取 stdin,遇到该节点时会输出等待报告,需要通过 workflow-code json --run-id <id> --resolved-user-inputs-json '<json>' 注入答案继续。

Branch

workflow.runIf 适合把业务条件写成可追踪的 workflow 分支,而不是只写在普通 TypeScript if 里:
const plan = await workflow.runIf(seed, context, {
  name: "Weapon branch",
  branches: [
    {
      label: "high intensity assault",
      condition: (mission) => mission.intensity >= 0.66,
      run: (mission) => ({ ...mission, weapon: "ak47" }),
    },
    {
      label: "stealth close quarter",
      condition: (mission) => mission.stealth >= 0.58,
      run: (mission) => ({ ...mission, weapon: "mp7" }),
    },
  ],
  else: {
    label: "balanced rifle",
    run: (mission) => ({ ...mission, weapon: "m4a1" }),
  },
});
branches 数组中的第一个元素在 Diagram 中显示为 if,后续元素显示为 else-ifelse 会显示成 fallback 分支。分支的 run 里可以继续调用 workflow.runNodeworkflow.runStreamNode,也可以嵌套另一个 workflow.runIf 形成多层分支。

Loop

workflow.runForworkflow.runWhile 适合把业务循环写成可追踪的 workflow 循环,而不是只写在普通 TypeScript for / while 里:
const rounded = await workflow.runFor(seed, context, {
  name: "Round loop",
  maxIterations: 6,
  items: (state) => Array.from({ length: state.rounds }, (_, index) => index + 1),
  async run(state, value, index, loopContext) {
    return workflow.runNode(addRoundNode, { ...state, value, index }, loopContext);
  },
});

const boosted = await workflow.runWhile(rounded, context, {
  name: "Boost until target",
  maxIterations: 8,
  condition: (state) => state.total < state.target,
  async run(state, loopContext, iteration) {
    return workflow.runIf(state, loopContext, {
      name: "Choose boost strategy",
      branches: [
        {
          label: "burst",
          condition: (current) => current.target - current.total >= 7,
          async run(current, branchContext) {
            return workflow.runFor(current, branchContext, {
              name: "Burst passes",
              maxIterations: 3,
              items: () => [4, 3],
              async run(passState, value, index, passContext) {
                return workflow.runNode(boostNode, { ...passState, value, index, iteration }, passContext);
              },
            });
          },
        },
      ],
      else: {
        label: "finish",
        async run(current, branchContext) {
          return workflow.runNode(boostNode, { ...current, iteration }, branchContext);
        },
      },
    });
  },
});
runFor 会按 items 生成的 iterable 顺序执行,默认最多 1000 次;runWhile 会在每轮开始前检查 condition,默认最多 100 次。两者的 run 都应返回下一轮状态。完成事件的 executionInfo 会记录 loopTypeiterationsiterationCountmaxIterationscompleted

Diagram 和节点协议

App diagram 使用两类信息展示 workflow:
  • 静态结构来自 TypeScript 源码分析。所有已知 workflow.* runtime API 调用会写入 definitions.apiCalls,并以静态 API 节点展示,例如 workflow.createInputNodeworkflow.defineWorkflowworkflow.getEnv。这些节点帮助理解源码使用了哪些平台能力,但不会单独执行,也不会串进 Input -> Result 执行路径。(issue #48)
  • 运行状态来自 workflow.runNode / workflow.runStreamNode / workflow.runIf / workflow.runFor / workflow.runWhile / workflow.runWorkflownodeHooks 报告。只有真正被这些 API 执行的节点会显示运行中、成功、失败、耗时和错误。
workflow.runIf 会作为 run-if 分支节点展示,App 会使用分支图标和语义色与 LLM、输出、provider、question-classifier 区分。完成事件的 executionInfo 会记录 selectedBranchselectedBranchIndexdescription,用于 Trace 查看实际走到了哪条分支。静态结构分析优先识别对象字面量形式的 branches / else;动态拼装分支仍可运行,但 Diagram 无法完整展开所有静态路径。 workflow.runForworkflow.runWhile 会作为 run-for / run-while 循环节点展示。App Diagram 会用带语义色的矩形背景容器包裹循环体 children,循环头节点仍可点击查看 itemsconditionmaxIterations 和运行时迭代信息。循环体可以继续嵌套 workflow.runIfworkflow.runForworkflow.runWhile,用于表达 while 内部分支和多层循环。静态结构分析优先识别对象字面量形式的 loop options;动态拼装 options 仍可运行,但 Diagram 无法完整展开循环体。 workflow.runWorkflow 会作为 run-workflow 调用节点展示。子 workflow 不会创建独立 run report,子节点事件会进入父 trace;顶层 executor 入口不会额外插入这个包裹节点。默认 conversationMode: "shared"kvMode: "shared"outputVisibility: "visible",只传 name 不会改变会话或输出语义;当父 workflow 传入 conversationMode: "shared-readonly" 时,子 workflow 可以读取父会话快照,但不会追加 transcript 或修改标题。子 workflow 会写专属 KV 状态时,可同时传入 kvMode: "isolated" 内置节点工厂返回的普通节点继承 BaseNode,流式节点继承 BaseStreamNode;它们共同提供 namestandardNamemetadatagetExecutionInfo() 等公共信息。Diagram 不直接绑定 BaseNode 类,而是读取 NodeDefinition / StreamNodeDefinition 执行协议和运行报告,因此未来自定义节点只要符合协议,也能被执行与记录。

Provider

当前 executor 的内置 provider 名称只有 "llm"。LLM provider 通过 createLLMProviderRef 延迟解析:
const llmProvider = workflow.createLLMProviderRef("default", () => {
  return workflow.createLLMProviderFromEnv();
});
业务节点依赖统一 provider 接口,不直接绑定具体厂商。createLLMProviderFromEnv 会从 workflow runtime 环境读取 LLM_TYPELLM_API_KEYLLM_BASE_URLLLM_MODEL

Executor

Executor 是平台发现和运行 workflow 的入口:
export const executor = workflow.defineExecutor<Input, OutputPayload>({
  workflow: exampleWorkflow,
  params: [
    {
      name: "message",
      flag: "--message",
      type: "string",
      required: true,
      description: "输入文本。",
    },
  ],
  createInput({ args }) {
    return parseArgs(args);
  },
});
createInput 会接收 workflowNameworkflowDirargsenvabortSignal 和可选 conversationId。App、CLI、server 和外部 API 最终都通过 executor 把参数转换为 workflow input;取消本地或 server 运行时,abortSignal 会传播到 workflow context,便于长耗时 provider/SDK 调用及时停止。

Registry

Registry 用于单节点调试和结构展示。它必须是静态数组字面量:
export const executor = workflow.defineExecutor<Input, OutputPayload>({
  workflow: exampleWorkflow,
  registry: [
    {
      name: "output",
      node: outputNode,
      stream: true,
      metadata: { type: "output", title: "Output", version: "1.0.0" },
      createInput({ args }) {
        return parseArgs(args);
      },
    },
  ],
  createInput({ args }) {
    return parseArgs(args);
  },
});
server 的 /api/workflows/{name}/debug/nodes/{nodeName} 会使用 registry 中的节点配置运行单节点调试。

包结构

workflow package 只声明真实第三方依赖,不声明 workflow-code。CLI、App 和 server 会提供外层运行时:
{
  "id": "1f221460-4fc7-48e2-bd23-651dee826692",
  "name": "@workflow-code/hello-workflow",
  "version": "0.1.0",
  "private": true,
  "type": "module"
}

追踪

本文档首版由 issue #32 记录。API 列表对齐 core/runtime-api.tscore/executor/types.ts