@ema.co/mcp-toolkit 2026.2.23 → 2026.2.27

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.

@@ -1,671 +0,0 @@
1
- /**
2
- * @deprecated Use `workflow-graph-optimizer.ts` instead.
3
- *
4
- * This file contains the legacy heuristic-based optimizer (health scores,
5
- * recommendations, scoring). It is superseded by the deterministic graph
6
- * optimizer which provides:
7
- * - Shared typed graph (`workflow-graph.ts`)
8
- * - Provably safe auto-fix transforms (`workflow-graph-transforms.ts`)
9
- * - Convergence-based orchestrator (`workflow-graph-optimizer.ts`)
10
- *
11
- * Remaining consumer: `test/intelligence-layer.test.ts` (legacy tests).
12
- * Remove this file once that test is migrated or deleted.
13
- */
14
- // ─────────────────────────────────────────────────────────────────────────────
15
- // Main Entry Point
16
- // ─────────────────────────────────────────────────────────────────────────────
17
- /**
18
- * Analyze workflow for optimization opportunities
19
- */
20
- export function analyzeOptimizations(workflow) {
21
- const actions = workflow.actions || [];
22
- const graph = buildDependencyGraph(actions);
23
- const metrics = calculateMetrics(actions, graph);
24
- const optimizations = [];
25
- // 1. Find consolidation opportunities (duplicate/redundant nodes)
26
- optimizations.push(...findConsolidationOpportunities(actions, graph));
27
- // 2. Find parallelization opportunities
28
- optimizations.push(...findParallelizationOpportunities(actions, graph));
29
- // 3. Find passthrough nodes to eliminate
30
- optimizations.push(...findPassthroughNodes(actions, graph));
31
- // 4. Find error handling gaps
32
- optimizations.push(...findErrorHandlingGaps(actions, graph));
33
- // 5. Find LLM call optimizations
34
- optimizations.push(...findLlmOptimizations(actions, graph));
35
- // 6. Find search optimizations
36
- optimizations.push(...findSearchOptimizations(actions, graph));
37
- // 7. Find naming improvements
38
- optimizations.push(...findNamingImprovements(actions));
39
- // 8. Find safety improvements (HITL for side effects)
40
- optimizations.push(...findSafetyImprovements(actions, graph));
41
- // Calculate health score
42
- const healthScore = calculateWorkflowHealth(actions, metrics, optimizations);
43
- // Categorize optimizations
44
- const quickWins = optimizations.filter(o => o.auto_applicable && o.risk === "none" && o.severity !== "suggestion");
45
- const strategic = optimizations.filter(o => !o.auto_applicable || o.risk !== "none");
46
- // Generate recommendations
47
- const recommendations = generateRecommendations(optimizations, metrics, healthScore);
48
- return {
49
- workflow_health_score: healthScore,
50
- grade: healthScoreToGrade(healthScore),
51
- optimizations,
52
- quick_wins: quickWins,
53
- strategic,
54
- metrics,
55
- recommendations,
56
- };
57
- }
58
- // ─────────────────────────────────────────────────────────────────────────────
59
- // Graph Building
60
- // ─────────────────────────────────────────────────────────────────────────────
61
- function buildDependencyGraph(actions) {
62
- const nodes = new Map();
63
- const edges = new Map();
64
- const reverseEdges = new Map();
65
- // Initialize nodes
66
- for (const action of actions) {
67
- const actionType = action.action?.name?.name || "unknown";
68
- nodes.set(action.name, {
69
- id: action.name,
70
- type: actionType,
71
- depth: 0,
72
- inputs: [],
73
- outputs: [],
74
- });
75
- edges.set(action.name, new Set());
76
- reverseEdges.set(action.name, new Set());
77
- }
78
- // Build edges from input bindings
79
- for (const action of actions) {
80
- const node = nodes.get(action.name);
81
- if (action.inputs) {
82
- for (const [inputName, binding] of Object.entries(action.inputs)) {
83
- const bindingObj = binding;
84
- const actionOutput = bindingObj?.actionOutput;
85
- if (actionOutput?.actionName) {
86
- const sourceNode = String(actionOutput.actionName);
87
- node.inputs.push(sourceNode);
88
- // Source → Target edge
89
- edges.get(sourceNode)?.add(action.name);
90
- reverseEdges.get(action.name)?.add(sourceNode);
91
- // Update source node outputs
92
- const sourceGraphNode = nodes.get(sourceNode);
93
- if (sourceGraphNode) {
94
- sourceGraphNode.outputs.push(action.name);
95
- }
96
- }
97
- }
98
- }
99
- }
100
- // Calculate depths (BFS from trigger)
101
- const trigger = actions.find(a => a.action?.name?.name?.includes("trigger"));
102
- if (trigger) {
103
- const visited = new Set();
104
- const queue = [{ node: trigger.name, depth: 0 }];
105
- while (queue.length > 0) {
106
- const { node, depth } = queue.shift();
107
- if (visited.has(node))
108
- continue;
109
- visited.add(node);
110
- const graphNode = nodes.get(node);
111
- if (graphNode) {
112
- graphNode.depth = depth;
113
- }
114
- for (const child of edges.get(node) || []) {
115
- if (!visited.has(child)) {
116
- queue.push({ node: child, depth: depth + 1 });
117
- }
118
- }
119
- }
120
- }
121
- return { nodes, edges, reverseEdges };
122
- }
123
- // ─────────────────────────────────────────────────────────────────────────────
124
- // Metrics Calculation
125
- // ─────────────────────────────────────────────────────────────────────────────
126
- function calculateMetrics(actions, graph) {
127
- // Count node types
128
- const nodeTypes = {};
129
- for (const action of actions) {
130
- const type = action.action?.name?.name || "unknown";
131
- nodeTypes[type] = (nodeTypes[type] || 0) + 1;
132
- }
133
- // Calculate max depth
134
- const maxDepth = Math.max(...Array.from(graph.nodes.values()).map(n => n.depth), 0);
135
- // Count branches (nodes with multiple outputs)
136
- const branchCount = Array.from(graph.nodes.values()).filter(n => n.outputs.length > 1).length;
137
- // Count parallel opportunities (nodes at same depth that could run together)
138
- const nodesByDepth = new Map();
139
- for (const [id, node] of graph.nodes) {
140
- const nodes = nodesByDepth.get(node.depth) || [];
141
- nodes.push(id);
142
- nodesByDepth.set(node.depth, nodes);
143
- }
144
- const parallelOpportunities = Array.from(nodesByDepth.values())
145
- .filter(nodes => nodes.length > 1).length;
146
- // Calculate redundancy score
147
- const redundancyScore = calculateRedundancyScore(actions, graph);
148
- // Calculate complexity score
149
- const complexityScore = calculateComplexityScore(actions, graph, maxDepth, branchCount);
150
- // Count sequential latency factors (LLM + search calls on critical path)
151
- const latencyFactors = countLatencyFactors(actions, graph);
152
- return {
153
- total_nodes: actions.length,
154
- node_types: nodeTypes,
155
- max_depth: maxDepth,
156
- branch_count: branchCount,
157
- parallel_opportunities: parallelOpportunities,
158
- redundancy_score: redundancyScore,
159
- complexity_score: complexityScore,
160
- estimated_latency_factors: latencyFactors,
161
- };
162
- }
163
- function calculateRedundancyScore(actions, graph) {
164
- let redundancy = 0;
165
- // Check for duplicate node types with same inputs
166
- const nodeSignatures = new Map();
167
- for (const action of actions) {
168
- const type = action.action?.name?.name || "unknown";
169
- const node = graph.nodes.get(action.name);
170
- const signature = `${type}:${node?.inputs.sort().join(",")}`;
171
- const existing = nodeSignatures.get(signature) || [];
172
- existing.push(action.name);
173
- nodeSignatures.set(signature, existing);
174
- }
175
- // Each duplicate adds to redundancy
176
- for (const [_, nodes] of nodeSignatures) {
177
- if (nodes.length > 1) {
178
- redundancy += (nodes.length - 1) * 10;
179
- }
180
- }
181
- return Math.min(redundancy, 100);
182
- }
183
- function calculateComplexityScore(actions, graph, maxDepth, branchCount) {
184
- // Base complexity from node count
185
- let complexity = Math.min(actions.length * 5, 30);
186
- // Add complexity for depth
187
- complexity += Math.min(maxDepth * 5, 20);
188
- // Add complexity for branching
189
- complexity += Math.min(branchCount * 10, 30);
190
- // Add complexity for conditional logic
191
- const conditionalNodes = actions.filter(a => a.runIf).length;
192
- complexity += Math.min(conditionalNodes * 5, 20);
193
- return Math.min(complexity, 100);
194
- }
195
- function countLatencyFactors(actions, graph) {
196
- // Count LLM and search nodes on the longest path
197
- const latencyNodes = actions.filter(a => {
198
- const type = a.action?.name?.name || "";
199
- return type.includes("llm") ||
200
- type.includes("respond") ||
201
- type.includes("search") ||
202
- type.includes("categorizer");
203
- });
204
- // Find the longest sequential chain of latency nodes
205
- let maxSequential = 0;
206
- for (const node of latencyNodes) {
207
- let count = 1;
208
- let current = node.name;
209
- // Follow the dependency chain
210
- while (true) {
211
- const graphNode = graph.nodes.get(current);
212
- if (!graphNode || graphNode.outputs.length === 0)
213
- break;
214
- // Find a latency node in outputs
215
- const nextLatency = graphNode.outputs.find(out => latencyNodes.some(ln => ln.name === out));
216
- if (!nextLatency)
217
- break;
218
- count++;
219
- current = nextLatency;
220
- }
221
- maxSequential = Math.max(maxSequential, count);
222
- }
223
- return maxSequential;
224
- }
225
- // ─────────────────────────────────────────────────────────────────────────────
226
- // Optimization Finders
227
- // ─────────────────────────────────────────────────────────────────────────────
228
- function findConsolidationOpportunities(actions, graph) {
229
- const opportunities = [];
230
- // Find duplicate search nodes with same query source
231
- const searchNodes = actions.filter(a => a.action?.name?.name?.includes("search") &&
232
- !a.action?.name?.name?.includes("combine"));
233
- const searchByQuery = new Map();
234
- for (const search of searchNodes) {
235
- const querySource = getQuerySource(search);
236
- const existing = searchByQuery.get(querySource) || [];
237
- existing.push(search);
238
- searchByQuery.set(querySource, existing);
239
- }
240
- for (const [query, duplicates] of searchByQuery) {
241
- if (duplicates.length > 1) {
242
- opportunities.push({
243
- id: `consolidate-search-${hashString(query)}`,
244
- type: "consolidate_nodes",
245
- severity: "recommended",
246
- title: "Consolidate Redundant Searches",
247
- current_state: `${duplicates.length} search nodes using same query source: "${query}"`,
248
- proposed_change: "Consolidate into single search node, share results via named_inputs",
249
- benefit: `Reduce API calls by ${duplicates.length - 1}x, faster execution, consistent results`,
250
- risk: "low",
251
- auto_applicable: false, // Requires careful rewiring
252
- affected_nodes: duplicates.map(d => d.name),
253
- implementation_hint: "Keep one search, remove others, use named_inputs to pass results to all consumers",
254
- });
255
- }
256
- }
257
- // Find duplicate LLM calls with similar prompts
258
- const llmNodes = actions.filter(a => a.action?.name?.name?.includes("llm") ||
259
- a.action?.name?.name?.includes("respond"));
260
- if (llmNodes.length > 2) {
261
- // Check for sequential LLM calls
262
- for (const node of llmNodes) {
263
- const graphNode = graph.nodes.get(node.name);
264
- if (graphNode) {
265
- const llmChildren = graphNode.outputs.filter(out => llmNodes.some(ln => ln.name === out));
266
- if (llmChildren.length > 0) {
267
- opportunities.push({
268
- id: `merge-llm-${node.name}`,
269
- type: "merge_llm_calls",
270
- severity: "suggestion",
271
- title: "Consider Merging Sequential LLM Calls",
272
- current_state: `${node.name} feeds into LLM node(s): ${llmChildren.join(", ")}`,
273
- proposed_change: "Merge into single comprehensive LLM call with structured prompt",
274
- benefit: "Reduce latency, potentially lower cost, more coherent output",
275
- risk: "medium",
276
- auto_applicable: false,
277
- affected_nodes: [node.name, ...llmChildren],
278
- implementation_hint: "Combine prompts, use structured output format, single call_llm with all context",
279
- });
280
- }
281
- }
282
- }
283
- }
284
- return opportunities;
285
- }
286
- function findParallelizationOpportunities(actions, graph) {
287
- const opportunities = [];
288
- // Find nodes at same depth that share a parent but are sequential
289
- const nodesByParent = new Map();
290
- for (const [nodeId, graphNode] of graph.nodes) {
291
- for (const input of graphNode.inputs) {
292
- const siblings = nodesByParent.get(input) || [];
293
- siblings.push(nodeId);
294
- nodesByParent.set(input, siblings);
295
- }
296
- }
297
- // Check for sequential siblings that could be parallel
298
- for (const [parent, children] of nodesByParent) {
299
- if (children.length > 1) {
300
- // Check if they're actually sequential (one depends on another)
301
- const sequential = children.filter(child => {
302
- const node = graph.nodes.get(child);
303
- return node && children.some(other => other !== child && node.inputs.includes(other));
304
- });
305
- if (sequential.length > 0 && sequential.length < children.length) {
306
- opportunities.push({
307
- id: `parallelize-${parent}`,
308
- type: "parallelize_paths",
309
- severity: "suggestion",
310
- title: "Parallelization Opportunity",
311
- current_state: `Children of ${parent} could potentially run in parallel`,
312
- proposed_change: "Restructure so independent operations branch from same source",
313
- benefit: "Reduced latency through parallel execution",
314
- risk: "low",
315
- auto_applicable: false,
316
- affected_nodes: [parent, ...children],
317
- });
318
- }
319
- }
320
- }
321
- return opportunities;
322
- }
323
- function findPassthroughNodes(actions, graph) {
324
- const opportunities = [];
325
- // Find nodes that just pass data through without transformation
326
- for (const action of actions) {
327
- const graphNode = graph.nodes.get(action.name);
328
- if (!graphNode)
329
- continue;
330
- // Skip trigger and output nodes
331
- const type = action.action?.name?.name || "";
332
- if (type.includes("trigger") || graphNode.outputs.length === 0)
333
- continue;
334
- // Check if this node has single input and single output with no transformation
335
- if (graphNode.inputs.length === 1 && graphNode.outputs.length === 1) {
336
- // Check if it's a simple passthrough (no significant configuration)
337
- const inputCount = Object.keys(action.inputs || {}).length;
338
- const hasTransformation = type.includes("llm") ||
339
- type.includes("search") ||
340
- type.includes("categorizer") ||
341
- type.includes("extract");
342
- if (!hasTransformation && inputCount <= 2) {
343
- opportunities.push({
344
- id: `eliminate-passthrough-${action.name}`,
345
- type: "eliminate_passthrough",
346
- severity: "suggestion",
347
- title: "Potential Passthrough Node",
348
- current_state: `${action.name} appears to pass data without significant transformation`,
349
- proposed_change: "Consider removing and connecting predecessor directly to successor",
350
- benefit: "Simpler workflow, reduced complexity",
351
- risk: "low",
352
- auto_applicable: false,
353
- affected_nodes: [action.name],
354
- });
355
- }
356
- }
357
- }
358
- return opportunities;
359
- }
360
- function findErrorHandlingGaps(actions, graph) {
361
- const opportunities = [];
362
- // Check for external action callers without error handling
363
- const externalCallers = actions.filter(a => a.action?.name?.name?.includes("external") ||
364
- a.action?.name?.name?.includes("email") ||
365
- a.action?.name?.name?.includes("tool"));
366
- for (const caller of externalCallers) {
367
- // Check if there's error handling downstream
368
- const graphNode = graph.nodes.get(caller.name);
369
- const hasErrorPath = graphNode?.outputs.some(out => {
370
- const action = actions.find(a => a.name === out);
371
- return action?.runIf && JSON.stringify(action.runIf).includes("error");
372
- });
373
- if (!hasErrorPath) {
374
- opportunities.push({
375
- id: `add-error-handling-${caller.name}`,
376
- type: "add_error_handling",
377
- severity: "recommended",
378
- title: "Add Error Handling",
379
- current_state: `External action ${caller.name} has no error handling path`,
380
- proposed_change: "Add conditional path for handling failures gracefully",
381
- benefit: "Better user experience when external services fail",
382
- risk: "none",
383
- auto_applicable: false,
384
- affected_nodes: [caller.name],
385
- implementation_hint: "Add runIf condition checking for error status, route to fallback response",
386
- });
387
- }
388
- }
389
- return opportunities;
390
- }
391
- function findLlmOptimizations(actions, graph) {
392
- const opportunities = [];
393
- const llmNodes = actions.filter(a => a.action?.name?.name?.includes("llm") ||
394
- a.action?.name?.name?.includes("respond"));
395
- // Check for call_llm being used instead of respond_with_sources for search results
396
- for (const node of llmNodes) {
397
- const type = node.action?.name?.name || "";
398
- const graphNode = graph.nodes.get(node.name);
399
- if (type === "call_llm" && graphNode) {
400
- // Check if input comes from search
401
- const hasSearchInput = graphNode.inputs.some(input => {
402
- const inputAction = actions.find(a => a.name === input);
403
- return inputAction?.action?.name?.name?.includes("search");
404
- });
405
- if (hasSearchInput) {
406
- opportunities.push({
407
- id: `use-respond-with-sources-${node.name}`,
408
- type: "improve_response_quality",
409
- severity: "recommended",
410
- title: "Use Purpose-Built Response Node",
411
- current_state: `${node.name} uses call_llm for search-based responses`,
412
- proposed_change: "Replace with respond_with_sources for better citation handling",
413
- benefit: "Better citations, grounded responses, less configuration needed",
414
- risk: "low",
415
- auto_applicable: false,
416
- affected_nodes: [node.name],
417
- });
418
- }
419
- }
420
- }
421
- return opportunities;
422
- }
423
- function findSearchOptimizations(actions, graph) {
424
- const opportunities = [];
425
- // Check for web search without KB search
426
- const hasKbSearch = actions.some(a => a.action?.name?.name === "search" ||
427
- a.action?.name?.name?.includes("file_search"));
428
- const hasWebSearch = actions.some(a => a.action?.name?.name?.includes("web_search") ||
429
- a.action?.name?.name?.includes("live_web"));
430
- if (hasWebSearch && !hasKbSearch) {
431
- opportunities.push({
432
- id: "add-kb-search",
433
- type: "optimize_search",
434
- severity: "recommended",
435
- title: "Add Knowledge Base Search",
436
- current_state: "Workflow uses web search but no internal KB search",
437
- proposed_change: "Add KB search as primary source, use web search as supplement",
438
- benefit: "Faster responses, more controlled content, reduced external dependencies",
439
- risk: "none",
440
- auto_applicable: false,
441
- affected_nodes: actions.filter(a => a.action?.name?.name?.includes("web")).map(a => a.name),
442
- });
443
- }
444
- return opportunities;
445
- }
446
- function findNamingImprovements(actions) {
447
- const opportunities = [];
448
- // Check for generic names
449
- const genericPatterns = [
450
- /^node_\d+$/i,
451
- /^action_\d+$/i,
452
- /^step_\d+$/i,
453
- /^untitled/i,
454
- /^new_/i,
455
- ];
456
- const poorlyNamed = actions.filter(a => genericPatterns.some(p => p.test(a.name)) ||
457
- genericPatterns.some(p => p.test(a.actionDisplayName || "")));
458
- if (poorlyNamed.length > 0) {
459
- opportunities.push({
460
- id: "improve-naming",
461
- type: "improve_naming",
462
- severity: "suggestion",
463
- title: "Improve Node Naming",
464
- current_state: `${poorlyNamed.length} nodes have generic names: ${poorlyNamed.map(n => n.name).join(", ")}`,
465
- proposed_change: "Use descriptive names that indicate purpose (e.g., 'search_customer_faq', 'respond_billing_inquiry')",
466
- benefit: "Better maintainability, easier debugging, clearer workflow understanding",
467
- risk: "none",
468
- auto_applicable: false,
469
- affected_nodes: poorlyNamed.map(n => n.name),
470
- });
471
- }
472
- return opportunities;
473
- }
474
- function findSafetyImprovements(actions, graph) {
475
- const opportunities = [];
476
- // Check for external actions without HITL
477
- const sideEffectNodes = actions.filter(a => {
478
- const type = a.action?.name?.name || "";
479
- return type.includes("email") ||
480
- type.includes("external") ||
481
- type.includes("create_") ||
482
- type.includes("update_") ||
483
- type.includes("delete_");
484
- });
485
- const hitlNodes = actions.filter(a => a.action?.name?.name?.includes("hitl"));
486
- for (const sideEffect of sideEffectNodes) {
487
- const graphNode = graph.nodes.get(sideEffect.name);
488
- if (!graphNode)
489
- continue;
490
- // Check if there's a HITL node upstream
491
- const hasUpstreamHitl = graphNode.inputs.some(input => hitlNodes.some(h => h.name === input));
492
- if (!hasUpstreamHitl) {
493
- opportunities.push({
494
- id: `add-hitl-${sideEffect.name}`,
495
- type: "add_hitl_for_safety",
496
- severity: "important",
497
- title: "Add Human Approval Before Side Effect",
498
- current_state: `${sideEffect.name} can execute without human confirmation`,
499
- proposed_change: "Add HITL node before this action to confirm before execution",
500
- benefit: "Prevent unintended external actions, maintain human oversight",
501
- risk: "none",
502
- auto_applicable: false,
503
- affected_nodes: [sideEffect.name],
504
- implementation_hint: "Insert general_hitl between the previous node and this action, add both success and failure paths",
505
- });
506
- }
507
- }
508
- return opportunities;
509
- }
510
- // ─────────────────────────────────────────────────────────────────────────────
511
- // Health Score Calculation
512
- // ─────────────────────────────────────────────────────────────────────────────
513
- function calculateWorkflowHealth(actions, metrics, optimizations) {
514
- let score = 100;
515
- // Deduct for optimizations based on severity
516
- for (const opt of optimizations) {
517
- switch (opt.severity) {
518
- case "important":
519
- score -= 15;
520
- break;
521
- case "recommended":
522
- score -= 8;
523
- break;
524
- case "suggestion":
525
- score -= 3;
526
- break;
527
- }
528
- }
529
- // Deduct for high complexity
530
- if (metrics.complexity_score > 70) {
531
- score -= 10;
532
- }
533
- else if (metrics.complexity_score > 50) {
534
- score -= 5;
535
- }
536
- // Deduct for high redundancy
537
- if (metrics.redundancy_score > 30) {
538
- score -= 10;
539
- }
540
- else if (metrics.redundancy_score > 10) {
541
- score -= 5;
542
- }
543
- // Deduct for many sequential latency factors
544
- if (metrics.estimated_latency_factors > 5) {
545
- score -= 10;
546
- }
547
- else if (metrics.estimated_latency_factors > 3) {
548
- score -= 5;
549
- }
550
- // Bonus for good practices
551
- const hasTrigger = actions.some(a => a.action?.name?.name?.includes("trigger"));
552
- const hasResponse = actions.some(a => a.action?.name?.name?.includes("respond") ||
553
- a.action?.name?.name?.includes("llm"));
554
- if (hasTrigger && hasResponse) {
555
- score = Math.min(score + 5, 100);
556
- }
557
- return Math.max(score, 0);
558
- }
559
- function healthScoreToGrade(score) {
560
- if (score >= 90)
561
- return "A";
562
- if (score >= 80)
563
- return "B";
564
- if (score >= 70)
565
- return "C";
566
- if (score >= 60)
567
- return "D";
568
- return "F";
569
- }
570
- // ─────────────────────────────────────────────────────────────────────────────
571
- // Recommendations Generator
572
- // ─────────────────────────────────────────────────────────────────────────────
573
- function generateRecommendations(optimizations, metrics, healthScore) {
574
- const recommendations = [];
575
- // Top priority optimizations
576
- const important = optimizations.filter(o => o.severity === "important");
577
- if (important.length > 0) {
578
- recommendations.push(`🔴 Address ${important.length} important issue(s): ${important.map(o => o.title).join(", ")}`);
579
- }
580
- // Quick wins
581
- const quickWins = optimizations.filter(o => o.auto_applicable && o.risk === "none");
582
- if (quickWins.length > 0) {
583
- recommendations.push(`✅ ${quickWins.length} quick win(s) available for immediate improvement`);
584
- }
585
- // Complexity warning
586
- if (metrics.complexity_score > 60) {
587
- recommendations.push(`⚠️ High complexity (${metrics.complexity_score}/100). Consider simplifying or breaking into sub-workflows.`);
588
- }
589
- // Latency warning
590
- if (metrics.estimated_latency_factors > 4) {
591
- recommendations.push(`⏱️ High latency risk: ${metrics.estimated_latency_factors} sequential LLM/search calls. Consider parallelization.`);
592
- }
593
- // Redundancy warning
594
- if (metrics.redundancy_score > 20) {
595
- recommendations.push(`🔄 Redundancy detected (${metrics.redundancy_score}/100). Consider consolidating duplicate operations.`);
596
- }
597
- // Overall assessment
598
- if (healthScore >= 90) {
599
- recommendations.push("🌟 Excellent workflow health! Minor optimizations optional.");
600
- }
601
- else if (healthScore >= 70) {
602
- recommendations.push("👍 Good workflow health. Apply recommended optimizations for best results.");
603
- }
604
- else if (healthScore >= 50) {
605
- recommendations.push("⚠️ Workflow needs attention. Apply important fixes first.");
606
- }
607
- else {
608
- recommendations.push("🚨 Workflow needs significant improvements. Review all recommendations.");
609
- }
610
- return recommendations;
611
- }
612
- // ─────────────────────────────────────────────────────────────────────────────
613
- // Utilities
614
- // ─────────────────────────────────────────────────────────────────────────────
615
- function getQuerySource(action) {
616
- const queryInput = action.inputs?.query;
617
- const actionOutput = queryInput?.actionOutput;
618
- if (actionOutput) {
619
- return `${actionOutput.actionName}.${actionOutput.output}`;
620
- }
621
- return "unknown";
622
- }
623
- function hashString(str) {
624
- let hash = 0;
625
- for (let i = 0; i < str.length; i++) {
626
- const char = str.charCodeAt(i);
627
- hash = ((hash << 5) - hash) + char;
628
- hash = hash & hash;
629
- }
630
- return Math.abs(hash).toString(36).substring(0, 8);
631
- }
632
- /**
633
- * Get a human-readable summary of the optimization report
634
- */
635
- export function summarizeOptimizationReport(report) {
636
- const lines = [];
637
- lines.push(`## Workflow Health Report`);
638
- lines.push("");
639
- lines.push(`**Health Score**: ${report.workflow_health_score}/100 (Grade: ${report.grade})`);
640
- lines.push("");
641
- lines.push("### Metrics");
642
- lines.push(`- Nodes: ${report.metrics.total_nodes}`);
643
- lines.push(`- Max Depth: ${report.metrics.max_depth}`);
644
- lines.push(`- Branches: ${report.metrics.branch_count}`);
645
- lines.push(`- Complexity: ${report.metrics.complexity_score}/100`);
646
- lines.push(`- Redundancy: ${report.metrics.redundancy_score}/100`);
647
- lines.push(`- Latency Factors: ${report.metrics.estimated_latency_factors}`);
648
- lines.push("");
649
- if (report.quick_wins.length > 0) {
650
- lines.push("### Quick Wins");
651
- for (const opt of report.quick_wins) {
652
- lines.push(`- **${opt.title}**: ${opt.proposed_change}`);
653
- }
654
- lines.push("");
655
- }
656
- if (report.strategic.length > 0) {
657
- lines.push("### Strategic Improvements");
658
- for (const opt of report.strategic) {
659
- const severity = opt.severity === "important" ? "🔴" :
660
- opt.severity === "recommended" ? "🟡" : "🟢";
661
- lines.push(`${severity} **${opt.title}**: ${opt.current_state}`);
662
- lines.push(` → ${opt.proposed_change}`);
663
- }
664
- lines.push("");
665
- }
666
- lines.push("### Recommendations");
667
- for (const rec of report.recommendations) {
668
- lines.push(`- ${rec}`);
669
- }
670
- return lines.join("\n");
671
- }