# VL-Workflow-Engine API Reference > Version: 0.4.0 | Spec: v3.16 core + VLCode extensions | Updated: 2026-03-14 --- ## 1. Architecture Overview ``` ┌─────────────────────────────────────────────────────┐ │ VL-Workflow-Engine (pure JS library, zero deps) │ │ - Parse / validate / execute workflow JSON │ │ - Event stream for all runtime state │ │ - Checkpoint / resume / re-run from any step │ │ - Adapter pattern for LLM, API, file I/O │ └──────────────────────┬──────────────────────────────┘ │ require('vl-workflow-engine') ┌──────────────────────▼──────────────────────────────┐ │ VLClaw Broker (HTTP server, port 9160) │ │ - Implements real adapters (Claude SDK, file I/O) │ │ - Converts engine events → SSE stream │ │ - Persists checkpoints for crash recovery │ │ - Exposes REST API for VLCode │ └──────────────────────┬──────────────────────────────┘ │ SSE / REST ┌──────────────────────▼──────────────────────────────┐ │ VL-Code IDE (Web IDE, port 3200) │ │ - Chat window, Detail Log, Status Bar, Flow Tab │ │ - Subscribes to SSE for real-time updates │ │ - Shows inputs/outputs per step │ │ - Supports re-run from any step (with overrides) │ └─────────────────────────────────────────────────────┘ ``` --- ## 2. Quick Start ```js const { Engine } = require('vl-workflow-engine'); const workflow = { /* workflow JSON per spec v3.16 */ }; const engine = new Engine(workflow, { onEvent: (event) => console.log(event), // global event listener onCheckpoint: (cp) => saveToStorage(cp) // auto-save after each step }); // Validate const errors = engine.validate(); if (errors.length) throw new Error(errors.join('\n')); // Execute const ctx = await engine.execute( { projectName: 'MyApp' }, // initial params { // adapters llm: myClaudeAdapter, file: myFileAdapter, doc: myDocAdapter, } ); console.log(ctx.snapshot()); // query current state console.log(ctx.checkpoint()); // serializable checkpoint for persistence ``` --- ## 3. Event System (Complete) All events are emitted via `ctx.onEvent(fn)`. Each event has this shape: ```ts interface RunEvent { type: string; // RunEventType value stepID: string|null; // which step emitted it (null for workflow-level) payload: object; // type-specific data seq: number; // auto-incrementing sequence number ts: string; // ISO 8601 timestamp runID: string; // workflow instance ID } ``` ### 3.1 Event Type Reference | Type | When | Payload | |------|------|---------| | `workflow_start` | Workflow begins | `{ name, version, params, resumedFrom? }` | | `workflow_done` | Workflow completes | `{ duration_ms, stop_id? }` | | `workflow_failed` | Workflow throws | `{ error, failed_step_id, duration_ms }` | | `workflow_cancelled` | Abort called | `{ duration_ms }` | | **`step_start`** | Step begins | `{ type, meta, resolvedInputs }` | | **`step_done`** | Step completes | `{ outputs, selected? }` | | `step_error` | Step throws | `{ error }` | | `step_skipped` | `step.if` is false | `{ condition }` | | `step_print` | `step.print` evaluated | `{ value }` | | `llm_token` | Response token streamed | `{ delta }` | | `llm_thinking` | Thinking/reasoning token | `{ delta }` | | `llm_tool_use` | LLM requests tool call | `{ tool_use_id, name, input }` | | `llm_tool_result` | Tool result fed back | `{ tool_use_id, content, is_error }` | | `tool_start` | VLCode `Tool_*` step begins | `{ name, input, inputSummary }` | | `tool_message` | Tool emits runtime progress/info | `{ name, level, message, data }` | | `tool_done` | VLCode `Tool_*` step completes | `{ name, output, outputSummary, toolResult }` | | `tool_error` | VLCode `Tool_*` step fails | `{ name, error, allowError, output, toolResult }` | | `llm_error` | LLM call failed | `{ error, type, code, retryable, latency_ms }` | | `llm_done` | LLM call completed | `{ latency_ms, finish_reason, model, usage, thinking_tokens, has_tool_use }` | | `file_start` | File write begins | `{ path }` | | `file_done` | File write done | `{ path, size_bytes }` | | `pause_start` | Waiting for human | `{ nodeId, waitToken, reason, expireAt }` | | `pause_resumed` | Human approved | `{ nodeId, requestId, resumedAt }` | | `pause_timeout` | Pause timed out | `{ nodeId, expiredAt, timeoutAction }` | | `pause_rejected` | Invalid resume token | `{ reason }` | | `var_changed` | Pipeline variable changed | `{ name, oldValue, newValue }` | > **Bold** = enhanced in v0.3.0 (new payload fields) ### 3.2 step_start Payload — resolvedInputs (NEW in v0.3.0) Every `step_start` event now includes the step's resolved input parameters: ```js { type: 'step_start', stepID: 'LLM_AnalyzeRequest', payload: { type: 'LLM', meta: { title: 'Analyze user request' }, resolvedInputs: { // ← NEW userRequest: "添加一个登录页面", model: "claude-opus-4-6", currentMeta: { _truncated: true, // auto-truncated (> 10KB) type: 'object', length: 52340, preview: '{"projectName":"MyApp","pages":[{"name":"Home"...' } } } } ``` - `resolvedInputs` is the result of evaluating all expressions in `step.in` - `null` if the step has no `in` block - Large values (> 10KB serialized) are auto-truncated to `{ _truncated, type, length, preview }` - Evaluation is best-effort: if an expression fails, `resolvedInputs` is `null` (step execution is unaffected) ### 3.3 step_done Payload — outputs (NEW in v0.3.0) Every `step_done` event now includes the step's output results: ```js { type: 'step_done', stepID: 'LLM_AnalyzeRequest', payload: { outputs: { // ← NEW '$plan': { action: 'add-page', pageName: 'Login', ... }, '$updatedMeta': { _truncated: true, type: 'object', length: 52340, preview: '{"projectName":"MyApp"...' } } } } // Branch step also includes `selected`: { type: 'step_done', stepID: 'Branch_Route', payload: { selected: 'LLM_AddPage', // which branch was taken outputs: null // Branch typically has no out mapping } } ``` - `outputs` is collected by reading back `step.out` targets from the execution context - For `$variable` targets: the current variable value (after output mapping) - For `/file/path` targets: `{ _file: true, path: '/path/to/file' }` (no file content) - `null` if the step has no `out` block - Large values auto-truncated (same 10KB threshold) ### 3.4 Truncation Format Any value > 10KB in `resolvedInputs` or `outputs` is replaced with: ```ts interface TruncatedValue { _truncated: true; type: string; // 'object' | 'string' length: number; // original JSON length in chars preview: string; // first 200 chars + '...' } ``` Consumers should check `value?._truncated === true` to detect truncated values. ### 3.5 Event Ordering Constraint For an LLM step, events are emitted in this order: ``` step_start { resolvedInputs } ← inputs visible → llm_thinking(xN) ← thinking/reasoning tokens → llm_token(xN) ← response tokens → llm_tool_use(0..N) ← tool call requests → llm_tool_result(0..N) ← tool execution results → [loop: llm_token → llm_tool_use → ...] ← agentic multi-turn → llm_done ← final summary with usage → var_changed(0..N) ← pipeline variable mutations → file_done(0..N) ← file writes from output mapping → step_print(0..1) ← optional print → step_done { outputs } ← outputs visible ``` On error: `step_start → llm_error → step_error` (no `step_done`). ### 3.6 VLCode Tool_* Runtime Events (Extension) `Tool_*` is a VLCode extension layered on top of the engine's custom handler mechanism. When VLCode registers a `Tool` custom handler, a tool step emits: ```txt step_start → tool_start → tool_message(0..N) → tool_done | tool_error → var_changed(0..N) → step_done | step_error ``` Runtime mapping variables during a `Tool_*` step: - `_result`: normalized primary tool payload - `_toolResult`: full raw tool envelope returned by the underlying VLCode tool Normalization rule used by VLCode: - If the tool returns `{ result: ... }`, `_result` is auto-unwrapped to that inner payload - If the workflow needs metadata such as `diff`, `undoId`, or the raw nested envelope, use `_toolResult` If `allowError` or `continueOnError` is set on the step: - `tool_error` still fires - `_result` becomes `{ ok: false, error, tool }` - the workflow continues to `step_done` Tool implementations may accept a second `runtime` argument: ```js execute: async (input, runtime = {}) => { runtime.info?.('Reading file', { file_path: input.file_path }); runtime.progress?.('Halfway done', { bytes: 4096 }); return { result: '...' }; } ``` VLCode exposes these helpers on `runtime`: - `emit(type, payload)` - `text(message, meta)` - `info(message, data)` - `warn(message, data)` - `error(message, data)` - `progress(message, data)` All of them are surfaced to workflow observers as `tool_message` events and carry `runID` plus `clientRunToken` through the HTTP/SSE layer. ### 3.7 Subscribing to Events ```js // Method 1: via Engine constructor (receives all events) const engine = new Engine(workflow, { onEvent: (event) => { /* ... */ } }); // Method 2: via ExecutionContext (after execute starts) const ctx = await engine.execute(params, adapters); ctx.onEvent((event) => { /* ... */ }); ``` --- ## 4. Checkpoint & Resume The engine supports **checkpointing** (saving execution state) and **resuming** (re-executing from any step). This enables three key scenarios: 1. **Crash recovery**: server crashes mid-workflow → restart from last completed step 2. **Re-run with edits**: user spots an error → tweaks a variable → re-runs from that step 3. **Partial resume**: parallel branches or loop iterations partially completed → only re-run what's missing ### 4.1 Checkpoint Object (v2) ```ts interface WorkflowCheckpoint { _type: 'vl_workflow_checkpoint'; _version: 2; workflowID: string; // "wf_1710000000000" currentStepID: string; // last step that was running status: string; // "running" | "completed" | "failed" | ... params: Record; // read-only input params paramTypes: Record; variables: Record; // current pipeline variables ($xxx) varTypes: Record; localVars: Record; // _result, _meta, etc. artifacts: Record; // files written so far completedSteps: string[]; // ordered list of completed step IDs // Phase 2: parallel & loop state completedBranches: Record; // { parentStepID: [completed child IDs] } loopProgress: Record; // { loopStepID: completedIterationCount } eventSeq: number; // last event sequence number version: string; // workflow spec version createdAt: string; // ISO 8601 timestamp } ``` > **Backward compatible**: v1 checkpoints (without `completedBranches`/`loopProgress`) still work — they are treated as having no parallel/loop state, so everything re-runs. ### 4.2 Creating Checkpoints ```js // Manual: call ctx.checkpoint() anytime const cp = ctx.checkpoint(); fs.writeFileSync('checkpoint.json', JSON.stringify(cp)); // Automatic: onCheckpoint callback fires after every step completion const engine = new Engine(workflow, { onCheckpoint: (cp) => { // Called after each step_done with the latest state redis.set(`checkpoint:${cp.workflowID}`, JSON.stringify(cp)); // or: fs.writeFileSync(`/tmp/${cp.workflowID}.json`, JSON.stringify(cp)); } }); ``` ### 4.3 Resuming from Checkpoint (Crash Recovery) ```js // Server crashed while running LLM_GenerateCode. // On restart, load the last checkpoint and resume from the crashed step: const cp = JSON.parse(redis.get('checkpoint:wf_171000000')); // cp.currentStepID is the last step that was running when the crash happened // cp.completedSteps shows what already completed const engine = new Engine(workflow); const ctx = await engine.executeFrom(cp, adapters); // → Starts from cp.currentStepID, all variables restored // → Does NOT re-run completed steps ``` ### 4.4 Re-run from a Specific Step (User-Initiated) ```js // User ran the workflow. LLM_GenerateCode produced bad output. // User wants to fix $plan and re-run from LLM_GenerateCode: // Option A: Use the last checkpoint, override variables const ctx = await engine.executeFrom( lastCheckpoint, adapters, { '$plan': userFixedPlan } // ← variable overrides ); // Option B: Build a minimal resume object manually const ctx = await engine.executeFrom( { currentStepID: 'LLM_GenerateCode', params: { projectName: 'MyApp' }, variables: { '$plan': userFixedPlan, '$meta': existingMeta, // ... all variables that the step needs } }, adapters ); ``` ### 4.5 executeFrom() Full Signature ```ts class Engine { async executeFrom( checkpoint: WorkflowCheckpoint | { currentStepID: string; // required: which step to start from params?: Record; variables?: Record; localVars?: Record; // ... any checkpoint fields }, adapters?: AdapterMap, overrides?: Record // applied AFTER checkpoint restore ): Promise; } ``` **Behavior**: - Rebuilds an `ExecutionContext` from the checkpoint data - Applies `overrides` on top (can override `$variables`, `_localVars`, or `params`) - Starts executing from `checkpoint.currentStepID` and follows the `next` chain to completion - Emits `workflow_start` with `{ resumedFrom: stepID }` in the payload - Emits all normal step events (step_start, step_done, etc.) - `onEvent` and `onCheckpoint` callbacks work normally **Important notes**: - Steps before `currentStepID` are NOT re-executed - The caller must ensure all variables that the target step needs are present in the checkpoint - Adapters (LLM, file, API) cannot be serialized — they must be re-injected at resume time - For Pause steps: if the workflow was paused when it crashed, you need to re-run from that Pause step ### 4.6 Checkpoint Lifecycle — Linear ``` execute() │ ▼ Set_Init ──step_done──→ onCheckpoint({completedSteps:['Set_Init']}) │ ▼ LLM_Analyze ──step_done──→ onCheckpoint({completedSteps:['Set_Init','LLM_Analyze']}) │ ▼ LLM_Generate ──💥 CRASH │ ▼ (on restart) executeFrom(lastCheckpoint) │ ▼ LLM_Generate ──step_done──→ onCheckpoint({completedSteps:[...,'LLM_Generate']}) │ ▼ Write_Output ──step_done──→ ... │ ▼ workflow_done ``` ### 4.7 Checkpoint Lifecycle — Parallel Branches ``` Noop_Fork ├──→ Set_A ✅ ──→ onCheckpoint({completedBranches: {'Noop_Fork': ['Set_A']}}) ├──→ Set_B ✅ ──→ onCheckpoint({completedBranches: {'Noop_Fork': ['Set_A','Set_B']}}) └──→ Set_C 💥 CRASH │ ▼ (on restart) executeFrom({currentStepID: 'Noop_Fork', completedBranches: {'Noop_Fork': ['Set_A','Set_B']}}) │ ▼ Noop_Fork ├── Set_A → SKIPPED (already in completedBranches) ├── Set_B → SKIPPED (already in completedBranches) └── Set_C → RUNS ✅ │ ▼ next step... ``` **Key behaviors**: - Each branch completion emits a checkpoint with updated `completedBranches` - On resume, `_executeChildren` filters out branches whose entry ID is in `completedBranches[parentStepID]` - If a branch is a chain (Set_A → Set_A2), the entire chain re-runs if the entry step (Set_A) is not marked complete - If ALL branches are completed, `_executeChildren` returns immediately and proceeds to `step.next` ### 4.8 Checkpoint Lifecycle — Loop Resume ``` Loop_Process (source: 100 items, serial mode) │ ├── iteration 0 ✅ → onCheckpoint({loopProgress: {'Loop_Process': 1}}) ├── iteration 1 ✅ → onCheckpoint({loopProgress: {'Loop_Process': 2}}) ├── ... ├── iteration 59 ✅ → onCheckpoint({loopProgress: {'Loop_Process': 60}}) ├── iteration 60 💥 CRASH │ ▼ (on restart) executeFrom({currentStepID: 'Loop_Process', loopProgress: {'Loop_Process': 60}}) │ ▼ Loop_Process ├── iterations 0-59 → SKIPPED ├── iteration 60 → RUNS (item=source[60], _index=60) ├── iteration 61 → RUNS ├── ... └── iteration 99 → RUNS ✅ │ ▼ next step... ``` **Key behaviors**: - Serial loops emit a checkpoint after **every iteration** — crash recovery is precise to single iteration - Parallel loops record total progress after all iterations complete - `_index` is the true array index (e.g., 60), not a relative offset - `_item` is `source[_index]` — correct even after resume - If `loopProgress[stepID] >= source.length`, the loop body doesn't execute at all ### 4.9 Checkpoint Timing Summary | Event | Checkpoint emitted? | What's tracked | |-------|-------------------|----------------| | Step completes (step_done) | Yes | `completedSteps` updated | | Parallel branch completes | Yes | `completedBranches[parent]` updated | | Serial loop iteration completes | Yes | `loopProgress[loopID]` incremented | | Parallel loop fully completes | Yes | `loopProgress[loopID]` = source.length | | Step error / workflow fail | No | Last good checkpoint is the recovery point | --- ## 5. LLM Adapter Interface The engine calls `ctx.llmAdapter.call(params, onToken, callbacks)`. ### 5.1 Signature ```ts interface LLMAdapter { call( params: LLMParams, onToken?: (delta: string) => void, // legacy: response tokens only callbacks?: { // extended: full observability onToken: (delta: string) => void; // response token onThinking: (delta: string) => void; // thinking/reasoning token onToolUse: (toolUse: ToolUseBlock) => void; onToolResult: (toolResult: ToolResultBlock) => void; } ): Promise; } ``` ### 5.2 LLMParams (input) ```ts interface LLMParams { model?: string; // e.g. "claude-opus-4-6" (MUST default to opus) system?: string; // system prompt (docs auto-injected here) messages: Message[]; // conversation history stream?: boolean; // enable streaming (default: false) tools?: Tool[]; // tool definitions for agentic loops docs?: number[]; // DocCenter IDs — auto-fetched and injected output_config?: { type: 'json_object' | 'json_schema'; schema?: object; schemaRef?: string; // resolved from registry }; max_tokens?: number; temperature?: number; thinking?: { type: 'enabled', budget_tokens: number }; // enable extended thinking } ``` ### 5.3 LLMResult (output) ```ts interface LLMResult { content: string | object; // response content model: string; // actual model used id?: string; // response ID finish_reason?: string; // 'stop' | 'end_turn' | 'tool_use' | ... stop_reason?: string; // alias usage: { input_tokens: number; output_tokens: number; thinking_tokens?: number; // extended thinking tokens consumed }; tool_calls?: ToolUseBlock[]; // if model requested tool use tool_use?: ToolUseBlock[]; // alias (Anthropic format) thinking?: string; // full thinking text (non-streaming) } ``` ### 5.4 Tool Use Types ```ts interface ToolUseBlock { id: string; // "toolu_xxx" name: string; // tool name input: object; // tool arguments } interface ToolResultBlock { tool_use_id: string; // matches ToolUseBlock.id content: string; // tool output is_error?: boolean; // was it an error } ``` ### 5.5 Adapter Implementation Guide (for VLClaw) ```js // Example: Claude adapter with full observability const Anthropic = require('@anthropic-ai/sdk'); const client = new Anthropic(); const claudeAdapter = { async call(params, onToken, callbacks) { const model = params.model || 'claude-opus-4-6'; if (params.stream) { const stream = await client.messages.stream({ model, system: params.system, messages: params.messages, max_tokens: params.max_tokens || 16384, tools: params.tools, thinking: params.thinking, }); let fullContent = ''; let fullThinking = ''; stream.on('contentBlockDelta', (delta) => { if (delta.delta.type === 'thinking_delta') { fullThinking += delta.delta.thinking; callbacks?.onThinking?.(delta.delta.thinking); } else if (delta.delta.type === 'text_delta') { fullContent += delta.delta.text; callbacks?.onToken?.(delta.delta.text); } }); stream.on('contentBlockStop', (block) => { if (block.content_block.type === 'tool_use') { callbacks?.onToolUse?.({ id: block.content_block.id, name: block.content_block.name, input: block.content_block.input, }); } }); const response = await stream.finalMessage(); const toolBlocks = response.content.filter(b => b.type === 'tool_use'); if (toolBlocks.length > 0) { for (const tb of toolBlocks) { const result = await executeToolLocally(tb.name, tb.input); callbacks?.onToolResult?.({ tool_use_id: tb.id, content: JSON.stringify(result), is_error: false, }); } } return { content: fullContent, model: response.model, id: response.id, finish_reason: response.stop_reason, usage: response.usage, tool_use: toolBlocks, thinking: fullThinking || undefined, }; } else { const response = await client.messages.create({ model, system: params.system, messages: params.messages, max_tokens: params.max_tokens || 16384, tools: params.tools, thinking: params.thinking, }); const textBlock = response.content.find(b => b.type === 'text'); const thinkBlock = response.content.find(b => b.type === 'thinking'); return { content: textBlock?.text || '', model: response.model, id: response.id, finish_reason: response.stop_reason, usage: response.usage, thinking: thinkBlock?.thinking || undefined, }; } } }; ``` ### 5.6 VLCode Tool_* Step Extension `Tool_*` steps let a workflow call any tool already registered in the local VLCode `ToolRegistry`. Minimal shape: ```json { "id": "Tool_010_ReadSpec", "tool": "ReadFile", "input": { "file_path": "=\"docs/spec.md\"" }, "out": { "$specText": "=_result", "$rawReadFileEnvelope": "=_toolResult" }, "next": "Stop_End" } ``` Supported fields in VLCode: - `tool` or `toolName`: explicit tool name - `input` or `in`: tool arguments - `timeout`: forwarded into the tool input if the tool input does not already include one - `allowError` or `continueOnError`: keep the workflow running after tool failure - `out`: normal workflow output mapping Name fallback: - If `tool` is omitted, VLCode derives it from the step id, for example `Tool_010_ReadFile` → `ReadFile` Scope notes: - This is a VLCode extension, not a pure engine-core step type - It works for normal execution, `executeFrom()`, and the `WorkflowRun` tool path - Tools may emit intermediate `tool_message` events through the optional `runtime` argument - UI consumers should correlate `tool_start/tool_message/tool_done/tool_error` by `runID` or `clientRunToken` --- ## 6. Status Snapshot API Query the current workflow execution state at any time via `ctx.snapshot()`. ### 6.1 Response Shape ```ts interface WorkflowSnapshot { runID: string; status: string; // "running" | "completed" | "failed" | "stopped" | "paused" currentStepID: string | null; elapsedMs: number; params: Record; variables: Record; localVars: Record; artifacts: string[]; paused: { nodeID: string; token: string } | null; eventSeq: number; version: string; } ``` ### 6.2 Usage in VLClaw Broker ```js app.get('/workflow/:runID/status', (req, res) => { const ctx = activeWorkflows.get(req.params.runID); if (!ctx) return res.status(404).json({ error: 'not found' }); res.json(ctx.snapshot()); }); ``` --- ## 7. Event → SSE Mapping (for VLClaw Broker) ### 7.1 SSE Format ``` event: data: id: ``` ### 7.2 Example SSE Stream (with resolvedInputs/outputs) ``` event: workflow_start data: {"name":"MetaDirect","version":"3.15","params":{"projectName":"MyApp"}} id: 1 event: step_start data: {"stepID":"LLM_010","type":"LLM","meta":{"title":"Analyze request"},"resolvedInputs":{"userRequest":"添加登录页","model":"claude-opus-4-6"}} id: 2 event: llm_thinking data: {"stepID":"LLM_010","delta":"Let me analyze the requirements..."} id: 3 event: llm_token data: {"stepID":"LLM_010","delta":"```json\n{\"action\":\"add-page\""} id: 4 event: llm_done data: {"stepID":"LLM_010","latency_ms":12340,"model":"claude-opus-4-6","usage":{"input_tokens":1200,"output_tokens":3400,"total_tokens":4600},"thinking_tokens":800} id: 5 event: tool_start data: {"stepID":"Tool_020_ReadFile","name":"ReadFile","input":{"file_path":"docs/spec.md"}} id: 5a event: tool_done data: {"stepID":"Tool_020_ReadFile","name":"ReadFile","output":"# Spec ..."} id: 5b event: var_changed data: {"stepID":"LLM_010","name":"$plan","oldValue":null,"newValue":{"action":"add-page","pageName":"Login"}} id: 6 event: step_done data: {"stepID":"LLM_010","outputs":{"$plan":{"action":"add-page","pageName":"Login"}}} id: 7 event: step_start data: {"stepID":"Write_020","type":"Write","meta":{"title":"Write page file"},"resolvedInputs":{"target":"/Apps/Login.vx","value":"..."}} id: 8 event: file_done data: {"stepID":"Write_020","path":"Apps/Login.vx","size_bytes":4096} id: 9 event: step_done data: {"stepID":"Write_020","outputs":{"/Apps/Login.vx":{"_file":true,"path":"/Apps/Login.vx"}}} id: 10 event: workflow_done data: {"duration_ms":45000} id: 11 ``` ### 7.3 Broker SSE Implementation ```js app.get('/workflow/:runID/events', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }); const ctx = activeWorkflows.get(req.params.runID); if (!ctx) { res.end(); return; } const listener = (event) => { res.write(`event: ${event.type}\n`); res.write(`data: ${JSON.stringify({ stepID: event.stepID, ...event.payload })}\n`); res.write(`id: ${event.seq}\n\n`); }; ctx.onEvent(listener); req.on('close', () => { /* remove listener */ }); }); ``` --- ## 8. REST API Endpoints (VLClaw Broker should implement) | Method | Path | Description | |--------|------|-------------| | `POST` | `/workflow/run` | Start a workflow, returns `{ runID }` | | `GET` | `/workflow/:runID/status` | Get `WorkflowSnapshot` | | `GET` | `/workflow/:runID/events` | SSE event stream | | `POST` | `/workflow/:runID/resume` | Resume a paused workflow `{ token, payload }` | | `POST` | `/workflow/:runID/abort` | Abort a running workflow | | `GET` | `/workflow/:runID/variables` | Get current pipeline variables | | `GET` | `/workflow/:runID/artifacts` | List written files | | **`GET`** | **`/workflow/:runID/checkpoint`** | **Get latest checkpoint (NEW)** | | **`POST`** | **`/workflow/rerun`** | **Re-run from step `{ checkpoint, stepID, overrides }` (NEW)** | ### 8.1 New Endpoints Detail #### GET /workflow/:runID/checkpoint Returns the latest `WorkflowCheckpoint` JSON. Broker should persist checkpoints using `onCheckpoint`. ```js // Broker implementation const checkpointStore = new Map(); // or Redis/file const engine = new Engine(workflow, { onCheckpoint: (cp) => { checkpointStore.set(cp.workflowID, cp); } }); app.get('/workflow/:runID/checkpoint', (req, res) => { const cp = checkpointStore.get(req.params.runID); if (!cp) return res.status(404).json({ error: 'no checkpoint' }); res.json(cp); }); ``` #### POST /workflow/rerun Re-run a workflow from a specific step, optionally with variable overrides. ```json // Request body { "workflowName": "MetaDirect", // which workflow to load "checkpoint": { ... }, // from GET /checkpoint or stored "stepID": "LLM_GenerateCode", // override currentStepID (optional) "overrides": { // variable overrides (optional) "$plan": { "action": "add-page", "pageName": "Login" } } } ``` ```js // Broker implementation app.post('/workflow/rerun', async (req, res) => { const { workflowName, checkpoint, stepID, overrides } = req.body; const workflow = loadWorkflow(workflowName); const engine = new Engine(workflow, { onEvent, onCheckpoint }); if (stepID) checkpoint.currentStepID = stepID; const ctx = await engine.executeFrom(checkpoint, adapters, overrides); res.json({ runID: ctx.workflowID, status: ctx.status }); }); ``` --- ## 9. VLCode Integration Guide ### 9.1 UI Panel Mapping (Updated) | Engine Event | VLCode Panel | Display | |-------------|-------------|---------| | `workflow_start` | Status Bar | "Running: {name}" (or "Resuming from: {resumedFrom}") | | `step_start` | Status Bar + Detail Log | Step title + **collapsible inputs panel** | | `step_done` | Detail Log | Checkmark + **collapsible outputs panel** | | `step_error` | Main Chat + Detail Log | Error message | | `llm_thinking` | Detail Log (collapsible) | Streaming thinking text | | `llm_token` | Detail Log | Streaming response text | | `llm_tool_use` | Detail Log (collapsible) | Tool name + input | | `llm_tool_result` | Detail Log (collapsible) | Tool output | | `tool_start` | Detail Log (collapsible) | Local tool name + input | | `tool_done` | Detail Log (collapsible) | Local tool output | | `tool_error` | Detail Log | Error, optionally marked continued | | `llm_done` | Detail Log | Model, tokens, latency | | `llm_error` | Main Chat + Detail Log | Error + retry info | | `var_changed` | Detail Log | Variable diff | | `file_done` | Detail Log | File path + size | | `pause_start` | Main Chat | Approval button | | `workflow_done` | Main Chat + Status Bar | Summary | | `workflow_failed` | Main Chat | Error + **"Re-run from step" button** | ### 9.2 Detail Log — Step Card Design Each step in the Detail Log should be a **card** with expandable sections: ``` ┌─────────────────────────────────────────────────┐ │ ✅ LLM_AnalyzeRequest — Analyze user request │ ← title from step.meta │ 12.3s │ ← duration ├─────────────────────────────────────────────────┤ │ ▶ Inputs │ ← collapsed by default │ userRequest: "添加一个登录页面" │ │ model: "claude-opus-4-6" │ │ currentMeta: (52KB, truncated) [展开] │ ├─────────────────────────────────────────────────┤ │ ▶ Thinking │ ← collapsed, from llm_thinking │ Let me analyze the requirements... │ ├─────────────────────────────────────────────────┤ │ ▼ Response │ ← expanded, from llm_token │ { "action": "add-page", "pageName": "Login" } │ ├─────────────────────────────────────────────────┤ │ ▶ Tool Calls (2) │ ← collapsed, from llm_tool_use │ read_file("spec.md") → "# Spec content..." │ │ read_file("theme.json") → { ... } │ ├─────────────────────────────────────────────────┤ │ ▶ Outputs │ ← collapsed by default │ $plan: { action: "add-page", ... } │ │ $updatedMeta: (52KB, truncated) [展开] │ ├─────────────────────────────────────────────────┤ │ ▶ Token Usage │ ← collapsed │ Input: 1,200 Output: 3,400 Thinking: 800 │ │ Model: claude-opus-4-6 Latency: 12.3s │ ├─────────────────────────────────────────────────┤ │ [🔄 从此步骤重跑] │ ← re-run button └─────────────────────────────────────────────────┘ ``` ### 9.3 Handling Truncated Values ```js function renderValue(value) { if (value?._truncated) { return `(${formatBytes(value.length)}, truncated) ` + ``; } if (value?._file) { return `📄 ${value.path}`; } return JSON.stringify(value, null, 2); } function formatBytes(chars) { if (chars < 1024) return `${chars}B`; return `${(chars / 1024).toFixed(1)}KB`; } ``` ### 9.4 Re-run from Step — UI Flow ``` 用户在 Detail Log 中点击某个已完成步骤的 [🔄 从此步骤重跑] 按钮 │ ▼ 弹出「重跑配置」对话框: ┌──────────────────────────────────────────┐ │ 从 LLM_GenerateCode 重新执行 │ │ │ │ 当前变量值: │ │ $plan: { action: "add-page", ... } │ │ [✏️ 编辑] │ │ $meta: (52KB) [查看] │ │ [✏️ 编辑] │ │ $userRequest: "添加登录页" │ │ [✏️ 编辑] │ │ │ │ ℹ️ 此步骤之前的所有步骤不会重新执行 │ │ ℹ️ 变量值来自该步骤执行前的快照 │ │ │ │ [取消] [🚀 开始重跑] │ └──────────────────────────────────────────┘ │ ▼ 前端调用 POST /workflow/rerun { checkpoint: lastCheckpoint, stepID: 'LLM_GenerateCode', overrides: { '$plan': userEditedPlan } // 用户编辑过的变量 } │ ▼ 新的 SSE stream 开始, Detail Log 追加新的执行记录 ``` ### 9.5 Connecting from VLCode (Updated) ```js const evtSource = new EventSource(`http://localhost:9160/workflow/${runID}/events`); evtSource.addEventListener('step_start', (e) => { const data = JSON.parse(e.data); statusBar.update(`Step: ${data.stepID} (${data.type})`); detailLog.addStepCard(data.stepID, data.type, data.meta); // NEW: show inputs if (data.resolvedInputs) { detailLog.setStepInputs(data.stepID, data.resolvedInputs); } }); evtSource.addEventListener('step_done', (e) => { const data = JSON.parse(e.data); detailLog.markStepDone(data.stepID); // NEW: show outputs if (data.outputs) { detailLog.setStepOutputs(data.stepID, data.outputs); } }); evtSource.addEventListener('llm_thinking', (e) => { const data = JSON.parse(e.data); detailLog.appendThinking(data.stepID, data.delta); }); evtSource.addEventListener('llm_token', (e) => { const data = JSON.parse(e.data); detailLog.appendResponse(data.stepID, data.delta); }); evtSource.addEventListener('llm_tool_use', (e) => { const data = JSON.parse(e.data); detailLog.addToolCall(data.stepID, data.name, data.input); }); evtSource.addEventListener('llm_tool_result', (e) => { const data = JSON.parse(e.data); detailLog.addToolResult(data.stepID, data.tool_use_id, data.content, data.is_error); }); evtSource.addEventListener('llm_done', (e) => { const data = JSON.parse(e.data); detailLog.setLLMStats(data.stepID, { model: data.model, latencyMs: data.latency_ms, usage: data.usage, thinkingTokens: data.thinking_tokens }); }); evtSource.addEventListener('var_changed', (e) => { const data = JSON.parse(e.data); detailLog.addVarChange(data.name, data.oldValue, data.newValue); }); evtSource.addEventListener('workflow_done', (e) => { const data = JSON.parse(e.data); mainChat.addMessage(`Workflow completed in ${(data.duration_ms/1000).toFixed(1)}s`); statusBar.update('Done'); }); evtSource.addEventListener('workflow_failed', (e) => { const data = JSON.parse(e.data); mainChat.addMessage(`Workflow failed at ${data.failed_step_id}: ${data.error}`); mainChat.addRerunButton(data.failed_step_id); // ← NEW: offer re-run }); // Re-run action async function rerunFromStep(stepID, overrides = {}) { const cpRes = await fetch(`/workflow/${runID}/checkpoint`); const checkpoint = await cpRes.json(); const res = await fetch('/workflow/rerun', { method: 'POST', body: JSON.stringify({ workflowName, checkpoint, stepID, overrides }) }); const { runID: newRunID } = await res.json(); // Subscribe to new SSE stream subscribeToWorkflow(newRunID); } ``` --- ## 10. Exports ```js const { // Core Engine, // Expression ExpressionEvaluator, toBool, toFloat, // Registry Registry, parseServiceSignature, parseVariableDeclaration, parseParamDeclaration, // Parallel ParallelExecutor, ParallelError, // Types & Constants WorkflowType, StepType, getStepType, ExecutionStatus, WriteMode, LoopMode, ParallelErrorStrategy, RunEventType, SUPPORTED_VERSIONS, isV310OrLater, LLMError, buildErrorMap, ExecutionContext, ChildExecutionContext, // Executor helpers applyOutputMapping, emitEvent } = require('vl-workflow-engine'); ``` --- ## 11. Changelog ### v0.4.0 (2026-03-12) - **Phase 2: Parallel branch resume**: `completedBranches` tracks which branches completed per parent step; on resume, only pending branches execute - **Phase 2: Loop mid-point resume**: `loopProgress` tracks iteration count per loop step; serial loops emit checkpoint per iteration for precise recovery - **Checkpoint v2**: new fields `completedBranches` and `loopProgress` (backward compatible with v1) - **Branch chain re-run**: if a branch entry is not in `completedBranches`, the entire chain re-runs - **Parallel loop resume**: `source.slice(startIndex)` ensures only unprocessed items execute - **ChildExecutionContext**: proxies `_completedSteps`, `_completedBranches`, `_loopProgress` - 12 new stress tests covering edge cases (all completed, none completed, partial, chain branches, etc.) - **VLCode extension documented**: `Tool_*`, `tool_start/tool_done/tool_error`, `_result`, and `_toolResult` ### v0.3.0 (2026-03-12) - **Checkpoint & Resume**: `ctx.checkpoint()` produces serializable state snapshot - **Execute from step**: `engine.executeFrom(checkpoint, adapters, overrides)` resumes from any step - **onCheckpoint callback**: auto-fires after each step completion for external persistence - **`step_start` enriched**: `resolvedInputs` field shows all evaluated input parameters - **`step_done` enriched**: `outputs` field shows all output mapping results - **Value truncation**: values > 10KB auto-truncated to `{ _truncated, type, length, preview }` - **Completed steps tracking**: `ctx._completedSteps` and `checkpoint.completedSteps` - **`workflow_start` extended**: `resumedFrom` field when using `executeFrom()` - Backward compatible: all existing event payloads unchanged (fields only added) ### v0.2.3 (2026-03-12) - **`step_start`/`step_done`** payload enrichment (resolvedInputs / outputs) - **Value truncation** for large payloads (summarizeIfLarge) ### v0.2.1 (2026-03-10) - **Breaking naming fix**: event payload field `chunk` → `delta` (aligned with Spec §13.3) - **Event ordering** documented: §3.2 defines the strict emit sequence for LLM steps ### v0.2.0 (2026-03-10) - **New events**: `llm_thinking`, `llm_tool_use`, `llm_tool_result`, `llm_error`, `var_changed` - **LLM adapter extended**: third `callbacks` parameter with `onThinking`, `onToolUse`, `onToolResult` - **Snapshot API**: `ctx.snapshot()` returns full execution state - **`llm_done` enhanced**: includes `thinking_tokens`, `has_tool_use` - **Variable tracking**: `$` variables emit `var_changed` on mutation - Backward compatible: existing adapters passing only `(params, onToken)` still work ### v0.1.0 - Initial release — full spec v3.15 support