API.md 39 KB

VL-Workflow-Engine API Reference

Version: 0.4.0 | Spec: v3.15 | Updated: 2026-03-12


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

const { Engine } = require('vl-workflow-engine');

const workflow = { /* workflow JSON per spec v3.15 */ };
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:

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 }
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:

{
  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:

{
  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:

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 Subscribing to Events

// 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)

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<string, any>;  // read-only input params
  paramTypes:     Record<string, string>;
  variables:      Record<string, any>;  // current pipeline variables ($xxx)
  varTypes:       Record<string, string>;
  localVars:      Record<string, any>;  // _result, _meta, etc.
  artifacts:      Record<string, string>;  // files written so far
  completedSteps: string[];        // ordered list of completed step IDs
  // Phase 2: parallel & loop state
  completedBranches: Record<string, string[]>;  // { parentStepID: [completed child IDs] }
  loopProgress:      Record<string, number>;    // { 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

// 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)

// 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)

// 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

class Engine {
  async executeFrom(
    checkpoint: WorkflowCheckpoint | {
      currentStepID: string;         // required: which step to start from
      params?: Record<string, any>;
      variables?: Record<string, any>;
      localVars?: Record<string, any>;
      // ... any checkpoint fields
    },
    adapters?: AdapterMap,
    overrides?: Record<string, any>  // applied AFTER checkpoint restore
  ): Promise<ExecutionContext>;
}

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

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<LLMResult>;
}

5.2 LLMParams (input)

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)

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

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)

// 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,
      };
    }
  }
};

6. Status Snapshot API

Query the current workflow execution state at any time via ctx.snapshot().

6.1 Response Shape

interface WorkflowSnapshot {
  runID:         string;
  status:        string;           // "running" | "completed" | "failed" | "stopped" | "paused"
  currentStepID: string | null;
  elapsedMs:     number;
  params:        Record<string, any>;
  variables:     Record<string, any>;
  localVars:     Record<string, any>;
  artifacts:     string[];
  paused:        { nodeID: string; token: string } | null;
  eventSeq:      number;
  version:       string;
}

6.2 Usage in VLClaw Broker

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: <RunEventType>
data: <JSON payload>
id: <seq>

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: 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

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.

// 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.

// 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" }
  }
}
// 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
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

function renderValue(value) {
  if (value?._truncated) {
    return `(${formatBytes(value.length)}, truncated) ` +
           `<button onclick="showPreview('${value.preview}')">展开预览</button>`;
  }
  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)

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

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.)

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 chunkdelta (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