@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
@@ -4,9 +4,9 @@
4
4
  * Each handler dispatches based on mode/flags following Unix CLI patterns.
5
5
  */
6
6
  // Import shared utilities from handlers module
7
- import { normalizeTriggerType, getTemplates, resolvePersona, } from "./handlers/index.js";
7
+ import { resolvePersona, } from "./handlers/index.js";
8
8
  // Import extracted persona mode handlers
9
- import { hasExtractedHandler, getPersonaModeHandler, } from "./handlers/persona/index.js";
9
+ import { hasExtractedHandler, getPersonaModeHandler, handleVersion, } from "./handlers/persona/index.js";
10
10
  // Import extracted handlers (Phase 1 modularization)
11
11
  import { handleEnv as handleEnvExtracted } from "./handlers/env/index.js";
12
12
  import { handleTemplate as handleTemplateExtracted } from "./handlers/template/index.js";
@@ -14,156 +14,38 @@ import { handleAction as handleActionExtracted } from "./handlers/action/index.j
14
14
  import { handleReference as handleReferenceExtracted } from "./handlers/reference/index.js";
15
15
  import { handleKnowledge as handleKnowledgeExtracted } from "./handlers/knowledge/index.js";
16
16
  import { handleSync as handleSyncExtracted } from "./handlers/sync/index.js";
17
- import { createVersionStorage } from "../sdk/version-storage.js";
18
- import { createVersionPolicyEngine } from "../sdk/version-policy.js";
19
- import { detectWorkflowIssues, validateWorkflowConnections, suggestWorkflowFixes, } from "../sdk/knowledge.js";
20
- import { compileWorkflow } from "../sdk/workflow-generator.js";
21
- import { ensureActionRegistry } from "../sdk/action-registry.js";
22
- import { parseInput, intentToSpec, generateWorkflow } from "../sdk/workflow-intent.js";
23
- import { ensureSchemaRegistry, validateWorkflowSpec, generateActionCatalogForLLM } from "../sdk/workflow-validator.js";
24
- import { runIntentArchitect } from "../sdk/intent-architect.js";
25
- import { analyzeExecutionFlow, generateASCIIFlow } from "../sdk/workflow-execution-analyzer.js";
26
- import { autoFixWorkflow } from "../sdk/workflow-fixer.js";
27
- import { SanitizationSession, detectWithPatterns, } from "../sdk/sanitizer.js";
17
+ // Import extracted workflow handler (Phase 2 modularization)
18
+ // Note: Imported as "Extracted" to avoid conflict with local definition during transition
19
+ // The local handleWorkflow will be removed once all tests pass
20
+ import { handleWorkflow as handleWorkflowExtracted } from "./handlers/workflow/index.js";
28
21
  // Args type imported from handlers/types.ts
29
22
  // ─────────────────────────────────────────────────────────────────────────────
30
- // Generation Templates for data(mode="generate")
23
+ // Generation Templates - MOVED to handlers/data/templates.ts
24
+ // Import via: import { GENERATION_TEMPLATES } from "./handlers/data/templates.js"
31
25
  // ─────────────────────────────────────────────────────────────────────────────
32
- /**
33
- * Built-in templates for document generation.
34
- * Used by data(mode="generate", from="...").
35
- */
36
- export const GENERATION_TEMPLATES = {
37
- // Reference data
38
- "countries": {
39
- description: "ISO 3166-1 countries with code and name",
40
- category: "reference",
41
- prompt: "Generate a JSON array of {count} ISO 3166-1 countries with fields: code (2-letter), name. Format: {format}",
42
- },
43
- "industries": {
44
- description: "NAICS industry classifications",
45
- category: "reference",
46
- prompt: "Generate a JSON array of {count} NAICS industry classifications with fields: code, name, description. Format: {format}",
47
- },
48
- "currencies": {
49
- description: "ISO 4217 currencies",
50
- category: "reference",
51
- prompt: "Generate a JSON array of {count} ISO 4217 currencies with fields: code (3-letter), name, symbol. Format: {format}",
52
- },
53
- // Demo kits
54
- "demo-sales-sdr": {
55
- description: "Sales SDR demo kit with prospects, products, and Q&A",
56
- category: "demo",
57
- prompt: `Generate a comprehensive sales demo kit in {format} format containing:
58
- 1. {count} realistic B2B prospect profiles (company name, contact, role, pain points, budget)
59
- 2. 3-5 product entries with features and pricing
60
- 3. 10 common sales objections with responses
61
- 4. Sample discovery call script`,
62
- },
63
- "demo-support": {
64
- description: "Customer support demo kit with customers, FAQs, and tickets",
65
- category: "demo",
66
- prompt: `Generate a customer support demo kit in {format} format containing:
67
- 1. {count} customer profiles (name, company, subscription tier, history)
68
- 2. 10 frequently asked questions with detailed answers
69
- 3. 5 sample support tickets with resolutions
70
- 4. Escalation guidelines`,
71
- },
72
- "demo-hr": {
73
- description: "HR assistant demo kit with employees, policies, and benefits",
74
- category: "demo",
75
- prompt: `Generate an HR demo kit in {format} format containing:
76
- 1. {count} employee profiles (name, department, role, start date)
77
- 2. 5 company policies (PTO, remote work, expenses)
78
- 3. 3 benefits descriptions (health, 401k, equity)
79
- 4. Onboarding checklist`,
80
- },
81
- // Entity templates
82
- "customer": {
83
- description: "B2B customer profile",
84
- category: "entity",
85
- prompt: `Generate {count} realistic B2B customer profiles in {format} format with fields:
86
- - company_name, industry, size (employees), annual_revenue
87
- - primary_contact (name, title, email, phone)
88
- - pain_points (array of 2-3 business challenges)
89
- - current_solutions (what they use today)
90
- - budget_range, decision_timeline`,
91
- },
92
- "product": {
93
- description: "Product catalog entry with pricing",
94
- category: "entity",
95
- prompt: `Generate {count} product catalog entries in {format} format with fields:
96
- - name, sku, category
97
- - description (2-3 sentences)
98
- - features (array of 3-5 key features)
99
- - pricing (base_price, currency, billing_period)
100
- - availability (in_stock, lead_time)`,
101
- },
102
- "faq": {
103
- description: "FAQ entries",
104
- category: "entity",
105
- prompt: `Generate {count} FAQ entries in {format} format with fields:
106
- - question (clear, specific question)
107
- - answer (comprehensive answer, 2-4 sentences)
108
- - category (e.g., billing, technical, getting-started)
109
- - related_questions (array of 2-3 related question titles)`,
110
- },
111
- "employee": {
112
- description: "Employee profile for HR systems",
113
- category: "entity",
114
- prompt: `Generate {count} employee profiles in {format} format with fields:
115
- - name, email, employee_id
116
- - department, title, manager
117
- - start_date, location (office or remote)
118
- - skills (array), certifications (array)`,
119
- },
120
- };
121
26
  // ─────────────────────────────────────────────────────────────────────────────
122
27
  // Widget Validation Helpers
123
28
  // ─────────────────────────────────────────────────────────────────────────────
124
29
  // isValidWidget and sanitizeWidgets imported from ../sdk/proto-config.js
125
30
  // validateWidgetsForApi imported from handlers/utils.ts
126
- // ─────────────────────────────────────────────────────────────────────────────
127
- // Deprecation tracking - helps users migrate to new param names
128
- // ─────────────────────────────────────────────────────────────────────────────
129
- const DEPRECATED_PARAMS = {
130
- identifier: { newName: "id", removeVersion: "2.0.0" },
131
- clone_from: { newName: "from", removeVersion: "2.0.0" },
132
- template_id: { newName: "from", removeVersion: "2.0.0" },
133
- clone_data: { newName: "include_data", removeVersion: "2.0.0" },
134
- };
135
- /**
136
- * Check for deprecated params and collect warnings.
137
- * Returns array of warning messages for deprecated params that were used.
138
- */
139
- function checkDeprecatedParams(args) {
140
- const warnings = [];
141
- for (const [oldName, { newName, removeVersion }] of Object.entries(DEPRECATED_PARAMS)) {
142
- if (args[oldName] !== undefined) {
143
- warnings.push(`'${oldName}' is deprecated, use '${newName}' instead (will be removed in v${removeVersion})`);
144
- }
145
- }
146
- return warnings;
147
- }
148
- /**
149
- * Add deprecation warnings to response if any deprecated params were used.
150
- */
151
- function addDeprecationWarnings(result, warnings) {
152
- if (warnings.length > 0) {
153
- return { ...result, _deprecation_warnings: warnings };
154
- }
155
- return result;
156
- }
31
+ // Deprecation tracking - single source of truth
32
+ import { checkDeprecatedParams, addDeprecationWarnings } from "./handlers/deprecation.js";
157
33
  // Template utilities imported from handlers/utils.ts:
158
34
  // - normalizeTriggerType
159
35
  // - getTemplates
160
36
  // - clearTemplateCache
161
37
  // - getPersonaTypeFromTemplate
162
38
  // ═══════════════════════════════════════════════════════════════════════════
163
- // ENV Handler
39
+ // Extracted Handler Exports
164
40
  // ═══════════════════════════════════════════════════════════════════════════
165
- // Delegate to extracted handler
41
+ // These handlers are fully extracted to handlers/*/ directories.
42
+ // Re-exported here for backwards compatibility with server.ts imports.
166
43
  export const handleEnv = handleEnvExtracted;
44
+ export const handleAction = handleActionExtracted;
45
+ export const handleTemplate = handleTemplateExtracted;
46
+ export const handleKnowledge = handleKnowledgeExtracted;
47
+ export const handleReference = handleReferenceExtracted;
48
+ export const handleSync = handleSyncExtracted;
167
49
  // ═══════════════════════════════════════════════════════════════════════════
168
50
  // PERSONA Handler
169
51
  // ═══════════════════════════════════════════════════════════════════════════
@@ -243,7 +125,9 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
243
125
  const fs = await import("fs/promises");
244
126
  return fs.readFile(path);
245
127
  };
246
- return handleData(dataArgs, client, readFile);
128
+ // Use extracted handler
129
+ const { handleData: handleDataExtracted } = await import("./handlers/data/index.js");
130
+ return handleDataExtracted(dataArgs, client, readFile);
247
131
  }
248
132
  // ═══════════════════════════════════════════════════════════════════════════
249
133
  // ROUTING: Workflow modifications
@@ -253,11 +137,18 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
253
137
  const workflowSpec = args.workflow_spec;
254
138
  const input = args.input;
255
139
  // ───────────────────────────────────────────────────────────────────────────
256
- // CRITICAL FIX: Natural language `input` goes to handleWorkflow
257
- // handleWorkflow has the FULL node addition/modification/removal logic
258
- // handleUpdate can only rewire/remove nodes, NOT add new ones
140
+ // LLM-DRIVEN ARCHITECTURE: Route workflow modifications to handleWorkflow
141
+ //
142
+ // handleWorkflow accepts STRUCTURED OPERATIONS from the Agent.
143
+ // It does NOT parse natural language - that's the Agent's job.
144
+ //
145
+ // Route to handleWorkflow when:
146
+ // - mode === "modify" (explicit modify mode)
147
+ // - mode === "update" with `operations` (Agent built structured ops)
148
+ // - mode === "update" with `input` (legacy - returns context for Agent)
259
149
  // ───────────────────────────────────────────────────────────────────────────
260
- if (mode === "update" && idOrName && input) {
150
+ const operations = args.operations;
151
+ if ((mode === "modify" || (mode === "update" && (input || operations))) && idOrName) {
261
152
  const workflowArgs = {
262
153
  ...args,
263
154
  persona_id: idOrName,
@@ -265,14 +156,13 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
265
156
  };
266
157
  delete workflowArgs.id;
267
158
  delete workflowArgs.identifier;
268
- return handleWorkflow(workflowArgs, client, getTemplateId);
159
+ return handleWorkflowExtracted(workflowArgs, client, getTemplateId);
269
160
  }
270
- // mode="update" without input → modular handleUpdate (config/workflow_spec changes only)
161
+ // mode="update" without input/operations → modular handleUpdate (config/workflow_spec changes only)
271
162
  if (mode === "update" && idOrName) {
272
163
  return getPersonaModeHandler("update")(args, client);
273
164
  }
274
- // Workflow operations (optimize, etc.) → handleWorkflow (legacy)
275
- // TODO: Deprecate handleWorkflow and move remaining logic to modular handlers
165
+ // Workflow operations (optimize, etc.) → handleWorkflowExtracted (extracted handler)
276
166
  if (optimize || (idOrName && workflowDef && mode !== "update")) {
277
167
  // Map persona args to workflow args
278
168
  const workflowArgs = {
@@ -281,7 +171,7 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
281
171
  };
282
172
  delete workflowArgs.id;
283
173
  delete workflowArgs.identifier;
284
- return handleWorkflow(workflowArgs, client, getTemplateId);
174
+ return handleWorkflowExtracted(workflowArgs, client, getTemplateId);
285
175
  }
286
176
  // ═══════════════════════════════════════════════════════════════════════════
287
177
  // Standard persona operations (get, list, compare, version management)
@@ -310,8 +200,8 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
310
200
  return {
311
201
  error: "Explicit method required",
312
202
  message: "You provided a persona id/name. What operation would you like to perform?",
313
- valid_methods: ["get", "update", "delete", "analyze", "sanitize", "snapshot", "history", "restore", "compare"],
314
- hint: "Specify method='get' to fetch, method='update' to modify",
203
+ valid_methods: ["get", "update", "delete", "sanitize", "snapshot", "history", "restore"],
204
+ hint: "LLM does analysis/comparison. Use method='get' to fetch data, then reason about it.",
315
205
  example: `persona(method="get", id="${idOrName}")`,
316
206
  };
317
207
  }
@@ -397,230 +287,35 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
397
287
  return { error: `Failed to get schema: ${error instanceof Error ? error.message : String(error)}` };
398
288
  }
399
289
  }
400
- // Compare, clone, create, analyze - all handled by extracted handlers
401
- case "compare":
290
+ // Clone and create - handled by extracted handlers
402
291
  case "clone":
