@particle-academy/fancy-flow 0.11.0 → 0.13.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/capabilities-B6r1Snfj.d.ts +77 -0
- package/dist/capabilities-D-V9Rxv1.d.cts +77 -0
- package/dist/chunk-BB45F6S3.js +38 -0
- package/dist/chunk-BB45F6S3.js.map +1 -0
- package/dist/{chunk-2NQT2A3I.js → chunk-L4AX73Q6.js} +5 -4
- package/dist/chunk-L4AX73Q6.js.map +1 -0
- package/dist/{chunk-3GMBLHTM.js → chunk-MFMRTRPO.js} +3 -3
- package/dist/{chunk-3GMBLHTM.js.map → chunk-MFMRTRPO.js.map} +1 -1
- package/dist/{chunk-YNOI7ONN.js → chunk-WEZJFMLV.js} +224 -51
- package/dist/chunk-WEZJFMLV.js.map +1 -0
- package/dist/engine.cjs +3 -2
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.cts +4 -2
- package/dist/engine.d.ts +4 -2
- package/dist/engine.js +1 -1
- package/dist/index.cjs +396 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/llm/vercel-ai.cjs +59 -0
- package/dist/llm/vercel-ai.cjs.map +1 -0
- package/dist/llm/vercel-ai.d.cts +59 -0
- package/dist/llm/vercel-ai.d.ts +59 -0
- package/dist/llm/vercel-ai.js +49 -0
- package/dist/llm/vercel-ai.js.map +1 -0
- package/dist/registry/index.d.cts +60 -4
- package/dist/registry/index.d.ts +60 -4
- package/dist/registry.cjs +455 -93
- package/dist/registry.cjs.map +1 -1
- package/dist/registry.js +3 -1
- package/dist/runtime/index.d.cts +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime.cjs +3 -2
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +2 -2
- package/dist/schema/index.d.cts +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/{types-CsEe3EQl.d.ts → types-CnnKPnpt.d.ts} +1 -1
- package/dist/{types-BwLr5tZV.d.cts → types-H60tQAxr.d.cts} +1 -1
- package/dist/{types-BS3Gwnkq.d.cts → types-fc0fFKkf.d.cts} +7 -1
- package/dist/{types-BS3Gwnkq.d.ts → types-fc0fFKkf.d.ts} +7 -1
- package/dist/ux.d.cts +2 -2
- package/dist/ux.d.ts +2 -2
- package/package.json +19 -3
- package/dist/chunk-2NQT2A3I.js.map +0 -1
- package/dist/chunk-YNOI7ONN.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -212,6 +212,299 @@ function RichInputPreview({ config }) {
|
|
|
212
212
|
] });
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
// src/registry/capabilities.ts
|
|
216
|
+
var llmClient = null;
|
|
217
|
+
function getLlmClient() {
|
|
218
|
+
return llmClient;
|
|
219
|
+
}
|
|
220
|
+
var workflowResolver = null;
|
|
221
|
+
function getWorkflowResolver() {
|
|
222
|
+
return workflowResolver;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/registry/llm-branch.ts
|
|
226
|
+
function declaredRoutes(config) {
|
|
227
|
+
const raw = config.routes;
|
|
228
|
+
if (!Array.isArray(raw)) return [];
|
|
229
|
+
return raw.map((r) => ({ port: String(r?.port ?? "").trim(), description: r?.description })).filter((r) => r.port !== "");
|
|
230
|
+
}
|
|
231
|
+
function resolveFallbackPort(routes, fallbackEnabled) {
|
|
232
|
+
if (fallbackEnabled) return "fallback";
|
|
233
|
+
return routes[0]?.port ?? "out";
|
|
234
|
+
}
|
|
235
|
+
var llmBranchExecutor = async (ctx) => {
|
|
236
|
+
const config = ctx.node.data?.config ?? {};
|
|
237
|
+
const routes = declaredRoutes(config);
|
|
238
|
+
if (routes.length === 0) {
|
|
239
|
+
ctx.abort("llm_branch has no routes configured");
|
|
240
|
+
}
|
|
241
|
+
const client = getLlmClient();
|
|
242
|
+
{
|
|
243
|
+
ctx.abort(
|
|
244
|
+
"No LLM client registered. Call registerLlmClient() with your provider adapter \u2014 fancy-flow ships the routing, not the model call."
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const fallbackEnabled = config.fallback !== false;
|
|
248
|
+
const choice = await client.chooseRoute({
|
|
249
|
+
system: typeof config.system === "string" ? config.system : void 0,
|
|
250
|
+
prompt: String(config.prompt ?? ctx.inputs ?? ""),
|
|
251
|
+
routes,
|
|
252
|
+
provider: typeof config.provider === "string" ? config.provider : void 0,
|
|
253
|
+
model: typeof config.model === "string" ? config.model : void 0,
|
|
254
|
+
credential: typeof config.credential === "string" ? config.credential : void 0
|
|
255
|
+
});
|
|
256
|
+
const offered = new Set(routes.map((r) => r.port));
|
|
257
|
+
let port = choice?.port ?? "";
|
|
258
|
+
let reason = choice?.reason;
|
|
259
|
+
if (!offered.has(port)) {
|
|
260
|
+
const safe = resolveFallbackPort(routes, fallbackEnabled);
|
|
261
|
+
ctx.emit({
|
|
262
|
+
type: "log",
|
|
263
|
+
nodeId: ctx.node.id,
|
|
264
|
+
level: "warn",
|
|
265
|
+
message: `llm_branch: model returned "${port || "(nothing)"}", which is not a declared route. Routing to "${safe}".`
|
|
266
|
+
});
|
|
267
|
+
reason = reason ?? `unrecognised route "${port}"`;
|
|
268
|
+
port = safe;
|
|
269
|
+
}
|
|
270
|
+
return { __port: port, value: { route: port, reason, input: ctx.inputs } };
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// src/registry/ports.ts
|
|
274
|
+
function resolvePortSpec(spec, config) {
|
|
275
|
+
if (spec === void 0) return void 0;
|
|
276
|
+
if (typeof spec !== "function") return spec;
|
|
277
|
+
try {
|
|
278
|
+
const resolved = spec(config);
|
|
279
|
+
return Array.isArray(resolved) ? resolved : void 0;
|
|
280
|
+
} catch {
|
|
281
|
+
return void 0;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function nodeConfig(node) {
|
|
285
|
+
return node.data?.config ?? {};
|
|
286
|
+
}
|
|
287
|
+
function resolveNodePorts(node, kind) {
|
|
288
|
+
const config = nodeConfig(node);
|
|
289
|
+
const data = node.data;
|
|
290
|
+
return {
|
|
291
|
+
inputs: data?.inputs ?? resolvePortSpec(kind?.inputs, config),
|
|
292
|
+
outputs: data?.outputs ?? resolvePortSpec(kind?.outputs, config)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/runtime/run-flow.ts
|
|
297
|
+
async function runFlow(graph, executors, onEvent = () => {
|
|
298
|
+
}, options = {}) {
|
|
299
|
+
const { signal, initialInputs = {}, timeoutMs, depth = 0 } = options;
|
|
300
|
+
const outputs = {};
|
|
301
|
+
const portValues = /* @__PURE__ */ new Map();
|
|
302
|
+
const completed = /* @__PURE__ */ new Set();
|
|
303
|
+
const errors = [];
|
|
304
|
+
const order = topoSort(graph);
|
|
305
|
+
if (order === null) {
|
|
306
|
+
const msg = "Cycle detected in flow graph \u2014 aborting.";
|
|
307
|
+
onEvent({ type: "run-error", error: msg });
|
|
308
|
+
return { ok: false, outputs, error: msg };
|
|
309
|
+
}
|
|
310
|
+
const incomingByNode = indexIncoming(graph.edges);
|
|
311
|
+
const timer2 = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;
|
|
312
|
+
onEvent({ type: "run-start" });
|
|
313
|
+
try {
|
|
314
|
+
for (const node of order) {
|
|
315
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
316
|
+
if (errors.length) break;
|
|
317
|
+
const incoming = incomingByNode.get(node.id) ?? [];
|
|
318
|
+
if (incoming.length > 0) {
|
|
319
|
+
const anyActive = incoming.some((e) => portValues.has(`${e.source}:${e.sourceHandle ?? "out"}`));
|
|
320
|
+
if (!anyActive) {
|
|
321
|
+
onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "skipped" });
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (node.type === "note") {
|
|
326
|
+
onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "annotation" });
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
onEvent({ type: "node-status", nodeId: node.id, status: "running" });
|
|
330
|
+
const inputs = collectInputs(node, incoming, portValues, initialInputs);
|
|
331
|
+
const exec = pickExecutor(executors, node);
|
|
332
|
+
if (!exec) {
|
|
333
|
+
const msg = `No executor registered for kind=${node.type}`;
|
|
334
|
+
errors.push(msg);
|
|
335
|
+
onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
|
|
336
|
+
onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
const result = await Promise.resolve(
|
|
341
|
+
exec({
|
|
342
|
+
node,
|
|
343
|
+
inputs,
|
|
344
|
+
abort: (reason) => {
|
|
345
|
+
throw new Error(reason ?? "aborted");
|
|
346
|
+
},
|
|
347
|
+
emit: onEvent,
|
|
348
|
+
depth
|
|
349
|
+
})
|
|
350
|
+
);
|
|
351
|
+
outputs[node.id] = result;
|
|
352
|
+
const activated = activatedPorts(node, result);
|
|
353
|
+
for (const portId of activated.ports) {
|
|
354
|
+
portValues.set(`${node.id}:${portId}`, activated.value);
|
|
355
|
+
onEvent({ type: "node-output", nodeId: node.id, portId, value: activated.value });
|
|
356
|
+
}
|
|
357
|
+
completed.add(node.id);
|
|
358
|
+
onEvent({ type: "node-status", nodeId: node.id, status: "done" });
|
|
359
|
+
} catch (e) {
|
|
360
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
361
|
+
errors.push(msg);
|
|
362
|
+
onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
|
|
363
|
+
onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
} finally {
|
|
368
|
+
if (timer2) clearTimeout(timer2);
|
|
369
|
+
}
|
|
370
|
+
const ok = errors.length === 0;
|
|
371
|
+
onEvent({ type: "run-end", ok });
|
|
372
|
+
return ok ? { ok, outputs } : { ok, outputs, error: errors[0] };
|
|
373
|
+
}
|
|
374
|
+
function indexIncoming(edges) {
|
|
375
|
+
const map = /* @__PURE__ */ new Map();
|
|
376
|
+
for (const e of edges) {
|
|
377
|
+
const list = map.get(e.target) ?? [];
|
|
378
|
+
list.push(e);
|
|
379
|
+
map.set(e.target, list);
|
|
380
|
+
}
|
|
381
|
+
return map;
|
|
382
|
+
}
|
|
383
|
+
function topoSort(graph) {
|
|
384
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
385
|
+
for (const n of graph.nodes) inDegree.set(n.id, 0);
|
|
386
|
+
for (const e of graph.edges) inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);
|
|
387
|
+
const queue = [];
|
|
388
|
+
for (const [id2, d] of inDegree) if (d === 0) queue.push(id2);
|
|
389
|
+
const ordered = [];
|
|
390
|
+
while (queue.length) {
|
|
391
|
+
const id2 = queue.shift();
|
|
392
|
+
ordered.push(id2);
|
|
393
|
+
for (const e of graph.edges) {
|
|
394
|
+
if (e.source !== id2) continue;
|
|
395
|
+
const next = (inDegree.get(e.target) ?? 0) - 1;
|
|
396
|
+
inDegree.set(e.target, next);
|
|
397
|
+
if (next === 0) queue.push(e.target);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (ordered.length !== graph.nodes.length) return null;
|
|
401
|
+
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
402
|
+
return ordered.map((id2) => byId.get(id2)).filter(Boolean);
|
|
403
|
+
}
|
|
404
|
+
function collectInputs(node, incoming, portValues, initial) {
|
|
405
|
+
const inputs = { ...initial[node.id] ?? {} };
|
|
406
|
+
for (const e of incoming) {
|
|
407
|
+
const portId = e.targetHandle ?? "in";
|
|
408
|
+
const val = portValues.get(`${e.source}:${e.sourceHandle ?? "out"}`);
|
|
409
|
+
inputs[portId] = val;
|
|
410
|
+
}
|
|
411
|
+
return inputs;
|
|
412
|
+
}
|
|
413
|
+
function pickExecutor(executors, node) {
|
|
414
|
+
if (executors[node.id]) return executors[node.id];
|
|
415
|
+
if (node.type && executors[node.type]) return executors[node.type];
|
|
416
|
+
const kind = node.type ? getNodeKind(node.type) : null;
|
|
417
|
+
if (kind) {
|
|
418
|
+
for (const id2 of kindIds(kind)) {
|
|
419
|
+
if (executors[id2]) return executors[id2];
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return executors["*"];
|
|
423
|
+
}
|
|
424
|
+
function activatedPorts(node, result) {
|
|
425
|
+
if (result && typeof result === "object") {
|
|
426
|
+
const r = result;
|
|
427
|
+
if (typeof r.__port === "string") {
|
|
428
|
+
return { ports: [r.__port], value: r.value };
|
|
429
|
+
}
|
|
430
|
+
if (typeof r.branch === "string") {
|
|
431
|
+
return { ports: [r.branch], value: r.value ?? r };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const kind = getNodeKind(node.data?.kind ?? node.type ?? "") ?? void 0;
|
|
435
|
+
const declared = resolveNodePorts(node, kind).outputs?.map((p) => p.id);
|
|
436
|
+
return { ports: declared?.length ? declared : ["out"], value: result };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/registry/subflow.ts
|
|
440
|
+
var DEFAULT_MAX_DEPTH = 8;
|
|
441
|
+
function subflowMode(config) {
|
|
442
|
+
const mode = config.mode;
|
|
443
|
+
return mode === "stream" || mode === "both" ? mode : "output";
|
|
444
|
+
}
|
|
445
|
+
function subflowPorts(config) {
|
|
446
|
+
const mode = subflowMode(config);
|
|
447
|
+
const ports = [{ id: "out", label: "result" }];
|
|
448
|
+
if (mode === "stream" || mode === "both") ports.unshift({ id: "stream", label: "stream" });
|
|
449
|
+
return ports;
|
|
450
|
+
}
|
|
451
|
+
var subflowExecutor = async (ctx) => {
|
|
452
|
+
const config = ctx.node.data?.config ?? {};
|
|
453
|
+
const ref = String(config.workflow ?? "").trim();
|
|
454
|
+
if (!ref) ctx.abort("subflow has no workflow reference configured");
|
|
455
|
+
const resolver = getWorkflowResolver();
|
|
456
|
+
{
|
|
457
|
+
ctx.abort(
|
|
458
|
+
"No workflow resolver registered. Call registerWorkflowResolver() so subflow can find the workflow it references."
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
const maxDepth = Number.isFinite(config.maxDepth) ? Number(config.maxDepth) : DEFAULT_MAX_DEPTH;
|
|
462
|
+
const depth = ctx.depth ?? 0;
|
|
463
|
+
if (depth + 1 > maxDepth) {
|
|
464
|
+
ctx.abort(
|
|
465
|
+
`subflow depth limit reached (${maxDepth}) at "${ref}" \u2014 a workflow is referencing itself, directly or through a chain.`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
const child = await resolver(ref);
|
|
469
|
+
if (!child) ctx.abort(`subflow could not resolve workflow "${ref}"`);
|
|
470
|
+
const mode = subflowMode(config);
|
|
471
|
+
const streaming = mode === "stream" || mode === "both";
|
|
472
|
+
const forward = (event) => {
|
|
473
|
+
if (!streaming) return;
|
|
474
|
+
const detail = event.type === "node-status" ? `${event.nodeId} ${event.status}` : event.type === "run-end" ? `finished (${event.ok ? "ok" : "failed"})` : event.type;
|
|
475
|
+
ctx.emit({
|
|
476
|
+
type: "log",
|
|
477
|
+
nodeId: ctx.node.id,
|
|
478
|
+
level: "info",
|
|
479
|
+
message: `[${ref}] ${detail}`
|
|
480
|
+
});
|
|
481
|
+
};
|
|
482
|
+
const result = await runFlow(
|
|
483
|
+
child,
|
|
484
|
+
config.executors ?? {},
|
|
485
|
+
forward,
|
|
486
|
+
{
|
|
487
|
+
initialInputs: config.inputs ?? {
|
|
488
|
+
// With no explicit mapping, hand the parent's inputs to the child's
|
|
489
|
+
// entry points — the obvious default, and it makes the simple case
|
|
490
|
+
// require no configuration at all.
|
|
491
|
+
__parent: ctx.inputs
|
|
492
|
+
},
|
|
493
|
+
depth: depth + 1
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
if (!result.ok) {
|
|
497
|
+
ctx.abort(`subflow "${ref}" failed: ${result.error ?? "unknown error"}`);
|
|
498
|
+
}
|
|
499
|
+
if (mode === "stream") {
|
|
500
|
+
return { __port: "stream", value: result.outputs };
|
|
501
|
+
}
|
|
502
|
+
if (mode === "both") {
|
|
503
|
+
return result.outputs;
|
|
504
|
+
}
|
|
505
|
+
return { __port: "out", value: result.outputs };
|
|
506
|
+
};
|
|
507
|
+
|
|
215
508
|
// src/registry/builtin.ts
|
|
216
509
|
function casePorts(cases) {
|
|
217
510
|
const byPort = /* @__PURE__ */ new Map();
|
|
@@ -258,8 +551,8 @@ var HTTP_METHODS = [
|
|
|
258
551
|
var KINDS = [
|
|
259
552
|
// ───────────── Triggers ─────────────
|
|
260
553
|
{
|
|
261
|
-
name: "@
|
|
262
|
-
aliases: ["manual_trigger"],
|
|
554
|
+
name: "@particle-academy/manual_trigger",
|
|
555
|
+
aliases: ["manual_trigger", "@fancy/manual_trigger"],
|
|
263
556
|
category: "trigger",
|
|
264
557
|
label: "Manual",
|
|
265
558
|
description: "Entry point fired when the user clicks Run.",
|
|
@@ -268,8 +561,8 @@ var KINDS = [
|
|
|
268
561
|
outputs: [{ id: "out" }]
|
|
269
562
|
},
|
|
270
563
|
{
|
|
271
|
-
name: "@
|
|
272
|
-
aliases: ["webhook_trigger"],
|
|
564
|
+
name: "@particle-academy/webhook_trigger",
|
|
565
|
+
aliases: ["webhook_trigger", "@fancy/webhook_trigger"],
|
|
273
566
|
category: "trigger",
|
|
274
567
|
label: "Webhook",
|
|
275
568
|
description: "Triggered by an inbound HTTP request to a host-provided URL.",
|
|
@@ -286,8 +579,8 @@ var KINDS = [
|
|
|
286
579
|
]
|
|
287
580
|
},
|
|
288
581
|
{
|
|
289
|
-
name: "@
|
|
290
|
-
aliases: ["schedule_trigger"],
|
|
582
|
+
name: "@particle-academy/schedule_trigger",
|
|
583
|
+
aliases: ["schedule_trigger", "@fancy/schedule_trigger"],
|
|
291
584
|
category: "trigger",
|
|
292
585
|
label: "Schedule",
|
|
293
586
|
description: "Fires on a cron schedule (host-implemented).",
|
|
@@ -307,8 +600,8 @@ var KINDS = [
|
|
|
307
600
|
]
|
|
308
601
|
},
|
|
309
602
|
{
|
|
310
|
-
name: "@
|
|
311
|
-
aliases: ["user_input"],
|
|
603
|
+
name: "@particle-academy/user_input",
|
|
604
|
+
aliases: ["user_input", "@fancy/user_input"],
|
|
312
605
|
category: "human",
|
|
313
606
|
label: "User Input",
|
|
314
607
|
description: "Pause the flow until the user submits the configured form.",
|
|
@@ -348,8 +641,8 @@ var KINDS = [
|
|
|
348
641
|
]
|
|
349
642
|
},
|
|
350
643
|
{
|
|
351
|
-
name: "@
|
|
352
|
-
aliases: ["rich_user_input"],
|
|
644
|
+
name: "@particle-academy/rich_user_input",
|
|
645
|
+
aliases: ["rich_user_input", "@fancy/rich_user_input"],
|
|
353
646
|
category: "human",
|
|
354
647
|
label: "Rich User Input",
|
|
355
648
|
description: "Pause the flow on a fully authored page \u2014 content, required reading, multi-section forms.",
|
|
@@ -372,10 +665,61 @@ var KINDS = [
|
|
|
372
665
|
// what the person hitting this step will actually see.
|
|
373
666
|
renderBody: (ctx) => ReactExports.createElement(RichInputPreview, { config: ctx.config ?? {} })
|
|
374
667
|
},
|
|
668
|
+
{
|
|
669
|
+
name: "@particle-academy/subflow",
|
|
670
|
+
aliases: ["subflow", "@fancy/subflow"],
|
|
671
|
+
category: "logic",
|
|
672
|
+
label: "SubFlow",
|
|
673
|
+
description: "Run another workflow and bring its result \u2014 or its live progress \u2014 back into this one.",
|
|
674
|
+
icon: "\u29C9",
|
|
675
|
+
inputs: [{ id: "in" }],
|
|
676
|
+
// The stream port only exists when something actually streams.
|
|
677
|
+
outputs: (config) => subflowPorts(config ?? {}),
|
|
678
|
+
// Core, not marketplace: it runs a child graph through this same engine and
|
|
679
|
+
// needs nothing from outside except where workflows live.
|
|
680
|
+
executor: subflowExecutor,
|
|
681
|
+
configSchema: [
|
|
682
|
+
{
|
|
683
|
+
type: "text",
|
|
684
|
+
key: "workflow",
|
|
685
|
+
label: "Workflow",
|
|
686
|
+
required: true,
|
|
687
|
+
placeholder: "onboarding-v2",
|
|
688
|
+
description: "Reference resolved by the host's registerWorkflowResolver()."
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
type: "select",
|
|
692
|
+
key: "mode",
|
|
693
|
+
label: "Return",
|
|
694
|
+
default: "output",
|
|
695
|
+
options: [
|
|
696
|
+
{ value: "output", label: "Output when it finishes" },
|
|
697
|
+
{ value: "stream", label: "Stream progress as it runs" },
|
|
698
|
+
{ value: "both", label: "Both \u2014 stream, then output" }
|
|
699
|
+
],
|
|
700
|
+
description: "Streaming adds a second port so a parent can show progress instead of a spinner."
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
type: "json",
|
|
704
|
+
key: "inputs",
|
|
705
|
+
label: "Input mapping",
|
|
706
|
+
description: "Entry-point inputs for the child run. Omit to pass this node's inputs straight through."
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
type: "number",
|
|
710
|
+
key: "maxDepth",
|
|
711
|
+
label: "Max nesting depth",
|
|
712
|
+
default: DEFAULT_MAX_DEPTH,
|
|
713
|
+
min: 1,
|
|
714
|
+
max: 32,
|
|
715
|
+
description: "Guards against a workflow referencing itself."
|
|
716
|
+
}
|
|
717
|
+
]
|
|
718
|
+
},
|
|
375
719
|
// ───────────── Logic ─────────────
|
|
376
720
|
{
|
|
377
|
-
name: "@
|
|
378
|
-
aliases: ["branch"],
|
|
721
|
+
name: "@particle-academy/branch",
|
|
722
|
+
aliases: ["branch", "@fancy/branch"],
|
|
379
723
|
category: "logic",
|
|
380
724
|
label: "Branch",
|
|
381
725
|
description: "Multi-way branch on a condition or value.",
|
|
@@ -387,8 +731,8 @@ var KINDS = [
|
|
|
387
731
|
]
|
|
388
732
|
},
|
|
389
733
|
{
|
|
390
|
-
name: "@
|
|
391
|
-
aliases: ["switch_case"],
|
|
734
|
+
name: "@particle-academy/switch_case",
|
|
735
|
+
aliases: ["switch_case", "@fancy/switch_case"],
|
|
392
736
|
category: "logic",
|
|
393
737
|
label: "Switch",
|
|
394
738
|
description: "Route to one of N labelled outputs based on a key.",
|
|
@@ -415,8 +759,8 @@ var KINDS = [
|
|
|
415
759
|
]
|
|
416
760
|
},
|
|
417
761
|
{
|
|
418
|
-
name: "@
|
|
419
|
-
aliases: ["for_each"],
|
|
762
|
+
name: "@particle-academy/for_each",
|
|
763
|
+
aliases: ["for_each", "@fancy/for_each"],
|
|
420
764
|
category: "logic",
|
|
421
765
|
label: "For Each",
|
|
422
766
|
description: "Iterate over a list, emitting each item on `item`.",
|
|
@@ -429,8 +773,8 @@ var KINDS = [
|
|
|
429
773
|
]
|
|
430
774
|
},
|
|
431
775
|
{
|
|
432
|
-
name: "@
|
|
433
|
-
aliases: ["merge"],
|
|
776
|
+
name: "@particle-academy/merge",
|
|
777
|
+
aliases: ["merge", "@fancy/merge"],
|
|
434
778
|
category: "logic",
|
|
435
779
|
label: "Merge",
|
|
436
780
|
description: "Combine multiple inputs into one object or array.",
|
|
@@ -448,8 +792,8 @@ var KINDS = [
|
|
|
448
792
|
]
|
|
449
793
|
},
|
|
450
794
|
{
|
|
451
|
-
name: "@
|
|
452
|
-
aliases: ["wait"],
|
|
795
|
+
name: "@particle-academy/wait",
|
|
796
|
+
aliases: ["wait", "@fancy/wait"],
|
|
453
797
|
category: "logic",
|
|
454
798
|
label: "Wait",
|
|
455
799
|
description: "Sleep or wait for an external event.",
|
|
@@ -466,8 +810,8 @@ var KINDS = [
|
|
|
466
810
|
]
|
|
467
811
|
},
|
|
468
812
|
{
|
|
469
|
-
name: "@
|
|
470
|
-
aliases: ["transform"],
|
|
813
|
+
name: "@particle-academy/transform",
|
|
814
|
+
aliases: ["transform", "@fancy/transform"],
|
|
471
815
|
category: "logic",
|
|
472
816
|
label: "Transform",
|
|
473
817
|
description: "Reshape data with an expression.",
|
|
@@ -484,8 +828,8 @@ var KINDS = [
|
|
|
484
828
|
},
|
|
485
829
|
// ───────────── Data ─────────────
|
|
486
830
|
{
|
|
487
|
-
name: "@
|
|
488
|
-
aliases: ["memory_store"],
|
|
831
|
+
name: "@particle-academy/memory_store",
|
|
832
|
+
aliases: ["memory_store", "@fancy/memory_store"],
|
|
489
833
|
category: "data",
|
|
490
834
|
label: "Memory Store",
|
|
491
835
|
description: "Read or write per-conversation memory.",
|
|
@@ -505,8 +849,8 @@ var KINDS = [
|
|
|
505
849
|
]
|
|
506
850
|
},
|
|
507
851
|
{
|
|
508
|
-
name: "@
|
|
509
|
-
aliases: ["data_store"],
|
|
852
|
+
name: "@particle-academy/data_store",
|
|
853
|
+
aliases: ["data_store", "@fancy/data_store"],
|
|
510
854
|
category: "data",
|
|
511
855
|
label: "Data Store",
|
|
512
856
|
description: "Key-value or table read/write against a host store.",
|
|
@@ -534,8 +878,8 @@ var KINDS = [
|
|
|
534
878
|
]
|
|
535
879
|
},
|
|
536
880
|
{
|
|
537
|
-
name: "@
|
|
538
|
-
aliases: ["variable"],
|
|
881
|
+
name: "@particle-academy/variable",
|
|
882
|
+
aliases: ["variable", "@fancy/variable"],
|
|
539
883
|
category: "data",
|
|
540
884
|
label: "Variable",
|
|
541
885
|
description: "Workflow-scoped value used by other nodes.",
|
|
@@ -547,8 +891,8 @@ var KINDS = [
|
|
|
547
891
|
},
|
|
548
892
|
// ───────────── AI ─────────────
|
|
549
893
|
{
|
|
550
|
-
name: "@
|
|
551
|
-
aliases: ["llm_call"],
|
|
894
|
+
name: "@particle-academy/llm_call",
|
|
895
|
+
aliases: ["llm_call", "@fancy/llm_call"],
|
|
552
896
|
category: "ai",
|
|
553
897
|
label: "LLM Call",
|
|
554
898
|
description: "Send a prompt + context to a model and receive a response.",
|
|
@@ -575,8 +919,8 @@ var KINDS = [
|
|
|
575
919
|
]
|
|
576
920
|
},
|
|
577
921
|
{
|
|
578
|
-
name: "@
|
|
579
|
-
aliases: ["llm_branch"],
|
|
922
|
+
name: "@particle-academy/llm_branch",
|
|
923
|
+
aliases: ["llm_branch", "@fancy/llm_branch"],
|
|
580
924
|
category: "ai",
|
|
581
925
|
label: "LLM Router",
|
|
582
926
|
description: "Let a model choose which route the flow takes.",
|
|
@@ -585,6 +929,10 @@ var KINDS = [
|
|
|
585
929
|
// Each declared route is a port. The executor returns `{ __port: id }`
|
|
586
930
|
// (or `Port.only(id)` on the PHP runtime) to pick one.
|
|
587
931
|
outputs: (config) => routePorts(config?.routes, config?.fallback),
|
|
932
|
+
// A shuttle, not an engine: it carries the routes out to whatever LLM
|
|
933
|
+
// client the host registered and carries the choice back. No provider SDK
|
|
934
|
+
// reaches core, so this stays a builtin without adding a dependency.
|
|
935
|
+
executor: llmBranchExecutor,
|
|
588
936
|
configSchema: [
|
|
589
937
|
{
|
|
590
938
|
type: "textarea",
|
|
@@ -646,8 +994,8 @@ var KINDS = [
|
|
|
646
994
|
]
|
|
647
995
|
},
|
|
648
996
|
{
|
|
649
|
-
name: "@
|
|
650
|
-
aliases: ["tool_use"],
|
|
997
|
+
name: "@particle-academy/tool_use",
|
|
998
|
+
aliases: ["tool_use", "@fancy/tool_use"],
|
|
651
999
|
category: "ai",
|
|
652
1000
|
label: "Tool Use",
|
|
653
1001
|
description: "Hand control to a host-registered tool by name.",
|
|
@@ -658,8 +1006,8 @@ var KINDS = [
|
|
|
658
1006
|
]
|
|
659
1007
|
},
|
|
660
1008
|
{
|
|
661
|
-
name: "@
|
|
662
|
-
aliases: ["embed_search"],
|
|
1009
|
+
name: "@particle-academy/embed_search",
|
|
1010
|
+
aliases: ["embed_search", "@fancy/embed_search"],
|
|
663
1011
|
category: "ai",
|
|
664
1012
|
label: "Embed & Search",
|
|
665
1013
|
description: "Embed a query and search a vector store.",
|
|
@@ -672,8 +1020,8 @@ var KINDS = [
|
|
|
672
1020
|
},
|
|
673
1021
|
// ───────────── IO ─────────────
|
|
674
1022
|
{
|
|
675
|
-
name: "@
|
|
676
|
-
aliases: ["api_request"],
|
|
1023
|
+
name: "@particle-academy/api_request",
|
|
1024
|
+
aliases: ["api_request", "@fancy/api_request"],
|
|
677
1025
|
category: "io",
|
|
678
1026
|
label: "API Request",
|
|
679
1027
|
description: "HTTP request to any URL.",
|
|
@@ -687,8 +1035,8 @@ var KINDS = [
|
|
|
687
1035
|
]
|
|
688
1036
|
},
|
|
689
1037
|
{
|
|
690
|
-
name: "@
|
|
691
|
-
aliases: ["webhook_out"],
|
|
1038
|
+
name: "@particle-academy/webhook_out",
|
|
1039
|
+
aliases: ["webhook_out", "@fancy/webhook_out"],
|
|
692
1040
|
category: "io",
|
|
693
1041
|
label: "Send Webhook",
|
|
694
1042
|
description: "POST a payload to a configured URL.",
|
|
@@ -701,8 +1049,8 @@ var KINDS = [
|
|
|
701
1049
|
},
|
|
702
1050
|
// ───────────── Human ─────────────
|
|
703
1051
|
{
|
|
704
|
-
name: "@
|
|
705
|
-
aliases: ["human_approval"],
|
|
1052
|
+
name: "@particle-academy/human_approval",
|
|
1053
|
+
aliases: ["human_approval", "@fancy/human_approval"],
|
|
706
1054
|
category: "human",
|
|
707
1055
|
label: "Human Approval",
|
|
708
1056
|
description: "Pause until a human approves or denies.",
|
|
@@ -716,8 +1064,8 @@ var KINDS = [
|
|
|
716
1064
|
]
|
|
717
1065
|
},
|
|
718
1066
|
{
|
|
719
|
-
name: "@
|
|
720
|
-
aliases: ["notify"],
|
|
1067
|
+
name: "@particle-academy/notify",
|
|
1068
|
+
aliases: ["notify", "@fancy/notify"],
|
|
721
1069
|
category: "human",
|
|
722
1070
|
label: "Notify",
|
|
723
1071
|
description: "Send a message via Slack / email / SMS / etc.",
|
|
@@ -741,8 +1089,8 @@ var KINDS = [
|
|
|
741
1089
|
},
|
|
742
1090
|
// ───────────── Output ─────────────
|
|
743
1091
|
{
|
|
744
|
-
name: "@
|
|
745
|
-
aliases: ["output"],
|
|
1092
|
+
name: "@particle-academy/output",
|
|
1093
|
+
aliases: ["output", "@fancy/output"],
|
|
746
1094
|
category: "output",
|
|
747
1095
|
label: "Output",
|
|
748
1096
|
description: "Terminal node \u2014 captures the workflow's result.",
|
|
@@ -751,8 +1099,8 @@ var KINDS = [
|
|
|
751
1099
|
outputs: []
|
|
752
1100
|
},
|
|
753
1101
|
{
|
|
754
|
-
name: "@
|
|
755
|
-
aliases: ["log"],
|
|
1102
|
+
name: "@particle-academy/log",
|
|
1103
|
+
aliases: ["log", "@fancy/log"],
|
|
756
1104
|
category: "output",
|
|
757
1105
|
label: "Log",
|
|
758
1106
|
description: "Send to the run feed.",
|
|
@@ -10435,173 +10783,6 @@ function useFlowState(initial) {
|
|
|
10435
10783
|
const toGraph = ReactExports.useCallback(() => ({ nodes, edges }), [nodes, edges]);
|
|
10436
10784
|
return { nodes, edges, setNodes, setEdges, onNodesChange, onEdgesChange, onConnect, toGraph };
|
|
10437
10785
|
}
|
|
10438
|
-
|
|
10439
|
-
// src/registry/ports.ts
|
|
10440
|
-
function resolvePortSpec(spec, config) {
|
|
10441
|
-
if (spec === void 0) return void 0;
|
|
10442
|
-
if (typeof spec !== "function") return spec;
|
|
10443
|
-
try {
|
|
10444
|
-
const resolved = spec(config);
|
|
10445
|
-
return Array.isArray(resolved) ? resolved : void 0;
|
|
10446
|
-
} catch {
|
|
10447
|
-
return void 0;
|
|
10448
|
-
}
|
|
10449
|
-
}
|
|
10450
|
-
function nodeConfig(node) {
|
|
10451
|
-
return node.data?.config ?? {};
|
|
10452
|
-
}
|
|
10453
|
-
function resolveNodePorts(node, kind) {
|
|
10454
|
-
const config = nodeConfig(node);
|
|
10455
|
-
const data = node.data;
|
|
10456
|
-
return {
|
|
10457
|
-
inputs: data?.inputs ?? resolvePortSpec(kind?.inputs, config),
|
|
10458
|
-
outputs: data?.outputs ?? resolvePortSpec(kind?.outputs, config)
|
|
10459
|
-
};
|
|
10460
|
-
}
|
|
10461
|
-
|
|
10462
|
-
// src/runtime/run-flow.ts
|
|
10463
|
-
async function runFlow(graph, executors, onEvent = () => {
|
|
10464
|
-
}, options = {}) {
|
|
10465
|
-
const { signal, initialInputs = {}, timeoutMs } = options;
|
|
10466
|
-
const outputs = {};
|
|
10467
|
-
const portValues = /* @__PURE__ */ new Map();
|
|
10468
|
-
const completed = /* @__PURE__ */ new Set();
|
|
10469
|
-
const errors = [];
|
|
10470
|
-
const order = topoSort(graph);
|
|
10471
|
-
if (order === null) {
|
|
10472
|
-
const msg = "Cycle detected in flow graph \u2014 aborting.";
|
|
10473
|
-
onEvent({ type: "run-error", error: msg });
|
|
10474
|
-
return { ok: false, outputs, error: msg };
|
|
10475
|
-
}
|
|
10476
|
-
const incomingByNode = indexIncoming(graph.edges);
|
|
10477
|
-
const timer2 = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;
|
|
10478
|
-
onEvent({ type: "run-start" });
|
|
10479
|
-
try {
|
|
10480
|
-
for (const node of order) {
|
|
10481
|
-
if (signal?.aborted) throw new Error("aborted");
|
|
10482
|
-
if (errors.length) break;
|
|
10483
|
-
const incoming = incomingByNode.get(node.id) ?? [];
|
|
10484
|
-
if (incoming.length > 0) {
|
|
10485
|
-
const anyActive = incoming.some((e) => portValues.has(`${e.source}:${e.sourceHandle ?? "out"}`));
|
|
10486
|
-
if (!anyActive) {
|
|
10487
|
-
onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "skipped" });
|
|
10488
|
-
continue;
|
|
10489
|
-
}
|
|
10490
|
-
}
|
|
10491
|
-
if (node.type === "note") {
|
|
10492
|
-
onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "annotation" });
|
|
10493
|
-
continue;
|
|
10494
|
-
}
|
|
10495
|
-
onEvent({ type: "node-status", nodeId: node.id, status: "running" });
|
|
10496
|
-
const inputs = collectInputs(node, incoming, portValues, initialInputs);
|
|
10497
|
-
const exec = pickExecutor(executors, node);
|
|
10498
|
-
if (!exec) {
|
|
10499
|
-
const msg = `No executor registered for kind=${node.type}`;
|
|
10500
|
-
errors.push(msg);
|
|
10501
|
-
onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
|
|
10502
|
-
onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
|
|
10503
|
-
break;
|
|
10504
|
-
}
|
|
10505
|
-
try {
|
|
10506
|
-
const result = await Promise.resolve(
|
|
10507
|
-
exec({
|
|
10508
|
-
node,
|
|
10509
|
-
inputs,
|
|
10510
|
-
abort: (reason) => {
|
|
10511
|
-
throw new Error(reason ?? "aborted");
|
|
10512
|
-
},
|
|
10513
|
-
emit: onEvent
|
|
10514
|
-
})
|
|
10515
|
-
);
|
|
10516
|
-
outputs[node.id] = result;
|
|
10517
|
-
const activated = activatedPorts(node, result);
|
|
10518
|
-
for (const portId of activated.ports) {
|
|
10519
|
-
portValues.set(`${node.id}:${portId}`, activated.value);
|
|
10520
|
-
onEvent({ type: "node-output", nodeId: node.id, portId, value: activated.value });
|
|
10521
|
-
}
|
|
10522
|
-
completed.add(node.id);
|
|
10523
|
-
onEvent({ type: "node-status", nodeId: node.id, status: "done" });
|
|
10524
|
-
} catch (e) {
|
|
10525
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
10526
|
-
errors.push(msg);
|
|
10527
|
-
onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
|
|
10528
|
-
onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
|
|
10529
|
-
break;
|
|
10530
|
-
}
|
|
10531
|
-
}
|
|
10532
|
-
} finally {
|
|
10533
|
-
if (timer2) clearTimeout(timer2);
|
|
10534
|
-
}
|
|
10535
|
-
const ok = errors.length === 0;
|
|
10536
|
-
onEvent({ type: "run-end", ok });
|
|
10537
|
-
return ok ? { ok, outputs } : { ok, outputs, error: errors[0] };
|
|
10538
|
-
}
|
|
10539
|
-
function indexIncoming(edges) {
|
|
10540
|
-
const map = /* @__PURE__ */ new Map();
|
|
10541
|
-
for (const e of edges) {
|
|
10542
|
-
const list = map.get(e.target) ?? [];
|
|
10543
|
-
list.push(e);
|
|
10544
|
-
map.set(e.target, list);
|
|
10545
|
-
}
|
|
10546
|
-
return map;
|
|
10547
|
-
}
|
|
10548
|
-
function topoSort(graph) {
|
|
10549
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
10550
|
-
for (const n of graph.nodes) inDegree.set(n.id, 0);
|
|
10551
|
-
for (const e of graph.edges) inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);
|
|
10552
|
-
const queue = [];
|
|
10553
|
-
for (const [id2, d] of inDegree) if (d === 0) queue.push(id2);
|
|
10554
|
-
const ordered = [];
|
|
10555
|
-
while (queue.length) {
|
|
10556
|
-
const id2 = queue.shift();
|
|
10557
|
-
ordered.push(id2);
|
|
10558
|
-
for (const e of graph.edges) {
|
|
10559
|
-
if (e.source !== id2) continue;
|
|
10560
|
-
const next = (inDegree.get(e.target) ?? 0) - 1;
|
|
10561
|
-
inDegree.set(e.target, next);
|
|
10562
|
-
if (next === 0) queue.push(e.target);
|
|
10563
|
-
}
|
|
10564
|
-
}
|
|
10565
|
-
if (ordered.length !== graph.nodes.length) return null;
|
|
10566
|
-
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
10567
|
-
return ordered.map((id2) => byId.get(id2)).filter(Boolean);
|
|
10568
|
-
}
|
|
10569
|
-
function collectInputs(node, incoming, portValues, initial) {
|
|
10570
|
-
const inputs = { ...initial[node.id] ?? {} };
|
|
10571
|
-
for (const e of incoming) {
|
|
10572
|
-
const portId = e.targetHandle ?? "in";
|
|
10573
|
-
const val = portValues.get(`${e.source}:${e.sourceHandle ?? "out"}`);
|
|
10574
|
-
inputs[portId] = val;
|
|
10575
|
-
}
|
|
10576
|
-
return inputs;
|
|
10577
|
-
}
|
|
10578
|
-
function pickExecutor(executors, node) {
|
|
10579
|
-
if (executors[node.id]) return executors[node.id];
|
|
10580
|
-
if (node.type && executors[node.type]) return executors[node.type];
|
|
10581
|
-
const kind = node.type ? getNodeKind(node.type) : null;
|
|
10582
|
-
if (kind) {
|
|
10583
|
-
for (const id2 of kindIds(kind)) {
|
|
10584
|
-
if (executors[id2]) return executors[id2];
|
|
10585
|
-
}
|
|
10586
|
-
}
|
|
10587
|
-
return executors["*"];
|
|
10588
|
-
}
|
|
10589
|
-
function activatedPorts(node, result) {
|
|
10590
|
-
if (result && typeof result === "object") {
|
|
10591
|
-
const r = result;
|
|
10592
|
-
if (typeof r.__port === "string") {
|
|
10593
|
-
return { ports: [r.__port], value: r.value };
|
|
10594
|
-
}
|
|
10595
|
-
if (typeof r.branch === "string") {
|
|
10596
|
-
return { ports: [r.branch], value: r.value ?? r };
|
|
10597
|
-
}
|
|
10598
|
-
}
|
|
10599
|
-
const kind = getNodeKind(node.data?.kind ?? node.type ?? "") ?? void 0;
|
|
10600
|
-
const declared = resolveNodePorts(node, kind).outputs?.map((p) => p.id);
|
|
10601
|
-
return { ports: declared?.length ? declared : ["out"], value: result };
|
|
10602
|
-
}
|
|
10603
|
-
|
|
10604
|
-
// src/runtime/use-flow-run.ts
|
|
10605
10786
|
function useFlowRun({ maxFeed = 200 } = {}) {
|
|
10606
10787
|
const [statuses, setStatuses] = ReactExports.useState({});
|
|
10607
10788
|
const [statusText, setStatusText] = ReactExports.useState({});
|