# 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

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

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

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

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

### 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,
      };
    }
  }
};
```

---

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

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

```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 |
| `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) ` +
           `<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)

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

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