@ema.co/mcp-toolkit 1.5.2 → 1.7.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.

Potentially problematic release.


This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.

@@ -0,0 +1,648 @@
1
+ /**
2
+ * Workflow Flow Tracer
3
+ *
4
+ * Simulates and visualizes how data flows through a workflow,
5
+ * helping debug and understand execution paths.
6
+ */
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+ // Latency Estimates (based on typical Ema platform metrics)
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ const LATENCY_ESTIMATES = {
11
+ // Trigger - instant
12
+ "chat_trigger": 10,
13
+ "voice_trigger": 10,
14
+ // Categorizers - LLM call
15
+ "chat_categorizer": 800,
16
+ "text_categorizer": 600,
17
+ // Search - depends on index size
18
+ "search": 500,
19
+ "live_web_search": 2000,
20
+ "knowledge_search": 500,
21
+ // LLM calls
22
+ "call_llm": 1500,
23
+ "respond_with_sources": 1200,
24
+ "conversation_to_search_query": 600,
25
+ // Entity extraction
26
+ "entity_extraction": 800,
27
+ "entity_extraction_with_documents": 1000,
28
+ // HITL - user dependent, estimate 30s average
29
+ "general_hitl": 30000,
30
+ "human_collaboration": 30000,
31
+ // External actions
32
+ "send_email_agent": 1000,
33
+ "external_action_caller": 2000,
34
+ // Quick operations
35
+ "json_mapper": 50,
36
+ "fixed_response": 50,
37
+ "custom_agent": 500,
38
+ // Default
39
+ "default": 500,
40
+ };
41
+ function estimateLatency(actionType) {
42
+ const normalized = actionType.toLowerCase();
43
+ for (const [pattern, latency] of Object.entries(LATENCY_ESTIMATES)) {
44
+ if (normalized.includes(pattern)) {
45
+ return latency;
46
+ }
47
+ }
48
+ return LATENCY_ESTIMATES.default;
49
+ }
50
+ // ─────────────────────────────────────────────────────────────────────────────
51
+ // Type Detection
52
+ // ─────────────────────────────────────────────────────────────────────────────
53
+ const TYPE_NAMES = {
54
+ "CHAT_CONVERSATION": "Chat History",
55
+ "TEXT_WITH_SOURCES": "Text + Sources",
56
+ "WELL_KNOWN_TYPE_ANY": "Any",
57
+ "DOCUMENT": "Document",
58
+ "FILE_PATH": "File Path",
59
+ "EMAIL": "Email",
60
+ "JSON": "JSON Object",
61
+ };
62
+ function friendlyTypeName(type) {
63
+ return TYPE_NAMES[type] || type.replace(/WELL_KNOWN_TYPE_/g, "").replace(/_/g, " ");
64
+ }
65
+ // ─────────────────────────────────────────────────────────────────────────────
66
+ // Sample Values (for simulation)
67
+ // ─────────────────────────────────────────────────────────────────────────────
68
+ const SAMPLE_VALUES = {
69
+ "chat_conversation": '{"messages": [{"role": "user", "content": "What is my account balance?"}]}',
70
+ "user_query": "What is my account balance?",
71
+ "summarized_conversation": "User is asking about their account balance",
72
+ "search_results": '[{"title": "Account FAQ", "content": "...", "score": 0.95}]',
73
+ "response_with_sources": "Your account balance is $1,234.56 [Source: Account System]",
74
+ "email_address": "customer@example.com",
75
+ "email_body": "Dear Customer, Your account balance is $1,234.56...",
76
+ "hitl_status": "HITL Success",
77
+ "category": "Account Inquiry",
78
+ };
79
+ function getSampleValue(outputName) {
80
+ const normalized = outputName.toLowerCase().replace(/_/g, "");
81
+ for (const [pattern, value] of Object.entries(SAMPLE_VALUES)) {
82
+ if (normalized.includes(pattern.toLowerCase().replace(/_/g, ""))) {
83
+ return value.length > 50 ? value.slice(0, 47) + "..." : value;
84
+ }
85
+ }
86
+ return `<${outputName}>`;
87
+ }
88
+ /**
89
+ * Trace execution flow through a workflow
90
+ */
91
+ export function traceWorkflow(workflowDef) {
92
+ const workflow = workflowDef;
93
+ const actions = workflow.actions || [];
94
+ // Build dependency graph
95
+ const graph = buildDependencyGraph(actions);
96
+ // Find all paths from trigger to outputs
97
+ const paths = findAllPaths(actions, graph, workflow.results || {});
98
+ // Calculate stats
99
+ const stats = calculateStats(paths, actions);
100
+ // Generate visualizations
101
+ const ascii = generateAsciiFlow(paths);
102
+ const mermaid = generateMermaidDiagram(actions, graph);
103
+ // Detect trigger type
104
+ const trigger = actions.find(a => a.action?.name?.name?.includes("trigger") || a.name?.includes("trigger"));
105
+ const triggerType = trigger?.action?.name?.name?.includes("voice") ? "voice" : "chat";
106
+ return {
107
+ workflowName: "Workflow",
108
+ triggerType,
109
+ paths,
110
+ stats,
111
+ ascii,
112
+ mermaid,
113
+ };
114
+ }
115
+ function buildDependencyGraph(actions) {
116
+ const dependencies = new Map();
117
+ const dependents = new Map();
118
+ const outputs = new Map();
119
+ const conditions = new Map();
120
+ for (const action of actions) {
121
+ const nodeName = action.name;
122
+ dependencies.set(nodeName, new Set());
123
+ if (!dependents.has(nodeName)) {
124
+ dependents.set(nodeName, new Set());
125
+ }
126
+ // Parse inputs to find dependencies
127
+ if (action.inputs) {
128
+ for (const [inputName, binding] of Object.entries(action.inputs)) {
129
+ const bindingObj = binding;
130
+ const actionOutput = bindingObj?.actionOutput;
131
+ if (actionOutput?.actionName) {
132
+ dependencies.get(nodeName).add(actionOutput.actionName);
133
+ if (!dependents.has(actionOutput.actionName)) {
134
+ dependents.set(actionOutput.actionName, new Set());
135
+ }
136
+ dependents.get(actionOutput.actionName).add(nodeName);
137
+ }
138
+ }
139
+ }
140
+ // Parse runIf conditions
141
+ if (action.runIf) {
142
+ if (action.runIf.enum) {
143
+ conditions.set(nodeName, `${action.runIf.enum.enumType}: ${action.runIf.enum.enumValue}`);
144
+ }
145
+ else if (action.runIf.actionOutput) {
146
+ conditions.set(nodeName, `${action.runIf.actionOutput.actionName}.${action.runIf.actionOutput.output}`);
147
+ dependencies.get(nodeName).add(action.runIf.actionOutput.actionName);
148
+ }
149
+ }
150
+ // Determine outputs
151
+ const actionType = action.action?.name?.name || action.name;
152
+ const nodeOutputs = inferOutputs(actionType, action);
153
+ outputs.set(nodeName, nodeOutputs);
154
+ }
155
+ return { dependencies, dependents, outputs, conditions };
156
+ }
157
+ function inferOutputs(actionType, action) {
158
+ const type = actionType.toLowerCase();
159
+ if (type.includes("categorizer")) {
160
+ // Categorizers output a category enum
161
+ const categories = action.typeArguments?.categories;
162
+ if (Array.isArray(categories)) {
163
+ return categories.map(c => c.name);
164
+ }
165
+ // Categories might be in a different format
166
+ return ["category"];
167
+ }
168
+ if (type.includes("search")) {
169
+ return ["search_results", "combined_results"];
170
+ }
171
+ if (type.includes("respond_with_sources") || type.includes("call_llm")) {
172
+ return ["response_with_sources", "generated_content"];
173
+ }
174
+ if (type.includes("entity_extraction")) {
175
+ return ["email_address", "phone_number", "extracted_fields"];
176
+ }
177
+ if (type.includes("hitl") || type.includes("human_collaboration")) {
178
+ return ["hitl_status", "human_input"];
179
+ }
180
+ if (type.includes("conversation_to_search") || type.includes("summarizer")) {
181
+ return ["summarized_conversation", "search_query"];
182
+ }
183
+ if (type.includes("trigger")) {
184
+ return ["chat_conversation", "user_query"];
185
+ }
186
+ return ["output"];
187
+ }
188
+ function findAllPaths(actions, graph, results) {
189
+ const paths = [];
190
+ // Find trigger node
191
+ const trigger = actions.find(a => a.action?.name?.name?.includes("trigger") || a.name?.includes("trigger"));
192
+ if (!trigger) {
193
+ return [{
194
+ name: "No Trigger",
195
+ steps: [],
196
+ totalLatencyMs: 0,
197
+ llmCalls: 0,
198
+ externalCalls: 0,
199
+ hasHitl: false,
200
+ sendsEmail: false,
201
+ }];
202
+ }
203
+ // Find categorizers (branching points)
204
+ const categorizers = actions.filter(a => a.action?.name?.name?.includes("categorizer") || a.name?.includes("categorizer"));
205
+ if (categorizers.length === 0) {
206
+ // Linear workflow - single path
207
+ const steps = buildLinearPath(actions, graph, trigger.name);
208
+ paths.push(createFlowPath("Main Flow", steps, actions));
209
+ }
210
+ else {
211
+ // Branching workflow - trace each branch
212
+ for (const categorizer of categorizers) {
213
+ const categories = categorizer.typeArguments?.categories;
214
+ if (Array.isArray(categories)) {
215
+ for (const category of categories) {
216
+ const branchSteps = buildBranchPath(actions, graph, trigger.name, categorizer.name, category.name);
217
+ paths.push(createFlowPath(`Branch: ${category.name}`, branchSteps, actions));
218
+ }
219
+ }
220
+ else {
221
+ // Single categorizer path if categories not properly defined
222
+ const steps = buildLinearPath(actions, graph, trigger.name);
223
+ paths.push(createFlowPath(`Via ${categorizer.displaySettings?.displayName || categorizer.name}`, steps, actions));
224
+ }
225
+ }
226
+ }
227
+ // If no paths were found, create a default main flow
228
+ if (paths.length === 0) {
229
+ const steps = buildLinearPath(actions, graph, trigger.name);
230
+ paths.push(createFlowPath("Main Flow", steps, actions));
231
+ }
232
+ return paths;
233
+ }
234
+ function buildLinearPath(actions, graph, startNode) {
235
+ const steps = [];
236
+ const visited = new Set();
237
+ const queue = [startNode];
238
+ let stepNum = 1;
239
+ while (queue.length > 0) {
240
+ const nodeName = queue.shift();
241
+ if (visited.has(nodeName))
242
+ continue;
243
+ visited.add(nodeName);
244
+ const action = actions.find(a => a.name === nodeName);
245
+ if (!action)
246
+ continue;
247
+ steps.push(createFlowStep(stepNum++, action, graph));
248
+ // Add dependents to queue
249
+ const dependents = graph.dependents.get(nodeName) || new Set();
250
+ for (const dep of dependents) {
251
+ if (!visited.has(dep)) {
252
+ queue.push(dep);
253
+ }
254
+ }
255
+ }
256
+ return steps;
257
+ }
258
+ function buildBranchPath(actions, graph, startNode, categorizerName, categoryName) {
259
+ const steps = [];
260
+ const visited = new Set();
261
+ const queue = [startNode];
262
+ let stepNum = 1;
263
+ while (queue.length > 0) {
264
+ const nodeName = queue.shift();
265
+ if (visited.has(nodeName))
266
+ continue;
267
+ visited.add(nodeName);
268
+ const action = actions.find(a => a.name === nodeName);
269
+ if (!action)
270
+ continue;
271
+ // Check if this node has a runIf condition that matches our branch
272
+ const condition = graph.conditions.get(nodeName);
273
+ if (condition) {
274
+ // Skip nodes that are conditioned on different categories
275
+ const isCategoryCondition = condition.includes(categorizerName);
276
+ if (isCategoryCondition && !condition.includes(categoryName)) {
277
+ continue; // Skip this branch
278
+ }
279
+ }
280
+ const step = createFlowStep(stepNum++, action, graph);
281
+ step.branch = categoryName;
282
+ steps.push(step);
283
+ // Add dependents to queue
284
+ const dependents = graph.dependents.get(nodeName) || new Set();
285
+ for (const dep of dependents) {
286
+ if (!visited.has(dep)) {
287
+ queue.push(dep);
288
+ }
289
+ }
290
+ }
291
+ return steps;
292
+ }
293
+ function createFlowStep(step, action, graph) {
294
+ const actionType = action.action?.name?.name || action.name || "unknown";
295
+ const displayName = action.displaySettings?.displayName || action.name;
296
+ // Build inputs
297
+ const inputs = [];
298
+ if (action.inputs) {
299
+ for (const [name, binding] of Object.entries(action.inputs)) {
300
+ const bindingObj = binding;
301
+ const actionOutput = bindingObj?.actionOutput;
302
+ inputs.push({
303
+ name,
304
+ source: actionOutput ? `${actionOutput.actionName}.${actionOutput.output}` : "static",
305
+ type: friendlyTypeName("TEXT_WITH_SOURCES"),
306
+ sampleValue: actionOutput ? getSampleValue(actionOutput.output || "") : undefined,
307
+ });
308
+ }
309
+ }
310
+ // Build outputs
311
+ const outputNames = graph.outputs.get(action.name) || ["output"];
312
+ const consumers = Array.from(graph.dependents.get(action.name) || []);
313
+ const outputs = outputNames.map(name => ({
314
+ name,
315
+ type: friendlyTypeName("TEXT_WITH_SOURCES"),
316
+ consumers,
317
+ sampleValue: getSampleValue(name),
318
+ }));
319
+ // Get condition
320
+ const condition = graph.conditions.get(action.name);
321
+ // Get notes
322
+ const notes = [];
323
+ if (actionType.includes("hitl") || actionType.includes("human")) {
324
+ notes.push("⏸️ Waits for human approval");
325
+ }
326
+ if (actionType.includes("email")) {
327
+ notes.push("📧 Sends external email");
328
+ }
329
+ if (actionType.includes("external_action")) {
330
+ notes.push("🔌 Calls external API");
331
+ }
332
+ return {
333
+ step,
334
+ node: action.name,
335
+ displayName,
336
+ actionType,
337
+ inputs,
338
+ outputs,
339
+ condition,
340
+ estimatedLatencyMs: estimateLatency(actionType),
341
+ notes: notes.length > 0 ? notes : undefined,
342
+ };
343
+ }
344
+ function createFlowPath(name, steps, actions) {
345
+ const totalLatencyMs = steps.reduce((sum, s) => sum + (s.estimatedLatencyMs || 0), 0);
346
+ const llmCalls = steps.filter(s => s.actionType.includes("categorizer") ||
347
+ s.actionType.includes("call_llm") ||
348
+ s.actionType.includes("respond")).length;
349
+ const externalCalls = steps.filter(s => s.actionType.includes("email") ||
350
+ s.actionType.includes("external_action")).length;
351
+ const hasHitl = steps.some(s => s.actionType.includes("hitl") || s.actionType.includes("human"));
352
+ const sendsEmail = steps.some(s => s.actionType.includes("email"));
353
+ return {
354
+ name,
355
+ steps,
356
+ totalLatencyMs,
357
+ llmCalls,
358
+ externalCalls,
359
+ hasHitl,
360
+ sendsEmail,
361
+ };
362
+ }
363
+ function calculateStats(paths, actions) {
364
+ const allNodeNames = new Set(actions.map(a => a.name));
365
+ const pathNodeSets = paths.map(p => new Set(p.steps.map(s => s.node)));
366
+ // Nodes in ALL paths
367
+ const commonNodes = Array.from(allNodeNames).filter(node => pathNodeSets.every(set => set.has(node)));
368
+ // Nodes in SOME but not all paths
369
+ const optionalNodes = Array.from(allNodeNames).filter(node => pathNodeSets.some(set => set.has(node)) && !commonNodes.includes(node));
370
+ const pathLengths = paths.map(p => p.steps.length);
371
+ const latencies = paths.map(p => p.totalLatencyMs);
372
+ return {
373
+ totalNodes: allNodeNames.size,
374
+ uniquePaths: paths.length,
375
+ maxDepth: Math.max(...pathLengths),
376
+ avgPathLength: pathLengths.reduce((a, b) => a + b, 0) / pathLengths.length,
377
+ commonNodes,
378
+ optionalNodes,
379
+ minLatencyMs: Math.min(...latencies),
380
+ maxLatencyMs: Math.max(...latencies),
381
+ };
382
+ }
383
+ // ─────────────────────────────────────────────────────────────────────────────
384
+ // Visualization: ASCII
385
+ // ─────────────────────────────────────────────────────────────────────────────
386
+ function generateAsciiFlow(paths) {
387
+ const lines = [];
388
+ lines.push("╔══════════════════════════════════════════════════════════════════╗");
389
+ lines.push("║ WORKFLOW EXECUTION FLOW ║");
390
+ lines.push("╚══════════════════════════════════════════════════════════════════╝");
391
+ lines.push("");
392
+ for (const path of paths) {
393
+ lines.push(`━━━ ${path.name} ━━━`);
394
+ lines.push(` Latency: ~${formatLatency(path.totalLatencyMs)} | LLM calls: ${path.llmCalls} | External: ${path.externalCalls}`);
395
+ if (path.hasHitl)
396
+ lines.push(" ⏸️ Human approval required");
397
+ if (path.sendsEmail)
398
+ lines.push(" 📧 Sends email");
399
+ lines.push("");
400
+ for (let i = 0; i < path.steps.length; i++) {
401
+ const step = path.steps[i];
402
+ const isLast = i === path.steps.length - 1;
403
+ // Node box
404
+ const icon = getNodeIcon(step.actionType);
405
+ const latency = step.estimatedLatencyMs ? ` (~${step.estimatedLatencyMs}ms)` : "";
406
+ lines.push(` ┌─────────────────────────────────────────┐`);
407
+ lines.push(` │ ${icon} ${step.displayName.slice(0, 35).padEnd(35)} │`);
408
+ lines.push(` │ ${step.actionType.slice(0, 37).padEnd(37)} │`);
409
+ // Show inputs
410
+ if (step.inputs.length > 0) {
411
+ lines.push(` │ ← ${step.inputs.map(i => i.source).join(", ").slice(0, 33).padEnd(33)} │`);
412
+ }
413
+ // Show condition
414
+ if (step.condition) {
415
+ lines.push(` │ 🔀 if: ${step.condition.slice(0, 28).padEnd(28)} │`);
416
+ }
417
+ // Show outputs
418
+ if (step.outputs.length > 0 && step.outputs[0].consumers.length > 0) {
419
+ lines.push(` │ → ${step.outputs[0].consumers.join(", ").slice(0, 33).padEnd(33)} │`);
420
+ }
421
+ lines.push(` └─────────────────────────────────────────┘`);
422
+ if (!isLast) {
423
+ lines.push(" │");
424
+ lines.push(" ▼");
425
+ }
426
+ }
427
+ lines.push("");
428
+ }
429
+ return lines.join("\n");
430
+ }
431
+ function getNodeIcon(actionType) {
432
+ const type = actionType.toLowerCase();
433
+ if (type.includes("trigger"))
434
+ return "🎯";
435
+ if (type.includes("categorizer"))
436
+ return "🔀";
437
+ if (type.includes("search"))
438
+ return "🔍";
439
+ if (type.includes("llm") || type.includes("respond"))
440
+ return "🤖";
441
+ if (type.includes("hitl") || type.includes("human"))
442
+ return "👤";
443
+ if (type.includes("email"))
444
+ return "📧";
445
+ if (type.includes("entity_extraction"))
446
+ return "📋";
447
+ if (type.includes("external"))
448
+ return "🔌";
449
+ return "⚙️";
450
+ }
451
+ function formatLatency(ms) {
452
+ if (ms < 1000)
453
+ return `${ms}ms`;
454
+ if (ms < 60000)
455
+ return `${(ms / 1000).toFixed(1)}s`;
456
+ return `${(ms / 60000).toFixed(1)}m`;
457
+ }
458
+ // ─────────────────────────────────────────────────────────────────────────────
459
+ // Visualization: Mermaid
460
+ // ─────────────────────────────────────────────────────────────────────────────
461
+ function generateMermaidDiagram(actions, graph) {
462
+ const lines = [];
463
+ lines.push("```mermaid");
464
+ lines.push("flowchart TD");
465
+ // Define nodes
466
+ for (const action of actions) {
467
+ const type = action.action?.name?.name || action.name;
468
+ const displayName = action.displaySettings?.displayName || action.name;
469
+ const icon = getNodeIcon(type);
470
+ // Node shape based on type
471
+ if (type.includes("trigger")) {
472
+ lines.push(` ${action.name}(("${icon} ${displayName}"))`);
473
+ }
474
+ else if (type.includes("categorizer")) {
475
+ lines.push(` ${action.name}{{"${icon} ${displayName}"}}`);
476
+ }
477
+ else if (type.includes("hitl") || type.includes("human")) {
478
+ lines.push(` ${action.name}[/"${icon} ${displayName}"/]`);
479
+ }
480
+ else if (type.includes("email") || type.includes("external")) {
481
+ lines.push(` ${action.name}[["${icon} ${displayName}"]]`);
482
+ }
483
+ else {
484
+ lines.push(` ${action.name}["${icon} ${displayName}"]`);
485
+ }
486
+ }
487
+ lines.push("");
488
+ // Define edges
489
+ for (const action of actions) {
490
+ const deps = graph.dependencies.get(action.name) || new Set();
491
+ const condition = graph.conditions.get(action.name);
492
+ for (const dep of deps) {
493
+ if (condition) {
494
+ // Extract just the category name from condition
495
+ const label = condition.split(":").pop()?.trim() || condition;
496
+ lines.push(` ${dep} -->|${label}| ${action.name}`);
497
+ }
498
+ else {
499
+ lines.push(` ${dep} --> ${action.name}`);
500
+ }
501
+ }
502
+ }
503
+ lines.push("```");
504
+ return lines.join("\n");
505
+ }
506
+ /**
507
+ * Generate a detailed trace with sample data showing what happens at each step
508
+ */
509
+ export function generateDetailedTrace(path) {
510
+ const steps = [];
511
+ // Accumulate outputs as we go (simulate data flow)
512
+ const availableData = {
513
+ "trigger.chat_conversation": '{"messages": [{"role": "user", "content": "What is my account balance?"}]}',
514
+ "trigger.user_query": "What is my account balance?",
515
+ };
516
+ for (const flowStep of path.steps) {
517
+ // Gather inputs for this step
518
+ const inputData = {};
519
+ for (const input of flowStep.inputs) {
520
+ inputData[input.name] = availableData[input.source] || input.sampleValue || `<from ${input.source}>`;
521
+ }
522
+ // Generate outputs for this step
523
+ const outputData = {};
524
+ for (const output of flowStep.outputs) {
525
+ const value = output.sampleValue || getSampleValue(output.name);
526
+ outputData[output.name] = value;
527
+ // Make available for downstream nodes
528
+ availableData[`${flowStep.node}.${output.name}`] = value;
529
+ }
530
+ steps.push({
531
+ step: flowStep.step,
532
+ node: flowStep.node,
533
+ description: generateStepDescription(flowStep, inputData),
534
+ inputData,
535
+ outputData,
536
+ timing: `~${flowStep.estimatedLatencyMs || 500}ms`,
537
+ });
538
+ }
539
+ return {
540
+ pathName: path.name,
541
+ steps,
542
+ };
543
+ }
544
+ function generateStepDescription(step, inputData) {
545
+ const type = step.actionType.toLowerCase();
546
+ if (type.includes("trigger")) {
547
+ return "📥 Receive user message and start workflow";
548
+ }
549
+ if (type.includes("categorizer")) {
550
+ return `🔀 Analyze message to determine intent category`;
551
+ }
552
+ if (type.includes("search")) {
553
+ const query = Object.values(inputData)[0] || "user query";
554
+ return `🔍 Search knowledge base for: "${query.slice(0, 30)}..."`;
555
+ }
556
+ if (type.includes("respond") || type.includes("call_llm")) {
557
+ return "🤖 Generate response using LLM with search results";
558
+ }
559
+ if (type.includes("entity_extraction")) {
560
+ return "📋 Extract structured data (email, phone, etc.) from conversation";
561
+ }
562
+ if (type.includes("hitl") || type.includes("human")) {
563
+ return "👤 WAIT for human approval before proceeding";
564
+ }
565
+ if (type.includes("email")) {
566
+ return "📧 Send email to extracted recipient";
567
+ }
568
+ if (type.includes("summarizer") || type.includes("conversation_to")) {
569
+ return "📝 Summarize conversation into search query";
570
+ }
571
+ return `⚙️ Execute ${step.displayName}`;
572
+ }
573
+ // ─────────────────────────────────────────────────────────────────────────────
574
+ // Format Helpers
575
+ // ─────────────────────────────────────────────────────────────────────────────
576
+ /**
577
+ * Format a flow trace as readable text
578
+ */
579
+ export function formatFlowTrace(trace) {
580
+ const lines = [];
581
+ lines.push("# Workflow Flow Analysis");
582
+ lines.push("");
583
+ lines.push(`**Trigger**: ${trace.triggerType}`);
584
+ lines.push(`**Total Nodes**: ${trace.stats.totalNodes}`);
585
+ lines.push(`**Unique Paths**: ${trace.stats.uniquePaths}`);
586
+ lines.push(`**Latency Range**: ${formatLatency(trace.stats.minLatencyMs)} - ${formatLatency(trace.stats.maxLatencyMs)}`);
587
+ lines.push("");
588
+ lines.push("## Common Nodes (all paths)");
589
+ lines.push(trace.stats.commonNodes.map(n => `- ${n}`).join("\n") || "- None");
590
+ lines.push("");
591
+ lines.push("## Branch-Specific Nodes");
592
+ lines.push(trace.stats.optionalNodes.map(n => `- ${n}`).join("\n") || "- None");
593
+ lines.push("");
594
+ lines.push("## Execution Paths");
595
+ for (const path of trace.paths) {
596
+ lines.push(`\n### ${path.name}`);
597
+ lines.push(`- **Steps**: ${path.steps.length}`);
598
+ lines.push(`- **Est. Latency**: ${formatLatency(path.totalLatencyMs)}`);
599
+ lines.push(`- **LLM Calls**: ${path.llmCalls}`);
600
+ lines.push(`- **External Calls**: ${path.externalCalls}`);
601
+ if (path.hasHitl)
602
+ lines.push("- ⚠️ **Requires human approval**");
603
+ if (path.sendsEmail)
604
+ lines.push("- 📧 **Sends email**");
605
+ lines.push("\n**Step-by-Step:**");
606
+ for (const step of path.steps) {
607
+ const condition = step.condition ? ` (if ${step.condition})` : "";
608
+ lines.push(`${step.step}. ${getNodeIcon(step.actionType)} **${step.displayName}**${condition}`);
609
+ if (step.inputs.length > 0) {
610
+ lines.push(` - Inputs: ${step.inputs.map(i => `\`${i.name}\` ← ${i.source}`).join(", ")}`);
611
+ }
612
+ }
613
+ }
614
+ lines.push("\n---\n");
615
+ lines.push("## ASCII Flow");
616
+ lines.push("```");
617
+ lines.push(trace.ascii);
618
+ lines.push("```");
619
+ lines.push("\n## Mermaid Diagram");
620
+ lines.push(trace.mermaid);
621
+ return lines.join("\n");
622
+ }
623
+ /**
624
+ * Format a detailed trace as readable text
625
+ */
626
+ export function formatDetailedTrace(trace) {
627
+ const lines = [];
628
+ lines.push(`# Detailed Execution: ${trace.pathName}`);
629
+ lines.push("");
630
+ for (const step of trace.steps) {
631
+ lines.push(`## Step ${step.step}: ${step.node}`);
632
+ lines.push(`**${step.description}** (${step.timing})`);
633
+ lines.push("");
634
+ lines.push("**Inputs:**");
635
+ for (const [name, value] of Object.entries(step.inputData)) {
636
+ lines.push(`- \`${name}\`: \`${value.slice(0, 60)}${value.length > 60 ? "..." : ""}\``);
637
+ }
638
+ lines.push("");
639
+ lines.push("**Outputs:**");
640
+ for (const [name, value] of Object.entries(step.outputData)) {
641
+ lines.push(`- \`${name}\`: \`${value.slice(0, 60)}${value.length > 60 ? "..." : ""}\``);
642
+ }
643
+ lines.push("");
644
+ lines.push("---");
645
+ lines.push("");
646
+ }
647
+ return lines.join("\n");
648
+ }
@@ -14,6 +14,7 @@
14
14
  * - No brittle keyword matching
15
15
  */
16
16
  import { compileWorkflow } from "./workflow-generator.js";
17
+ import { STRUCTURAL_RULES_FOR_LLM } from "./structural-rules.js";
17
18
  // ═══════════════════════════════════════════════════════════════════════════
18
19
  // SCHEMA DOCUMENTATION FOR LLM CONTEXT
19
20
  // ═══════════════════════════════════════════════════════════════════════════
@@ -263,7 +264,16 @@ inputs: { to_email: { type: "action_output", actionName: "entity_extraction", ou
263
264
  1. Find the categorizer node
264
265
  2. Add to the categories array
265
266
  3. Add a handler node with runIf condition for the new category
267
+
268
+ ${STRUCTURAL_RULES_FOR_LLM}
266
269
  `;
270
+ /**
271
+ * Get the complete schema including structural validation rules.
272
+ * Use this for LLM context when generating or modifying workflows.
273
+ */
274
+ export function getCompleteSchemaForLLM() {
275
+ return WORKFLOW_SCHEMA_FOR_LLM;
276
+ }
267
277
  // ═══════════════════════════════════════════════════════════════════════════
268
278
  // DECOMPILER: workflow_def JSON → WorkflowSpec
269
279
  // ═══════════════════════════════════════════════════════════════════════════