@ema.co/mcp-toolkit 2026.1.25 → 2026.1.26-4

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.

Files changed (87) hide show
  1. package/README.md +10 -2
  2. package/dist/mcp/handlers/action/index.js +3 -18
  3. package/dist/mcp/handlers/data/index.js +385 -41
  4. package/dist/mcp/handlers/data/templates.js +107 -0
  5. package/dist/mcp/handlers/deprecation.js +50 -0
  6. package/dist/mcp/handlers/env/index.js +8 -4
  7. package/dist/mcp/handlers/knowledge/index.js +44 -237
  8. package/dist/mcp/handlers/persona/create.js +47 -18
  9. package/dist/mcp/handlers/persona/index.js +14 -11
  10. package/dist/mcp/handlers/persona/update.js +4 -2
  11. package/dist/mcp/handlers/persona/version.js +234 -0
  12. package/dist/mcp/handlers/sync/index.js +3 -18
  13. package/dist/mcp/handlers/template/index.js +75 -10
  14. package/dist/mcp/handlers/workflow/analyze.js +171 -0
  15. package/dist/mcp/handlers/workflow/compare.js +70 -0
  16. package/dist/mcp/handlers/workflow/deploy.js +73 -0
  17. package/dist/mcp/handlers/workflow/generate.js +350 -0
  18. package/dist/mcp/handlers/workflow/index.js +294 -0
  19. package/dist/mcp/handlers/workflow/modify.js +456 -0
  20. package/dist/mcp/handlers/workflow/optimize.js +136 -0
  21. package/dist/mcp/handlers/workflow/types.js +4 -0
  22. package/dist/mcp/handlers/workflow/utils.js +30 -0
  23. package/dist/mcp/handlers-consolidated.js +73 -2696
  24. package/dist/mcp/prompts.js +83 -43
  25. package/dist/mcp/resources.js +382 -57
  26. package/dist/mcp/server.js +199 -391
  27. package/dist/mcp/{tools-v2.js → tools.js} +20 -54
  28. package/dist/mcp/workflow-operations.js +2 -2
  29. package/dist/sdk/client-adapter.js +267 -32
  30. package/dist/sdk/client.js +45 -16
  31. package/dist/sdk/ema-client.js +183 -0
  32. package/dist/sdk/generated/deprecated-actions.js +171 -0
  33. package/dist/sdk/generated/template-fallbacks.js +123 -0
  34. package/dist/sdk/guidance.js +65 -11
  35. package/dist/sdk/index.js +3 -1
  36. package/dist/sdk/knowledge.js +139 -86
  37. package/dist/sdk/workflow-intent.js +27 -0
  38. package/dist/sdk/workflow-transformer.js +0 -342
  39. package/docs/mcp-tools-guide.md +37 -45
  40. package/package.json +10 -4
  41. package/dist/mcp/handlers/persona/analyze.js +0 -275
  42. package/dist/mcp/handlers/persona/compare.js +0 -32
  43. package/dist/mcp/tools-consolidated.js +0 -875
  44. package/dist/mcp/tools-legacy.js +0 -736
  45. package/docs/CODEBASE-ANALYSIS-2026-01-23.md +0 -936
  46. package/docs/CODEBASE-ANALYSIS-PRIORITIZED.md +0 -774
  47. package/docs/api-contracts.md +0 -216
  48. package/docs/auto-builder-analysis.md +0 -271
  49. package/docs/blog/mcp-tool-design-lessons.md +0 -309
  50. package/docs/data-architecture.md +0 -166
  51. package/docs/demos/ap-invoice-generation.md +0 -347
  52. package/docs/demos/ap-invoice-processing.md +0 -271
  53. package/docs/ema-auto-builder-guide.html +0 -394
  54. package/docs/lessons-learned.md +0 -209
  55. package/docs/llm-native-workflow-design.md +0 -252
  56. package/docs/local-generation.md +0 -508
  57. package/docs/mcp-flow-diagram.md +0 -135
  58. package/docs/migration/action-composition-migration.md +0 -270
  59. package/docs/naming-conventions.md +0 -278
  60. package/docs/proposals/HANDOFF-tool-restructure.md +0 -526
  61. package/docs/proposals/action-composition.md +0 -490
  62. package/docs/proposals/explicit-method-restructure.md +0 -328
  63. package/docs/proposals/mcp-tool-restructure-2026-01.md +0 -366
  64. package/docs/proposals/self-contained-guidance.md +0 -427
  65. package/docs/proto-sdk-generation.md +0 -242
  66. package/docs/release-impact.md +0 -102
  67. package/docs/release-process.md +0 -157
  68. package/docs/staging.RULE.md +0 -142
  69. package/docs/test-persona-creation.md +0 -196
  70. package/docs/tool-consolidation-v2.md +0 -225
  71. package/docs/tool-response-standards.md +0 -256
  72. package/resources/demo-kits/README.md +0 -175
  73. package/resources/demo-kits/finance-ap/manifest.json +0 -150
  74. package/resources/demo-kits/tags.json +0 -91
  75. package/resources/docs/getting-started.md +0 -97
  76. package/resources/templates/auto-builder-rules.md +0 -224
  77. package/resources/templates/chat-ai/README.md +0 -119
  78. package/resources/templates/chat-ai/persona-config.json +0 -111
  79. package/resources/templates/dashboard-ai/README.md +0 -156
  80. package/resources/templates/dashboard-ai/persona-config.json +0 -180
  81. package/resources/templates/demo-scenarios/README.md +0 -63
  82. package/resources/templates/demo-scenarios/test-published-package.md +0 -116
  83. package/resources/templates/document-gen-ai/README.md +0 -132
  84. package/resources/templates/document-gen-ai/persona-config.json +0 -316
  85. package/resources/templates/voice-ai/README.md +0 -123
  86. package/resources/templates/voice-ai/persona-config.json +0 -74
  87. package/resources/templates/voice-ai/workflow-prompt.md +0 -121
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Workflow Modify Handler
3
+ *
4
+ * Handles brownfield workflow modifications via STRUCTURED OPERATIONS.
5
+ *
6
+ * CRITICAL: This handler does NOT parse natural language.
7
+ * The Agent (LLM) builds structured operations and passes them here.
8
+ *
9
+ * See: src/mcp/AGENTS.md for anti-patterns to avoid.
10
+ */
11
+ import { detectWorkflowIssues, suggestWorkflowFixes } from "../../../sdk/knowledge.js";
12
+ import { sanitizeWorkflowForDeploy } from "./utils.js";
13
+ import { ensureSchemaRegistry } from "../../../sdk/workflow-validator.js";
14
+ // ─────────────────────────────────────────────────────────────────────────
15
+ // Action Building Helpers (use ActionRegistry for metadata)
16
+ // ─────────────────────────────────────────────────────────────────────────
17
+ /**
18
+ * Get action metadata from registry.
19
+ * Falls back to defaults if action not found.
20
+ */
21
+ function getActionMetadata(actionType, registry) {
22
+ // Try to get from registry first
23
+ if (registry?.isLoaded()) {
24
+ const action = registry.getAction(actionType);
25
+ if (action) {
26
+ return {
27
+ namespaces: ["actions", "emainternal"], // Standard namespace
28
+ version: action.version,
29
+ apiName: actionType,
30
+ };
31
+ }
32
+ }
33
+ // Fallback defaults (registry should have these, but just in case)
34
+ return {
35
+ namespaces: ["actions", "emainternal"],
36
+ version: "v0",
37
+ apiName: actionType,
38
+ };
39
+ }
40
+ /**
41
+ * Build an input binding for a workflow action.
42
+ */
43
+ function buildInputBinding(config) {
44
+ switch (config.type) {
45
+ case "action_output":
46
+ return {
47
+ actionOutput: {
48
+ actionName: config.actionName,
49
+ output: config.output,
50
+ },
51
+ autoDetectedBinding: false,
52
+ };
53
+ case "inline_string":
54
+ return {
55
+ inline: { wellKnown: { stringValue: config.value } },
56
+ autoDetectedBinding: false,
57
+ };
58
+ case "inline_number":
59
+ return {
60
+ inline: { number: config.value },
61
+ autoDetectedBinding: false,
62
+ };
63
+ case "llm_inferred":
64
+ default:
65
+ return { llmInferred: {} };
66
+ }
67
+ }
68
+ /**
69
+ * Build a complete workflow action structure from InsertNodeConfig.
70
+ */
71
+ function buildWorkflowAction(config, registry) {
72
+ const nodeName = config.node_name || `${config.action_type}_${Date.now()}`;
73
+ const metadata = getActionMetadata(config.action_type, registry);
74
+ const action = {
75
+ name: nodeName,
76
+ action: {
77
+ name: {
78
+ namespaces: metadata.namespaces,
79
+ name: metadata.apiName,
80
+ },
81
+ version: metadata.version,
82
+ },
83
+ inputs: {},
84
+ displaySettings: {
85
+ displayName: config.display_name,
86
+ description: config.description || "",
87
+ coordinates: { x: 1200, y: 400 },
88
+ },
89
+ typeArguments: {},
90
+ tools: [],
91
+ disableHumanInteraction: false,
92
+ };
93
+ // Build inputs from config
94
+ if (config.inputs) {
95
+ const inputs = {};
96
+ for (const [inputName, binding] of Object.entries(config.inputs)) {
97
+ inputs[inputName] = buildInputBinding(binding);
98
+ }
99
+ action.inputs = inputs;
100
+ }
101
+ // Build runIf condition if provided
102
+ if (config.runIf) {
103
+ const operatorMap = { eq: 1, neq: 2 };
104
+ action.runIf = {
105
+ lhs: {
106
+ actionOutput: {
107
+ actionName: config.runIf.sourceAction,
108
+ output: config.runIf.sourceOutput,
109
+ },
110
+ autoDetectedBinding: false,
111
+ },
112
+ operator: operatorMap[config.runIf.operator] || 1,
113
+ rhs: {
114
+ inline: { enumValue: config.runIf.value },
115
+ autoDetectedBinding: false,
116
+ },
117
+ };
118
+ }
119
+ return action;
120
+ }
121
+ // ─────────────────────────────────────────────────────────────────────────
122
+ // Operation Executors
123
+ // ─────────────────────────────────────────────────────────────────────────
124
+ /**
125
+ * Execute an insert operation.
126
+ */
127
+ function executeInsert(actions, op, registry, result) {
128
+ const config = op.insert;
129
+ if (!config) {
130
+ result.changesApplied.push("Insert failed: no configuration provided");
131
+ return;
132
+ }
133
+ if (!config.action_type || !config.display_name) {
134
+ result.changesApplied.push("Insert failed: action_type and display_name required");
135
+ return;
136
+ }
137
+ // Build the new action
138
+ const newAction = buildWorkflowAction(config, registry);
139
+ const newNodeName = String(newAction.name);
140
+ // Add to actions array
141
+ actions.push(newAction);
142
+ result.nodesAdded.push(newNodeName);
143
+ result.changesApplied.push(`Inserted node "${newNodeName}" (${config.display_name})`);
144
+ // Handle insert_before: modify downstream node
145
+ if (config.insert_before) {
146
+ const targetNode = actions.find(a => String(a.name || "").toLowerCase() === config.insert_before.toLowerCase());
147
+ if (!targetNode) {
148
+ result.changesApplied.push(`Warning: target node "${config.insert_before}" not found for downstream modification`);
149
+ return;
150
+ }
151
+ // Option 1: Add runIf to downstream node
152
+ if (config.add_runif_to_downstream && config.new_node_output) {
153
+ const operatorMap = { eq: 1, neq: 2 };
154
+ targetNode.runIf = {
155
+ lhs: {
156
+ actionOutput: {
157
+ actionName: newNodeName,
158
+ output: config.new_node_output,
159
+ },
160
+ autoDetectedBinding: false,
161
+ },
162
+ operator: operatorMap["eq"],
163
+ rhs: {
164
+ inline: { enumValue: "HITL Success" },
165
+ autoDetectedBinding: false,
166
+ },
167
+ };
168
+ result.nodesModified.push(String(targetNode.name));
169
+ result.changesApplied.push(`Added runIf to ${targetNode.name}: waits for ${newNodeName}.${config.new_node_output}`);
170
+ }
171
+ }
172
+ }
173
+ /**
174
+ * Execute a remove operation.
175
+ */
176
+ function executeRemove(actions, op, result) {
177
+ const removeConfig = op.remove;
178
+ if (!removeConfig?.nodes?.length) {
179
+ result.changesApplied.push("Remove failed: no nodes specified");
180
+ return;
181
+ }
182
+ for (const nodeName of removeConfig.nodes) {
183
+ const index = actions.findIndex(a => String(a.name || "") === nodeName);
184
+ if (index >= 0) {
185
+ actions.splice(index, 1);
186
+ result.nodesRemoved.push(nodeName);
187
+ result.changesApplied.push(`Removed node: ${nodeName}`);
188
+ }
189
+ else {
190
+ result.changesApplied.push(`Warning: node "${nodeName}" not found`);
191
+ }
192
+ }
193
+ }
194
+ /**
195
+ * Execute a rewire operation.
196
+ */
197
+ function executeRewire(actions, op, result) {
198
+ const rewireConfig = op.rewire;
199
+ if (!rewireConfig) {
200
+ result.changesApplied.push("Rewire failed: no configuration provided");
201
+ return;
202
+ }
203
+ const { target_node, target_input, source_node, source_output } = rewireConfig;
204
+ // Find target node
205
+ const targetAction = actions.find(a => String(a.name || "").toLowerCase() === target_node.toLowerCase());
206
+ if (!targetAction) {
207
+ result.changesApplied.push(`Rewire failed: target node "${target_node}" not found`);
208
+ return;
209
+ }
210
+ // Update the input binding
211
+ const inputs = (targetAction.inputs || {});
212
+ inputs[target_input] = {
213
+ actionOutput: {
214
+ actionName: source_node,
215
+ output: source_output,
216
+ },
217
+ autoDetectedBinding: false,
218
+ };
219
+ targetAction.inputs = inputs;
220
+ result.nodesModified.push(target_node);
221
+ result.connectionsChanged.push(`${source_node}.${source_output} → ${target_node}.${target_input}`);
222
+ result.changesApplied.push(`Rewired ${target_node}.${target_input} to use ${source_node}.${source_output}`);
223
+ }
224
+ /**
225
+ * Execute a config update operation.
226
+ */
227
+ function executeConfigUpdate(workflow, op, result) {
228
+ const configUpdates = op.config;
229
+ if (!configUpdates) {
230
+ result.changesApplied.push("Config update failed: no configuration provided");
231
+ return;
232
+ }
233
+ // Config updates are typically applied to proto_config, not workflow_def
234
+ // Store them for the caller to apply
235
+ result.changesApplied.push(`Config updates prepared: ${Object.keys(configUpdates).join(", ")}`);
236
+ }
237
+ // ─────────────────────────────────────────────────────────────────────────
238
+ // Main Operation Applier
239
+ // ─────────────────────────────────────────────────────────────────────────
240
+ /**
241
+ * Apply structured modification operations to a workflow.
242
+ *
243
+ * IMPORTANT: This function executes STRUCTURED operations.
244
+ * It does NOT parse natural language - that's the Agent's job.
245
+ */
246
+ function applyWorkflowModifications(existingWorkflow, operations, registry) {
247
+ // Deep copy workflow
248
+ const workflow = JSON.parse(JSON.stringify(existingWorkflow));
249
+ const actions = (workflow.actions || []);
250
+ const result = {
251
+ workflow,
252
+ changesApplied: [],
253
+ nodesAdded: [],
254
+ nodesRemoved: [],
255
+ nodesModified: [],
256
+ connectionsChanged: [],
257
+ };
258
+ for (const op of operations) {
259
+ switch (op.type) {
260
+ case "insert":
261
+ executeInsert(actions, op, registry, result);
262
+ break;
263
+ case "remove":
264
+ executeRemove(actions, op, result);
265
+ break;
266
+ case "rewire":
267
+ executeRewire(actions, op, result);
268
+ break;
269
+ case "update_config":
270
+ executeConfigUpdate(workflow, op, result);
271
+ break;
272
+ case "consolidate":
273
+ // Consolidation (merging multiple nodes into one) is a multi-step operation
274
+ // that requires the Agent to use multiple operations instead:
275
+ // 1. Add the new consolidated node (insert)
276
+ // 2. Rewire connections to use the new node (rewire)
277
+ // 3. Remove the old nodes (remove)
278
+ // Mark as not executed - Agent should break this down
279
+ result.changesApplied.push("SKIPPED: consolidate not supported as a single operation. " +
280
+ "Use insert + rewire + remove operations instead.");
281
+ break;
282
+ default:
283
+ result.changesApplied.push(`Unknown operation type: ${op.type}`);
284
+ }
285
+ }
286
+ workflow.actions = actions;
287
+ result.workflow = workflow;
288
+ return result;
289
+ }
290
+ // ─────────────────────────────────────────────────────────────────────────
291
+ // Main Handler
292
+ // ─────────────────────────────────────────────────────────────────────────
293
+ /**
294
+ * Handle workflow modify/extend mode.
295
+ *
296
+ * This handler accepts STRUCTURED OPERATIONS from the Agent.
297
+ * It does NOT parse natural language input.
298
+ *
299
+ * @param args.persona_id - The persona to modify
300
+ * @param args.operations - Array of structured ModificationOperation
301
+ * @param args.preview - If true, return preview without deploying
302
+ */
303
+ export async function handleWorkflowModify(args, client) {
304
+ const personaId = args.persona_id || args.id;
305
+ if (!personaId) {
306
+ return {
307
+ error: "persona_id required",
308
+ hint: "Specify the persona to modify: persona_id='<id>'",
309
+ };
310
+ }
311
+ // Get operations from args
312
+ const operations = args.operations;
313
+ const preview = Boolean(args.preview);
314
+ // If no structured operations provided, return guidance
315
+ if (!operations || operations.length === 0) {
316
+ // Fetch persona for context
317
+ const persona = await client.getPersonaById(personaId);
318
+ if (!persona) {
319
+ return { error: `Persona "${personaId}" not found` };
320
+ }
321
+ const workflow = persona.workflow_def;
322
+ if (!workflow) {
323
+ return { error: "Persona has no workflow_def" };
324
+ }
325
+ const actions = (workflow.actions || []);
326
+ // Run analysis to give Agent context
327
+ const issues = detectWorkflowIssues(workflow);
328
+ const suggestions = suggestWorkflowFixes(issues);
329
+ // Load schema registry for action catalog
330
+ const schemaRegistry = await ensureSchemaRegistry(client);
331
+ const availableActions = schemaRegistry.isLoaded()
332
+ ? schemaRegistry.getAllActions().map(a => ({ name: a.name, category: a.category, description: a.description }))
333
+ : [];
334
+ return {
335
+ message: "No operations provided. Returning workflow context for Agent to build operations.",
336
+ persona_id: personaId,
337
+ persona_name: persona.name,
338
+ // Current workflow state
339
+ current_nodes: actions.map(a => ({
340
+ name: a.name,
341
+ display_name: a.displaySettings?.displayName,
342
+ action_type: a.action?.name?.name,
343
+ })),
344
+ // Analysis
345
+ issues: issues.map(i => ({ severity: i.severity, type: i.type, node: i.node, reason: i.reason })),
346
+ suggestions,
347
+ // Available actions from registry
348
+ available_actions: availableActions.slice(0, 20), // Top 20 for context
349
+ // Guidance for Agent
350
+ _tip: "Build ModificationOperation[] and pass as 'operations' parameter",
351
+ _next_step: "Use the current_nodes and available_actions to build structured operations",
352
+ // Example operation structures
353
+ example_operations: {
354
+ insert_hitl: {
355
+ type: "insert",
356
+ insert: {
357
+ action_type: "hitl",
358
+ display_name: "Approval Required",
359
+ insert_before: "target_node_name",
360
+ add_runif_to_downstream: true,
361
+ new_node_output: "hitl_status_HITL Success",
362
+ },
363
+ },
364
+ remove_nodes: {
365
+ type: "remove",
366
+ remove: { nodes: ["node_to_remove"] },
367
+ },
368
+ rewire: {
369
+ type: "rewire",
370
+ rewire: {
371
+ target_node: "downstream_node",
372
+ target_input: "query",
373
+ source_node: "upstream_node",
374
+ source_output: "response",
375
+ },
376
+ },
377
+ },
378
+ };
379
+ }
380
+ // Fetch persona
381
+ const persona = await client.getPersonaById(personaId);
382
+ if (!persona) {
383
+ return { error: `Persona "${personaId}" not found` };
384
+ }
385
+ const workflow = persona.workflow_def;
386
+ if (!workflow) {
387
+ return { error: "Persona has no workflow_def to modify" };
388
+ }
389
+ // Load schema registry for action metadata
390
+ const schemaRegistry = await ensureSchemaRegistry(client);
391
+ // Apply the structured operations
392
+ const result = applyWorkflowModifications(workflow, operations, schemaRegistry);
393
+ // Sanitize before deploy
394
+ const sanitized = sanitizeWorkflowForDeploy(result.workflow);
395
+ // Run validation
396
+ const postIssues = detectWorkflowIssues(sanitized);
397
+ const errors = postIssues.filter(i => i.severity === "critical");
398
+ if (preview) {
399
+ return {
400
+ preview: true,
401
+ persona_id: personaId,
402
+ persona_name: persona.name,
403
+ // What would change
404
+ changes_applied: result.changesApplied,
405
+ nodes_added: result.nodesAdded,
406
+ nodes_removed: result.nodesRemoved,
407
+ nodes_modified: result.nodesModified,
408
+ connections_changed: result.connectionsChanged,
409
+ // Validation
410
+ validation_errors: errors.map(e => e.reason || e.type),
411
+ validation_warnings: postIssues.filter(i => i.severity === "warning").map(w => w.reason || w.type),
412
+ // The modified workflow
413
+ modified_workflow: sanitized,
414
+ _tip: errors.length > 0
415
+ ? "Fix validation errors before deploying"
416
+ : "Preview looks good. Call without preview=true to deploy",
417
+ };
418
+ }
419
+ // Deploy if validation passes
420
+ if (errors.length > 0) {
421
+ return {
422
+ error: "Validation failed - cannot deploy",
423
+ validation_errors: errors.map(e => e.reason || e.type),
424
+ changes_attempted: result.changesApplied,
425
+ _tip: "Fix validation errors and try again",
426
+ };
427
+ }
428
+ // Deploy the workflow
429
+ try {
430
+ await client.updateAiEmployee({
431
+ persona_id: personaId,
432
+ workflow: sanitized,
433
+ });
434
+ return {
435
+ success: true,
436
+ persona_id: personaId,
437
+ persona_name: persona.name,
438
+ changes_applied: result.changesApplied,
439
+ nodes_added: result.nodesAdded,
440
+ nodes_removed: result.nodesRemoved,
441
+ nodes_modified: result.nodesModified,
442
+ connections_changed: result.connectionsChanged,
443
+ _tip: "Workflow updated successfully",
444
+ };
445
+ }
446
+ catch (error) {
447
+ return {
448
+ error: `Deploy failed: ${error instanceof Error ? error.message : String(error)}`,
449
+ changes_attempted: result.changesApplied,
450
+ };
451
+ }
452
+ }
453
+ // ─────────────────────────────────────────────────────────────────────────
454
+ // Exports
455
+ // ─────────────────────────────────────────────────────────────────────────
456
+ export { applyWorkflowModifications, buildWorkflowAction, buildInputBinding };
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Workflow Optimize Handler
3
+ *
4
+ * Detects and fixes workflow issues.
5
+ */
6
+ import { detectWorkflowIssues, suggestWorkflowFixes } from "../../../sdk/knowledge.js";
7
+ /**
8
+ * Apply simple workflow fixes for auto_fixable issues
9
+ * Handles orphan removal with cascading dependency cleanup
10
+ */
11
+ function applySimpleFixes(workflow, issues) {
12
+ // Deep clone to avoid mutating original
13
+ const fixed = JSON.parse(JSON.stringify(workflow));
14
+ // Get nodes array from workflow (handle different structures)
15
+ const getNodes = (w) => {
16
+ if (Array.isArray(w.nodes))
17
+ return w.nodes;
18
+ const wd = w.workflow_def;
19
+ if (wd && Array.isArray(wd.nodes))
20
+ return wd.nodes;
21
+ return undefined;
22
+ };
23
+ const nodes = getNodes(fixed);
24
+ if (!nodes)
25
+ return fixed;
26
+ // Collect all orphan node IDs to remove
27
+ const nodesToRemove = new Set();
28
+ for (const issue of issues) {
29
+ if (issue.type === "orphan" && issue.auto_fixable && issue.node) {
30
+ nodesToRemove.add(issue.node);
31
+ }
32
+ }
33
+ if (nodesToRemove.size === 0)
34
+ return fixed;
35
+ // Step 1: Remove orphan nodes
36
+ const filteredNodes = nodes.filter(n => {
37
+ const nodeId = n.id;
38
+ return nodeId && !nodesToRemove.has(nodeId);
39
+ });
40
+ // Step 2: Clean up dangling references in remaining nodes
41
+ for (const node of filteredNodes) {
42
+ const incomingEdges = node.incoming_edges;
43
+ if (incomingEdges && Array.isArray(incomingEdges)) {
44
+ // Filter out edges that reference removed nodes
45
+ node.incoming_edges = incomingEdges.filter(edge => {
46
+ const sourceNodeId = edge.source_node_id;
47
+ return sourceNodeId && !nodesToRemove.has(sourceNodeId);
48
+ });
49
+ }
50
+ // Also clean up inputBindings if they exist (different workflow format)
51
+ const inputBindings = node.inputBindings;
52
+ if (inputBindings && Array.isArray(inputBindings)) {
53
+ node.inputBindings = inputBindings.filter(binding => {
54
+ const actionOutput = binding.actionOutput;
55
+ const actionName = actionOutput?.actionName;
56
+ return !actionName || !nodesToRemove.has(actionName);
57
+ });
58
+ }
59
+ }
60
+ // Update nodes in the workflow
61
+ if (Array.isArray(fixed.nodes)) {
62
+ fixed.nodes = filteredNodes;
63
+ }
64
+ else {
65
+ const wd = fixed.workflow_def;
66
+ if (wd) {
67
+ wd.nodes = filteredNodes;
68
+ }
69
+ }
70
+ return fixed;
71
+ }
72
+ /**
73
+ * Handle workflow optimize mode
74
+ */
75
+ export async function handleWorkflowOptimize(args, client) {
76
+ const personaId = args.persona_id;
77
+ const preview = args.preview !== false;
78
+ if (!personaId) {
79
+ return { error: "persona_id required for optimize mode" };
80
+ }
81
+ const persona = await client.getPersonaById(personaId);
82
+ if (!persona) {
83
+ return { error: `Persona not found: ${personaId}` };
84
+ }
85
+ const existingWorkflow = persona.workflow_def;
86
+ if (!existingWorkflow) {
87
+ return {
88
+ error: `Persona "${persona.name}" has no workflow to optimize`,
89
+ hint: "Use mode='generate' to create a workflow first",
90
+ };
91
+ }
92
+ // Analyze and detect issues
93
+ const issues = detectWorkflowIssues(existingWorkflow);
94
+ const fixes = suggestWorkflowFixes(issues);
95
+ if (issues.length === 0) {
96
+ return {
97
+ mode: "optimize",
98
+ status: "✅ No issues found",
99
+ persona_id: personaId,
100
+ persona_name: persona.name,
101
+ workflow_healthy: true,
102
+ };
103
+ }
104
+ // Apply fixes
105
+ const fixedWorkflow = applySimpleFixes(existingWorkflow, issues);
106
+ const result = {
107
+ mode: "optimize",
108
+ status: preview ? "preview" : "deployed",
109
+ persona_id: personaId,
110
+ persona_name: persona.name,
111
+ issues_found: issues.length,
112
+ issues: issues.map(i => ({
113
+ type: i.type,
114
+ severity: i.severity,
115
+ reason: i.reason,
116
+ })),
117
+ suggested_fixes: fixes,
118
+ fixed_workflow: fixedWorkflow,
119
+ };
120
+ // If preview=false, deploy the fixed workflow
121
+ if (!preview) {
122
+ await client.updateAiEmployee({
123
+ persona_id: personaId,
124
+ workflow: fixedWorkflow,
125
+ proto_config: args.proto_config || persona.proto_config,
126
+ });
127
+ result.deployed = true;
128
+ }
129
+ else {
130
+ result.next_steps = [
131
+ "Review the suggested fixes and fixed_workflow",
132
+ `Deploy with: workflow(mode="optimize", persona_id="${personaId}", preview=false)`,
133
+ ];
134
+ }
135
+ return result;
136
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Shared types for workflow handlers
3
+ */
4
+ export {};
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shared utilities for workflow handlers
3
+ */
4
+ /**
5
+ * Sanitize workflow for deployment
6
+ * Removes internal-only fields, validates structure
7
+ */
8
+ export function sanitizeWorkflowForDeploy(workflow) {
9
+ const sanitized = { ...workflow };
10
+ // Remove internal fields that shouldn't be sent to API
11
+ delete sanitized._internal;
12
+ delete sanitized._metadata;
13
+ delete sanitized._debug;
14
+ // Ensure actions array exists
15
+ if (!sanitized.actions) {
16
+ sanitized.actions = [];
17
+ }
18
+ // Validate and clean actions
19
+ const actions = sanitized.actions;
20
+ sanitized.actions = actions.filter(a => {
21
+ // Must have name
22
+ if (!a.name || typeof a.name !== "string")
23
+ return false;
24
+ // Must have id
25
+ if (!a.id && !a.name)
26
+ return false;
27
+ return true;
28
+ });
29
+ return sanitized;
30
+ }