@synergenius/flow-weaver 0.10.12 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/generate-in-place.js +5 -4
- package/dist/api/inline-runtime.js +42 -0
- package/dist/cli/commands/context.d.ts +13 -0
- package/dist/cli/commands/context.js +53 -0
- package/dist/cli/commands/run.d.ts +8 -0
- package/dist/cli/commands/run.js +396 -4
- package/dist/cli/flow-weaver.mjs +2115 -499
- package/dist/cli/index.js +24 -0
- package/dist/context/index.d.ts +30 -0
- package/dist/context/index.js +182 -0
- package/dist/doc-metadata/extractors/mcp-tools.js +229 -0
- package/dist/doc-metadata/types.d.ts +1 -1
- package/dist/generator/unified.js +112 -35
- package/dist/mcp/debug-session.d.ts +30 -0
- package/dist/mcp/debug-session.js +25 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/server.js +4 -0
- package/dist/mcp/tools-context.d.ts +3 -0
- package/dist/mcp/tools-context.js +51 -0
- package/dist/mcp/tools-debug.d.ts +3 -0
- package/dist/mcp/tools-debug.js +451 -0
- package/dist/mcp/workflow-executor.d.ts +2 -0
- package/dist/mcp/workflow-executor.js +12 -2
- package/dist/runtime/ExecutionContext.d.ts +19 -0
- package/dist/runtime/ExecutionContext.js +43 -0
- package/dist/runtime/checkpoint.d.ts +84 -0
- package/dist/runtime/checkpoint.js +225 -0
- package/dist/runtime/debug-controller.d.ts +110 -0
- package/dist/runtime/debug-controller.js +247 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +2 -0
- package/docs/reference/cli-reference.md +42 -2
- package/docs/reference/debugging.md +152 -5
- package/package.json +1 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint serialization for crash recovery.
|
|
3
|
+
*
|
|
4
|
+
* Writes workflow state to disk after each node completes. If the process
|
|
5
|
+
* crashes, the checkpoint file persists and can be used to resume execution
|
|
6
|
+
* from the last completed node.
|
|
7
|
+
*
|
|
8
|
+
* Checkpoint files live in .fw-checkpoints/ next to the workflow file and
|
|
9
|
+
* are auto-deleted after successful workflow completion.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as crypto from 'crypto';
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Serialization helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
function isUnserializableMarker(value) {
|
|
18
|
+
return (typeof value === 'object' &&
|
|
19
|
+
value !== null &&
|
|
20
|
+
value.__fw_unserializable__ === true);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Try to serialize a value. If it contains functions, invoke them first.
|
|
24
|
+
* If serialization fails, return a marker.
|
|
25
|
+
*/
|
|
26
|
+
function serializeValue(key, value, unsafeNodes) {
|
|
27
|
+
// Resolve function values (pull execution lazy evaluation)
|
|
28
|
+
if (typeof value === 'function') {
|
|
29
|
+
try {
|
|
30
|
+
value = value();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
const parts = key.split(':');
|
|
34
|
+
unsafeNodes.add(parts[0]);
|
|
35
|
+
return {
|
|
36
|
+
__fw_unserializable__: true,
|
|
37
|
+
nodeId: parts[0],
|
|
38
|
+
portName: parts[1] || 'unknown',
|
|
39
|
+
reason: 'Function invocation failed',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Handle Promises: can't serialize, mark as unsafe
|
|
44
|
+
if (value instanceof Promise) {
|
|
45
|
+
const parts = key.split(':');
|
|
46
|
+
unsafeNodes.add(parts[0]);
|
|
47
|
+
return {
|
|
48
|
+
__fw_unserializable__: true,
|
|
49
|
+
nodeId: parts[0],
|
|
50
|
+
portName: parts[1] || 'unknown',
|
|
51
|
+
reason: 'Promise value',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Test serialization
|
|
55
|
+
try {
|
|
56
|
+
JSON.stringify(value);
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
const parts = key.split(':');
|
|
61
|
+
unsafeNodes.add(parts[0]);
|
|
62
|
+
return {
|
|
63
|
+
__fw_unserializable__: true,
|
|
64
|
+
nodeId: parts[0],
|
|
65
|
+
portName: parts[1] || 'unknown',
|
|
66
|
+
reason: 'Not JSON-serializable',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Compute SHA-256 hash of a file's contents.
|
|
72
|
+
*/
|
|
73
|
+
function hashFile(filePath) {
|
|
74
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
75
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
76
|
+
}
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// CheckpointWriter
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
export class CheckpointWriter {
|
|
81
|
+
dir;
|
|
82
|
+
filePath;
|
|
83
|
+
workflowName;
|
|
84
|
+
runId;
|
|
85
|
+
params;
|
|
86
|
+
workflowHash;
|
|
87
|
+
checkpointPath;
|
|
88
|
+
writeLock = Promise.resolve();
|
|
89
|
+
constructor(workflowFilePath, workflowName, runId, params = {}) {
|
|
90
|
+
this.filePath = path.resolve(workflowFilePath);
|
|
91
|
+
this.workflowName = workflowName;
|
|
92
|
+
this.runId = runId;
|
|
93
|
+
this.params = params;
|
|
94
|
+
this.dir = path.join(path.dirname(this.filePath), '.fw-checkpoints');
|
|
95
|
+
this.checkpointPath = path.join(this.dir, `${workflowName}-${runId}.json`);
|
|
96
|
+
this.workflowHash = hashFile(this.filePath);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Write a checkpoint after a node completes. Uses a write lock so
|
|
100
|
+
* concurrent calls from parallel nodes are serialized.
|
|
101
|
+
*/
|
|
102
|
+
async write(completedNodes, executionOrder, position, ctx) {
|
|
103
|
+
// Serialize under a lock to prevent concurrent writes (parallel nodes)
|
|
104
|
+
this.writeLock = this.writeLock.then(() => this._writeCheckpoint(completedNodes, executionOrder, position, ctx));
|
|
105
|
+
await this.writeLock;
|
|
106
|
+
}
|
|
107
|
+
/** Clean up checkpoint file after successful completion */
|
|
108
|
+
cleanup() {
|
|
109
|
+
try {
|
|
110
|
+
if (fs.existsSync(this.checkpointPath)) {
|
|
111
|
+
fs.unlinkSync(this.checkpointPath);
|
|
112
|
+
}
|
|
113
|
+
// Remove directory if empty
|
|
114
|
+
if (fs.existsSync(this.dir)) {
|
|
115
|
+
const remaining = fs.readdirSync(this.dir);
|
|
116
|
+
if (remaining.length === 0) {
|
|
117
|
+
fs.rmdirSync(this.dir);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Best-effort cleanup
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
getCheckpointPath() {
|
|
126
|
+
return this.checkpointPath;
|
|
127
|
+
}
|
|
128
|
+
_writeCheckpoint(completedNodes, executionOrder, position, ctx) {
|
|
129
|
+
const serialized = ctx.serialize();
|
|
130
|
+
const unsafeNodes = new Set();
|
|
131
|
+
// Serialize variables, handling unserializable values
|
|
132
|
+
const variables = {};
|
|
133
|
+
for (const [key, value] of Object.entries(serialized.variables)) {
|
|
134
|
+
variables[key] = serializeValue(key, value, unsafeNodes);
|
|
135
|
+
}
|
|
136
|
+
const data = {
|
|
137
|
+
version: 1,
|
|
138
|
+
workflowHash: this.workflowHash,
|
|
139
|
+
workflowName: this.workflowName,
|
|
140
|
+
filePath: this.filePath,
|
|
141
|
+
params: this.params,
|
|
142
|
+
timestamp: new Date().toISOString(),
|
|
143
|
+
completedNodes: [...completedNodes],
|
|
144
|
+
executionOrder: [...executionOrder],
|
|
145
|
+
position,
|
|
146
|
+
variables,
|
|
147
|
+
executions: serialized.executions,
|
|
148
|
+
executionCounter: serialized.executionCounter,
|
|
149
|
+
nodeExecutionCounts: serialized.nodeExecutionCounts,
|
|
150
|
+
unsafeNodes: [...unsafeNodes],
|
|
151
|
+
};
|
|
152
|
+
// Ensure directory exists
|
|
153
|
+
if (!fs.existsSync(this.dir)) {
|
|
154
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
155
|
+
}
|
|
156
|
+
fs.writeFileSync(this.checkpointPath, JSON.stringify(data, null, 2), 'utf8');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Checkpoint reading and resume
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
/**
|
|
163
|
+
* Load a checkpoint file and validate it against the current workflow.
|
|
164
|
+
* Returns the checkpoint data and a list of nodes that need to be re-run
|
|
165
|
+
* (because their outputs weren't serializable).
|
|
166
|
+
*/
|
|
167
|
+
export function loadCheckpoint(checkpointPath, workflowFilePath) {
|
|
168
|
+
const raw = fs.readFileSync(checkpointPath, 'utf8');
|
|
169
|
+
const data = JSON.parse(raw);
|
|
170
|
+
if (data.version !== 1) {
|
|
171
|
+
throw new Error(`Unsupported checkpoint version: ${data.version}`);
|
|
172
|
+
}
|
|
173
|
+
// Check if the workflow has changed since the checkpoint was written
|
|
174
|
+
let stale = false;
|
|
175
|
+
if (workflowFilePath) {
|
|
176
|
+
const currentHash = hashFile(path.resolve(workflowFilePath));
|
|
177
|
+
stale = currentHash !== data.workflowHash;
|
|
178
|
+
}
|
|
179
|
+
// Determine which nodes can be skipped (all outputs serialized) vs
|
|
180
|
+
// which need re-running (any output was unserializable)
|
|
181
|
+
const unsafeSet = new Set(data.unsafeNodes);
|
|
182
|
+
const rerunNodes = [];
|
|
183
|
+
const skipNodes = new Map();
|
|
184
|
+
// Walk execution order up to the checkpoint position.
|
|
185
|
+
// Once we hit an unsafe node, everything from that point forward re-runs.
|
|
186
|
+
let hitUnsafe = false;
|
|
187
|
+
for (const nodeId of data.completedNodes) {
|
|
188
|
+
if (hitUnsafe || unsafeSet.has(nodeId)) {
|
|
189
|
+
hitUnsafe = true;
|
|
190
|
+
rerunNodes.push(nodeId);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Collect this node's outputs from the variables map
|
|
194
|
+
const nodeOutputs = {};
|
|
195
|
+
const prefix = `${nodeId}:`;
|
|
196
|
+
for (const [key, value] of Object.entries(data.variables)) {
|
|
197
|
+
if (key.startsWith(prefix) && !isUnserializableMarker(value)) {
|
|
198
|
+
// Store with portName:executionIndex as key
|
|
199
|
+
const rest = key.substring(prefix.length);
|
|
200
|
+
nodeOutputs[rest] = value;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
skipNodes.set(nodeId, nodeOutputs);
|
|
204
|
+
}
|
|
205
|
+
return { data, stale, rerunNodes, skipNodes };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Find the most recent checkpoint file for a workflow.
|
|
209
|
+
*/
|
|
210
|
+
export function findLatestCheckpoint(workflowFilePath, workflowName) {
|
|
211
|
+
const dir = path.join(path.dirname(path.resolve(workflowFilePath)), '.fw-checkpoints');
|
|
212
|
+
if (!fs.existsSync(dir))
|
|
213
|
+
return null;
|
|
214
|
+
const files = fs.readdirSync(dir)
|
|
215
|
+
.filter((f) => f.endsWith('.json'))
|
|
216
|
+
.filter((f) => !workflowName || f.startsWith(`${workflowName}-`));
|
|
217
|
+
if (files.length === 0)
|
|
218
|
+
return null;
|
|
219
|
+
// Sort by modification time, newest first
|
|
220
|
+
const sorted = files
|
|
221
|
+
.map((f) => ({ name: f, mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
|
|
222
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
223
|
+
return path.join(dir, sorted[0].name);
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=checkpoint.js.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DebugController intercepts workflow execution at node boundaries,
|
|
3
|
+
* enabling step-through debugging and checkpoint/resume.
|
|
4
|
+
*
|
|
5
|
+
* Injected via globalThis.__fw_debug_controller__ (same pattern as
|
|
6
|
+
* __fw_debugger__ and __fw_agent_channel__). The generated code calls
|
|
7
|
+
* beforeNode/afterNode at each node boundary; the controller decides
|
|
8
|
+
* whether to skip, pause, checkpoint, or continue.
|
|
9
|
+
*/
|
|
10
|
+
import type { GeneratedExecutionContext } from './ExecutionContext.js';
|
|
11
|
+
import type { CheckpointWriter } from './checkpoint.js';
|
|
12
|
+
export type DebugMode = 'step' | 'continue' | 'continueToBreakpoint' | 'run';
|
|
13
|
+
export interface DebugPauseState {
|
|
14
|
+
/** Node we're paused at */
|
|
15
|
+
currentNodeId: string;
|
|
16
|
+
/** Whether we paused before or after the node executed */
|
|
17
|
+
phase: 'before' | 'after';
|
|
18
|
+
/** Nodes that have finished executing */
|
|
19
|
+
completedNodes: string[];
|
|
20
|
+
/** Full topological execution order */
|
|
21
|
+
executionOrder: string[];
|
|
22
|
+
/** Current index in executionOrder */
|
|
23
|
+
position: number;
|
|
24
|
+
/** All variable values, keyed by "nodeId:portName" */
|
|
25
|
+
variables: Record<string, unknown>;
|
|
26
|
+
/** Outputs of the most recently completed node (convenience shortcut) */
|
|
27
|
+
currentNodeOutputs?: Record<string, unknown>;
|
|
28
|
+
/** Active breakpoints */
|
|
29
|
+
breakpoints: string[];
|
|
30
|
+
}
|
|
31
|
+
export type DebugResumeAction = {
|
|
32
|
+
type: 'step';
|
|
33
|
+
} | {
|
|
34
|
+
type: 'continue';
|
|
35
|
+
} | {
|
|
36
|
+
type: 'continueToBreakpoint';
|
|
37
|
+
} | {
|
|
38
|
+
type: 'abort';
|
|
39
|
+
};
|
|
40
|
+
export interface DebugControllerConfig {
|
|
41
|
+
/** Enable step-through debugging (pauses before first node) */
|
|
42
|
+
debug?: boolean;
|
|
43
|
+
/** Enable checkpointing to disk after each node */
|
|
44
|
+
checkpoint?: boolean;
|
|
45
|
+
/** Checkpoint writer instance (required when checkpoint=true) */
|
|
46
|
+
checkpointWriter?: CheckpointWriter;
|
|
47
|
+
/** Initial breakpoint node IDs */
|
|
48
|
+
breakpoints?: string[];
|
|
49
|
+
/** Execution order (set by executor after compilation) */
|
|
50
|
+
executionOrder?: string[];
|
|
51
|
+
/** Nodes to skip on resume (loaded from checkpoint) */
|
|
52
|
+
skipNodes?: Map<string, Record<string, unknown>>;
|
|
53
|
+
}
|
|
54
|
+
export declare class DebugController {
|
|
55
|
+
private mode;
|
|
56
|
+
private breakpoints;
|
|
57
|
+
private completedNodes;
|
|
58
|
+
private completedSet;
|
|
59
|
+
private executionOrder;
|
|
60
|
+
private position;
|
|
61
|
+
private lastCompletedNodeId;
|
|
62
|
+
private checkpointEnabled;
|
|
63
|
+
private checkpointWriter;
|
|
64
|
+
private skipNodes;
|
|
65
|
+
private _gateResolve;
|
|
66
|
+
private _pauseResolve;
|
|
67
|
+
private _pausePromise;
|
|
68
|
+
private pendingModifications;
|
|
69
|
+
constructor(config?: DebugControllerConfig);
|
|
70
|
+
/** Set the execution order (called by executor after compilation) */
|
|
71
|
+
setExecutionOrder(order: string[]): void;
|
|
72
|
+
/**
|
|
73
|
+
* Called before a node executes.
|
|
74
|
+
* Returns true if the node should execute, false to skip.
|
|
75
|
+
*/
|
|
76
|
+
beforeNode(nodeId: string, ctx: GeneratedExecutionContext): Promise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* Called after a node completes successfully.
|
|
79
|
+
*/
|
|
80
|
+
afterNode(nodeId: string, ctx: GeneratedExecutionContext): Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Awaited by the executor to detect when the controller pauses.
|
|
83
|
+
* Resolves with the current debug state.
|
|
84
|
+
*/
|
|
85
|
+
onPause(): Promise<DebugPauseState>;
|
|
86
|
+
/**
|
|
87
|
+
* Called by MCP tools or CLI to resume execution.
|
|
88
|
+
*/
|
|
89
|
+
resume(action: DebugResumeAction): void;
|
|
90
|
+
/**
|
|
91
|
+
* Queue a variable modification. Applied before the next node runs.
|
|
92
|
+
* Key format: "nodeId:portName:executionIndex"
|
|
93
|
+
*/
|
|
94
|
+
setVariable(key: string, value: unknown): void;
|
|
95
|
+
addBreakpoint(nodeId: string): void;
|
|
96
|
+
removeBreakpoint(nodeId: string): void;
|
|
97
|
+
getBreakpoints(): string[];
|
|
98
|
+
/** Build the current debug state for external consumers */
|
|
99
|
+
buildState(nodeId: string, phase: 'before' | 'after', ctx: GeneratedExecutionContext): DebugPauseState;
|
|
100
|
+
/** Get completed nodes list */
|
|
101
|
+
getCompletedNodes(): string[];
|
|
102
|
+
private pause;
|
|
103
|
+
private applyAction;
|
|
104
|
+
private applyPendingModifications;
|
|
105
|
+
private restoreNodeOutputs;
|
|
106
|
+
private extractVariables;
|
|
107
|
+
private extractNodeOutputs;
|
|
108
|
+
private _createPausePromise;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=debug-controller.d.ts.map
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DebugController intercepts workflow execution at node boundaries,
|
|
3
|
+
* enabling step-through debugging and checkpoint/resume.
|
|
4
|
+
*
|
|
5
|
+
* Injected via globalThis.__fw_debug_controller__ (same pattern as
|
|
6
|
+
* __fw_debugger__ and __fw_agent_channel__). The generated code calls
|
|
7
|
+
* beforeNode/afterNode at each node boundary; the controller decides
|
|
8
|
+
* whether to skip, pause, checkpoint, or continue.
|
|
9
|
+
*/
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// DebugController
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export class DebugController {
|
|
14
|
+
mode;
|
|
15
|
+
breakpoints;
|
|
16
|
+
completedNodes = [];
|
|
17
|
+
completedSet = new Set();
|
|
18
|
+
executionOrder = [];
|
|
19
|
+
position = 0;
|
|
20
|
+
lastCompletedNodeId = null;
|
|
21
|
+
// Checkpoint
|
|
22
|
+
checkpointEnabled;
|
|
23
|
+
checkpointWriter;
|
|
24
|
+
// Skip nodes (for resume from checkpoint)
|
|
25
|
+
skipNodes;
|
|
26
|
+
// Pause/resume channel (mirrors AgentChannel pattern)
|
|
27
|
+
_gateResolve = null;
|
|
28
|
+
_pauseResolve = null;
|
|
29
|
+
_pausePromise;
|
|
30
|
+
// Variable modification buffer: applied before next node runs
|
|
31
|
+
pendingModifications = new Map();
|
|
32
|
+
constructor(config = {}) {
|
|
33
|
+
this.mode = config.debug ? 'step' : 'run';
|
|
34
|
+
this.breakpoints = new Set(config.breakpoints ?? []);
|
|
35
|
+
this.checkpointEnabled = config.checkpoint ?? false;
|
|
36
|
+
this.checkpointWriter = config.checkpointWriter ?? null;
|
|
37
|
+
this.executionOrder = config.executionOrder ?? [];
|
|
38
|
+
this.skipNodes = config.skipNodes ?? new Map();
|
|
39
|
+
this._pausePromise = this._createPausePromise();
|
|
40
|
+
}
|
|
41
|
+
/** Set the execution order (called by executor after compilation) */
|
|
42
|
+
setExecutionOrder(order) {
|
|
43
|
+
this.executionOrder = order;
|
|
44
|
+
}
|
|
45
|
+
// -----------------------------------------------------------------------
|
|
46
|
+
// Node boundary hooks (called by generated code)
|
|
47
|
+
// -----------------------------------------------------------------------
|
|
48
|
+
/**
|
|
49
|
+
* Called before a node executes.
|
|
50
|
+
* Returns true if the node should execute, false to skip.
|
|
51
|
+
*/
|
|
52
|
+
async beforeNode(nodeId, ctx) {
|
|
53
|
+
// Apply any pending variable modifications
|
|
54
|
+
this.applyPendingModifications(ctx);
|
|
55
|
+
// If this node should be skipped (resume from checkpoint), restore its
|
|
56
|
+
// outputs into the context and return false
|
|
57
|
+
if (this.skipNodes.has(nodeId)) {
|
|
58
|
+
const savedOutputs = this.skipNodes.get(nodeId);
|
|
59
|
+
this.restoreNodeOutputs(nodeId, savedOutputs, ctx);
|
|
60
|
+
this.completedNodes.push(nodeId);
|
|
61
|
+
this.completedSet.add(nodeId);
|
|
62
|
+
this.lastCompletedNodeId = nodeId;
|
|
63
|
+
this.position++;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
// Check if we should pause here
|
|
67
|
+
const shouldPause = this.mode === 'step' ||
|
|
68
|
+
(this.mode === 'continueToBreakpoint' && this.breakpoints.has(nodeId));
|
|
69
|
+
if (shouldPause) {
|
|
70
|
+
const action = await this.pause(nodeId, 'before', ctx);
|
|
71
|
+
if (action.type === 'abort') {
|
|
72
|
+
throw new Error(`Debug session aborted at node "${nodeId}"`);
|
|
73
|
+
}
|
|
74
|
+
// Action may change mode for subsequent nodes
|
|
75
|
+
this.applyAction(action);
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Called after a node completes successfully.
|
|
81
|
+
*/
|
|
82
|
+
async afterNode(nodeId, ctx) {
|
|
83
|
+
this.completedNodes.push(nodeId);
|
|
84
|
+
this.completedSet.add(nodeId);
|
|
85
|
+
this.lastCompletedNodeId = nodeId;
|
|
86
|
+
this.position++;
|
|
87
|
+
// Write checkpoint to disk
|
|
88
|
+
if (this.checkpointEnabled && this.checkpointWriter) {
|
|
89
|
+
await this.checkpointWriter.write(this.completedNodes, this.executionOrder, this.position, ctx);
|
|
90
|
+
}
|
|
91
|
+
// Pause after node in step mode
|
|
92
|
+
if (this.mode === 'step') {
|
|
93
|
+
const action = await this.pause(nodeId, 'after', ctx);
|
|
94
|
+
if (action.type === 'abort') {
|
|
95
|
+
throw new Error(`Debug session aborted after node "${nodeId}"`);
|
|
96
|
+
}
|
|
97
|
+
this.applyAction(action);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// -----------------------------------------------------------------------
|
|
101
|
+
// Pause/resume channel
|
|
102
|
+
// -----------------------------------------------------------------------
|
|
103
|
+
/**
|
|
104
|
+
* Awaited by the executor to detect when the controller pauses.
|
|
105
|
+
* Resolves with the current debug state.
|
|
106
|
+
*/
|
|
107
|
+
onPause() {
|
|
108
|
+
return this._pausePromise;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Called by MCP tools or CLI to resume execution.
|
|
112
|
+
*/
|
|
113
|
+
resume(action) {
|
|
114
|
+
if (action.type !== 'abort') {
|
|
115
|
+
this.applyAction(action);
|
|
116
|
+
}
|
|
117
|
+
this._gateResolve?.(action);
|
|
118
|
+
this._gateResolve = null;
|
|
119
|
+
this._pausePromise = this._createPausePromise();
|
|
120
|
+
}
|
|
121
|
+
// -----------------------------------------------------------------------
|
|
122
|
+
// Variable modification
|
|
123
|
+
// -----------------------------------------------------------------------
|
|
124
|
+
/**
|
|
125
|
+
* Queue a variable modification. Applied before the next node runs.
|
|
126
|
+
* Key format: "nodeId:portName:executionIndex"
|
|
127
|
+
*/
|
|
128
|
+
setVariable(key, value) {
|
|
129
|
+
this.pendingModifications.set(key, value);
|
|
130
|
+
}
|
|
131
|
+
// -----------------------------------------------------------------------
|
|
132
|
+
// Breakpoints
|
|
133
|
+
// -----------------------------------------------------------------------
|
|
134
|
+
addBreakpoint(nodeId) {
|
|
135
|
+
this.breakpoints.add(nodeId);
|
|
136
|
+
}
|
|
137
|
+
removeBreakpoint(nodeId) {
|
|
138
|
+
this.breakpoints.delete(nodeId);
|
|
139
|
+
}
|
|
140
|
+
getBreakpoints() {
|
|
141
|
+
return [...this.breakpoints];
|
|
142
|
+
}
|
|
143
|
+
// -----------------------------------------------------------------------
|
|
144
|
+
// State inspection
|
|
145
|
+
// -----------------------------------------------------------------------
|
|
146
|
+
/** Build the current debug state for external consumers */
|
|
147
|
+
buildState(nodeId, phase, ctx) {
|
|
148
|
+
const variables = this.extractVariables(ctx);
|
|
149
|
+
const currentNodeOutputs = this.lastCompletedNodeId
|
|
150
|
+
? this.extractNodeOutputs(this.lastCompletedNodeId, variables)
|
|
151
|
+
: undefined;
|
|
152
|
+
return {
|
|
153
|
+
currentNodeId: nodeId,
|
|
154
|
+
phase,
|
|
155
|
+
completedNodes: [...this.completedNodes],
|
|
156
|
+
executionOrder: [...this.executionOrder],
|
|
157
|
+
position: this.position,
|
|
158
|
+
variables,
|
|
159
|
+
currentNodeOutputs,
|
|
160
|
+
breakpoints: [...this.breakpoints],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Get completed nodes list */
|
|
164
|
+
getCompletedNodes() {
|
|
165
|
+
return [...this.completedNodes];
|
|
166
|
+
}
|
|
167
|
+
// -----------------------------------------------------------------------
|
|
168
|
+
// Internal helpers
|
|
169
|
+
// -----------------------------------------------------------------------
|
|
170
|
+
async pause(nodeId, phase, ctx) {
|
|
171
|
+
const state = this.buildState(nodeId, phase, ctx);
|
|
172
|
+
// Signal the executor that we're paused
|
|
173
|
+
this._pauseResolve?.(state);
|
|
174
|
+
// Suspend on a gate Promise until resume() is called
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
this._gateResolve = (action) => resolve(action);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
applyAction(action) {
|
|
180
|
+
switch (action.type) {
|
|
181
|
+
case 'step':
|
|
182
|
+
this.mode = 'step';
|
|
183
|
+
break;
|
|
184
|
+
case 'continue':
|
|
185
|
+
this.mode = 'continue';
|
|
186
|
+
break;
|
|
187
|
+
case 'continueToBreakpoint':
|
|
188
|
+
this.mode = 'continueToBreakpoint';
|
|
189
|
+
break;
|
|
190
|
+
// 'abort' is handled by the caller (throws)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
applyPendingModifications(ctx) {
|
|
194
|
+
if (this.pendingModifications.size === 0)
|
|
195
|
+
return;
|
|
196
|
+
for (const [key, value] of this.pendingModifications) {
|
|
197
|
+
// Key format: "nodeId:portName:executionIndex"
|
|
198
|
+
const parts = key.split(':');
|
|
199
|
+
if (parts.length >= 3) {
|
|
200
|
+
const address = {
|
|
201
|
+
id: parts[0],
|
|
202
|
+
portName: parts[1],
|
|
203
|
+
executionIndex: parseInt(parts[2], 10),
|
|
204
|
+
};
|
|
205
|
+
ctx.setVariable(address, value);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
this.pendingModifications.clear();
|
|
209
|
+
}
|
|
210
|
+
restoreNodeOutputs(nodeId, outputs, ctx) {
|
|
211
|
+
// outputs is keyed by "portName:executionIndex" -> value
|
|
212
|
+
// Don't call ctx.addExecution here: the generated code's else block handles
|
|
213
|
+
// execution registration so local variables (e.g. dIdx) are set correctly.
|
|
214
|
+
for (const [portKey, value] of Object.entries(outputs)) {
|
|
215
|
+
const colonIdx = portKey.lastIndexOf(':');
|
|
216
|
+
if (colonIdx === -1)
|
|
217
|
+
continue;
|
|
218
|
+
const portName = portKey.substring(0, colonIdx);
|
|
219
|
+
const executionIndex = parseInt(portKey.substring(colonIdx + 1), 10);
|
|
220
|
+
ctx.setVariable({ id: nodeId, portName, executionIndex }, value);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
extractVariables(ctx) {
|
|
224
|
+
const serialized = ctx.serialize();
|
|
225
|
+
return serialized.variables;
|
|
226
|
+
}
|
|
227
|
+
extractNodeOutputs(nodeId, allVariables) {
|
|
228
|
+
const outputs = {};
|
|
229
|
+
const prefix = `${nodeId}:`;
|
|
230
|
+
for (const [key, value] of Object.entries(allVariables)) {
|
|
231
|
+
if (key.startsWith(prefix)) {
|
|
232
|
+
// Extract portName from key "nodeId:portName:executionIndex"
|
|
233
|
+
const rest = key.substring(prefix.length);
|
|
234
|
+
const colonIdx = rest.lastIndexOf(':');
|
|
235
|
+
const portName = colonIdx >= 0 ? rest.substring(0, colonIdx) : rest;
|
|
236
|
+
outputs[portName] = value;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return outputs;
|
|
240
|
+
}
|
|
241
|
+
_createPausePromise() {
|
|
242
|
+
return new Promise((resolve) => {
|
|
243
|
+
this._pauseResolve = resolve;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=debug-controller.js.map
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export { GeneratedExecutionContext } from "./ExecutionContext.js";
|
|
2
2
|
export { CancellationError } from "./CancellationError.js";
|
|
3
|
+
export { DebugController } from "./debug-controller.js";
|
|
4
|
+
export type { DebugMode, DebugPauseState, DebugResumeAction, DebugControllerConfig } from "./debug-controller.js";
|
|
5
|
+
export { CheckpointWriter, loadCheckpoint, findLatestCheckpoint } from "./checkpoint.js";
|
|
6
|
+
export type { CheckpointData } from "./checkpoint.js";
|
|
3
7
|
export * from "./events.js";
|
|
4
8
|
export * from "./function-registry.js";
|
|
5
9
|
export * from "./parameter-resolver.js";
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { GeneratedExecutionContext } from "./ExecutionContext.js";
|
|
2
2
|
export { CancellationError } from "./CancellationError.js";
|
|
3
|
+
export { DebugController } from "./debug-controller.js";
|
|
4
|
+
export { CheckpointWriter, loadCheckpoint, findLatestCheckpoint } from "./checkpoint.js";
|
|
3
5
|
export * from "./events.js";
|
|
4
6
|
export * from "./function-registry.js";
|
|
5
7
|
export * from "./parameter-resolver.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: CLI Reference
|
|
3
3
|
description: Complete reference for all Flow Weaver CLI commands, flags, and options
|
|
4
|
-
keywords: [cli, commands, compile, validate, strip, run, watch, dev, serve, export, diagram, diff, doctor, init, migrate, marketplace, plugin, grammar, changelog, openapi, pattern, create, templates]
|
|
4
|
+
keywords: [cli, commands, compile, validate, strip, run, watch, dev, serve, export, diagram, diff, doctor, init, migrate, marketplace, plugin, grammar, changelog, openapi, pattern, create, templates, context]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# CLI Reference
|
|
@@ -34,6 +34,7 @@ Complete reference for all `flow-weaver` CLI commands.
|
|
|
34
34
|
| `changelog` | Generate changelog from git |
|
|
35
35
|
| `market` | Marketplace packages |
|
|
36
36
|
| `plugin` | External plugins |
|
|
37
|
+
| `context` | Generate LLM context bundle |
|
|
37
38
|
| `docs` | Browse reference documentation |
|
|
38
39
|
| `ui` | Send commands to the editor |
|
|
39
40
|
| `listen` | Stream editor events |
|
|
@@ -176,6 +177,10 @@ flow-weaver run <input> [options]
|
|
|
176
177
|
| `--timeout <ms>` | Execution timeout in milliseconds | — |
|
|
177
178
|
| `--mocks <json>` | Mock config for built-in nodes as JSON | — |
|
|
178
179
|
| `--mocks-file <path>` | Path to JSON file with mock config | — |
|
|
180
|
+
| `-d, --debug` | Start in step-through debug mode | `false` |
|
|
181
|
+
| `--checkpoint` | Enable checkpointing to disk after each node | `false` |
|
|
182
|
+
| `--resume [file]` | Resume from a checkpoint file (auto-detects latest if no file given) | — |
|
|
183
|
+
| `-b, --breakpoint <nodeIds...>` | Set initial breakpoints (repeatable) | — |
|
|
179
184
|
|
|
180
185
|
**Examples:**
|
|
181
186
|
```bash
|
|
@@ -183,9 +188,13 @@ flow-weaver run workflow.ts --params '{"amount": 500}'
|
|
|
183
188
|
flow-weaver run workflow.ts --params-file input.json --trace
|
|
184
189
|
flow-weaver run workflow.ts --mocks '{"fast": true, "events": {"app/approved": {"status": "ok"}}}'
|
|
185
190
|
flow-weaver run workflow.ts --timeout 30000 --json
|
|
191
|
+
flow-weaver run workflow.ts --debug
|
|
192
|
+
flow-weaver run workflow.ts --checkpoint
|
|
193
|
+
flow-weaver run workflow.ts --resume
|
|
194
|
+
flow-weaver run workflow.ts --debug --breakpoint processData --breakpoint validate
|
|
186
195
|
```
|
|
187
196
|
|
|
188
|
-
> See also: [Built-in Nodes](built-in-nodes) for mock configuration details.
|
|
197
|
+
> See also: [Built-in Nodes](built-in-nodes) for mock configuration details, [Debugging](debugging) for debug REPL commands and checkpoint details.
|
|
189
198
|
|
|
190
199
|
---
|
|
191
200
|
|
|
@@ -797,6 +806,37 @@ flow-weaver docs search "missing workflow"
|
|
|
797
806
|
|
|
798
807
|
---
|
|
799
808
|
|
|
809
|
+
### context
|
|
810
|
+
|
|
811
|
+
Generate a self-contained LLM context bundle from documentation and annotation grammar. Two profiles control the output format: `standalone` produces a complete reference for pasting into any LLM, `assistant` produces a leaner version that assumes MCP tools are available.
|
|
812
|
+
|
|
813
|
+
```bash
|
|
814
|
+
flow-weaver context [preset] [options]
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
| Flag | Description | Default |
|
|
818
|
+
|------|-------------|---------|
|
|
819
|
+
| `--profile <profile>` | `standalone` or `assistant` | `standalone` |
|
|
820
|
+
| `--topics <slugs>` | Comma-separated topic slugs (overrides preset) | — |
|
|
821
|
+
| `--add <slugs>` | Extra topic slugs on top of preset | — |
|
|
822
|
+
| `--no-grammar` | Omit EBNF grammar section | grammar included |
|
|
823
|
+
| `-o, --output <path>` | Write to file instead of stdout | stdout |
|
|
824
|
+
| `--list` | List available presets and exit | — |
|
|
825
|
+
|
|
826
|
+
Built-in presets: `core` (concepts, grammar, tutorial), `authoring` (concepts, grammar, annotations, built-in nodes, scaffold, node-conversion, patterns), `ops` (CLI, compilation, deployment, export, debugging, error-codes), `full` (all 16 topics).
|
|
827
|
+
|
|
828
|
+
**Examples:**
|
|
829
|
+
```bash
|
|
830
|
+
flow-weaver context core | pbcopy
|
|
831
|
+
flow-weaver context full -o .flow-weaver-context.md
|
|
832
|
+
flow-weaver context authoring --profile assistant
|
|
833
|
+
flow-weaver context --topics concepts,jsdoc-grammar,error-codes
|
|
834
|
+
flow-weaver context core --add error-codes
|
|
835
|
+
flow-weaver context --list
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
800
840
|
## Editor Integration
|
|
801
841
|
|
|
802
842
|
### ui focus-node
|