403
- case "create":
404
- case "analyze": {
292
+ case "create": {
405
293
  // These modes have been extracted to handlers/persona/*.ts
406
294
  // - create.ts: handleCreate (~410 lines)
407
- // - analyze.ts: handleAnalyze (~195 lines)
408
295
  return { error: `Mode "${effectiveMode}" should be handled by extracted handler` };
409
296
  }
410
- // ─────────────── Version Management Modes ───────────────
411
- // Schema exposes: snapshot, history, restore
412
- // Handlers use: version_create, version_list, version_restore
413
- case "snapshot": // Alias for version_create
414
- case "version_create": {
415
- if (!idOrName) {
416
- return { error: "id required for version_create mode" };
417
- }
418
- if (!versionContext) {
419
- return { error: "Version tracking not configured. Provide workspaceRoot in context." };
420
- }
421
- const persona = await resolvePersona(client, idOrName);
422
- if (!persona) {
423
- return { error: `Persona not found: ${idOrName}` };
424
- }
425
- // Fetch full persona with workflow
426
- const fullPersona = await client.getPersonaById(persona.id);
427
- if (!fullPersona) {
428
- return { error: `Could not fetch full persona: ${persona.id}` };
429
- }
430
- const storage = createVersionStorage(versionContext.workspaceRoot);
431
- const engine = createVersionPolicyEngine(storage);
432
- const result = engine.forceCreateVersion(fullPersona, {
433
- environment: versionContext.environment,
434
- tenant_id: versionContext.tenant_id,
435
- message: args.message,
436
- created_by: "mcp-toolkit",
437
- });
438
- if (!result.created || !result.version) {
439
- return { error: result.reason };
440
- }
441
- return {
442
- success: true,
443
- version: {
444
- id: result.version.id,
445
- version_number: result.version.version_number,
446
- version_name: result.version.version_name,
447
- content_hash: result.version.content_hash,
448
- created_at: result.version.created_at,
449
- message: result.version.message,
450
- },
451
- changes_from_parent: result.changes_from_parent,
452
- versions_pruned: result.versions_pruned,
453
- };
454
- }
455
- case "history": // Alias for version_list
456
- case "version_list": {
457
- if (!idOrName) {
458
- return { error: "id required for history mode" };
459
- }
460
- if (!versionContext) {
461
- return { error: "Version tracking not configured. Provide workspaceRoot in context." };
462
- }
463
- const persona = await resolvePersona(client, idOrName);
464
- if (!persona) {
465
- return { error: `Persona not found: ${idOrName}` };
466
- }
467
- const storage = createVersionStorage(versionContext.workspaceRoot);
468
- const engine = createVersionPolicyEngine(storage);
469
- const versions = engine.listVersions(persona.id, {
470
- limit: args.limit,
471
- });
472
- return {
473
- persona_id: persona.id,
474
- persona_name: persona.name,
475
- versions,
476
- count: versions.length,
477
- };
478
- }
479
- case "version_get": {
480
- if (!idOrName) {
481
- return { error: "id required for version_get mode" };
482
- }
483
- if (!versionContext) {
484
- return { error: "Version tracking not configured. Provide workspaceRoot in context." };
485
- }
486
- const persona = await resolvePersona(client, idOrName);
487
- if (!persona) {
488
- return { error: `Persona not found: ${idOrName}` };
489
- }
490
- const versionId = args.version ?? "latest";
491
- const storage = createVersionStorage(versionContext.workspaceRoot);
492
- const engine = createVersionPolicyEngine(storage);
493
- const version = engine.getVersion(persona.id, versionId);
494
- if (!version) {
495
- return { error: `Version not found: ${versionId}` };
496
- }
497
- return {
498
- persona_id: persona.id,
499
- persona_name: persona.name,
500
- version: {
501
- id: version.id,
502
- version_number: version.version_number,
503
- version_name: version.version_name,
504
- content_hash: version.content_hash,
505
- created_at: version.created_at,
506
- created_by: version.created_by,
507
- trigger: version.trigger,
508
- message: version.message,
509
- changes_summary: version.changes_summary,
510
- },
511
- snapshot: {
512
- display_name: version.snapshot.display_name,
513
- description: version.snapshot.description,
514
- workflow_id: version.snapshot.workflow_id,
515
- trigger_type: version.snapshot.trigger_type,
516
- embedding_enabled: version.snapshot.embedding_enabled,
517
- has_workflow: !!version.snapshot.workflow_definition,
518
- has_proto_config: !!version.snapshot.proto_config,
519
- },
520
- };
521
- }
522
- case "version_compare": {
523
- if (!idOrName) {
524
- return { error: "id required for version_compare mode" };
525
- }
526
- if (!versionContext) {
527
- return { error: "Version tracking not configured. Provide workspaceRoot in context." };
528
- }
529
- const v1 = args.v1;
530
- const v2 = args.v2;
531
- if (!v1 || !v2) {
532
- return { error: "v1 and v2 required for version_compare mode" };
533
- }
534
- const persona = await resolvePersona(client, idOrName);
535
- if (!persona) {
536
- return { error: `Persona not found: ${idOrName}` };
537
- }
538
- const storage = createVersionStorage(versionContext.workspaceRoot);
539
- const engine = createVersionPolicyEngine(storage);
540
- const result = engine.compareVersions(persona.id, v1, v2);
541
- if (!result.success) {
542
- return { error: result.error };
543
- }
544
- return {
545
- persona_id: persona.id,
546
- persona_name: persona.name,
547
- comparison: {
548
- v1: result.diff?.v1,
549
- v2: result.diff?.v2,
550
- identical: result.diff?.identical,
551
- changed_fields: result.diff?.changed_fields,
552
- workflow_diff: result.diff?.workflow_diff,
553
- },
554
- summary: result.summary,
555
- };
556
- }
557
- case "restore": // Alias for version_restore
558
- case "version_restore": {
559
- if (!idOrName) {
560
- return { error: "id required for restore mode" };
561
- }
562
- if (!versionContext) {
563
- return { error: "Version tracking not configured. Provide workspaceRoot in context." };
564
- }
565
- const versionId = args.version;
566
- if (!versionId) {
567
- return { error: "version required for version_restore mode" };
568
- }
569
- const persona = await resolvePersona(client, idOrName);
570
- if (!persona) {
571
- return { error: `Persona not found: ${idOrName}` };
572
- }
573
- const storage = createVersionStorage(versionContext.workspaceRoot);
574
- const engine = createVersionPolicyEngine(storage);
575
- const restoreData = engine.getRestoreData(persona.id, versionId);
576
- if (!restoreData.success || !restoreData.restore_payload) {
577
- return { error: restoreData.error ?? "Failed to get restore data" };
578
- }
579
- // Create a version snapshot before restoring (audit trail)
580
- const fullPersona = await client.getPersonaById(persona.id);
581
- if (fullPersona) {
582
- engine.forceCreateVersion(fullPersona, {
583
- environment: versionContext.environment,
584
- tenant_id: versionContext.tenant_id,
585
- message: `Before restore to ${restoreData.version?.version_name}`,
586
- created_by: "mcp-toolkit",
587
- });
588
- }
589
- // Apply the restore
590
- const payload = restoreData.restore_payload;
591
- await client.updateAiEmployee({
592
- persona_id: payload.persona_id,
593
- name: payload.name,
594
- description: payload.description,
595
- proto_config: payload.proto_config,
596
- workflow: payload.workflow ?? undefined,
597
- welcome_messages: payload.welcome_messages ?? undefined,
598
- embedding_enabled: payload.embedding_enabled ?? undefined,
599
- });
600
- // Create post-restore version
601
- const restoredPersona = await client.getPersonaById(persona.id);
602
- if (restoredPersona) {
603
- engine.forceCreateVersion(restoredPersona, {
604
- environment: versionContext.environment,
605
- tenant_id: versionContext.tenant_id,
606
- message: `Restored to ${restoreData.version?.version_name}`,
607
- created_by: "mcp-toolkit",
608
- });
609
- }
297
+ // Analyze and compare - LLM does this, not MCP
298
+ case "analyze":
299
+ case "compare": {
610
300
  return {
611
- success: true,
612
- persona_id: persona.id,
613
- restored_to: {
614
- version_id: restoreData.version?.id,
615
- version_name: restoreData.version?.version_name,
616
- version_number: restoreData.version?.version_number,
617
- },
618
- message: `Persona restored to ${restoreData.version?.version_name}`,
301
+ error: `Method "${effectiveMode}" removed - LLM does analysis/comparison`,
302
+ hint: "Use method='get' to fetch persona data, then do your own analysis/comparison.",
303
+ example: `persona(method="get", id="...", include_workflow=true)`,
619
304
  };
620
305
  }
306
+ // ─────────────── Version Management Modes ───────────────
307
+ // Extracted to handlers/persona/version.ts
308
+ case "snapshot":
309
+ case "history":
310
+ case "restore":
311
+ case "version_create":
312
+ case "version_list":
313
+ case "version_get":
314
+ case "version_compare":
315
+ case "version_restore":
621
316
  case "version_policy": {
622
317
  if (!idOrName) {
623
- return { error: "id required for version_policy mode" };
318
+ return { error: `id required for ${effectiveMode} mode` };
624
319
  }
625
320
  if (!versionContext) {
626
321
  return { error: "Version tracking not configured. Provide workspaceRoot in context." };
@@ -629,2345 +324,27 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
629
324
  if (!persona) {
630
325
  return { error: `Persona not found: ${idOrName}` };
631
326
  }
632
- const storage = createVersionStorage(versionContext.workspaceRoot);
633
- const engine = createVersionPolicyEngine(storage);
634
- // Check if we're updating or just getting
635
- const hasUpdates = args.auto_on_deploy !== undefined ||
636
- args.auto_on_sync !== undefined ||
637
- args.max_versions !== undefined;
638
- if (hasUpdates) {
639
- const updated = engine.updatePolicy(persona.id, {
640
- auto_version_on_deploy: args.auto_on_deploy,
641
- auto_version_on_sync: args.auto_on_sync,
642
- max_versions: args.max_versions,
643
- });
644
- return {
645
- success: true,
646
- persona_id: persona.id,
647
- persona_name: persona.name,
648
- policy: updated,
649
- message: "Policy updated",
650
- };
651
- }
652
- // Just get current policy
653
- const policy = engine.getPolicy(persona.id);
654
- return {
655
- persona_id: persona.id,
656
- persona_name: persona.name,
657
- policy,
658
- };
659
- }
660
- default:
661
- return { error: `Unknown mode: ${effectiveMode}` };
662
- }
663
- }
664
- // ═══════════════════════════════════════════════════════════════════════════
665
- // WORKFLOW Handler - Unified greenfield/brownfield operations
666
- // ═══════════════════════════════════════════════════════════════════════════
667
- export async function handleWorkflow(args, client, getTemplateId) {
668
- const personaId = args.persona_id;
669
- const workflowDef = args.workflow_def;
670
- const input = args.input;
671
- const optimize = args.optimize;
672
- const compareTo = args.compare_to;
673
- // Default to preview=true for safety (no accidental deploys)
674
- const preview = args.preview !== false;
675
- // Smart mode detection - user doesn't need to specify
676
- // Priority: extraction_schema > compare > optimize > brownfield > greenfield > analyze
677
- let effectiveMode;
678
- if (args.mode === "compile") {
679
- // Explicit compile mode (for advanced users)
680
- effectiveMode = "compile";
681
- }
682
- else if (compareTo) {
683
- // Compare two workflows
684
- effectiveMode = "compare";
685
- }
686
- else if (optimize && personaId) {
687
- // Auto-fix issues
688
- effectiveMode = "optimize";
689
- }
690
- else if (personaId && workflowDef) {
691
- // DEPLOY: Persona exists + workflow_def provided → deploy directly (no input needed)
692
- effectiveMode = "deploy";
693
- }
694
- else if (personaId && input) {
695
- // MODIFY: Persona exists + input provided → generate new workflow from input
696
- effectiveMode = "modify"; // New unified mode for all brownfield changes
697
- }
698
- else if (input && !personaId) {
699
- // GREENFIELD: Create new workflow
700
- effectiveMode = "generate";
701
- }
702
- else if (personaId || workflowDef || args.include) {
703
- // Just analyzing
704
- effectiveMode = "analyze";
705
- }
706
- else {
707
- effectiveMode = "analyze";
708
- }
709
- // Legacy mode support (backwards compatibility)
710
- const legacyMode = args.mode;
711
- if (legacyMode === "extend")
712
- effectiveMode = "modify";
713
- if (legacyMode === "deploy") {
714
- // If explicit deploy mode, use it (works with or without input)
715
- effectiveMode = "deploy";
716
- }
717
- switch (effectiveMode) {
718
- case "generate": {
719
- if (!input) {
720
- return { error: "input required for generate mode" };
721
- }
722
- // Load action registry for API-driven action versions/namespaces
723
- const actionRegistry = await ensureActionRegistry(client);
724
- const parseResult = parseInput(input);
725
- if (!parseResult.validation.complete) {
726
- return {
727
- status: "incomplete",
728
- input_type: parseResult.input_type,
729
- missing: parseResult.validation.missing,
730
- questions: parseResult.validation.questions,
731
- };
732
- }
733
- // Override persona_type from args.type if provided (args.type takes precedence)
734
- if (args.type) {
735
- parseResult.intent.persona_type = args.type;
736
- }
737
- // Load schema registry for API-driven validation (graceful degradation if unavailable)
738
- let schemaRegistry;
739
- try {
740
- schemaRegistry = await ensureSchemaRegistry(client);
741
- }
742
- catch {
743
- // Schema registry unavailable - skip API validation
744
- schemaRegistry = null;
745
- }
746
- // ═══════════════════════════════════════════════════════════════════════════
747
- // PERSONA WIDGET CONTEXT: If persona_id provided, fetch available widgets
748
- // Workflows reference widgets by name - agent needs to know what's available
749
- // ═══════════════════════════════════════════════════════════════════════════
750
- let personaWidgets = [];
751
- if (personaId) {
752
- try {
753
- const persona = await client.getPersonaById(personaId);
754
- const protoConfig = persona?.proto_config;
755
- if (protoConfig?.widgets) {
756
- personaWidgets = protoConfig.widgets
757
- .filter(w => typeof w.name === "string" && w.name.trim().length > 0)
758
- .map(w => ({
759
- name: w.name,
760
- type: String(w.type ?? "unknown"),
761
- title: w.title,
762
- }));
763
- }
764
- }
765
- catch {
766
- // Persona not found or no widgets - continue without context
767
- }
768
- }
769
- // ═══════════════════════════════════════════════════════════════════════════
770
- // PROGRESSIVE ENHANCEMENT: Use Intent Architect for moderate/complex requests
771
- // ═══════════════════════════════════════════════════════════════════════════
772
- //
773
- // Uses the canonical runIntentArchitect() entrypoint which:
774
- // - Detects complexity with typed scores (not enums)
775
- // - Returns qualification questions or LLM prompt
776
- // - Supports iterative refinement with previous_answers
777
- //
778
- // Configure rollout via max_complexity option (default: allow all levels)
779
- const maxComplexity = args.max_complexity || undefined;
780
- const inputStr = input; // Already validated above
781
- const architectResult = runIntentArchitect(inputStr, {
782
- persona_type: parseResult.intent.persona_type,
783
- available_integrations: schemaRegistry?.getAllActions().slice(0, 20).map(a => a.displayName || a.name),
784
- }, { max_complexity: maxComplexity });
785
- // For moderate/complex: return Intent Architect result with questions or prompt
786
- if (!architectResult.strategy.can_proceed) {
787
- let availableActions = [];
788
- let availableTemplates = [];
789
- // Enhance prompt with action catalog and widget context
790
- let enhancedPrompt = architectResult.prompt_package;
791
- if (enhancedPrompt) {
792
- let systemAdditions = "";
793
- // Add action catalog if available
794
- if (schemaRegistry) {
795
- const actionCatalog = generateActionCatalogForLLM(schemaRegistry);
796
- systemAdditions += "\n\n## Available Actions\n" + actionCatalog;
797
- availableActions = schemaRegistry.getAllActions().map(a => a.name);
798
- availableTemplates = schemaRegistry.getAllTemplates().map(t => ({ id: t.id, name: t.name, type: t.type }));
799
- }
800
- // Add persona widget context if available
801
- if (personaWidgets.length > 0) {
802
- systemAdditions += "\n\n## Available Persona Widgets\n";
803
- systemAdditions += "Use these exact widget names in workflow bindings (widgetName field):\n";
804
- for (const w of personaWidgets) {
805
- systemAdditions += `- \`${w.name}\` (${w.type})${w.title ? ` - "${w.title}"` : ""}\n`;
806
- }
807
- }
808
- if (systemAdditions) {
809
- enhancedPrompt = {
810
- system: enhancedPrompt.system + systemAdditions,
811
- user: enhancedPrompt.user,
812
- };
813
- }
814
- }
815
- return {
816
- status: "needs_intent_architect",
817
- // Persona widget context (for workflow bindings)
818
- available_widgets: personaWidgets.length > 0 ? personaWidgets : undefined,
819
- // Typed assessment (new)
820
- assessment: architectResult.assessment,
821
- // Strategy decision (new)
822
- strategy: architectResult.strategy,
823
- // Qualification questions (new)
824
- questions: architectResult.questions,
825
- // LLM prompt (if full architect)
826
- llm_prompt: enhancedPrompt,
827
- // Hint for what to do next
828
- hint: architectResult.strategy.next_step,
829
- // Legacy fields for backward compatibility
830
- reason: architectResult.legacy?.signals.reason,
831
- complexity: architectResult.legacy?.complexity,
832
- approach: architectResult.strategy.approach,
833
- gates_to_ask: architectResult.strategy.gates_to_ask,
834
- // Also provide basic spec as fallback
835
- fallback_spec: intentToSpec(parseResult.intent),
836
- // Include catalogs for reference (if available)
837
- available_actions: availableActions,
838
- available_templates: availableTemplates,
839
- };
840
- }
841
- // SIMPLE complexity: Check if intent requires LLM-driven generation (legacy path)
842
- const genResult = generateWorkflow(parseResult.intent);
843
- if (genResult.needs_llm) {
844
- // Complex workflow detected by legacy system - return prompt for LLM to generate
845
- // This is a fallback for cases the new complexity analyzer might miss
846
- let enhancedPrompt = genResult.llm_prompt;
847
- let availableActions = [];
848
- let availableTemplates = [];
849
- if (enhancedPrompt) {
850
- let systemAdditions = "";
851
- if (schemaRegistry) {
852
- const actionCatalog = generateActionCatalogForLLM(schemaRegistry);
853
- systemAdditions += "\n\n" + actionCatalog;
854
- availableActions = schemaRegistry.getAllActions().map(a => a.name);
855
- availableTemplates = schemaRegistry.getAllTemplates().map(t => ({ id: t.id, name: t.name, type: t.type }));
856
- }
857
- // Add persona widget context if available
858
- if (personaWidgets.length > 0) {
859
- systemAdditions += "\n\n## Available Persona Widgets\n";
860
- systemAdditions += "Use these exact widget names in workflow bindings (widgetName field):\n";
861
- for (const w of personaWidgets) {
862
- systemAdditions += `- \`${w.name}\` (${w.type})${w.title ? ` - "${w.title}"` : ""}\n`;
863
- }
864
- }
865
- if (systemAdditions) {
866
- enhancedPrompt = {
867
- system: enhancedPrompt.system + systemAdditions,
868
- user: enhancedPrompt.user,
869
- };
870
- }
871
- }
872
- return {
873
- status: "needs_llm_generation",
874
- reason: genResult.reason,
875
- complexity: genResult.complexity,
876
- llm_prompt: enhancedPrompt,
877
- hint: "Send llm_prompt to an LLM, then call persona(workflow_def=<parsed_response>) to deploy.",
878
- // Also provide basic spec as fallback (without complex chains)
879
- fallback_spec: intentToSpec(parseResult.intent),
880
- // Include catalogs for reference (if available)
881
- available_actions: availableActions,
882
- available_templates: availableTemplates,
883
- // Persona widget context (for workflow bindings)
884
- available_widgets: personaWidgets.length > 0 ? personaWidgets : undefined,
885
- };
886
- }
887
- // Simple workflow - use generated spec directly
888
- const spec = genResult.spec;
889
- // Override spec name with provided name if given (args.name takes precedence over parsed name)
890
- if (args.name) {
891
- spec.name = args.name;
892
- }
893
- if (args.description) {
894
- spec.description = args.description;
895
- }
896
- // Validate spec against API schemas BEFORE compiling (if schema registry available)
897
- let specValidation = null;
898
- if (schemaRegistry) {
899
- specValidation = validateWorkflowSpec(spec, schemaRegistry);
900
- if (!specValidation.valid) {
901
- return {
902
- status: "validation_failed",
903
- errors: specValidation.errors,
904
- warnings: specValidation.warnings,
905
- action_coverage: specValidation.action_coverage,
906
- hint: "Fix the validation errors and try again. Unknown actions may need to be checked against ListActions.",
907
- spec_attempted: spec,
908
- };
909
- }
910
- }
911
- const compiled = compileWorkflow(spec, { registry: actionRegistry });
912
- // Validate the generated workflow (structural checks)
913
- const issues = detectWorkflowIssues(compiled.workflow_def);
914
- const result = {
915
- mode: "generate",
916
- status: preview ? "preview" : "deployed",
917
- workflow_def: compiled.workflow_def,
918
- proto_config: compiled.proto_config,
919
- validation: parseResult.validation,
920
- };
921
- // Include API validation results if available
922
- if (specValidation) {
923
- result.api_validation = {
924
- valid: specValidation.valid,
925
- warnings: specValidation.warnings,
926
- action_coverage: specValidation.action_coverage,
927
- };
928
- }
929
- if (issues.length > 0) {
930
- result.issues = issues;
931
- }
932
- // If preview=false and persona_id provided, deploy
933
- if (!preview && personaId) {
934
- const persona = await client.getPersonaById(personaId);
935
- if (!persona) {
936
- return { error: `Persona not found: ${personaId}` };
937
- }
938
- await client.updateAiEmployee({
939
- persona_id: personaId,
940
- workflow: compiled.workflow_def,
941
- proto_config: args.proto_config || compiled.proto_config || persona.proto_config,
942
- });
943
- result.deployed_to = { persona_id: personaId, persona_name: persona.name };
944
- }
945
- else if (!preview && !personaId) {
946
- // GREENFIELD: Create new persona from template, then deploy workflow + proto_config
947
- const personaName = args.name;
948
- if (personaName) {
949
- const personaType = args.type || "chat";
950
- // Dynamic template lookup - templates are tenant-specific
951
- // Match by normalized trigger_type label
952
- const templates = await getTemplates(client);
953
- const matchingTemplate = templates.find(t => normalizeTriggerType(t.trigger_type) === personaType.toLowerCase());
954
- // Try getTemplateId callback, then registry, then dynamic lookup
955
- const templateFromRegistry = actionRegistry.getTemplateForType(personaType);
956
- const templateId = getTemplateId?.(personaType) || templateFromRegistry?.id || matchingTemplate?.id;
957
- if (!templateId) {
958
- const availableTypes = [...new Set(templates.map(t => normalizeTriggerType(t.trigger_type)).filter(Boolean))];
959
- return {
960
- error: `No template found for type "${personaType}".`,
961
- available_types: availableTypes,
962
- hint: "Provide template_id directly or use one of the available types.",
963
- };
964
- }
965
- // Step 1: Create the persona from template
966
- const createResult = await client.createAiEmployee({
967
- name: personaName,
968
- description: args.description,
969
- template_id: templateId,
970
- });
971
- const newPersonaId = createResult.persona_id ?? createResult.id;
972
- if (!newPersonaId) {
973
- return { error: "Failed to create persona: no ID returned" };
974
- }
975
- // Step 2: Fetch the newly created persona to get template's valid structure
976
- const newPersona = await client.getPersonaById(newPersonaId);
977
- if (!newPersona) {
978
- return { error: `Failed to fetch newly created persona: ${newPersonaId}` };
979
- }
980
- // Step 3: Merge proto_config - keep template structure, update widget values
981
- const existingProtoConfig = (newPersona.proto_config ?? {});
982
- const generatedProtoConfig = args.proto_config || compiled.proto_config || {};
983
- // Merge widgets by name - template widgets provide structure, generated values override
984
- const existingWidgets = (existingProtoConfig.widgets ?? []);
985
- const generatedWidgets = (generatedProtoConfig.widgets ?? []);
986
- const widgetMap = new Map();
987
- // Start with template widgets (valid structure)
988
- for (const w of existingWidgets) {
989
- if (typeof w.name === "string" && w.name.trim().length > 0) {
990
- widgetMap.set(w.name, w);
991
- }
992
- }
993
- // Merge generated widget VALUES into template structure
994
- for (const genWidget of generatedWidgets) {
995
- const widgetName = genWidget.name;
996
- if (typeof widgetName === "string" && widgetName.trim().length > 0) {
997
- const existing = widgetMap.get(widgetName);
998
- if (existing) {
999
- // Merge: keep template structure, update config values
1000
- const merged = { ...existing };
1001
- // The widget config is stored under the widget name (e.g., conversationSettings)
1002
- if (genWidget[widgetName]) {
1003
- merged[widgetName] = { ...(existing[widgetName] || {}), ...genWidget[widgetName] };
1004
- }
1005
- widgetMap.set(widgetName, merged);
1006
- }
1007
- else {
1008
- // New widget not in template - add as-is
1009
- widgetMap.set(widgetName, genWidget);
1010
- }
1011
- }
1012
- }
1013
- const mergedProtoConfig = {
1014
- ...existingProtoConfig,
1015
- widgets: Array.from(widgetMap.values()),
1016
- };
1017
- // Step 4: Deploy BOTH workflow AND proto_config for ALL persona types
1018
- // Voice AI IS driven by workflow (not just identityAndPurpose)
1019
- const existingWorkflow = newPersona.workflow_def;
1020
- const existingWfName = existingWorkflow?.workflowName;
1021
- // Clone our compiled workflow and set the namespace from the existing workflow
1022
- const workflowForDeploy = JSON.parse(JSON.stringify(compiled.workflow_def));
1023
- const wfName = workflowForDeploy.workflowName;
1024
- if (wfName?.name && existingWfName?.name) {
1025
- // Copy the exact namespace from the template workflow
1026
- wfName.name.namespaces = existingWfName.name.namespaces;
1027
- wfName.name.name = existingWfName.name.name; // template_id
1028
- }
1029
- // Fix results format - use "<actionName>.<outputName>" keys
1030
- const compiledResults = workflowForDeploy.results;
1031
- if (compiledResults) {
1032
- const newResults = {};
1033
- for (const [, value] of Object.entries(compiledResults)) {
1034
- if (value.actionName && value.outputName) {
1035
- const key = `${value.actionName}.${value.outputName}`;
1036
- newResults[key] = value;
1037
- }
1038
- }
1039
- workflowForDeploy.results = newResults;
1040
- }
1041
- try {
1042
- await client.updateAiEmployee({
1043
- persona_id: newPersonaId,
1044
- workflow: workflowForDeploy,
1045
- proto_config: mergedProtoConfig,
1046
- }, { verbose: true });
1047
- }
1048
- catch (deployError) {
1049
- // Workflow deploy failed - still set proto_config
1050
- const errMsg = deployError instanceof Error ? deployError.message : String(deployError);
1051
- await client.updateAiEmployee({
1052
- persona_id: newPersonaId,
1053
- proto_config: mergedProtoConfig,
1054
- });
1055
- result.workflow_deploy_error = errMsg;
1056
- result.workflow_attempted = workflowForDeploy;
1057
- result.status = "partial";
1058
- result.hint = "Persona created, config set, but workflow deploy failed. Check workflow_attempted for details.";
1059
- result.deployed_to = {
1060
- persona_id: newPersonaId,
1061
- persona_name: personaName,
1062
- created: true,
1063
- workflow_deployed: false,
1064
- };
1065
- return result;
1066
- }
1067
- // Success - workflow deployed for all persona types
1068
- result.deployed_to = {
1069
- persona_id: newPersonaId,
1070
- persona_name: personaName,
1071
- created: true
1072
- };
1073
- result.status = "deployed";
1074
- result.next_steps = [
1075
- `Persona "${personaName}" created with template workflow.`,
1076
- `To customize the workflow, use: workflow(persona_id="${newPersonaId}", input="add search node", preview=false)`,
1077
- ];
1078
- }
1079
- else {
1080
- result.hint = "Provide name to create new persona, or persona_id to deploy to existing persona";
1081
- }
1082
- }
1083
- if (preview) {
1084
- result.next_steps = [
1085
- "Review the generated workflow_def",
1086
- personaId
1087
- ? `Deploy with: workflow(mode="generate", input=..., persona_id="${personaId}", preview=false)`
1088
- : "Create persona first: persona(mode='create', name='...', type='...')",
1089
- ];
1090
- }
1091
- return result;
1092
- }
1093
- case "modify":
1094
- case "extend": {
1095
- // BROWNFIELD: Intelligently modify existing workflow
1096
- // Handles: add, remove, replace, consolidate, rewire - all in one
1097
- if (!personaId) {
1098
- return { error: "persona_id required for workflow modifications" };
1099
- }
1100
- if (!input) {
1101
- return { error: "input required. Describe what you want to change." };
1102
- }
1103
- const persona = await client.getPersonaById(personaId);
1104
- if (!persona) {
1105
- return { error: `Persona not found: ${personaId}` };
1106
- }
1107
- // ─────────────────────────────────────────────────────────────────────────
1108
- // CONFIG-ONLY DETECTION: Route to proto_config update instead of workflow
1109
- // ─────────────────────────────────────────────────────────────────────────
1110
- const lowerInput = input.toLowerCase();
1111
- const isConfigOnlyUpdate = ((lowerInput.includes("voice settings") || lowerInput.includes("voice config")) ||
1112
- (lowerInput.includes("welcome message") && !lowerInput.includes("add") && !lowerInput.includes("node")) ||
1113
- (lowerInput.includes("identity") && lowerInput.includes("purpose")) ||
1114
- (lowerInput.includes("speech characteristics")) ||
1115
- (lowerInput.includes("hangup instructions") || lowerInput.includes("hangup")) ||
1116
- (lowerInput.includes("update") && (lowerInput.includes("persona settings") ||
1117
- lowerInput.includes("conversation settings") ||
1118
- lowerInput.includes("proto_config") ||
1119
- lowerInput.includes("chat settings"))));
1120
- if (isConfigOnlyUpdate) {
1121
- // This is a config update, not a workflow modification
1122
- // Parse the config values from the input and update proto_config only
1123
- const existingProtoConfig = (persona.proto_config ?? {});
1124
- const existingWidgets = (existingProtoConfig.widgets ?? []);
1125
- // Find and update the conversationSettings widget
1126
- const widgetMap = new Map();
1127
- for (const w of existingWidgets) {
1128
- if (typeof w.name === "string" && w.name.trim().length > 0) {
1129
- widgetMap.set(w.name, { ...w });
1130
- }
1131
- }
1132
- // Parse voice config updates from input
1133
- const configUpdates = {};
1134
- // Extract welcome message
1135
- const welcomeMatch = input.match(/welcome\s*message[:\s]*["']?([^"'\n]+(?:\n(?![0-9]+\.).*)*?)(?:["']|(?=\n\n|\n[0-9]+\.))/i);
1136
- if (welcomeMatch) {
1137
- configUpdates.welcomeMessage = welcomeMatch[1].trim().replace(/^["']|["']$/g, '');
1138
- }
1139
- // Extract identity and purpose
1140
- const identityMatch = input.match(/identity\s*(?:and\s*)?purpose[:\s]*["']?(.+?)(?:["']|(?=\n\n[0-9]+\.|$))/is);
1141
- if (identityMatch) {
1142
- configUpdates.identityAndPurpose = identityMatch[1].trim().replace(/^["']|["']$/g, '');
1143
- }
1144
- // Extract speech characteristics
1145
- const speechMatch = input.match(/speech\s*characteristics?[:\s]*["']?([^"'\n]+)["']?/i);
1146
- if (speechMatch) {
1147
- configUpdates.speechCharacteristics = speechMatch[1].trim().replace(/^["']|["']$/g, '');
1148
- }
1149
- // Extract hangup instructions
1150
- const hangupMatch = input.match(/hangup\s*instructions?[:\s]*["']?(.+?)(?:["']|$)/is);
1151
- if (hangupMatch) {
1152
- configUpdates.hangupInstructions = hangupMatch[1].trim().replace(/^["']|["']$/g, '');
1153
- }
1154
- // Update conversationSettings widget
1155
- const convWidget = widgetMap.get("conversationSettings") || {
1156
- name: "conversationSettings",
1157
- type: 39,
1158
- conversationSettings: {},
1159
- };
1160
- const convSettings = (convWidget.conversationSettings ?? {});
1161
- // Merge config updates
1162
- for (const [key, value] of Object.entries(configUpdates)) {
1163
- convSettings[key] = value;
1164
- }
1165
- convWidget.conversationSettings = convSettings;
1166
- widgetMap.set("conversationSettings", convWidget);
1167
- const mergedProtoConfig = {
1168
- ...existingProtoConfig,
1169
- widgets: Array.from(widgetMap.values()),
1170
- };
1171
- if (!preview) {
1172
- // Deploy config update only - preserve existing workflow
1173
- await client.updateAiEmployee({
1174
- persona_id: personaId,
1175
- proto_config: mergedProtoConfig,
1176
- workflow: persona.workflow_def,
1177
- });
1178
- }
1179
- return {
1180
- mode: "config_update",
1181
- status: preview ? "preview" : "deployed",
1182
- persona_id: personaId,
1183
- persona_name: persona.name,
1184
- config_updates: configUpdates,
1185
- note: "Updated persona configuration only. Workflow unchanged.",
1186
- hint: "To modify the workflow itself, describe workflow changes like 'add search node' or 'add HITL before email'.",
1187
- };
1188
- }
1189
- const existingWorkflow = persona.workflow_def;
1190
- if (!existingWorkflow) {
1191
- return {
1192
- error: `Persona "${persona.name}" has no workflow to modify`,
1193
- hint: "Provide just input= to create a new workflow (greenfield)",
1194
- };
1195
- }
1196
- // Intelligently analyze what the user wants to do
1197
- const changeAnalysis = analyzeModificationRequest(input, existingWorkflow);
1198
- // Apply the changes to the workflow
1199
- const modifyResult = applyWorkflowModifications(existingWorkflow, changeAnalysis);
1200
- // Validate the result
1201
- const issues = detectWorkflowIssues(modifyResult.workflow);
1202
- const fixes = issues.length > 0 ? suggestWorkflowFixes(issues) : [];
1203
- const result = {
1204
- mode: "modify",
1205
- status: preview ? "preview" : "deploying",
1206
- persona_id: personaId,
1207
- persona_name: persona.name,
1208
- // What we understood from the request
1209
- analysis: {
1210
- understood_intent: changeAnalysis.intent,
1211
- operations: changeAnalysis.operations.map(op => ({
1212
- type: op.type,
1213
- description: op.description,
1214
- })),
1215
- },
1216
- // What we did
1217
- changes_applied: modifyResult.changesApplied,
1218
- nodes_added: modifyResult.nodesAdded,
1219
- nodes_removed: modifyResult.nodesRemoved,
1220
- nodes_modified: modifyResult.nodesModified,
1221
- connections_changed: modifyResult.connectionsChanged,
1222
- // The result
1223
- modified_workflow: modifyResult.workflow,
1224
- };
1225
- // Add contextual tip when no changes were made (likely a content change request)
1226
- if (changeAnalysis.operations.length === 0 || modifyResult.changesApplied.length === 0) {
1227
- result._tip = "No structural changes detected. If you're trying to change DATA inside nodes " +
1228
- "(fixed_response content, call_llm prompts), use this approach instead: " +
1229
- "1) persona(id='...', include_workflow=true) to get workflow_def, " +
1230
- "2) Edit the node's inputs.data.stringValue in the JSON, " +
1231
- "3) workflow(mode='deploy', persona_id='...', workflow_def={edited})";
1232
- result._content_change_hint = {
1233
- step1: `persona(id="${personaId}", include_workflow=true)`,
1234
- step2: "Edit workflow_def.actions[N].inputs.data.stringValue",
1235
- step3: `workflow(mode="deploy", persona_id="${personaId}", workflow_def={...})`,
1236
- };
1237
- }
1238
- if (issues.length > 0) {
1239
- result.remaining_issues = issues.length;
1240
- result.issues = issues.slice(0, 5); // Show first 5
1241
- result.suggested_fixes = fixes.slice(0, 5);
1242
- }
1243
- // Deploy if preview=false
1244
- if (!preview) {
1245
- const sanitizedWorkflow = sanitizeWorkflowForDeploy(modifyResult.workflow);
1246
- try {
1247
- await client.updateAiEmployee({
1248
- persona_id: personaId,
1249
- workflow: sanitizedWorkflow,
1250
- proto_config: args.proto_config || persona.proto_config,
1251
- });
1252
- result.status = "deployed";
1253
- result.deployed = true;
1254
- result.modified_workflow = sanitizedWorkflow;
1255
- }
1256
- catch (err) {
1257
- const errorMessage = err instanceof Error ? err.message : String(err);
1258
- result.deployed = false;
1259
- result.deploy_error = errorMessage;
1260
- result.status = "preview_only";
1261
- result.hint = `Deployment failed: ${errorMessage}. The modified_workflow is ready - you may need to deploy via UI for complex structural changes.`;
1262
- }
1263
- }
1264
- else {
1265
- result.next_steps = [
1266
- "Review the changes_applied and modified_workflow",
1267
- issues.length > 0 ? `Address ${issues.length} remaining issues` : null,
1268
- `Deploy with: workflow(persona_id="${personaId}", input="...", preview=false)`,
1269
- ].filter(Boolean);
1270
- }
1271
- return result;
1272
- }
1273
- case "analyze": {
1274
- let workflow = workflowDef;
1275
- let persona = null;
1276
- if (personaId && !workflow) {
1277
- persona = await client.getPersonaById(personaId);
1278
- if (!persona) {
1279
- return { error: `Persona not found: ${personaId}` };
1280
- }
1281
- workflow = persona.workflow_def;
1282
- }
1283
- if (!workflow) {
1284
- return { error: "No workflow to analyze. Provide workflow_def or persona_id." };
1285
- }
1286
- // Determine what to include (now supports execution_flow!)
1287
- const include = args.include || ["issues", "connections", "fixes", "metrics"];
1288
- const result = {
1289
- mode: "analyze",
1290
- persona_id: personaId,
1291
- persona_name: persona?.name,
1292
- environment: "demo",
1293
- };
1294
- if (include.includes("issues") || include.includes("fixes")) {
1295
- const issues = detectWorkflowIssues(workflow);
1296
- if (include.includes("issues")) {
1297
- result.issues = issues;
1298
- }
1299
- if (include.includes("fixes")) {
1300
- result.fixes = suggestWorkflowFixes(issues);
1301
- }
1302
- result.issue_summary = {
1303
- total: issues.length,
1304
- critical: issues.filter((i) => i.severity === "critical").length,
1305
- warning: issues.filter((i) => i.severity === "warning").length,
1306
- info: issues.filter((i) => i.severity === "info").length,
1307
- };
1308
- result.validation_passed = issues.filter((i) => i.severity === "critical").length === 0;
1309
- }
1310
- if (include.includes("connections")) {
1311
- result.connections = validateWorkflowConnections(workflow);
1312
- }
1313
- if (include.includes("metrics")) {
1314
- result.metrics = calculateMetrics(workflow);
1315
- }
1316
- // NEW: Execution flow analysis - detects loops, multiple responders, redundant classifiers
1317
- if (include.includes("execution_flow")) {
1318
- const execAnalysis = analyzeExecutionFlow(workflow);
1319
- result.execution_flow = {
1320
- summary: execAnalysis.summary,
1321
- loops: execAnalysis.loops,
1322
- multiple_responder_issues: execAnalysis.multipleResponderIssues,
1323
- redundant_classifiers: execAnalysis.redundantClassifiers,
1324
- data_flow_issues: execAnalysis.dataFlowIssues,
1325
- dead_code_paths: execAnalysis.deadCodePaths,
1326
- };
1327
- // Include ASCII visualization if requested
1328
- if (args.visualize) {
1329
- result.execution_flow_ascii = generateASCIIFlow(execAnalysis);
1330
- }
1331
- // Add specific warnings for triple response risk
1332
- if (execAnalysis.summary.mayRepeatResponses) {
1333
- result.triple_response_warning = "⚠️ This workflow may cause duplicate/triple responses due to ungated parallel responders";
1334
- }
1335
- }
1336
- if (persona) {
1337
- result.persona = { id: persona.id, name: persona.name };
1338
- }
1339
- // Always provide optimization guidance
1340
- const issues = result.issues;
1341
- const criticalCount = issues?.filter(i => i.severity === "critical").length ?? 0;
1342
- const warningCount = issues?.filter(i => i.severity === "warning").length ?? 0;
1343
- // AUTO-FIX: If fix=true, apply fixes and deploy
1344
- if (args.fix && personaId && persona && workflow) {
1345
- const fixResult = autoFixWorkflow(workflow);
1346
- result.fixes_applied = fixResult.fixesApplied;
1347
- result.fixes_warnings = fixResult.warnings;
1348
- if (fixResult.fixesApplied.length > 0 && fixResult.success) {
1349
- // Deploy the fixed workflow
1350
- try {
1351
- const fixedWorkflow = fixResult.workflowDef;
1352
- await client.updateAiEmployee({
1353
- persona_id: personaId,
1354
- workflow: fixedWorkflow,
1355
- proto_config: persona.proto_config,
1356
- });
1357
- result.fix_status = "deployed";
1358
- result.fixes_deployed = fixResult.fixesApplied.length;
1359
- result.status = `✅ Applied ${fixResult.fixesApplied.length} fixes and deployed`;
1360
- }
1361
- catch (deployErr) {
1362
- result.fix_status = "failed";
1363
- result.fix_error = deployErr instanceof Error ? deployErr.message : String(deployErr);
1364
- result.status = `⚠️ Fixes applied but deploy failed: ${result.fix_error}`;
1365
- }
1366
- }
1367
- else if (fixResult.fixesApplied.length === 0) {
1368
- result.fix_status = "no_fixes_needed";
1369
- result.status = "✅ No auto-fixable issues found";
1370
- }
1371
- else {
1372
- result.fix_status = "failed";
1373
- result.status = "⚠️ Fix generation failed";
1374
- }
1375
- return result;
1376
- }
1377
- if (criticalCount > 0 || warningCount > 0) {
1378
- result.optimization_suggestion = `This workflow has ${criticalCount} critical and ${warningCount} warning issues. ` +
1379
- `Use workflow(persona_id="${personaId ?? 'ID'}", optimize=true) to auto-fix.`;
1380
- }
1381
- else {
1382
- result.status = "✅ Workflow is healthy - no issues detected";
1383
- }
1384
- return result;
1385
- }
1386
- case "deploy": {
1387
- // DEPLOY: Direct workflow deployment to existing persona (no input required)
1388
- if (!personaId) {
1389
- return { error: "persona_id required for deploy mode" };
1390
- }
1391
- if (!workflowDef) {
1392
- return { error: "workflow_def required for deploy mode" };
1393
- }
1394
- const persona = await client.getPersonaById(personaId);
1395
- if (!persona) {
1396
- return { error: `Persona not found: ${personaId}` };
1397
- }
1398
- // Sanitize workflow before deployment
1399
- const sanitizedWorkflow = sanitizeWorkflowForDeploy(workflowDef);
1400
- // Determine proto_config to use: provided > existing
1401
- const existingProtoConfig = persona.proto_config;
1402
- const providedProtoConfig = args.proto_config;
1403
- const protoConfigToUse = providedProtoConfig || existingProtoConfig || {};
1404
- // Check if this is a voice persona and warn if critical settings are missing
1405
- const projectSettings = protoConfigToUse.projectSettings;
1406
- const isVoice = projectSettings?.projectType === 5;
1407
- const warnings = [];
1408
- if (isVoice && !providedProtoConfig) {
1409
- // Check if existing proto_config has conversationSettings populated
1410
- const widgets = (protoConfigToUse.widgets ?? []);
1411
- const convSettings = widgets.find(w => w.name === "conversationSettings");
1412
- const convConfig = convSettings?.conversationSettings;
1413
- if (!convConfig?.welcomeMessage || !convConfig?.identityAndPurpose) {
1414
- warnings.push("Voice persona detected but proto_config not provided. Voice settings (welcomeMessage, identityAndPurpose, etc.) may be empty or generic. " +
1415
- "If you generated the workflow with workflow(input=...), pass the generated proto_config along with workflow_def to preserve voice settings.");
1416
- }
1417
- }
1418
- // Deploy workflow (always deploy, preview is not applicable for explicit deploy mode)
1419
- try {
1420
- await client.updateAiEmployee({
1421
- persona_id: personaId,
1422
- workflow: sanitizedWorkflow,
1423
- proto_config: protoConfigToUse,
1424
- });
1425
- const result = {
1426
- mode: "deploy",
1427
- status: "deployed",
1428
- persona_id: personaId,
1429
- persona_name: persona.name,
1430
- workflow_deployed: true,
1431
- };
1432
- if (warnings.length > 0) {
1433
- result.warnings = warnings;
1434
- }
1435
- return result;
1436
- }
1437
- catch (err) {
1438
- const errorMessage = err instanceof Error ? err.message : String(err);
1439
- return {
1440
- mode: "deploy",
1441
- status: "failed",
1442
- persona_id: personaId,
1443
- persona_name: persona.name,
1444
- error: errorMessage,
1445
- };
1446
- }
1447
- }
1448
- case "compare": {
1449
- if (!personaId || !args.compare_to) {
1450
- return { error: "persona_id and compare_to required for compare mode" };
1451
- }
1452
- const persona1 = await client.getPersonaById(personaId);
1453
- const persona2 = await client.getPersonaById(args.compare_to);
1454
- if (!persona1 || !persona2) {
1455
- return { error: "One or both personas not found" };
1456
- }
1457
- const workflow1 = persona1.workflow_def;
1458
- const workflow2 = persona2.workflow_def;
1459
- return {
1460
- persona_1: { id: persona1.id, name: persona1.name },
1461
- persona_2: { id: persona2.id, name: persona2.name },
1462
- differences: compareWorkflows(workflow1, workflow2),
1463
- };
1464
- }
1465
- case "compile": {
1466
- const nodes = args.nodes;
1467
- const resultMappings = args.result_mappings;
1468
- if (!nodes) {
1469
- return { error: "nodes required for compile mode" };
1470
- }
1471
- // Load action registry for API-driven action versions/namespaces
1472
- const compileRegistry = await ensureActionRegistry(client);
1473
- const spec = {
1474
- name: args.name || "Compiled Workflow",
1475
- description: args.description || "Generated workflow",
1476
- persona_type: args.type || "chat",
1477
- nodes: nodes,
1478
- result_mappings: resultMappings || [],
1479
- };
1480
- const compiled = compileWorkflow(spec, { registry: compileRegistry });
1481
- return {
1482
- workflow_def: compiled.workflow_def,
1483
- proto_config: compiled.proto_config,
1484
- registry_loaded: compileRegistry.isLoaded(),
1485
- };
1486
- }
1487
- case "optimize": {
1488
- // FIX issues in existing workflow (not extend - that's mode="extend")
1489
- if (!personaId) {
1490
- return { error: "persona_id required for optimize mode" };
1491
- }
1492
- const persona = await client.getPersonaById(personaId);
1493
- if (!persona) {
1494
- return { error: `Persona not found: ${personaId}` };
1495
- }
1496
- const existingWorkflow = persona.workflow_def;
1497
- if (!existingWorkflow) {
1498
- return {
1499
- error: `Persona "${persona.name}" has no workflow to optimize`,
1500
- hint: "Use mode='generate' to create a workflow first",
1501
- };
1502
- }
1503
- // Analyze and detect issues
1504
- const issues = detectWorkflowIssues(existingWorkflow);
1505
- const fixes = suggestWorkflowFixes(issues);
1506
- if (issues.length === 0) {
1507
- return {
1508
- mode: "optimize",
1509
- status: "✅ No issues found",
1510
- persona_id: personaId,
1511
- persona_name: persona.name,
1512
- workflow_healthy: true,
1513
- };
1514
- }
1515
- // Apply fixes
1516
- const fixedWorkflow = applySimpleFixes(existingWorkflow, issues);
1517
- const result = {
1518
- mode: "optimize",
1519
- status: preview ? "preview" : "deployed",
1520
- persona_id: personaId,
1521
- persona_name: persona.name,
1522
- issues_found: issues.length,
1523
- issues: issues.map(i => ({
1524
- type: i.type,
1525
- severity: i.severity,
1526
- reason: i.reason,
1527
- })),
1528
- suggested_fixes: fixes,
1529
- fixed_workflow: fixedWorkflow,
1530
- };
1531
- // If preview=false, deploy the fixed workflow
1532
- if (!preview) {
1533
- await client.updateAiEmployee({
1534
- persona_id: personaId,
1535
- workflow: fixedWorkflow,
1536
- proto_config: args.proto_config || persona.proto_config,
1537
- });
1538
- result.deployed = true;
1539
- }
1540
- else {
1541
- result.next_steps = [
1542
- "Review the suggested fixes and fixed_workflow",
1543
- `Deploy with: workflow(mode="optimize", persona_id="${personaId}", preview=false)`,
1544
- ];
1545
- }
1546
- return result;
327
+ return handleVersion(effectiveMode, args, client, persona, versionContext);
1547
328
  }
1548
329
  default:
1549
330
  return { error: `Unknown mode: ${effectiveMode}` };
1550
331
  }
1551
332
  }
1552
333
  // ═══════════════════════════════════════════════════════════════════════════
1553
- // ACTION Handler
1554
- // ═══════════════════════════════════════════════════════════════════════════
1555
- // Delegate to extracted handler
1556
- export const handleAction = handleActionExtracted;
1557
- // ═══════════════════════════════════════════════════════════════════════════
1558
- // TEMPLATE Handler
1559
- // ═══════════════════════════════════════════════════════════════════════════
1560
- // Delegate to extracted handler
1561
- export const handleTemplate = handleTemplateExtracted;
1562
- // ═══════════════════════════════════════════════════════════════════════════
1563
- // KNOWLEDGE Handler
1564
- // ═══════════════════════════════════════════════════════════════════════════
1565
- // Delegate to extracted handler
1566
- export const handleKnowledge = handleKnowledgeExtracted;
1567
- // ═══════════════════════════════════════════════════════════════════════════
1568
- // REFERENCE Handler (v2 unified: envs, actions, templates, patterns, concepts, guidance)
1569
- // ═══════════════════════════════════════════════════════════════════════════
1570
- // Delegate to extracted handler
1571
- export const handleReference = handleReferenceExtracted;
334
+ // WORKFLOW Handler - REMOVED
1572
335
  // ═══════════════════════════════════════════════════════════════════════════
1573
- // DATA Handler (v2 alias for knowledge)
336
+ // The handleWorkflow function has been fully extracted to handlers/workflow/
337
+ // Import via: import { handleWorkflow } from "./handlers/workflow/index.js"
338
+ //
339
+ // Modular structure:
340
+ // - handlers/workflow/analyze.ts
341
+ // - handlers/workflow/compare.ts
342
+ // - handlers/workflow/compile.ts
343
+ // - handlers/workflow/deploy.ts
344
+ // - handlers/workflow/generate.ts
345
+ // - handlers/workflow/modify.ts
346
+ // - handlers/workflow/optimize.ts
347
+ // - handlers/workflow/index.ts (router)
1574
348
  // ═══════════════════════════════════════════════════════════════════════════
1575
- export async function handleData(args, client, readFile) {
1576
- const mode = args.mode;
1577
- const personaId = args.persona_id;
1578
- // ─────────────────────────────────────────────────────────────────────────
1579
- // Clarification when mode is omitted
1580
- // ─────────────────────────────────────────────────────────────────────────
1581
- if (!mode) {
1582
- // Try to infer from params if unambiguous
1583
- if (args.file && !args.file_id) {
1584
- // Has file path but no file_id → likely wants upload
1585
- return {
1586
- status: "clarification_needed",
1587
- message: "What operation would you like to perform?",
1588
- inferred: "upload",
1589
- hint: "You provided a file path. Did you mean mode='upload'?",
1590
- options: ["upload", "list", "generate", "embedding"],
1591
- };
1592
- }
1593
- if (args.file_id && args.input) {
1594
- // Has file_id and input → likely wants modify
1595
- return {
1596
- status: "clarification_needed",
1597
- message: "What operation would you like to perform?",
1598
- inferred: "modify",
1599
- hint: "You provided file_id and input. Did you mean mode='modify'?",
1600
- options: ["modify", "delete", "get"],
1601
- };
1602
- }
1603
- if (args.input || args.from) {
1604
- // Has input or from → likely wants generate
1605
- return {
1606
- status: "clarification_needed",
1607
- message: "What operation would you like to perform?",
1608
- inferred: "generate",
1609
- hint: "You provided input/from. Did you mean mode='generate'?",
1610
- options: ["generate", "list"],
1611
- };
1612
- }
1613
- if (args.template) {
1614
- // Wants a specific template
1615
- return {
1616
- status: "clarification_needed",
1617
- message: "What operation would you like to perform?",
1618
- inferred: "templates",
1619
- hint: "You provided template name. Did you mean mode='templates'?",
1620
- options: ["templates"],
1621
- };
1622
- }
1623
- // Default clarification
1624
- return {
1625
- status: "clarification_needed",
1626
- message: "What operation would you like to perform?",
1627
- options: ["list", "upload", "generate", "modify", "sanitize", "delete", "embedding", "templates"],
1628
- hint: "Specify mode='...' for direct execution. Example: data(persona_id='abc', mode='list')",
1629
- };
1630
- }
1631
- // ─────────────────────────────────────────────────────────────────────────
1632
- // Templates mode - list or get generation templates
1633
- // ─────────────────────────────────────────────────────────────────────────
1634
- if (mode === "templates") {
1635
- const templateName = args.template;
1636
- if (templateName) {
1637
- const template = GENERATION_TEMPLATES[templateName];
1638
- if (!template) {
1639
- return {
1640
- error: `Template not found: ${templateName}`,
1641
- available: Object.keys(GENERATION_TEMPLATES),
1642
- };
1643
- }
1644
- return {
1645
- name: templateName,
1646
- ...template,
1647
- };
1648
- }
1649
- // List all templates
1650
- return {
1651
- templates: Object.entries(GENERATION_TEMPLATES).map(([name, t]) => ({
1652
- name,
1653
- description: t.description,
1654
- category: t.category,
1655
- })),
1656
- };
1657
- }
1658
- // ─────────────────────────────────────────────────────────────────────────
1659
- // Generate mode - create new content via Document Generation API
1660
- // ─────────────────────────────────────────────────────────────────────────
1661
- if (mode === "generate") {
1662
- if (!personaId) {
1663
- return { error: "persona_id is required for generate mode" };
1664
- }
1665
- const from = args.from;
1666
- const input = args.input;
1667
- const count = args.count ?? 1;
1668
- const format = args.format ?? "markdown";
1669
- const data = args.data;
1670
- if (!from && !input) {
1671
- return {
1672
- error: "Either 'from' (template name) or 'input' (natural language) is required for generate mode",
1673
- available_templates: Object.keys(GENERATION_TEMPLATES),
1674
- };
1675
- }
1676
- // Build instructions from template or input
1677
- let instructions;
1678
- if (from) {
1679
- const template = GENERATION_TEMPLATES[from];
1680
- if (!template) {
1681
- return {
1682
- error: `Template not found: ${from}`,
1683
- available: Object.keys(GENERATION_TEMPLATES),
1684
- };
1685
- }
1686
- // Build instructions from template
1687
- instructions = template.prompt
1688
- .replace("{count}", String(count))
1689
- .replace("{format}", format);
1690
- if (data) {
1691
- // Inject additional context
1692
- instructions += `\n\nAdditional context: ${JSON.stringify(data)}`;
1693
- }
1694
- }
1695
- else {
1696
- instructions = input;
1697
- if (count > 1) {
1698
- instructions = `Generate ${count} items: ${instructions}`;
1699
- }
1700
- instructions += `\n\nOutput format: ${format}`;
1701
- }
1702
- // Call Document Generation API
1703
- try {
1704
- const result = await client.generateDocument(personaId, instructions, {
1705
- timeoutMs: 120_000,
1706
- pollIntervalMs: 2_000,
1707
- });
1708
- return {
1709
- success: true,
1710
- document_id: result.document_id,
1711
- project_id: result.project_id,
1712
- status: result.status,
1713
- content: result.content,
1714
- format,
1715
- template: from,
1716
- };
1717
- }
1718
- catch (err) {
1719
- const error = err;
1720
- return {
1721
- error: `Document generation failed: ${error.message}`,
1722
- template: from,
1723
- instructions_length: instructions.length,
1724
- };
1725
- }
1726
- }
1727
- // ─────────────────────────────────────────────────────────────────────────
1728
- // Modify mode - modify existing document
1729
- // ─────────────────────────────────────────────────────────────────────────
1730
- if (mode === "modify") {
1731
- if (!personaId) {
1732
- return { error: "persona_id is required for modify mode" };
1733
- }
1734
- const fileId = args.file_id;
1735
- const input = args.input;
1736
- if (!fileId) {
1737
- return { error: "file_id is required for modify mode" };
1738
- }
1739
- if (!input) {
1740
- return { error: "input (modification instructions) is required for modify mode" };
1741
- }
1742
- // For now, regenerate uses the Document Generation API's regenerate endpoint
1743
- // This requires document_id and project_id - we'd need to track these
1744
- // For MVP, return guidance
1745
- return {
1746
- status: "not_yet_implemented",
1747
- message: "Document modification via regenerate API requires document_id and project_id from the original generation.",
1748
- hint: "Use mode='generate' to create new content, or manually edit the file.",
1749
- file_id: fileId,
1750
- instructions: input,
1751
- };
1752
- }
1753
- // ─────────────────────────────────────────────────────────────────────────
1754
- // Get mode - get specific file details
1755
- // ─────────────────────────────────────────────────────────────────────────
1756
- if (mode === "get") {
1757
- if (!personaId) {
1758
- return { error: "persona_id is required for get mode" };
1759
- }
1760
- const fileId = args.file_id;
1761
- if (!fileId) {
1762
- return { error: "file_id is required for get mode" };
1763
- }
1764
- // List files and filter to the specific one
1765
- const files = await client.listDataSourceFiles(personaId, { limit: 1000 });
1766
- const file = files.files.find(f => f.id === fileId);
1767
- if (!file) {
1768
- return { error: `File not found: ${fileId}` };
1769
- }
1770
- return {
1771
- file,
1772
- persona_id: personaId,
1773
- };
1774
- }
1775
- // ─────────────────────────────────────────────────────────────────────────
1776
- // Sanitize mode - existing implementation
1777
- // ─────────────────────────────────────────────────────────────────────────
1778
- if (mode === "sanitize") {
1779
- // Data sanitization - needs special handling
1780
- const personaId = args.persona_id;
1781
- const includeWorkflow = args.include_workflow;
1782
- const sanitizeExamples = args.sanitize_examples;
1783
- const persona = await client.getPersonaById(personaId);
1784
- if (!persona) {
1785
- return { error: `Persona not found: ${personaId}` };
1786
- }
1787
- // Create sanitization session
1788
- const session = new SanitizationSession();
1789
- // Detect and sanitize in proto_config
1790
- const protoConfig = persona.proto_config;
1791
- let sanitizedConfig = protoConfig;
1792
- if (protoConfig) {
1793
- // Apply pattern-based detection and sanitization
1794
- const clonedConfig = JSON.parse(JSON.stringify(protoConfig)); // Deep clone
1795
- // Recursively find and sanitize string values
1796
- const sanitizeObject = (obj) => {
1797
- for (const key of Object.keys(obj)) {
1798
- const val = obj[key];
1799
- if (typeof val === "string" && val.length > 0) {
1800
- const detected = detectWithPatterns(val);
1801
- for (const entity of detected) {
1802
- const replacement = session.getOrCreateReplacement(entity.value, entity.type);
1803
- obj[key] = obj[key].split(entity.value).join(replacement);
1804
- }
1805
- // Also apply user examples
1806
- if (sanitizeExamples) {
1807
- for (const example of sanitizeExamples) {
1808
- if (obj[key].includes(example)) {
1809
- const replacement = session.getOrCreateReplacement(example, "unknown");
1810
- obj[key] = obj[key].split(example).join(replacement);
1811
- }
1812
- }
1813
- }
1814
- }
1815
- else if (typeof val === "object" && val !== null) {
1816
- sanitizeObject(val);
1817
- }
1818
- }
1819
- };
1820
- sanitizeObject(clonedConfig);
1821
- sanitizedConfig = clonedConfig;
1822
- }
1823
- // Optionally sanitize workflow
1824
- let sanitizedWorkflow = persona.workflow_def;
1825
- if (includeWorkflow && sanitizedWorkflow) {
1826
- const clonedWorkflow = JSON.parse(JSON.stringify(sanitizedWorkflow));
1827
- const sanitizeWfObj = (obj) => {
1828
- for (const key of Object.keys(obj)) {
1829
- const val = obj[key];
1830
- if (typeof val === "string" && val.length > 0) {
1831
- const detected = detectWithPatterns(val);
1832
- for (const entity of detected) {
1833
- const replacement = session.getOrCreateReplacement(entity.value, entity.type);
1834
- obj[key] = obj[key].split(entity.value).join(replacement);
1835
- }
1836
- }
1837
- else if (typeof val === "object" && val !== null) {
1838
- sanitizeWfObj(val);
1839
- }
1840
- }
1841
- };
1842
- sanitizeWfObj(clonedWorkflow);
1843
- sanitizedWorkflow = clonedWorkflow;
1844
- }
1845
- // Apply updates
1846
- await client.updateAiEmployee({
1847
- persona_id: personaId,
1848
- proto_config: sanitizedConfig ?? {},
1849
- workflow: includeWorkflow ? sanitizedWorkflow : undefined,
1850
- });
1851
- // Convert Map to array for JSON response
1852
- const mappingsMap = session.getMappings();
1853
- const mappingsArray = Array.from(mappingsMap.entries()).map(([original, replacement]) => ({
1854
- original,
1855
- replacement,
1856
- }));
1857
- return {
1858
- success: true,
1859
- persona_id: personaId,
1860
- entities_sanitized: mappingsMap.size,
1861
- mappings: mappingsArray,
1862
- workflow_sanitized: !!includeWorkflow,
1863
- };
1864
- }
1865
- // ─────────────────────────────────────────────────────────────────────────
1866
- // Embedding mode - status or toggle
1867
- // ─────────────────────────────────────────────────────────────────────────
1868
- if (mode === "embedding") {
1869
- if (!personaId) {
1870
- return { error: "persona_id is required for embedding mode" };
1871
- }
1872
- const knowledgeArgs = { ...args };
1873
- if (args.enabled !== undefined) {
1874
- knowledgeArgs.mode = "toggle";
1875
- }
1876
- else {
1877
- knowledgeArgs.mode = "status";
1878
- }
1879
- // Call handleKnowledge without deprecation warning (data is the new interface)
1880
- return handleKnowledge(knowledgeArgs, client, readFile, { includeDeprecationWarning: false });
1881
- }
1882
- // ─────────────────────────────────────────────────────────────────────────
1883
- // Replicate mode - copy data by reference (fast, no re-upload)
1884
- // ─────────────────────────────────────────────────────────────────────────
1885
- if (mode === "replicate") {
1886
- if (!personaId) {
1887
- return { error: "persona_id is required for replicate mode (this is the TARGET persona)" };
1888
- }
1889
- const sourcePersonaId = args.from;
1890
- if (!sourcePersonaId) {
1891
- return {
1892
- error: "from is required for replicate mode (source persona ID)",
1893
- hint: "Use data(method='replicate', from='source-persona-id') to copy data by reference",
1894
- };
1895
- }
1896
- // Parse and validate widget mappings if provided
1897
- const widgetMappingsRaw = args.widget_mappings;
1898
- let widgetMappings;
1899
- if (widgetMappingsRaw !== undefined) {
1900
- // Validate that widget_mappings is an array
1901
- if (!Array.isArray(widgetMappingsRaw)) {
1902
- return {
1903
- error: "widget_mappings must be an array",
1904
- hint: "Use widget_mappings=[{source_widget: 'kb', target_widget: 'kb'}]",
1905
- };
1906
- }
1907
- // Validate each mapping has at least source_widget
1908
- try {
1909
- widgetMappings = widgetMappingsRaw.map((m, i) => {
1910
- if (typeof m !== "object" || m === null) {
1911
- throw new Error(`widget_mappings[${i}] must be an object`);
1912
- }
1913
- const mapping = m;
1914
- const sourceWidget = mapping.source_widget ?? mapping.sourceWidget;
1915
- if (!sourceWidget || typeof sourceWidget !== "string") {
1916
- throw new Error(`widget_mappings[${i}].source_widget is required`);
1917
- }
1918
- return {
1919
- sourceWidget,
1920
- targetWidget: mapping.target_widget ?? mapping.targetWidget,
1921
- };
1922
- });
1923
- }
1924
- catch (validationErr) {
1925
- return {
1926
- error: `Invalid widget_mappings: ${validationErr.message}`,
1927
- hint: "Each mapping must have source_widget (string). target_widget is optional.",
1928
- };
1929
- }
1930
- }
1931
- else {
1932
- // Default: replicate knowledge_base
1933
- widgetMappings = [{ sourceWidget: "knowledge_base" }];
1934
- }
1935
- const shouldWait = args.wait !== false; // Default to true
1936
- try {
1937
- // Call replicateData - copies S3 references, not actual files
1938
- const result = await client.replicateData({ type: "persona", id: sourcePersonaId }, { type: "persona", id: personaId }, widgetMappings);
1939
- // If wait=true (default), poll for completion
1940
- if (shouldWait && result.results.length > 0) {
1941
- const requestIds = result.results.map(r => r.requestId).filter(Boolean);
1942
- const statuses = [];
1943
- for (const requestId of requestIds) {
1944
- try {
1945
- const status = await client.waitForReplication(requestId, personaId, {
1946
- timeoutMs: 60_000,
1947
- pollIntervalMs: 1_000,
1948
- });
1949
- statuses.push({ requestId, status: status.status, failedReason: status.failedReason });
1950
- }
1951
- catch (pollErr) {
1952
- statuses.push({ requestId, status: "timeout", failedReason: String(pollErr) });
1953
- }
1954
- }
1955
- const allCompleted = statuses.every(s => s.status === "completed");
1956
- const anyFailed = statuses.some(s => s.status === "failed");
1957
- return {
1958
- success: allCompleted,
1959
- source_persona_id: sourcePersonaId,
1960
- target_persona_id: personaId,
1961
- replication_results: result.results,
1962
- final_statuses: statuses,
1963
- message: allCompleted
1964
- ? "Data replicated successfully (by reference, no files copied)"
1965
- : anyFailed
1966
- ? "Some replications failed"
1967
- : "Replication completed with warnings",
1968
- _tip: "Replication copies S3 references - much faster than downloading and re-uploading files",
1969
- };
1970
- }
1971
- // If wait=false, return immediately with request IDs
1972
- return {
1973
- success: true,
1974
- source_persona_id: sourcePersonaId,
1975
- target_persona_id: personaId,
1976
- replication_results: result.results,
1977
- message: "Replication initiated (async)",
1978
- _tip: "Use getReplicationStatus with request IDs to check progress",
1979
- };
1980
- }
1981
- catch (err) {
1982
- const error = err;
1983
- return {
1984
- error: `Replication failed: ${error.message}`,
1985
- source_persona_id: sourcePersonaId,
1986
- target_persona_id: personaId,
1987
- hint: "Ensure both personas exist and have compatible data structures",
1988
- };
1989
- }
1990
- }
1991
- // ─────────────────────────────────────────────────────────────────────────
1992
- // Stats mode - get file counts by status
1993
- // ─────────────────────────────────────────────────────────────────────────
1994
- if (mode === "stats") {
1995
- if (!personaId) {
1996
- return { error: "persona_id is required for stats mode" };
1997
- }
1998
- const widgetName = args.widget_name;
1999
- try {
2000
- const aggregates = await client.getDataSourceAggregates(personaId, widgetName);
2001
- return {
2002
- persona_id: personaId,
2003
- widget_name: aggregates.widgetName ?? widgetName ?? "all",
2004
- total: aggregates.total,
2005
- pending: aggregates.pending,
2006
- success: aggregates.success,
2007
- failed: aggregates.failed,
2008
- _tip: "Use data(method='list') to see individual files",
2009
- };
2010
- }
2011
- catch (err) {
2012
- const error = err;
2013
- return {
2014
- error: `Failed to get data stats: ${error.message}`,
2015
- persona_id: personaId,
2016
- };
2017
- }
2018
- }
2019
- // ─────────────────────────────────────────────────────────────────────────
2020
- // Remaining modes (list, upload, delete) - delegate to knowledge handler
2021
- // ─────────────────────────────────────────────────────────────────────────
2022
- if (!personaId && mode !== "templates") {
2023
- return { error: "persona_id is required for this operation" };
2024
- }
2025
- // Call handleKnowledge without deprecation warning (data is the new interface)
2026
- return handleKnowledge(args, client, readFile, { includeDeprecationWarning: false });
2027
- }
2028
- // ═══════════════════════════════════════════════════════════════════════════
2029
- // SYNC Handler
2030
- // ═══════════════════════════════════════════════════════════════════════════
2031
- // Delegate to extracted handler
2032
- export const handleSync = handleSyncExtracted;
2033
- /**
2034
- * Analyze a natural language modification request and determine what operations are needed.
2035
- * This is the "smart" part - understanding user intent.
2036
- */
2037
- function analyzeModificationRequest(input, existingWorkflow) {
2038
- const lowerInput = input.toLowerCase();
2039
- const operations = [];
2040
- let intent = "modify workflow";
2041
- const actions = (existingWorkflow.actions || []);
2042
- // ─────────────────────────────────────────────────────────────────────────
2043
- // CONSOLIDATION: "consolidate X agents into one", "merge", "combine"
2044
- // ─────────────────────────────────────────────────────────────────────────
2045
- if (lowerInput.includes("consolidat") || lowerInput.includes("merge") ||
2046
- lowerInput.includes("combine") || lowerInput.includes("into one") ||
2047
- lowerInput.includes("single agent") || lowerInput.includes("unified")) {
2048
- intent = "consolidate multiple nodes into one";
2049
- // Find nodes to consolidate - look for patterns like "8 custom agents", "the agents"
2050
- const customAgents = actions.filter(a => {
2051
- const actionType = getActionType(a);
2052
- return actionType === "custom_agent" || actionType.includes("custom");
2053
- });
2054
- if (customAgents.length > 1) {
2055
- operations.push({
2056
- type: "consolidate",
2057
- description: `Consolidate ${customAgents.length} custom_agent nodes into a single unified agent`,
2058
- target_nodes: customAgents.map(a => String(a.name || "")),
2059
- });
2060
- }
2061
- }
2062
- // ─────────────────────────────────────────────────────────────────────────
2063
- // REMOVAL: "remove", "delete", "clean up orphan"
2064
- // ─────────────────────────────────────────────────────────────────────────
2065
- if (lowerInput.includes("remove") || lowerInput.includes("delete") ||
2066
- lowerInput.includes("clean up") || lowerInput.includes("orphan")) {
2067
- intent = "remove nodes";
2068
- // Check if removing orphans specifically
2069
- if (lowerInput.includes("orphan")) {
2070
- // Find orphan nodes by checking reachability
2071
- const orphanNodes = findOrphanNodes(existingWorkflow);
2072
- if (orphanNodes.length > 0) {
2073
- operations.push({
2074
- type: "remove",
2075
- description: `Remove ${orphanNodes.length} orphan nodes`,
2076
- target_nodes: orphanNodes,
2077
- });
2078
- }
2079
- }
2080
- // Check for specific node names mentioned
2081
- // Strategy 1: Look for "remove the node NODENAME" pattern
2082
- const explicitNodeMatch = input.match(/remove\s+(?:the\s+)?node\s+["']?([a-zA-Z0-9_]+)["']?/i);
2083
- if (explicitNodeMatch) {
2084
- const nodeName = explicitNodeMatch[1];
2085
- const matchingNodes = actions.filter(a => String(a.name || "").toLowerCase() === nodeName.toLowerCase() ||
2086
- String(a.name || "").toLowerCase().includes(nodeName.toLowerCase()));
2087
- if (matchingNodes.length > 0) {
2088
- operations.push({
2089
- type: "remove",
2090
- description: `Remove node(s) matching "${nodeName}"`,
2091
- target_nodes: matchingNodes.map(a => String(a.name || "")),
2092
- });
2093
- }
2094
- }
2095
- // Strategy 2: Check if any actual node ID appears in the input text
2096
- if (operations.length === 0) {
2097
- const nodeNames = actions.map(a => String(a.name || "")).filter(n => n.length > 0);
2098
- const foundNodes = nodeNames.filter(nodeName => input.toLowerCase().includes(nodeName.toLowerCase()));
2099
- if (foundNodes.length > 0) {
2100
- operations.push({
2101
- type: "remove",
2102
- description: `Remove node(s): ${foundNodes.join(", ")}`,
2103
- target_nodes: foundNodes,
2104
- });
2105
- }
2106
- }
2107
- // Strategy 3: Fallback - simple "remove X" pattern (for short names)
2108
- if (operations.length === 0) {
2109
- const simpleMatch = input.match(/remove\s+(?:the\s+)?(\w+)/i);
2110
- if (simpleMatch && simpleMatch[1].toLowerCase() !== "node") {
2111
- const nodeName = simpleMatch[1];
2112
- const matchingNodes = actions.filter(a => String(a.name || "").toLowerCase().includes(nodeName.toLowerCase()));
2113
- if (matchingNodes.length > 0) {
2114
- operations.push({
2115
- type: "remove",
2116
- description: `Remove node(s) matching "${nodeName}"`,
2117
- target_nodes: matchingNodes.map(a => String(a.name || "")),
2118
- });
2119
- }
2120
- }
2121
- }
2122
- }
2123
- // ─────────────────────────────────────────────────────────────────────────
2124
- // REPLACEMENT: "replace X with Y", "swap", "use X instead of Y"
2125
- // ─────────────────────────────────────────────────────────────────────────
2126
- if (lowerInput.includes("replace") || lowerInput.includes("swap") ||
2127
- lowerInput.includes("instead of") || lowerInput.includes("use entity_extraction")) {
2128
- intent = "replace nodes";
2129
- // Parse "replace X with Y" pattern
2130
- const replaceMatch = input.match(/replace\s+(?:the\s+)?(.+?)\s+with\s+(.+)/i);
2131
- if (replaceMatch) {
2132
- operations.push({
2133
- type: "replace",
2134
- description: `Replace ${replaceMatch[1]} with ${replaceMatch[2]}`,
2135
- source: replaceMatch[1],
2136
- target: replaceMatch[2],
2137
- });
2138
- }
2139
- // Special case: "use entity_extraction" suggests replacing LLM-based extraction
2140
- if (lowerInput.includes("entity_extraction")) {
2141
- const llmNodes = actions.filter(a => {
2142
- const actionType = getActionType(a);
2143
- const displayName = a.displaySettings?.displayName || "";
2144
- return (actionType === "call_llm" &&
2145
- (displayName.toLowerCase().includes("email") ||
2146
- displayName.toLowerCase().includes("extract")));
2147
- });
2148
- if (llmNodes.length > 0) {
2149
- operations.push({
2150
- type: "replace",
2151
- description: `Replace ${llmNodes.length} LLM extraction nodes with entity_extraction`,
2152
- target_nodes: llmNodes.map(a => String(a.name || "")),
2153
- });
2154
- }
2155
- }
2156
- }
2157
- // ─────────────────────────────────────────────────────────────────────────
2158
- // HITL: "add hitl", "add approval", "confirm before"
2159
- // ─────────────────────────────────────────────────────────────────────────
2160
- if (lowerInput.includes("hitl") || lowerInput.includes("approval") ||
2161
- lowerInput.includes("confirm") || lowerInput.includes("human")) {
2162
- intent = "add human-in-the-loop";
2163
- // Find the action to add HITL before
2164
- const beforeMatch = input.match(/before\s+(\w+)/i);
2165
- const targetAction = beforeMatch ? beforeMatch[1] : "email";
2166
- // Find email/send nodes
2167
- const emailNodes = actions.filter(a => {
2168
- const actionType = getActionType(a);
2169
- const nodeName = String(a.name || "").toLowerCase();
2170
- return actionType.includes("email") || nodeName.includes("email") ||
2171
- nodeName.includes("send") || nodeName.includes(targetAction.toLowerCase());
2172
- });
2173
- operations.push({
2174
- type: "add",
2175
- description: `Add HITL approval before ${emailNodes.length > 0 ? emailNodes[0].name : targetAction}`,
2176
- new_node: {
2177
- type: "general_hitl",
2178
- name: `hitl_approval_${Date.now()}`,
2179
- before: emailNodes.length > 0 ? String(emailNodes[0].name) : targetAction,
2180
- },
2181
- });
2182
- }
2183
- // ─────────────────────────────────────────────────────────────────────────
2184
- // WIRING: "connect", "wire", "link X to Y"
2185
- // ─────────────────────────────────────────────────────────────────────────
2186
- if (lowerInput.includes("connect") || lowerInput.includes("wire") ||
2187
- lowerInput.includes("link")) {
2188
- intent = "rewire connections";
2189
- // Parse "connect X to Y" pattern
2190
- const connectMatch = input.match(/(?:connect|wire|link)\s+(\w+)\s+to\s+(\w+)/i);
2191
- if (connectMatch) {
2192
- operations.push({
2193
- type: "rewire",
2194
- description: `Connect ${connectMatch[1]} to ${connectMatch[2]}`,
2195
- source: connectMatch[1],
2196
- target: connectMatch[2],
2197
- });
2198
- }
2199
- }
2200
- // ─────────────────────────────────────────────────────────────────────────
2201
- // ADD: "add", "include", "new"
2202
- // ─────────────────────────────────────────────────────────────────────────
2203
- if (operations.length === 0 && (lowerInput.includes("add") || lowerInput.includes("new") || lowerInput.includes("include"))) {
2204
- intent = "add new capabilities";
2205
- // Use existing parseExtensionInput for additive changes
2206
- const addChanges = parseExtensionInput(input);
2207
- for (const change of addChanges) {
2208
- operations.push({
2209
- type: "add",
2210
- description: change.description,
2211
- config: change.details,
2212
- });
2213
- }
2214
- }
2215
- // Determine confidence based on how specific the operations are
2216
- const confidence = operations.length > 0 && operations.every(op => op.target_nodes?.length || op.new_node) ? "high" :
2217
- operations.length > 0 ? "medium" : "low";
2218
- return { intent, operations, confidence };
2219
- }
2220
- /**
2221
- * Find orphan nodes (not reachable from trigger).
2222
- */
2223
- function findOrphanNodes(workflow) {
2224
- const actions = (workflow.actions || []);
2225
- const orphans = [];
2226
- // Build a set of all referenced nodes (nodes that receive input from other nodes)
2227
- const referencedNodes = new Set();
2228
- referencedNodes.add("trigger"); // Trigger is always reachable
2229
- // Find all nodes that are referenced in actionOutput bindings
2230
- for (const action of actions) {
2231
- const inputs = (action.inputs || {});
2232
- for (const inputValue of Object.values(inputs)) {
2233
- const binding = inputValue;
2234
- if (binding?.actionOutput) {
2235
- const actionOutput = binding.actionOutput;
2236
- referencedNodes.add(String(actionOutput.actionName || ""));
2237
- }
2238
- // Check multiBinding
2239
- if (binding?.multiBinding) {
2240
- const multi = binding.multiBinding;
2241
- const elements = (multi.elements || []);
2242
- for (const elem of elements) {
2243
- if (elem.namedBinding) {
2244
- const named = elem.namedBinding;
2245
- const value = named.value;
2246
- if (value?.actionOutput) {
2247
- const ao = value.actionOutput;
2248
- referencedNodes.add(String(ao.actionName || ""));
2249
- }
2250
- }
2251
- }
2252
- }
2253
- }
2254
- }
2255
- // Find nodes that don't receive input from trigger chain
2256
- // This is a simplified check - proper reachability would need graph traversal
2257
- for (const action of actions) {
2258
- const nodeName = String(action.name || "");
2259
- const hasInputFromTrigger = checkNodeHasPath(nodeName, actions, "trigger", new Set());
2260
- if (!hasInputFromTrigger && nodeName !== "trigger") {
2261
- orphans.push(nodeName);
2262
- }
2263
- }
2264
- return orphans;
2265
- }
2266
- /**
2267
- * Check if a node has a path from source (simple BFS).
2268
- */
2269
- function checkNodeHasPath(nodeName, actions, source, visited) {
2270
- if (visited.has(nodeName))
2271
- return false;
2272
- visited.add(nodeName);
2273
- const node = actions.find(a => a.name === nodeName);
2274
- if (!node)
2275
- return false;
2276
- const inputs = (node.inputs || {});
2277
- for (const inputValue of Object.values(inputs)) {
2278
- const binding = inputValue;
2279
- if (binding?.actionOutput) {
2280
- const actionOutput = binding.actionOutput;
2281
- const sourceNode = String(actionOutput.actionName || "");
2282
- if (sourceNode === source)
2283
- return true;
2284
- if (checkNodeHasPath(sourceNode, actions, source, visited))
2285
- return true;
2286
- }
2287
- }
2288
- return false;
2289
- }
2290
- /**
2291
- * Apply modifications to the workflow based on analyzed operations.
2292
- */
2293
- function applyWorkflowModifications(existingWorkflow, analysis) {
2294
- const workflow = JSON.parse(JSON.stringify(existingWorkflow));
2295
- const actions = (workflow.actions || []);
2296
- const enumTypes = (workflow.enumTypes || []);
2297
- const results = (workflow.results || {});
2298
- const changesApplied = [];
2299
- const nodesAdded = [];
2300
- const nodesRemoved = [];
2301
- const nodesModified = [];
2302
- const connectionsChanged = [];
2303
- for (const op of analysis.operations) {
2304
- switch (op.type) {
2305
- case "consolidate": {
2306
- // Consolidate multiple nodes into one unified agent
2307
- const targetNodes = op.target_nodes || [];
2308
- if (targetNodes.length < 2) {
2309
- changesApplied.push("Consolidation skipped: need 2+ nodes to consolidate");
2310
- break;
2311
- }
2312
- // Get the first node as template
2313
- const templateIdx = actions.findIndex(a => a.name === targetNodes[0]);
2314
- if (templateIdx < 0)
2315
- break;
2316
- const template = actions[templateIdx];
2317
- // Create unified agent node
2318
- const unifiedNode = {
2319
- name: "unified_analysis_agent",
2320
- action: template.action,
2321
- inputs: {
2322
- ...template.inputs,
2323
- // Update instructions to be dynamic
2324
- role_instructions: {
2325
- inline: {
2326
- wellKnown: {
2327
- stringValue: "You are a versatile analysis agent. Read your agent definition from the knowledge base based on the current intent type and adopt that role.",
2328
- },
2329
- },
2330
- autoDetectedBinding: false,
2331
- },
2332
- task_instructions: {
2333
- inline: {
2334
- wellKnown: {
2335
- stringValue: "Execute the task based on the intent type. The knowledge base contains definitions for each intent with INPUT DATA, ANALYSIS FRAMEWORK, and OUTPUT SCHEMA. Follow those precisely.",
2336
- },
2337
- },
2338
- autoDetectedBinding: false,
2339
- },
2340
- },
2341
- displaySettings: {
2342
- displayName: "Unified Analysis Agent",
2343
- description: `Consolidated from ${targetNodes.length} separate agents`,
2344
- coordinates: template.displaySettings?.coordinates || { x: 2000, y: 1500 },
2345
- },
2346
- typeArguments: template.typeArguments || {},
2347
- tools: template.tools || [],
2348
- disableHumanInteraction: false,
2349
- // No runIf - runs for all intents, uses internal routing
2350
- };
2351
- // Remove all target nodes
2352
- for (const nodeName of targetNodes) {
2353
- const idx = actions.findIndex(a => a.name === nodeName);
2354
- if (idx >= 0) {
2355
- actions.splice(idx, 1);
2356
- nodesRemoved.push(nodeName);
2357
- // Also remove from results mapping
2358
- const resultKey = Object.keys(results).find(k => k.startsWith(nodeName + "."));
2359
- if (resultKey) {
2360
- delete results[resultKey];
2361
- }
2362
- }
2363
- }
2364
- // Add unified node
2365
- actions.push(unifiedNode);
2366
- nodesAdded.push("unified_analysis_agent");
2367
- // Add to results mapping
2368
- results["unified_analysis_agent.response_with_sources"] = {
2369
- actionName: "unified_analysis_agent",
2370
- outputName: "response_with_sources",
2371
- };
2372
- changesApplied.push(`Consolidated ${targetNodes.length} nodes into unified_analysis_agent`);
2373
- break;
2374
- }
2375
- case "remove": {
2376
- const targetNodes = op.target_nodes || [];
2377
- for (const nodeName of targetNodes) {
2378
- const idx = actions.findIndex(a => a.name === nodeName);
2379
- if (idx >= 0) {
2380
- actions.splice(idx, 1);
2381
- nodesRemoved.push(nodeName);
2382
- // Also remove from results
2383
- const resultKey = Object.keys(results).find(k => k.startsWith(nodeName + "."));
2384
- if (resultKey) {
2385
- delete results[resultKey];
2386
- }
2387
- changesApplied.push(`Removed node: ${nodeName}`);
2388
- }
2389
- }
2390
- break;
2391
- }
2392
- case "replace": {
2393
- // For now, just note it - full replacement is complex
2394
- changesApplied.push(`Replace operation noted: ${op.description}. Manual verification needed.`);
2395
- break;
2396
- }
2397
- case "rewire": {
2398
- const source = op.source;
2399
- const target = op.target;
2400
- if (source && target) {
2401
- // Find target node and update its inputs
2402
- const targetNode = actions.find(a => String(a.name || "").toLowerCase().includes(target.toLowerCase()));
2403
- if (targetNode) {
2404
- nodesModified.push(String(targetNode.name));
2405
- connectionsChanged.push(`${source} → ${targetNode.name}`);
2406
- changesApplied.push(`Rewiring: ${source} → ${targetNode.name}`);
2407
- }
2408
- }
2409
- break;
2410
- }
2411
- case "add": {
2412
- if (op.new_node) {
2413
- const newNode = op.new_node;
2414
- const nodeName = String(newNode.name || `new_node_${Date.now()}`);
2415
- nodesAdded.push(nodeName);
2416
- changesApplied.push(`Added node: ${nodeName}`);
2417
- }
2418
- else if (op.config) {
2419
- // Use legacy merge for additive changes
2420
- const legacyChanges = parseExtensionInput(op.description);
2421
- const legacyResult = mergeWorkflowChanges(workflow, legacyChanges);
2422
- changesApplied.push(...legacyResult.changesApplied);
2423
- }
2424
- break;
2425
- }
2426
- case "modify_config": {
2427
- changesApplied.push(`Config modification noted: ${op.description}`);
2428
- break;
2429
- }
2430
- }
2431
- }
2432
- workflow.actions = actions;
2433
- workflow.enumTypes = enumTypes;
2434
- workflow.results = results;
2435
- return {
2436
- workflow,
2437
- changesApplied,
2438
- nodesAdded,
2439
- nodesRemoved,
2440
- nodesModified,
2441
- connectionsChanged,
2442
- };
2443
- }
2444
- /**
2445
- * Parse natural language input to detect what changes are being requested.
2446
- * Handles multiple changes in one input (e.g., "add X, add Y, add Z").
2447
- *
2448
- * IMPORTANT: We need to be smart about splitting - don't split enum options!
2449
- * "add caller_type enum options Advisor and Client" should keep "Advisor and Client" together
2450
- */
2451
- function parseExtensionInput(input) {
2452
- const changes = [];
2453
- // First, identify major change boundaries (comma-separated distinct changes)
2454
- // But be careful not to split within an enum options list
2455
- const segments = splitIntoChanges(input);
2456
- for (const segment of segments) {
2457
- const lowerSegment = segment.toLowerCase();
2458
- // Detect categorizer/enum changes - parse options carefully
2459
- if (lowerSegment.includes("categorizer") || lowerSegment.includes("enum") ||
2460
- lowerSegment.includes("caller_type") || lowerSegment.includes("actor")) {
2461
- // Extract options: "with Advisor and Client" or "options Advisor/Client" or "Advisor, Client"
2462
- const optionsMatch = segment.match(/(?:options?|with)\s+(.+?)(?:,\s*add|$)/i);
2463
- let options = [];
2464
- if (optionsMatch) {
2465
- // Split on "and", "/", or "," but preserve the values
2466
- options = optionsMatch[1]
2467
- .split(/\s+and\s+|[,/]/i)
2468
- .map(s => s.trim())
2469
- .filter(s => s.length > 0 && !s.toLowerCase().includes('add'));
2470
- }
2471
- // Also look for target categorizer name
2472
- const categorizerMatch = segment.match(/(?:to\s+)?(\w+_categorizer|\w+categorizer)/i);
2473
- const targetCategorizer = categorizerMatch ? categorizerMatch[1] : undefined;
2474
- changes.push({
2475
- type: "categorizer",
2476
- description: `Add enum options: ${options.join(', ')}`,
2477
- details: {
2478
- raw: segment,
2479
- options,
2480
- target_categorizer: targetCategorizer,
2481
- },
2482
- });
2483
- }
2484
- // Detect HITL/approval changes
2485
- else if (lowerSegment.includes("hitl") || lowerSegment.includes("approval") ||
2486
- lowerSegment.includes("confirm") || lowerSegment.includes("human")) {
2487
- // Parse "before email" or "before send_email" or "before send"
2488
- const beforeMatch = segment.match(/before\s+(\w+(?:_\w+)?)/i);
2489
- const targetAction = beforeMatch ? beforeMatch[1] : "unspecified";
2490
- changes.push({
2491
- type: "hitl",
2492
- description: `Add human-in-the-loop approval before ${targetAction}`,
2493
- details: { raw: segment, before_action: targetAction },
2494
- });
2495
- }
2496
- // Detect wiring/connection changes - parse source → target(s)
2497
- else if (lowerSegment.includes("connect") || lowerSegment.includes("wire") ||
2498
- lowerSegment.includes("link") || lowerSegment.match(/\w+\s+to\s+\w+\.\w+/)) {
2499
- // Parse: "connect entity_extraction_1 to send_communications_handler to_email and subject"
2500
- // Supports multiple target inputs separated by "and"
2501
- const wiringMatch = segment.match(/(?:connect\s+)?(\w+)(?:\.(\w+))?\s+(?:to|→|->)\s+(\w+)(?:\.|\s+)(.+)/i);
2502
- if (wiringMatch) {
2503
- const sourceNode = wiringMatch[1];
2504
- const sourceOutput = wiringMatch[2] || "extracted_entities";
2505
- const targetNode = wiringMatch[3];
2506
- // Parse multiple target inputs: "to_email and subject" or "to_email, subject"
2507
- const targetInputsRaw = wiringMatch[4];
2508
- const targetInputs = targetInputsRaw
2509
- .split(/\s+and\s+|[,]/i)
2510
- .map(s => s.trim())
2511
- .filter(s => s.length > 0 && /^\w+$/.test(s));
2512
- // Create a wiring change for each target input
2513
- for (const targetInput of targetInputs) {
2514
- changes.push({
2515
- type: "wiring",
2516
- description: `Connect ${sourceNode} to ${targetNode}.${targetInput}`,
2517
- details: {
2518
- raw: segment,
2519
- source_node: sourceNode,
2520
- source_output: sourceOutput,
2521
- target_node: targetNode,
2522
- target_input: targetInput,
2523
- },
2524
- });
2525
- }
2526
- if (targetInputs.length === 0) {
2527
- changes.push({
2528
- type: "wiring",
2529
- description: `Modify connections: ${segment}`,
2530
- details: { raw: segment },
2531
- });
2532
- }
2533
- }
2534
- else {
2535
- changes.push({
2536
- type: "wiring",
2537
- description: `Modify connections: ${segment}`,
2538
- details: { raw: segment },
2539
- });
2540
- }
2541
- }
2542
- // Detect intent changes
2543
- else if (lowerSegment.includes("intent")) {
2544
- const intentMatch = segment.match(/(?:add|new)\s+(?:intent:?\s*)?(\w+)/i);
2545
- const intentName = intentMatch ? intentMatch[1] : "new_intent";
2546
- changes.push({
2547
- type: "intent",
2548
- description: `Add new intent: ${intentName}`,
2549
- details: { raw: segment, intent_name: intentName },
2550
- });
2551
- }
2552
- // Detect new node/capability additions
2553
- else if (lowerSegment.includes("add") || lowerSegment.includes("new") || lowerSegment.includes("include")) {
2554
- changes.push({
2555
- type: "add_node",
2556
- description: `Add new capability: ${segment}`,
2557
- details: { raw: segment },
2558
- });
2559
- }
2560
- }
2561
- return changes;
2562
- }
2563
- /**
2564
- * Split input into distinct change requests, being careful not to split
2565
- * within enum option lists.
2566
- */
2567
- function splitIntoChanges(input) {
2568
- const changes = [];
2569
- // Split on ", add", ", connect", ", wire" patterns - these start new change requests
2570
- // Use a regex that keeps the keyword with the following text
2571
- const parts = input.split(/,\s*(?=add\s|connect\s|wire\s|link\s)/i);
2572
- for (const part of parts) {
2573
- const trimmed = part.trim();
2574
- // Skip very short fragments that are just keywords
2575
- if (trimmed.length > 5) {
2576
- changes.push(trimmed);
2577
- }
2578
- }
2579
- // If no splits found, return the whole input
2580
- if (changes.length === 0 && input.trim().length > 0) {
2581
- changes.push(input.trim());
2582
- }
2583
- return changes;
2584
- }
2585
- /**
2586
- * Extract the action type from a workflow action node.
2587
- * Handles nested structure: action.action.name.name or action.action.name
2588
- */
2589
- function getActionType(action) {
2590
- const actionObj = action.action;
2591
- if (!actionObj)
2592
- return "";
2593
- const nameObj = actionObj.name;
2594
- if (!nameObj)
2595
- return "";
2596
- // Nested structure: action.name.name
2597
- if (typeof nameObj === "object" && "name" in nameObj) {
2598
- return String(nameObj.name || "");
2599
- }
2600
- // Direct string
2601
- if (typeof nameObj === "string")
2602
- return nameObj;
2603
- return "";
2604
- }
2605
- /**
2606
- * Merge detected changes into an existing workflow.
2607
- * Returns the modified workflow and list of new nodes that need configuration.
2608
- */
2609
- function mergeWorkflowChanges(existingWorkflow, changes) {
2610
- const workflow = JSON.parse(JSON.stringify(existingWorkflow));
2611
- const actions = (workflow.actions || []);
2612
- const enumTypes = (workflow.enumTypes || []);
2613
- const newNodes = [];
2614
- const changesApplied = [];
2615
- for (const change of changes) {
2616
- switch (change.type) {
2617
- case "categorizer":
2618
- case "enum_option": {
2619
- const options = change.details.options || [];
2620
- const targetCategorizerName = change.details.target_categorizer;
2621
- if (options.length === 0) {
2622
- changesApplied.push("No enum options specified");
2623
- break;
2624
- }
2625
- // Find the target categorizer action
2626
- const categorizer = actions.find(a => {
2627
- const nodeName = String(a.name || "");
2628
- const actionType = getActionType(a);
2629
- // Match by specific name or by type
2630
- if (targetCategorizerName) {
2631
- return nodeName.toLowerCase().includes(targetCategorizerName.toLowerCase());
2632
- }
2633
- return actionType.includes("categorizer");
2634
- });
2635
- if (!categorizer) {
2636
- changesApplied.push(`No categorizer found to add options: ${options.join(', ')}`);
2637
- break;
2638
- }
2639
- const categorizerName = String(categorizer.name || "");
2640
- // Find the corresponding enumType for this categorizer
2641
- // EnumType structure: { name: { name: { name: "EnumName", namespaces: [] } }, options: [...] }
2642
- let targetEnum = enumTypes.find(e => {
2643
- const enumName = extractEnumTypeName(e);
2644
- // Match by categorizer name or by checking typeArguments reference
2645
- return enumName && (enumName.toLowerCase().includes(categorizerName.toLowerCase()) ||
2646
- categorizerName.toLowerCase().includes(enumName.toLowerCase().replace(/_categories$/i, '')));
2647
- });
2648
- // If no matching enum found, try to find any enum (for simple workflows)
2649
- if (!targetEnum && enumTypes.length === 1) {
2650
- targetEnum = enumTypes[0];
2651
- }
2652
- if (targetEnum) {
2653
- // Add options to enumTypes[].options[]
2654
- const enumOptions = (targetEnum.options || []);
2655
- for (const opt of options) {
2656
- const optLower = opt.toLowerCase();
2657
- const exists = enumOptions.some(o => (o.name || "").toLowerCase() === optLower);
2658
- if (!exists) {
2659
- enumOptions.push({
2660
- name: opt,
2661
- description: `Added via MCP extend`
2662
- });
2663
- changesApplied.push(`Added "${opt}" to enumType.options`);
2664
- }
2665
- }
2666
- targetEnum.options = enumOptions;
2667
- }
2668
- else {
2669
- // No enumType found - note it for manual action
2670
- changesApplied.push(`Enum options noted but no enumType found for ${categorizerName}: ${options.join(', ')}. Create enumType manually.`);
2671
- }
2672
- break;
2673
- }
2674
- case "hitl": {
2675
- const targetAction = change.details.before_action;
2676
- const hitlNodeName = `hitl_${targetAction || "approval"}_${Date.now()}`;
2677
- // Find the target action to insert HITL before
2678
- const targetNode = actions.find(a => {
2679
- const nodeName = String(a.name || "").toLowerCase();
2680
- return nodeName.includes(targetAction.toLowerCase());
2681
- });
2682
- newNodes.push({
2683
- name: hitlNodeName,
2684
- type: "human_in_the_loop",
2685
- recommended_agent: "general_hitl",
2686
- });
2687
- if (targetNode) {
2688
- changesApplied.push(`HITL node "${hitlNodeName}" to be inserted before "${targetNode.name}"`);
2689
- }
2690
- else {
2691
- changesApplied.push(`HITL node "${hitlNodeName}" identified (target "${targetAction}" not found - wire manually)`);
2692
- }
2693
- break;
2694
- }
2695
- case "wiring": {
2696
- const sourceNode = change.details.source_node;
2697
- const sourceOutput = change.details.source_output;
2698
- const targetNode = change.details.target_node;
2699
- const targetInput = change.details.target_input;
2700
- if (sourceNode && targetNode && targetInput) {
2701
- // Find the target action and update its input
2702
- const target = actions.find(a => {
2703
- const nodeName = String(a.name || "").toLowerCase();
2704
- return nodeName.includes(targetNode.toLowerCase());
2705
- });
2706
- if (target && target.inputs) {
2707
- const inputs = target.inputs;
2708
- // Find the input field to update (may be nested as to_email, email_to, subject, etc.)
2709
- const inputKey = Object.keys(inputs).find(k => k.toLowerCase().includes(targetInput.toLowerCase()) ||
2710
- targetInput.toLowerCase().includes(k.toLowerCase()));
2711
- if (inputKey) {
2712
- // Update the actionOutput reference
2713
- inputs[inputKey] = {
2714
- actionOutput: {
2715
- actionName: sourceNode,
2716
- output: sourceOutput || "extracted_entities",
2717
- },
2718
- autoDetectedBinding: false,
2719
- };
2720
- changesApplied.push(`Wired ${sourceNode}.${sourceOutput || "output"} → ${target.name}.${inputKey}`);
2721
- }
2722
- else {
2723
- changesApplied.push(`Input "${targetInput}" not found on "${target.name}" - wire manually`);
2724
- }
2725
- }
2726
- else {
2727
- changesApplied.push(`Target node "${targetNode}" not found - wire manually`);
2728
- }
2729
- }
2730
- else {
2731
- changesApplied.push(`Wiring change noted: ${change.description} (incomplete - specify source and target)`);
2732
- }
2733
- break;
2734
- }
2735
- case "intent": {
2736
- const intentName = change.details.intent_name;
2737
- // Add to enumType options (same as categorizer case)
2738
- const categorizer = actions.find(a => getActionType(a).includes("categorizer"));
2739
- if (categorizer && enumTypes.length > 0) {
2740
- const targetEnum = enumTypes[0]; // Use first enum for intents
2741
- const enumOptions = (targetEnum.options || []);
2742
- const exists = enumOptions.some(o => (o.name || "").toLowerCase() === intentName.toLowerCase());
2743
- if (!exists) {
2744
- enumOptions.push({ name: intentName });
2745
- targetEnum.options = enumOptions;
2746
- changesApplied.push(`Added intent "${intentName}" to enumType.options`);
2747
- }
2748
- }
2749
- else {
2750
- changesApplied.push(`Intent "${intentName}" noted - add categorizer with enumType first`);
2751
- }
2752
- break;
2753
- }
2754
- case "add_node": {
2755
- const raw = change.details.raw.toLowerCase();
2756
- let nodeType = "unknown";
2757
- let recommended = "call_llm";
2758
- if (raw.includes("search") || raw.includes("kb") || raw.includes("knowledge")) {
2759
- nodeType = "search";
2760
- recommended = "search";
2761
- }
2762
- else if (raw.includes("email")) {
2763
- nodeType = "send_email";
2764
- recommended = "send_email_agent";
2765
- }
2766
- else if (raw.includes("ticket")) {
2767
- nodeType = "external_action";
2768
- recommended = "external_action_caller";
2769
- }
2770
- else if (raw.includes("extract")) {
2771
- nodeType = "entity_extraction";
2772
- recommended = "entity_extraction_with_documents";
2773
- }
2774
- newNodes.push({
2775
- name: `${nodeType}_${Date.now()}`,
2776
- type: nodeType,
2777
- recommended_agent: recommended,
2778
- });
2779
- changesApplied.push(`New node identified: ${nodeType}`);
2780
- break;
2781
- }
2782
- }
2783
- }
2784
- workflow.actions = actions;
2785
- workflow.enumTypes = enumTypes;
2786
- return { workflow, newNodes, changesApplied };
2787
- }
2788
- /**
2789
- * Extract the name from an enumType's nested name structure.
2790
- * Structure: { name: { name: { name: "ActualName", namespaces: [] } } }
2791
- */
2792
- function extractEnumTypeName(enumType) {
2793
- try {
2794
- const outerName = enumType.name;
2795
- if (!outerName)
2796
- return null;
2797
- const innerName = outerName.name;
2798
- if (typeof innerName === "string")
2799
- return innerName;
2800
- if (!innerName)
2801
- return null;
2802
- const actualName = innerName.name;
2803
- if (typeof actualName === "string")
2804
- return actualName;
2805
- return null;
2806
- }
2807
- catch {
2808
- return null;
2809
- }
2810
- }
2811
- /**
2812
- * Sanitize workflow before deployment to prevent API errors.
2813
- * Same logic as deploy_workflow uses.
2814
- */
2815
- function sanitizeWorkflowForDeploy(workflow) {
2816
- const sanitized = JSON.parse(JSON.stringify(workflow));
2817
- // Fix enumTypes - remove entries with empty/missing names (causes server panic)
2818
- // EnumType structure: { name: { name: { name: "string", namespaces: [] } }, options: [...] }
2819
- const enumTypes = sanitized.enumTypes;
2820
- if (Array.isArray(enumTypes)) {
2821
- const validEnumTypes = enumTypes.filter(e => {
2822
- const outerName = e.name;
2823
- const innerName = outerName?.name;
2824
- const actualName = innerName?.name;
2825
- return typeof actualName === "string" && actualName.trim().length > 0;
2826
- });
2827
- if (validEnumTypes.length > 0) {
2828
- sanitized.enumTypes = validEnumTypes;
2829
- }
2830
- else {
2831
- delete sanitized.enumTypes;
2832
- }
2833
- }
2834
- // Ensure all actions have a 'name' field (node identifier)
2835
- const actions = sanitized.actions;
2836
- if (Array.isArray(actions)) {
2837
- for (const action of actions) {
2838
- // If action has 'actionName' but not 'name', fix it
2839
- if (!action.name && action.actionName) {
2840
- action.name = action.actionName;
2841
- }
2842
- // Ensure name is a non-empty string
2843
- if (typeof action.name !== "string" || !action.name.trim()) {
2844
- // Generate a name from action type if possible
2845
- const actionType = getActionType(action);
2846
- if (actionType) {
2847
- action.name = `${actionType}_${Date.now()}`;
2848
- }
2849
- }
2850
- }
2851
- }
2852
- return sanitized;
2853
- }
2854
- // ═══════════════════════════════════════════════════════════════════════════
2855
- // Helper Functions
2856
- // ═══════════════════════════════════════════════════════════════════════════
2857
- // resolvePersona imported from handlers/utils.ts
2858
- function comparePersonas(p1, p2) {
2859
- const diffs = {};
2860
- if (p1.name !== p2.name)
2861
- diffs.name = { from: p1.name, to: p2.name };
2862
- if (p1.description !== p2.description)
2863
- diffs.description = "different";
2864
- if (p1.status !== p2.status)
2865
- diffs.status = { from: p1.status, to: p2.status };
2866
- if (p1.template_id !== p2.template_id)
2867
- diffs.template_id = { from: p1.template_id, to: p2.template_id };
2868
- // Compare workflows
2869
- const w1 = p1.workflow_def;
2870
- const w2 = p2.workflow_def;
2871
- if (JSON.stringify(w1) !== JSON.stringify(w2)) {
2872
- diffs.workflow = "different";
2873
- }
2874
- return diffs;
2875
- }
2876
- function compareWorkflows(w1, w2) {
2877
- if (!w1 && !w2)
2878
- return { equal: true };
2879
- if (!w1 || !w2)
2880
- return { equal: false, reason: "one workflow missing" };
2881
- const actions1 = w1.actions || [];
2882
- const actions2 = w2.actions || [];
2883
- return {
2884
- equal: JSON.stringify(w1) === JSON.stringify(w2),
2885
- node_count: { before: actions1.length, after: actions2.length },
2886
- };
2887
- }
2888
- function calculateMetrics(workflow) {
2889
- const actions = workflow.actions || [];
2890
- // Helper to get action type from nested structure
2891
- const getType = (a) => {
2892
- const action = a.action;
2893
- if (!action?.name)
2894
- return "";
2895
- const nameObj = action.name;
2896
- if (typeof nameObj === "string")
2897
- return nameObj;
2898
- if (typeof nameObj === "object" && "name" in nameObj)
2899
- return String(nameObj.name || "");
2900
- return "";
2901
- };
2902
- return {
2903
- node_count: actions.length,
2904
- has_categorizer: actions.some(a => getType(a).includes("categorizer")),
2905
- has_hitl: actions.some(a => getType(a) === "general_hitl"),
2906
- has_external: actions.some(a => getType(a) === "external_action_caller"),
2907
- };
2908
- }
2909
- /**
2910
- * Apply simple workflow fixes for auto_fixable issues
2911
- * Handles orphan removal with cascading dependency cleanup
2912
- */
2913
- function applySimpleFixes(workflow, issues) {
2914
- // Deep clone to avoid mutating original
2915
- const fixed = JSON.parse(JSON.stringify(workflow));
2916
- // Get nodes array from workflow (handle different structures)
2917
- const getNodes = (w) => {
2918
- if (Array.isArray(w.nodes))
2919
- return w.nodes;
2920
- const wd = w.workflow_def;
2921
- if (wd && Array.isArray(wd.nodes))
2922
- return wd.nodes;
2923
- return undefined;
2924
- };
2925
- const nodes = getNodes(fixed);
2926
- if (!nodes)
2927
- return fixed;
2928
- // Collect all orphan node IDs to remove
2929
- const nodesToRemove = new Set();
2930
- for (const issue of issues) {
2931
- if (issue.type === "orphan" && issue.auto_fixable && issue.node) {
2932
- nodesToRemove.add(issue.node);
2933
- }
2934
- }
2935
- if (nodesToRemove.size === 0)
2936
- return fixed;
2937
- // Step 1: Remove orphan nodes
2938
- const filteredNodes = nodes.filter(n => {
2939
- const nodeId = n.id;
2940
- return nodeId && !nodesToRemove.has(nodeId);
2941
- });
2942
- // Step 2: Clean up dangling references in remaining nodes
2943
- for (const node of filteredNodes) {
2944
- const incomingEdges = node.incoming_edges;
2945
- if (incomingEdges && Array.isArray(incomingEdges)) {
2946
- // Filter out edges that reference removed nodes
2947
- node.incoming_edges = incomingEdges.filter(edge => {
2948
- const sourceNodeId = edge.source_node_id;
2949
- return sourceNodeId && !nodesToRemove.has(sourceNodeId);
2950
- });
2951
- }
2952
- // Also clean up inputBindings if they exist (different workflow format)
2953
- const inputBindings = node.inputBindings;
2954
- if (inputBindings && Array.isArray(inputBindings)) {
2955
- node.inputBindings = inputBindings.filter(binding => {
2956
- const actionOutput = binding.actionOutput;
2957
- const actionName = actionOutput?.actionName;
2958
- return !actionName || !nodesToRemove.has(actionName);
2959
- });
2960
- }
2961
- }
2962
- // Update nodes in the workflow
2963
- if (Array.isArray(fixed.nodes)) {
2964
- fixed.nodes = filteredNodes;
2965
- }
2966
- else {
2967
- const wd = fixed.workflow_def;
2968
- if (wd) {
2969
- wd.nodes = filteredNodes;
2970
- }
2971
- }
2972
- return fixed;
2973
- }
349
+ // Re-export the extracted handler for backwards compatibility
350
+ export { handleWorkflow } from "./handlers/workflow/index.js";