clementine-agent 1.2.2 → 1.3.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/agent/assistant.js +12 -0
- package/dist/cli/dashboard.js +3034 -734
- package/dist/cli/static/LICENSE-NOTICES.md +12 -0
- package/dist/cli/static/drawflow.min.css +1 -0
- package/dist/cli/static/drawflow.min.js +1 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +16 -0
- package/dist/dashboard/builder/dry-run.d.ts +31 -0
- package/dist/dashboard/builder/dry-run.js +138 -0
- package/dist/dashboard/builder/events.d.ts +23 -0
- package/dist/dashboard/builder/events.js +28 -0
- package/dist/dashboard/builder/mcp-invoke.d.ts +25 -0
- package/dist/dashboard/builder/mcp-invoke.js +143 -0
- package/dist/dashboard/builder/runner.d.ts +68 -0
- package/dist/dashboard/builder/runner.js +418 -0
- package/dist/dashboard/builder/serializer.d.ts +79 -0
- package/dist/dashboard/builder/serializer.js +547 -0
- package/dist/dashboard/builder/snapshots.d.ts +32 -0
- package/dist/dashboard/builder/snapshots.js +138 -0
- package/dist/dashboard/builder/validation.d.ts +26 -0
- package/dist/dashboard/builder/validation.js +183 -0
- package/dist/gateway/router.js +31 -2
- package/dist/index.js +38 -0
- package/dist/memory/chunker.js +13 -2
- package/dist/memory/hot-cache.d.ts +38 -0
- package/dist/memory/hot-cache.js +73 -0
- package/dist/memory/integrity.d.ts +28 -0
- package/dist/memory/integrity.js +119 -0
- package/dist/memory/maintenance.d.ts +23 -2
- package/dist/memory/maintenance.js +140 -3
- package/dist/memory/store.d.ts +259 -2
- package/dist/memory/store.js +751 -21
- package/dist/memory/write-queue.d.ts +96 -0
- package/dist/memory/write-queue.js +165 -0
- package/dist/tools/builder-tools.d.ts +13 -0
- package/dist/tools/builder-tools.js +437 -0
- package/dist/tools/mcp-server.js +2 -0
- package/dist/tools/memory-tools.js +38 -1
- package/dist/types.d.ts +56 -2
- package/package.json +2 -2
- package/vault/00-System/skills/builder-canvas.md +126 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder workflow test runner.
|
|
3
|
+
*
|
|
4
|
+
* Executes a workflow's step DAG in topological waves, streaming per-step
|
|
5
|
+
* status + output events through the events bus so the dashboard canvas
|
|
6
|
+
* can light up in real time.
|
|
7
|
+
*
|
|
8
|
+
* Safety posture for **test mode** (the default for canvas runs):
|
|
9
|
+
* - Prompt steps: stubbed by default (no LLM tokens, no agent turns).
|
|
10
|
+
* Real-mode runs through the production scheduler, not here.
|
|
11
|
+
* - MCP steps: read-only-shaped tools invoke for real (list_/get_/
|
|
12
|
+
* read_/search_); write-shaped tools are stubbed unless mode='real'.
|
|
13
|
+
* - Channel steps: always stubbed in the test runner. Real channel
|
|
14
|
+
* sends should happen on a scheduled run, not from a canvas test.
|
|
15
|
+
* - Transform / conditional / loop: real evaluation (sandboxed JS,
|
|
16
|
+
* short timeout). They have no external side effects.
|
|
17
|
+
*
|
|
18
|
+
* Long-running awareness:
|
|
19
|
+
* - Per-step timeout (default 30s)
|
|
20
|
+
* - Total budget cap (default 60s wall-clock)
|
|
21
|
+
* - AbortSignal cancellation; canceller killed between steps and
|
|
22
|
+
* during transform/conditional/loop evaluation.
|
|
23
|
+
*/
|
|
24
|
+
import type { WorkflowDefinition } from '../../types.js';
|
|
25
|
+
export type RunMode = 'mock' | 'real';
|
|
26
|
+
export type StepStatus = 'pending' | 'running' | 'done' | 'failed' | 'skipped' | 'cancelled' | 'timeout';
|
|
27
|
+
export interface RunOptions {
|
|
28
|
+
workflowId: string;
|
|
29
|
+
runId?: string;
|
|
30
|
+
mode?: RunMode;
|
|
31
|
+
perStepTimeoutMs?: number;
|
|
32
|
+
totalBudgetMs?: number;
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
export interface StepResult {
|
|
36
|
+
stepId: string;
|
|
37
|
+
status: StepStatus;
|
|
38
|
+
durationMs: number;
|
|
39
|
+
output?: unknown;
|
|
40
|
+
error?: string;
|
|
41
|
+
mocked?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface RunResult {
|
|
44
|
+
runId: string;
|
|
45
|
+
workflowId: string;
|
|
46
|
+
mode: RunMode;
|
|
47
|
+
status: 'ok' | 'error' | 'cancelled' | 'timeout';
|
|
48
|
+
startedAt: string;
|
|
49
|
+
finishedAt: string;
|
|
50
|
+
durationMs: number;
|
|
51
|
+
stepResults: StepResult[];
|
|
52
|
+
}
|
|
53
|
+
export declare function cancelRun(runId: string): boolean;
|
|
54
|
+
export declare function isRunActive(runId: string): boolean;
|
|
55
|
+
export declare function runWorkflowTest(wf: WorkflowDefinition, opts: RunOptions): Promise<RunResult>;
|
|
56
|
+
export type McpInvokeFn = (args: {
|
|
57
|
+
server: string;
|
|
58
|
+
tool: string;
|
|
59
|
+
inputs: Record<string, unknown>;
|
|
60
|
+
signal: AbortSignal;
|
|
61
|
+
}) => Promise<unknown>;
|
|
62
|
+
/**
|
|
63
|
+
* Daemon registers a callback the runner uses to invoke MCP tools for
|
|
64
|
+
* real. Kept as a runtime injection so the runner doesn't pull the
|
|
65
|
+
* agent-bridge into its module graph (which would create a cycle).
|
|
66
|
+
*/
|
|
67
|
+
export declare function registerMcpInvokeHandler(handler: McpInvokeFn): void;
|
|
68
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder workflow test runner.
|
|
3
|
+
*
|
|
4
|
+
* Executes a workflow's step DAG in topological waves, streaming per-step
|
|
5
|
+
* status + output events through the events bus so the dashboard canvas
|
|
6
|
+
* can light up in real time.
|
|
7
|
+
*
|
|
8
|
+
* Safety posture for **test mode** (the default for canvas runs):
|
|
9
|
+
* - Prompt steps: stubbed by default (no LLM tokens, no agent turns).
|
|
10
|
+
* Real-mode runs through the production scheduler, not here.
|
|
11
|
+
* - MCP steps: read-only-shaped tools invoke for real (list_/get_/
|
|
12
|
+
* read_/search_); write-shaped tools are stubbed unless mode='real'.
|
|
13
|
+
* - Channel steps: always stubbed in the test runner. Real channel
|
|
14
|
+
* sends should happen on a scheduled run, not from a canvas test.
|
|
15
|
+
* - Transform / conditional / loop: real evaluation (sandboxed JS,
|
|
16
|
+
* short timeout). They have no external side effects.
|
|
17
|
+
*
|
|
18
|
+
* Long-running awareness:
|
|
19
|
+
* - Per-step timeout (default 30s)
|
|
20
|
+
* - Total budget cap (default 60s wall-clock)
|
|
21
|
+
* - AbortSignal cancellation; canceller killed between steps and
|
|
22
|
+
* during transform/conditional/loop evaluation.
|
|
23
|
+
*/
|
|
24
|
+
import { performance } from 'node:perf_hooks';
|
|
25
|
+
import vm from 'node:vm';
|
|
26
|
+
import { randomUUID } from 'node:crypto';
|
|
27
|
+
import { emitBuilderEvent } from './events.js';
|
|
28
|
+
/** Map of running runId → AbortController so a separate cancel call can kill in-flight runs. */
|
|
29
|
+
const activeRuns = new Map();
|
|
30
|
+
export function cancelRun(runId) {
|
|
31
|
+
const controller = activeRuns.get(runId);
|
|
32
|
+
if (!controller)
|
|
33
|
+
return false;
|
|
34
|
+
controller.abort();
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
export function isRunActive(runId) {
|
|
38
|
+
return activeRuns.has(runId);
|
|
39
|
+
}
|
|
40
|
+
export async function runWorkflowTest(wf, opts) {
|
|
41
|
+
const runId = opts.runId ?? randomUUID();
|
|
42
|
+
const mode = opts.mode ?? 'mock';
|
|
43
|
+
const perStepTimeoutMs = opts.perStepTimeoutMs ?? 30_000;
|
|
44
|
+
const totalBudgetMs = opts.totalBudgetMs ?? 60_000;
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
activeRuns.set(runId, controller);
|
|
47
|
+
if (opts.signal) {
|
|
48
|
+
if (opts.signal.aborted)
|
|
49
|
+
controller.abort();
|
|
50
|
+
else
|
|
51
|
+
opts.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
52
|
+
}
|
|
53
|
+
const startedAt = new Date().toISOString();
|
|
54
|
+
const startMs = performance.now();
|
|
55
|
+
const outputs = new Map();
|
|
56
|
+
const stepResults = [];
|
|
57
|
+
emitBuilderEvent({
|
|
58
|
+
type: 'run:started',
|
|
59
|
+
workflowId: opts.workflowId,
|
|
60
|
+
runId,
|
|
61
|
+
payload: { mode, stepCount: wf.steps.length, perStepTimeoutMs, totalBudgetMs },
|
|
62
|
+
});
|
|
63
|
+
let overallStatus = 'ok';
|
|
64
|
+
const waves = computeWaves(wf.steps);
|
|
65
|
+
try {
|
|
66
|
+
for (const wave of waves) {
|
|
67
|
+
if (controller.signal.aborted) {
|
|
68
|
+
overallStatus = 'cancelled';
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
if (performance.now() - startMs > totalBudgetMs) {
|
|
72
|
+
overallStatus = 'timeout';
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
for (const step of wave) {
|
|
76
|
+
if (controller.signal.aborted) {
|
|
77
|
+
overallStatus = 'cancelled';
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (performance.now() - startMs > totalBudgetMs) {
|
|
81
|
+
overallStatus = 'timeout';
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
const skipReason = shouldSkipFromConditional(step, wf, outputs);
|
|
85
|
+
if (skipReason) {
|
|
86
|
+
stepResults.push({ stepId: step.id, status: 'skipped', durationMs: 0, output: skipReason, mocked: true });
|
|
87
|
+
emitBuilderEvent({ type: 'run:step-status', workflowId: opts.workflowId, runId, payload: { stepId: step.id, status: 'skipped', reason: skipReason } });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
emitBuilderEvent({ type: 'run:step-status', workflowId: opts.workflowId, runId, payload: { stepId: step.id, status: 'running' } });
|
|
91
|
+
const stepStart = performance.now();
|
|
92
|
+
let result;
|
|
93
|
+
try {
|
|
94
|
+
const out = await runWithTimeout(() => executeStep(step, wf, outputs, mode, controller.signal), perStepTimeoutMs, controller.signal);
|
|
95
|
+
outputs.set(step.id, out.output);
|
|
96
|
+
result = {
|
|
97
|
+
stepId: step.id,
|
|
98
|
+
status: 'done',
|
|
99
|
+
durationMs: Math.round(performance.now() - stepStart),
|
|
100
|
+
output: out.output,
|
|
101
|
+
mocked: out.mocked,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const e = err;
|
|
106
|
+
const status = e?.name === 'AbortError' ? 'cancelled' : (e?.name === 'TimeoutError' ? 'timeout' : 'failed');
|
|
107
|
+
result = {
|
|
108
|
+
stepId: step.id,
|
|
109
|
+
status,
|
|
110
|
+
durationMs: Math.round(performance.now() - stepStart),
|
|
111
|
+
error: e?.message ?? String(err),
|
|
112
|
+
};
|
|
113
|
+
if (status === 'failed')
|
|
114
|
+
overallStatus = 'error';
|
|
115
|
+
if (status === 'cancelled')
|
|
116
|
+
overallStatus = 'cancelled';
|
|
117
|
+
if (status === 'timeout')
|
|
118
|
+
overallStatus = 'timeout';
|
|
119
|
+
}
|
|
120
|
+
stepResults.push(result);
|
|
121
|
+
emitBuilderEvent({
|
|
122
|
+
type: 'run:step-status',
|
|
123
|
+
workflowId: opts.workflowId,
|
|
124
|
+
runId,
|
|
125
|
+
payload: { stepId: step.id, status: result.status, durationMs: result.durationMs, error: result.error, mocked: result.mocked },
|
|
126
|
+
});
|
|
127
|
+
emitBuilderEvent({
|
|
128
|
+
type: 'run:step-output',
|
|
129
|
+
workflowId: opts.workflowId,
|
|
130
|
+
runId,
|
|
131
|
+
payload: { stepId: step.id, output: previewOf(result.output), error: result.error, status: result.status },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (overallStatus !== 'ok' && overallStatus !== 'cancelled' && overallStatus !== 'timeout') {
|
|
135
|
+
// Continue executing later waves even after a step failed — caller can decide what to do.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
activeRuns.delete(runId);
|
|
141
|
+
}
|
|
142
|
+
const finishedAt = new Date().toISOString();
|
|
143
|
+
const durationMs = Math.round(performance.now() - startMs);
|
|
144
|
+
const finalStatus = overallStatus;
|
|
145
|
+
emitBuilderEvent({
|
|
146
|
+
type: finalStatus === 'cancelled' ? 'run:cancelled' : 'run:completed',
|
|
147
|
+
workflowId: opts.workflowId,
|
|
148
|
+
runId,
|
|
149
|
+
payload: { status: finalStatus, durationMs, stepCount: stepResults.length },
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
runId,
|
|
153
|
+
workflowId: opts.workflowId,
|
|
154
|
+
mode,
|
|
155
|
+
status: finalStatus,
|
|
156
|
+
startedAt,
|
|
157
|
+
finishedAt,
|
|
158
|
+
durationMs,
|
|
159
|
+
stepResults,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async function executeStep(step, wf, priorOutputs, mode, signal) {
|
|
163
|
+
const kind = step.kind ?? 'prompt';
|
|
164
|
+
const ctx = buildExecContext(wf, priorOutputs);
|
|
165
|
+
switch (kind) {
|
|
166
|
+
case 'prompt':
|
|
167
|
+
return executePromptStep(step, mode);
|
|
168
|
+
case 'mcp':
|
|
169
|
+
return executeMcpStep(step, mode, ctx, signal);
|
|
170
|
+
case 'channel':
|
|
171
|
+
return executeChannelStep(step, ctx);
|
|
172
|
+
case 'transform':
|
|
173
|
+
return executeTransformStep(step, ctx);
|
|
174
|
+
case 'conditional':
|
|
175
|
+
return executeConditionalStep(step, ctx);
|
|
176
|
+
case 'loop':
|
|
177
|
+
return executeLoopStep(step, ctx);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function executePromptStep(step, mode) {
|
|
181
|
+
if (mode === 'real') {
|
|
182
|
+
// Real prompt execution would route through the live agent. Today the
|
|
183
|
+
// canvas test runner intentionally stays mock-only for prompt steps —
|
|
184
|
+
// schedule a real run via cron for full LLM execution.
|
|
185
|
+
return {
|
|
186
|
+
mocked: true,
|
|
187
|
+
output: `[real-mode requested but prompt steps are stubbed in canvas test runner — schedule a cron for full execution] prompt: ${truncate(step.prompt, 200)}`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
mocked: true,
|
|
192
|
+
output: `[mock] would call agent (model: ${step.model ?? 'default'}, maxTurns: ${step.maxTurns}). prompt: ${truncate(step.prompt, 200)}`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async function executeMcpStep(step, mode, ctx, signal) {
|
|
196
|
+
if (!step.mcp || !step.mcp.server || !step.mcp.tool) {
|
|
197
|
+
throw new Error('MCP step missing server or tool');
|
|
198
|
+
}
|
|
199
|
+
const looksDestructive = /(create|delete|update|push|send|post|drop|write|patch|set_)/i.test(step.mcp.tool);
|
|
200
|
+
const shouldMock = mode === 'mock' && looksDestructive;
|
|
201
|
+
if (shouldMock) {
|
|
202
|
+
return {
|
|
203
|
+
mocked: true,
|
|
204
|
+
output: `[mock-write] would call ${step.mcp.server}.${step.mcp.tool} with inputs ${JSON.stringify(resolveInputs(step.mcp.inputs ?? {}, ctx))}`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
// Read-only or real-mode: invoke for real via the daemon's MCP bridge.
|
|
208
|
+
// We delegate through a runtime callback registered by the daemon to
|
|
209
|
+
// avoid pulling agent-bridge code into the runner module's dependency
|
|
210
|
+
// tree directly. Falls back to a clear stub if no handler is wired.
|
|
211
|
+
const handler = globalThis.__clementineMcpInvoke;
|
|
212
|
+
if (!handler) {
|
|
213
|
+
return {
|
|
214
|
+
mocked: true,
|
|
215
|
+
output: `[mock — daemon MCP bridge not registered] would call ${step.mcp.server}.${step.mcp.tool}`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const inputs = resolveInputs(step.mcp.inputs ?? {}, ctx);
|
|
219
|
+
const result = await handler({ server: step.mcp.server, tool: step.mcp.tool, inputs, signal });
|
|
220
|
+
return { mocked: false, output: result };
|
|
221
|
+
}
|
|
222
|
+
function executeChannelStep(step, ctx) {
|
|
223
|
+
if (!step.channel)
|
|
224
|
+
throw new Error('Channel step missing config');
|
|
225
|
+
const content = templatize(step.channel.content, ctx);
|
|
226
|
+
return {
|
|
227
|
+
mocked: true,
|
|
228
|
+
output: `[mock] would send to ${step.channel.channel} → ${step.channel.target}: ${truncate(content, 200)}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function executeTransformStep(step, ctx) {
|
|
232
|
+
if (!step.transform || !step.transform.expression)
|
|
233
|
+
throw new Error('Transform step missing expression');
|
|
234
|
+
const value = evalSandbox(step.transform.expression, ctx);
|
|
235
|
+
return { mocked: false, output: value };
|
|
236
|
+
}
|
|
237
|
+
function executeConditionalStep(step, ctx) {
|
|
238
|
+
if (!step.conditional || !step.conditional.condition)
|
|
239
|
+
throw new Error('Conditional step missing condition');
|
|
240
|
+
const value = !!evalSandbox(step.conditional.condition, ctx);
|
|
241
|
+
return {
|
|
242
|
+
mocked: false,
|
|
243
|
+
output: { result: value, branch: value ? 'true' : 'false' },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function executeLoopStep(step, ctx) {
|
|
247
|
+
if (!step.loop || !step.loop.items)
|
|
248
|
+
throw new Error('Loop step missing items');
|
|
249
|
+
const items = evalSandbox(step.loop.items, ctx);
|
|
250
|
+
const iterable = Array.isArray(items) ? items : Object.values(items ?? {});
|
|
251
|
+
return {
|
|
252
|
+
mocked: false,
|
|
253
|
+
output: { itemCount: iterable.length, sample: iterable.slice(0, 3) },
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function buildExecContext(_wf, priorOutputs) {
|
|
257
|
+
const steps = {};
|
|
258
|
+
for (const [k, v] of priorOutputs)
|
|
259
|
+
steps[k] = v;
|
|
260
|
+
// For loop body steps, downstream may reference the upstream loop step by id.
|
|
261
|
+
return { steps, input: undefined };
|
|
262
|
+
}
|
|
263
|
+
function evalSandbox(expression, ctx) {
|
|
264
|
+
// Sandbox JS expressions. Don't expose require, process, etc.
|
|
265
|
+
const sandbox = { steps: ctx.steps, input: ctx.input };
|
|
266
|
+
const script = new vm.Script('(' + expression + ')');
|
|
267
|
+
const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
|
|
268
|
+
return script.runInContext(context, { timeout: 1500 });
|
|
269
|
+
}
|
|
270
|
+
function templatize(template, ctx) {
|
|
271
|
+
if (!template)
|
|
272
|
+
return '';
|
|
273
|
+
return template.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_m, expr) => {
|
|
274
|
+
try {
|
|
275
|
+
const v = evalSandbox(String(expr), ctx);
|
|
276
|
+
return v == null ? '' : String(v);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return '';
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function resolveInputs(inputs, ctx) {
|
|
284
|
+
const out = {};
|
|
285
|
+
for (const [k, v] of Object.entries(inputs)) {
|
|
286
|
+
if (typeof v === 'string' && v.includes('{{'))
|
|
287
|
+
out[k] = templatize(v, ctx);
|
|
288
|
+
else
|
|
289
|
+
out[k] = v;
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
// ── conditional branch handling ─────────────────────────────────────
|
|
294
|
+
function shouldSkipFromConditional(step, wf, outputs) {
|
|
295
|
+
// If a parent conditional ran and chose the other branch, skip this step.
|
|
296
|
+
for (const dep of step.dependsOn) {
|
|
297
|
+
const parent = wf.steps.find(s => s.id === dep);
|
|
298
|
+
if (!parent || parent.kind !== 'conditional')
|
|
299
|
+
continue;
|
|
300
|
+
const parentOut = outputs.get(parent.id);
|
|
301
|
+
if (!parentOut)
|
|
302
|
+
continue;
|
|
303
|
+
const trueNext = parent.conditional?.trueNext ?? [];
|
|
304
|
+
const falseNext = parent.conditional?.falseNext ?? [];
|
|
305
|
+
if (parentOut.branch === 'true' && falseNext.includes(step.id) && !trueNext.includes(step.id)) {
|
|
306
|
+
return `parent ${parent.id} took true branch`;
|
|
307
|
+
}
|
|
308
|
+
if (parentOut.branch === 'false' && trueNext.includes(step.id) && !falseNext.includes(step.id)) {
|
|
309
|
+
return `parent ${parent.id} took false branch`;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
// ── topological waves ──────────────────────────────────────────────
|
|
315
|
+
function computeWaves(steps) {
|
|
316
|
+
const byId = new Map(steps.map(s => [s.id, s]));
|
|
317
|
+
const remaining = new Set(steps.map(s => s.id));
|
|
318
|
+
const waves = [];
|
|
319
|
+
const seen = new Set();
|
|
320
|
+
let safety = steps.length + 1;
|
|
321
|
+
while (remaining.size > 0 && safety-- > 0) {
|
|
322
|
+
const wave = [];
|
|
323
|
+
for (const id of remaining) {
|
|
324
|
+
const s = byId.get(id);
|
|
325
|
+
if (!s)
|
|
326
|
+
continue;
|
|
327
|
+
if (s.dependsOn.every(d => !byId.has(d) || seen.has(d))) {
|
|
328
|
+
wave.push(s);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (wave.length === 0) {
|
|
332
|
+
// Cycle or malformed graph — drop remaining into one final wave so we don't lock up
|
|
333
|
+
for (const id of remaining) {
|
|
334
|
+
const s = byId.get(id);
|
|
335
|
+
if (s)
|
|
336
|
+
wave.push(s);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
for (const s of wave) {
|
|
340
|
+
seen.add(s.id);
|
|
341
|
+
remaining.delete(s.id);
|
|
342
|
+
}
|
|
343
|
+
waves.push(wave);
|
|
344
|
+
}
|
|
345
|
+
return waves;
|
|
346
|
+
}
|
|
347
|
+
// ── helpers ────────────────────────────────────────────────────────
|
|
348
|
+
class TimeoutError extends Error {
|
|
349
|
+
constructor(message = 'Timed out') { super(message); this.name = 'TimeoutError'; }
|
|
350
|
+
}
|
|
351
|
+
function runWithTimeout(fn, ms, signal) {
|
|
352
|
+
return new Promise((resolve, reject) => {
|
|
353
|
+
let settled = false;
|
|
354
|
+
const t = setTimeout(() => {
|
|
355
|
+
if (settled)
|
|
356
|
+
return;
|
|
357
|
+
settled = true;
|
|
358
|
+
reject(new TimeoutError());
|
|
359
|
+
}, ms);
|
|
360
|
+
const onAbort = () => {
|
|
361
|
+
if (settled)
|
|
362
|
+
return;
|
|
363
|
+
settled = true;
|
|
364
|
+
clearTimeout(t);
|
|
365
|
+
const err = new Error('Aborted');
|
|
366
|
+
err.name = 'AbortError';
|
|
367
|
+
reject(err);
|
|
368
|
+
};
|
|
369
|
+
if (signal.aborted) {
|
|
370
|
+
onAbort();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
374
|
+
Promise.resolve()
|
|
375
|
+
.then(fn)
|
|
376
|
+
.then(v => {
|
|
377
|
+
if (settled)
|
|
378
|
+
return;
|
|
379
|
+
settled = true;
|
|
380
|
+
clearTimeout(t);
|
|
381
|
+
signal.removeEventListener('abort', onAbort);
|
|
382
|
+
resolve(v);
|
|
383
|
+
})
|
|
384
|
+
.catch(err => {
|
|
385
|
+
if (settled)
|
|
386
|
+
return;
|
|
387
|
+
settled = true;
|
|
388
|
+
clearTimeout(t);
|
|
389
|
+
signal.removeEventListener('abort', onAbort);
|
|
390
|
+
reject(err);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function truncate(s, n) {
|
|
395
|
+
if (!s)
|
|
396
|
+
return '';
|
|
397
|
+
if (s.length <= n)
|
|
398
|
+
return s;
|
|
399
|
+
return s.slice(0, n) + '…';
|
|
400
|
+
}
|
|
401
|
+
function previewOf(out, n = 1500) {
|
|
402
|
+
try {
|
|
403
|
+
const s = typeof out === 'string' ? out : JSON.stringify(out, null, 2);
|
|
404
|
+
return s.length <= n ? s : s.slice(0, n) + '…';
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return String(out);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Daemon registers a callback the runner uses to invoke MCP tools for
|
|
412
|
+
* real. Kept as a runtime injection so the runner doesn't pull the
|
|
413
|
+
* agent-bridge into its module graph (which would create a cycle).
|
|
414
|
+
*/
|
|
415
|
+
export function registerMcpInvokeHandler(handler) {
|
|
416
|
+
globalThis.__clementineMcpInvoke = handler;
|
|
417
|
+
}
|
|
418
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder serializer.
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities:
|
|
5
|
+
*
|
|
6
|
+
* 1. Unified read/write of crons + workflows as WorkflowDefinition objects
|
|
7
|
+
* so the visual canvas can edit both with one shape. CRON.md entries
|
|
8
|
+
* round-trip as virtual single-step workflows; multi-step workflow
|
|
9
|
+
* files round-trip via the existing workflow-runner format.
|
|
10
|
+
*
|
|
11
|
+
* 2. Convert WorkflowDefinition ⇄ Drawflow's canvas data shape so the
|
|
12
|
+
* frontend can drop the JSON straight into drawflow.import().
|
|
13
|
+
*
|
|
14
|
+
* Backwards-compatible: existing CRON.md and workflow files are unchanged
|
|
15
|
+
* unless edited through this module, and edits preserve unrelated fields.
|
|
16
|
+
*/
|
|
17
|
+
import type { WorkflowDefinition, CronJobDefinition, BuilderWorkflowSummary, WorkflowOriginKind } from '../../types.js';
|
|
18
|
+
export declare function cronId(name: string): string;
|
|
19
|
+
export declare function workflowId(filename: string): string;
|
|
20
|
+
export declare function parseBuilderId(id: string): {
|
|
21
|
+
origin: WorkflowOriginKind;
|
|
22
|
+
key: string;
|
|
23
|
+
} | null;
|
|
24
|
+
export declare function listAllForBuilder(): BuilderWorkflowSummary[];
|
|
25
|
+
export declare function readWorkflow(id: string): WorkflowDefinition | null;
|
|
26
|
+
export declare function cronJobToWorkflow(job: CronJobDefinition): WorkflowDefinition;
|
|
27
|
+
/** True if a workflow is shaped like a CRON.md entry (single prompt step + cron schedule). */
|
|
28
|
+
export declare function isCronShape(wf: WorkflowDefinition): boolean;
|
|
29
|
+
export declare function saveWorkflow(id: string, wf: WorkflowDefinition): {
|
|
30
|
+
ok: true;
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
error: string;
|
|
34
|
+
};
|
|
35
|
+
/** Resolve the on-disk file path for a builder id (cron entries all share CRON_FILE). */
|
|
36
|
+
export declare function sourceFileForId(id: string, parsedHint?: {
|
|
37
|
+
origin: WorkflowOriginKind;
|
|
38
|
+
key: string;
|
|
39
|
+
}): string | null;
|
|
40
|
+
/** Drawflow node shape (subset we use). */
|
|
41
|
+
interface DrawflowNode {
|
|
42
|
+
id: number;
|
|
43
|
+
name: string;
|
|
44
|
+
data: Record<string, unknown>;
|
|
45
|
+
class: string;
|
|
46
|
+
html: string;
|
|
47
|
+
typenode: boolean;
|
|
48
|
+
inputs: {
|
|
49
|
+
input_1: {
|
|
50
|
+
connections: Array<{
|
|
51
|
+
node: string;
|
|
52
|
+
input: string;
|
|
53
|
+
}>;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
outputs: {
|
|
57
|
+
output_1: {
|
|
58
|
+
connections: Array<{
|
|
59
|
+
node: string;
|
|
60
|
+
output: string;
|
|
61
|
+
}>;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
pos_x: number;
|
|
65
|
+
pos_y: number;
|
|
66
|
+
}
|
|
67
|
+
export interface DrawflowExport {
|
|
68
|
+
drawflow: {
|
|
69
|
+
Home: {
|
|
70
|
+
data: Record<string, DrawflowNode>;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export declare function workflowToDrawflow(wf: WorkflowDefinition): DrawflowExport;
|
|
75
|
+
export declare function drawflowToWorkflow(exportData: DrawflowExport, base: WorkflowDefinition): WorkflowDefinition;
|
|
76
|
+
/** Stringify a workflow's frontmatter for visual inspection. */
|
|
77
|
+
export declare function workflowFrontmatterString(wf: WorkflowDefinition): string;
|
|
78
|
+
export {};
|
|
79
|
+
//# sourceMappingURL=serializer.d.ts.map
|