@ema.co/mcp-toolkit 2026.2.5 → 2026.2.19

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 (67) hide show
  1. package/.context/public/guides/ema-user-guide.md +12 -16
  2. package/.context/public/guides/mcp-tools-guide.md +203 -334
  3. package/LICENSE +29 -21
  4. package/README.md +58 -35
  5. package/dist/mcp/domain/loop-detection.js +97 -0
  6. package/dist/mcp/domain/proto-constraints.js +284 -0
  7. package/dist/mcp/domain/structural-rules.js +12 -5
  8. package/dist/mcp/domain/validation-rules.js +107 -20
  9. package/dist/mcp/domain/workflow-graph-optimizer.js +235 -0
  10. package/dist/mcp/domain/workflow-graph-transforms.js +808 -0
  11. package/dist/mcp/domain/workflow-graph.js +374 -0
  12. package/dist/mcp/domain/workflow-optimizer.js +10 -4
  13. package/dist/mcp/guidance.js +54 -31
  14. package/dist/mcp/handlers/feedback/index.js +139 -0
  15. package/dist/mcp/handlers/feedback/store.js +262 -0
  16. package/dist/mcp/handlers/persona/index.js +237 -8
  17. package/dist/mcp/handlers/persona/schema.js +27 -0
  18. package/dist/mcp/handlers/reference/index.js +6 -4
  19. package/dist/mcp/handlers/workflow/index.js +25 -28
  20. package/dist/mcp/handlers/workflow/optimize.js +73 -33
  21. package/dist/mcp/handlers/workflow/validation.js +1 -1
  22. package/dist/mcp/knowledge-types.js +7 -0
  23. package/dist/mcp/knowledge.js +146 -834
  24. package/dist/mcp/resources.js +610 -18
  25. package/dist/mcp/server.js +233 -2156
  26. package/dist/mcp/tools.js +91 -5
  27. package/dist/sdk/generated/agent-catalog.js +615 -0
  28. package/dist/sdk/generated/deprecated-actions.js +182 -96
  29. package/dist/sdk/generated/proto-fields.js +2 -1
  30. package/dist/sdk/generated/protos/service/agent_qa/v1/agent_qa_pb.js +460 -21
  31. package/dist/sdk/generated/protos/service/auth/v1/auth_pb.js +11 -1
  32. package/dist/sdk/generated/protos/service/dataingest/v1/dataingest_pb.js +173 -66
  33. package/dist/sdk/generated/protos/service/feedback/v1/feedback_pb.js +43 -1
  34. package/dist/sdk/generated/protos/service/llmservice/v1/llmservice_pb.js +26 -21
  35. package/dist/sdk/generated/protos/service/persona/v1/persona_config_pb.js +100 -89
  36. package/dist/sdk/generated/protos/service/persona/v1/persona_pb.js +126 -116
  37. package/dist/sdk/generated/protos/service/persona/v1/shared_widgets/widget_types_pb.js +33 -1
  38. package/dist/sdk/generated/protos/service/persona/v1/voicebot_widgets/widget_types_pb.js +60 -11
  39. package/dist/sdk/generated/protos/service/tenant/v1/tenant_pb.js +1 -1
  40. package/dist/sdk/generated/protos/service/user/v1/user_pb.js +1 -1
  41. package/dist/sdk/generated/protos/service/utils/v1/agent_qa_pb.js +35 -0
  42. package/dist/sdk/generated/protos/service/workflows/v1/action_registry_pb.js +1 -1
  43. package/dist/sdk/generated/protos/service/workflows/v1/action_type_pb.js +6 -1
  44. package/dist/sdk/generated/protos/service/workflows/v1/chatbot_pb.js +106 -11
  45. package/dist/sdk/generated/protos/service/workflows/v1/common_forms_pb.js +1 -1
  46. package/dist/sdk/generated/protos/service/workflows/v1/coordinator_pb.js +1 -1
  47. package/dist/sdk/generated/protos/service/workflows/v1/external_actions_pb.js +31 -1
  48. package/dist/sdk/generated/protos/service/workflows/v1/well_known_pb.js +5 -1
  49. package/dist/sdk/generated/protos/service/workflows/v1/workflow_pb.js +1 -1
  50. package/dist/sdk/generated/protos/util/tracking_metadata_pb.js +1 -1
  51. package/dist/sdk/generated/widget-catalog.js +60 -0
  52. package/docs/README.md +17 -9
  53. package/package.json +2 -2
  54. package/.context/public/guides/dashboard-operations.md +0 -286
  55. package/.context/public/guides/email-patterns.md +0 -125
  56. package/dist/mcp/domain/intent-architect.js +0 -914
  57. package/dist/mcp/domain/quality-gates.js +0 -110
  58. package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
  59. package/dist/mcp/domain/workflow-intent.js +0 -1806
  60. package/dist/mcp/domain/workflow-merge.js +0 -449
  61. package/dist/mcp/domain/workflow-tracer.js +0 -648
  62. package/dist/mcp/domain/workflow-transformer.js +0 -742
  63. package/dist/mcp/handlers/persona/intent.js +0 -141
  64. package/dist/mcp/handlers/workflow/analyze.js +0 -119
  65. package/dist/mcp/handlers/workflow/compare.js +0 -70
  66. package/dist/mcp/handlers/workflow/generate.js +0 -384
  67. package/dist/mcp/handlers-consolidated.js +0 -333
@@ -1,742 +0,0 @@
1
- /**
2
- * Workflow Transformer
3
- *
4
- * The LLM-native approach to workflow manipulation:
5
- * 1. Decompile raw workflow_def to typed WorkflowSpec
6
- * 2. Expose schema for LLM context
7
- * 3. LLM transforms the spec directly
8
- * 4. Compile back to workflow_def
9
- *
10
- * WHY THIS APPROACH:
11
- * - LLMs excel at structured data transformation
12
- * - Type definitions serve as schema documentation
13
- * - Validation happens at compile time
14
- * - No brittle keyword matching
15
- */
16
- import { compileWorkflow } from "./workflow-generator.js";
17
- import { STRUCTURAL_RULES_FOR_LLM } from "./structural-rules.js";
18
- // ═══════════════════════════════════════════════════════════════════════════
19
- // SCHEMA DOCUMENTATION FOR LLM CONTEXT
20
- // ═══════════════════════════════════════════════════════════════════════════
21
- /**
22
- * This schema is designed to be included in LLM context.
23
- * The LLM reads this, understands the structure, and outputs valid specs.
24
- */
25
- export const WORKFLOW_SCHEMA_FOR_LLM = `
26
- # Ema Workflow Schema
27
-
28
- ## Overview
29
- Workflows are defined as a WorkflowSpec object with nodes and connections.
30
- Each node has typed inputs/outputs. Connections are defined via InputBinding.
31
-
32
- ## Core Types
33
-
34
- ### WorkflowSpec
35
- \`\`\`typescript
36
- interface WorkflowSpec {
37
- name: string; // Workflow identifier
38
- description: string; // What this workflow does
39
- personaType: "voice" | "chat" | "dashboard";
40
- nodes: Node[]; // All workflow nodes
41
- resultMappings: ResultMapping[]; // Which outputs go to WORKFLOW_OUTPUT
42
- }
43
- \`\`\`
44
-
45
- ### Node
46
- \`\`\`typescript
47
- interface Node {
48
- id: string; // Unique node identifier (e.g., "search_1", "categorizer_main")
49
- actionType: ActionType; // Node type (see ActionType below)
50
- displayName: string; // Human-readable name
51
- description?: string; // What this node does
52
- inputs?: Record<string, InputBinding>; // Input connections
53
- runIf?: RunIfCondition; // Conditional execution
54
- categories?: Category[]; // For categorizer nodes only
55
- tools?: Tool[]; // For custom_agent nodes
56
- disableHitl?: boolean; // Skip human-in-the-loop
57
- }
58
- \`\`\`
59
-
60
- ### ActionType (Available Node Types)
61
- \`\`\`typescript
62
- type ActionType =
63
- // Triggers (entry points)
64
- | "chat_trigger" // Chat message entry point
65
- | "voice_trigger" // Phone/voice call entry point
66
- | "document_trigger" // Document upload entry point
67
-
68
- // Routing (categorization)
69
- | "chat_categorizer" // Categorizes chat conversation
70
- | "text_categorizer" // Categorizes text
71
-
72
- // Search (retrieval)
73
- | "search" // Knowledge base search
74
- | "live_web_search" // Real-time web search
75
- | "combine_search_results" // Merge multiple searches
76
- | "conversation_to_search_query" // Summarize conversation for search
77
-
78
- // Generation (LLM responses)
79
- | "respond_with_sources" // RAG response with citations
80
- | "call_llm" // Custom LLM generation
81
- | "fixed_response" // Template response (no LLM)
82
-
83
- // Entity extraction
84
- | "entity_extraction" // Extract structured data from text
85
-
86
- // External actions
87
- | "external_action_caller" // Call APIs (Salesforce, ServiceNow, etc.)
88
- | "send_email_agent" // Send email
89
-
90
- // Human-in-the-loop
91
- | "general_hitl" // Human approval step
92
-
93
- // Validation
94
- | "response_validator"; // Validate LLM output
95
- \`\`\`
96
-
97
- ### InputBinding (How Nodes Connect)
98
- \`\`\`typescript
99
- interface InputBinding {
100
- type: "action_output" | "inline_string" | "inline_number" | "inline_bool" | "widget_config" | "llm_inferred";
101
-
102
- // For action_output (connecting nodes):
103
- actionName?: string; // Source node ID
104
- output?: string; // Source output name
105
-
106
- // For inline values:
107
- value?: string | number | boolean;
108
-
109
- // For widget config (persona settings):
110
- widgetName?: string;
111
-
112
- // For call_llm named inputs:
113
- namedInputs?: Array<{ name: string; binding: InputBinding }>;
114
- }
115
- \`\`\`
116
-
117
- ### Common Input/Output Names
118
-
119
- #### trigger (chat_trigger)
120
- - Outputs: user_query, chat_conversation
121
-
122
- #### categorizer (chat_categorizer, text_categorizer)
123
- - Inputs: text (the text to categorize)
124
- - Outputs: category (the selected category as enum)
125
-
126
- #### search
127
- - Inputs: query (search query text)
128
- - Outputs: search_results (SEARCH_RESULT type)
129
-
130
- #### call_llm
131
- - Inputs: query, instructions, named_inputs (for additional context)
132
- - Outputs: response_with_sources (TEXT_WITH_SOURCES type)
133
-
134
- #### respond_with_sources
135
- - Inputs: query, search_results
136
- - Outputs: response_with_sources (TEXT_WITH_SOURCES type)
137
-
138
- #### entity_extraction
139
- - Inputs: text_input, extraction_columns (schema)
140
- - Outputs: extracted_entities (structured data)
141
-
142
- #### send_email_agent
143
- - Inputs: to_email, subject, email_body, sender_email
144
- - Outputs: email_result
145
-
146
- #### general_hitl
147
- - Inputs: summary (what to approve)
148
- - Outputs: approval_decision (approve/reject enum)
149
-
150
- ### RunIfCondition (Conditional Execution)
151
- \`\`\`typescript
152
- interface RunIfCondition {
153
- sourceAction: string; // Node ID to check
154
- sourceOutput: string; // Output to compare
155
- operator: "eq" | "neq"; // Comparison operator
156
- value: string; // Expected value (category name for categorizers)
157
- }
158
- \`\`\`
159
-
160
- ### Category (For Categorizers)
161
- \`\`\`typescript
162
- interface Category {
163
- name: string; // Category identifier (e.g., "billing_inquiry")
164
- description: string; // When this category applies
165
- examples?: string[]; // Example user inputs
166
- }
167
- \`\`\`
168
-
169
- ### ResultMapping (WORKFLOW_OUTPUT)
170
- \`\`\`typescript
171
- interface ResultMapping {
172
- nodeId: string; // Which node's output to expose
173
- output: string; // Which output (usually "response_with_sources")
174
- }
175
- \`\`\`
176
-
177
- ## Common Patterns
178
-
179
- ### RAG Pattern (Search + Generate)
180
- \`\`\`typescript
181
- nodes: [
182
- { id: "trigger", actionType: "chat_trigger", displayName: "Trigger" },
183
- { id: "search_1", actionType: "search", displayName: "Knowledge Search",
184
- inputs: { query: { type: "action_output", actionName: "trigger", output: "user_query" } }
185
- },
186
- { id: "respond", actionType: "respond_with_sources", displayName: "Generate Response",
187
- inputs: {
188
- query: { type: "action_output", actionName: "trigger", output: "user_query" },
189
- search_results: { type: "action_output", actionName: "search_1", output: "search_results" }
190
- }
191
- }
192
- ]
193
- \`\`\`
194
-
195
- ### Intent Routing Pattern
196
- \`\`\`typescript
197
- nodes: [
198
- { id: "trigger", actionType: "chat_trigger", displayName: "Trigger" },
199
- { id: "categorizer", actionType: "chat_categorizer", displayName: "Intent Router",
200
- inputs: { text: { type: "action_output", actionName: "trigger", output: "user_query" } },
201
- categories: [
202
- { name: "billing", description: "Questions about bills, payments, invoices" },
203
- { name: "technical", description: "Technical support issues" },
204
- { name: "Fallback", description: "Unclear or general questions" }
205
- ]
206
- },
207
- { id: "billing_response", actionType: "call_llm", displayName: "Billing Handler",
208
- runIf: { sourceAction: "categorizer", sourceOutput: "category", operator: "eq", value: "billing" },
209
- inputs: {
210
- query: { type: "action_output", actionName: "trigger", output: "user_query" },
211
- instructions: { type: "inline_string", value: "Help with billing inquiries..." }
212
- }
213
- }
214
- ]
215
- \`\`\`
216
-
217
- ### Email with HITL Pattern
218
- \`\`\`typescript
219
- nodes: [
220
- { id: "extract_email", actionType: "entity_extraction", displayName: "Extract Email",
221
- inputs: { text_input: { type: "action_output", actionName: "summarizer", output: "summarized_conversation" } }
222
- },
223
- { id: "approval", actionType: "general_hitl", displayName: "Confirm Email",
224
- inputs: { summary: { type: "inline_string", value: "Ready to send email. Approve?" } }
225
- },
226
- { id: "send_email", actionType: "send_email_agent", displayName: "Send Email",
227
- runIf: { sourceAction: "approval", sourceOutput: "approval_decision", operator: "eq", value: "approve" },
228
- inputs: {
229
- to_email: { type: "action_output", actionName: "extract_email", output: "extracted_entities" },
230
- email_body: { type: "action_output", actionName: "body_generator", output: "fixed_response_with_sources" }
231
- }
232
- }
233
- ]
234
- \`\`\`
235
-
236
- ## Transformation Rules
237
-
238
- ### Rewiring a Connection
239
- To change where a node gets its input:
240
- 1. Find the node in the nodes array
241
- 2. Update the InputBinding for the target input
242
- 3. Change actionName/output to point to new source
243
-
244
- Example: Change \`send_email.to_email\` from \`llm_extractor\` to \`entity_extraction\`:
245
- \`\`\`typescript
246
- // Before:
247
- inputs: { to_email: { type: "action_output", actionName: "llm_extractor", output: "response_with_sources" } }
248
-
249
- // After:
250
- inputs: { to_email: { type: "action_output", actionName: "entity_extraction", output: "extracted_entities" } }
251
- \`\`\`
252
-
253
- ### Adding HITL Before an Action
254
- 1. Create a new general_hitl node with unique ID
255
- 2. Add runIf condition to the target node checking HITL approval
256
- 3. Ensure HITL summary describes what's being approved
257
-
258
- ### Removing a Node
259
- 1. Remove the node from the nodes array
260
- 2. Update any nodes that referenced it to use alternative source
261
- 3. Remove from resultMappings if present
262
-
263
- ### Adding a Category
264
- 1. Find the categorizer node
265
- 2. Add to the categories array
266
- 3. Add a handler node with runIf condition for the new category
267
-
268
- ${STRUCTURAL_RULES_FOR_LLM}
269
- `;
270
- /**
271
- * Get the complete schema including structural validation rules.
272
- * Use this for LLM context when generating or modifying workflows.
273
- */
274
- export function getCompleteSchemaForLLM() {
275
- return WORKFLOW_SCHEMA_FOR_LLM;
276
- }
277
- // ═══════════════════════════════════════════════════════════════════════════
278
- // DECOMPILER: workflow_def JSON → WorkflowSpec
279
- // ═══════════════════════════════════════════════════════════════════════════
280
- /**
281
- * Decompile a raw workflow_def JSON back to a typed WorkflowSpec.
282
- * This allows LLM to work with clean typed objects instead of raw JSON.
283
- */
284
- export function decompileWorkflow(workflowDef, personaType = "chat") {
285
- const actions = (workflowDef.actions || []);
286
- const results = (workflowDef.results || {});
287
- const enumTypes = (workflowDef.enumTypes || []);
288
- // Build enum lookup for category extraction
289
- // DEFENSIVE: Handle multiple API formats for enumType.name
290
- const enumLookup = new Map();
291
- for (const et of enumTypes) {
292
- // Extract name from multiple possible formats:
293
- // 1. { name: "string" } - simple format
294
- // 2. { name: { name: "string" } } - nested format
295
- // 3. { name: { name: { name: "string" } } } - deeply nested API format
296
- let name;
297
- const rawName = et.name;
298
- if (typeof rawName === "string") {
299
- name = rawName;
300
- }
301
- else if (rawName && typeof rawName === "object") {
302
- const nameObj = rawName;
303
- if (typeof nameObj.name === "string") {
304
- name = nameObj.name;
305
- }
306
- else if (nameObj.name && typeof nameObj.name === "object") {
307
- const nestedName = nameObj.name;
308
- if (typeof nestedName.name === "string") {
309
- name = nestedName.name;
310
- }
311
- }
312
- }
313
- // DEFENSIVE: Check both options (API format) and values (compiled format)
314
- const optionsArray = (et.options || et.values || []);
315
- if (name && optionsArray.length > 0) {
316
- enumLookup.set(name, optionsArray
317
- .filter(o => typeof o.name === "string")
318
- .map(o => ({
319
- name: o.name,
320
- description: o.description || "",
321
- examples: o.examples,
322
- })));
323
- }
324
- else if (!name && optionsArray.length > 0) {
325
- console.warn(`[workflow-transformer] EnumType has categories but name could not be extracted:`, JSON.stringify(rawName).slice(0, 100));
326
- }
327
- }
328
- // Decompile each action to a Node
329
- const nodes = actions.map(action => decompileAction(action, enumLookup));
330
- // Decompile result mappings
331
- const resultMappings = Object.values(results).map(r => ({
332
- nodeId: r.actionName,
333
- output: r.outputName,
334
- }));
335
- return {
336
- name: extractWorkflowName(workflowDef),
337
- description: workflowDef.description || "",
338
- personaType,
339
- nodes,
340
- resultMappings,
341
- };
342
- }
343
- /**
344
- * Extract workflow name/persona ID from workflowName namespace.
345
- * DEFENSIVE: Handles multiple namespace formats:
346
- * - ["ema", "personas", "<id>"] - standard 3-part
347
- * - ["ema", "tenant", "personas", "<id>"] - 4-part with tenant
348
- * - Last segment is typically the persona ID
349
- */
350
- function extractWorkflowName(workflowDef) {
351
- const wn = workflowDef.workflowName;
352
- // Try to extract from namespaces array
353
- if (wn?.name?.namespaces && Array.isArray(wn.name.namespaces)) {
354
- const namespaces = wn.name.namespaces;
355
- // Standard format: ["ema", "personas", "<persona_id>"]
356
- if (namespaces.length >= 3 && namespaces[1] === "personas") {
357
- return namespaces[2];
358
- }
359
- // Alternative format: ["ema", "workflows", "<id>"]
360
- if (namespaces.length >= 3 && namespaces[1] === "workflows") {
361
- return namespaces[2];
362
- }
363
- // Extended format with tenant: ["ema", "<tenant>", "personas", "<persona_id>"]
364
- if (namespaces.length >= 4 && namespaces[2] === "personas") {
365
- return namespaces[3];
366
- }
367
- // Fallback: last non-empty segment (best guess)
368
- for (let i = namespaces.length - 1; i >= 0; i--) {
369
- if (typeof namespaces[i] === "string" && namespaces[i].length > 0) {
370
- console.warn(`[workflow-transformer] Unexpected namespace format: ${namespaces.join("/")} - using last segment`);
371
- return namespaces[i];
372
- }
373
- }
374
- }
375
- // Try wn.name.name directly
376
- if (wn?.name?.name && typeof wn.name.name === "string") {
377
- return wn.name.name;
378
- }
379
- return workflowDef.displayName || "workflow";
380
- }
381
- function decompileAction(action, enumLookup) {
382
- // DEFENSIVE: Handle multiple action definition formats
383
- // Format 1: { action: { name: { name: "search" } } }
384
- // Format 2: { actionType: "search" }
385
- // Format 3: { action: "search" } (string directly)
386
- let actionName = "unknown";
387
- const actionDef = action.action;
388
- if (typeof actionDef === "string") {
389
- actionName = actionDef;
390
- }
391
- else if (actionDef && typeof actionDef === "object") {
392
- const actionObj = actionDef;
393
- if (typeof actionObj.name === "string") {
394
- actionName = actionObj.name;
395
- }
396
- else if (actionObj.name && typeof actionObj.name === "object") {
397
- const nameObj = actionObj.name;
398
- if (typeof nameObj.name === "string") {
399
- actionName = nameObj.name;
400
- }
401
- }
402
- }
403
- // Fallback to actionType field if action.name extraction failed
404
- if (actionName === "unknown" && typeof action.actionType === "string") {
405
- actionName = action.actionType;
406
- }
407
- if (actionName === "unknown") {
408
- console.warn(`[workflow-transformer] Could not extract action type for node "${action.name}". ` +
409
- `action field: ${JSON.stringify(action.action).slice(0, 100)}`);
410
- }
411
- const displaySettings = action.displaySettings;
412
- const inputs = (action.inputs || {});
413
- const runIf = action.runIf;
414
- const typeArgs = action.typeArguments;
415
- // Map raw action name to ActionType
416
- const actionType = mapToActionType(actionName);
417
- // Decompile inputs
418
- const decompInputs = {};
419
- for (const [key, value] of Object.entries(inputs)) {
420
- const binding = decompileBinding(value);
421
- if (binding) {
422
- decompInputs[key] = binding;
423
- }
424
- }
425
- // Decompile runIf
426
- let decompRunIf;
427
- if (runIf) {
428
- decompRunIf = decompileRunIf(runIf);
429
- }
430
- // Get categories for categorizers
431
- let categories;
432
- if (actionType.includes("categorizer") && typeArgs?.categories?.enumType?.name?.name) {
433
- categories = enumLookup.get(typeArgs.categories.enumType.name.name);
434
- }
435
- return {
436
- id: action.name,
437
- actionType,
438
- displayName: displaySettings?.displayName || action.name,
439
- description: displaySettings?.description,
440
- inputs: Object.keys(decompInputs).length > 0 ? decompInputs : undefined,
441
- runIf: decompRunIf,
442
- categories,
443
- disableHitl: action.disableHumanInteraction,
444
- };
445
- }
446
- function mapToActionType(rawName) {
447
- const mapping = {
448
- chat_trigger: "chat_trigger",
449
- voice_trigger: "voice_trigger",
450
- document_trigger: "document_trigger",
451
- chat_categorizer: "chat_categorizer",
452
- text_categorizer: "text_categorizer",
453
- conversation_to_search_query: "conversation_to_search_query",
454
- search: "search",
455
- live_web_search: "live_web_search",
456
- combine_search_results: "combine_search_results",
457
- respond_with_sources: "respond_with_sources",
458
- call_llm: "call_llm",
459
- fixed_response: "fixed_response",
460
- entity_extraction: "entity_extraction",
461
- entity_extraction_with_documents: "entity_extraction",
462
- external_action_caller: "external_action_caller",
463
- send_email_agent: "send_email_agent",
464
- general_hitl: "general_hitl",
465
- hitl: "general_hitl",
466
- response_validator: "response_validator",
467
- // Additional aliases/mappings
468
- dashboard_trigger: "document_trigger",
469
- };
470
- const result = mapping[rawName];
471
- if (!result) {
472
- // WARN but preserve the raw name - don't silently convert to call_llm
473
- // This allows the caller to see unknown types and handle appropriately
474
- // Unknown types are cast to ActionType - compilation will succeed but
475
- // the workflow may need additional handling downstream
476
- console.warn(`[workflow-transformer] Unknown action type: "${rawName}" - preserving as-is. May need to add to mapping.`);
477
- return rawName;
478
- }
479
- return result;
480
- }
481
- function decompileBinding(value) {
482
- const v = value;
483
- // Action output binding
484
- if (v.actionOutput) {
485
- const ao = v.actionOutput;
486
- return {
487
- type: "action_output",
488
- actionName: ao.actionName,
489
- output: ao.output,
490
- };
491
- }
492
- // Widget config binding
493
- if (v.widgetConfig) {
494
- const wc = v.widgetConfig;
495
- return {
496
- type: "widget_config",
497
- widgetName: wc.widgetName,
498
- };
499
- }
500
- // Inline value binding
501
- if (v.inline) {
502
- const inline = v.inline;
503
- if (inline.wellKnown?.stringValue !== undefined) {
504
- return {
505
- type: "inline_string",
506
- value: inline.wellKnown.stringValue,
507
- };
508
- }
509
- if (inline.wellKnown?.boolValue !== undefined) {
510
- return {
511
- type: "inline_bool",
512
- value: inline.wellKnown.boolValue,
513
- };
514
- }
515
- if (inline.wellKnown?.int64Value !== undefined) {
516
- return {
517
- type: "inline_number",
518
- value: parseInt(inline.wellKnown.int64Value, 10),
519
- };
520
- }
521
- if (inline.enumValue) {
522
- return {
523
- type: "inline_string",
524
- value: inline.enumValue,
525
- };
526
- }
527
- }
528
- // Multi-binding (named inputs)
529
- if (v.multiBinding) {
530
- const mb = v.multiBinding;
531
- const namedInputs = [];
532
- for (const elem of mb.elements || []) {
533
- if (elem.namedBinding?.name && elem.namedBinding.value) {
534
- const binding = decompileBinding(elem.namedBinding.value);
535
- if (binding) {
536
- namedInputs.push({ name: elem.namedBinding.name, binding });
537
- }
538
- }
539
- }
540
- if (namedInputs.length > 0) {
541
- return {
542
- type: "action_output", // Container type
543
- namedInputs,
544
- };
545
- }
546
- }
547
- return null;
548
- }
549
- function decompileRunIf(runIf) {
550
- const lhs = runIf.lhs;
551
- const rhs = runIf.rhs;
552
- const operator = runIf.operator;
553
- if (!lhs?.actionOutput || !rhs?.inline?.enumValue)
554
- return undefined;
555
- const opMap = {
556
- 1: "eq",
557
- 2: "neq",
558
- 3: "gt",
559
- 4: "lt",
560
- 5: "gte",
561
- 6: "lte",
562
- };
563
- return {
564
- sourceAction: lhs.actionOutput.actionName || "",
565
- sourceOutput: lhs.actionOutput.output || "",
566
- operator: opMap[operator] || "eq",
567
- value: rhs.inline.enumValue,
568
- };
569
- }
570
- /**
571
- * Transform a workflow using an LLM-generated spec modification.
572
- *
573
- * The expected workflow:
574
- * 1. Decompile existing workflow to WorkflowSpec
575
- * 2. LLM reads spec + schema + user request
576
- * 3. LLM outputs modified WorkflowSpec (or a diff/patch)
577
- * 4. This function compiles back to workflow_def
578
- *
579
- * @param originalDef - The original workflow_def JSON
580
- * @param modifiedSpec - The LLM-modified WorkflowSpec
581
- * @returns TransformResult with compiled workflow
582
- */
583
- /**
584
- * Validate workflow namespace structure.
585
- * The Ema API requires a specific namespace format for deployment.
586
- * Invalid namespaces cause SILENT deployment failures (returns 200 OK but nothing changes).
587
- */
588
- export function validateWorkflowNamespace(ns) {
589
- if (!ns) {
590
- return { valid: false, error: "Workflow namespace (workflowName) is missing" };
591
- }
592
- if (typeof ns !== "object") {
593
- return { valid: false, error: `Workflow namespace must be an object, got ${typeof ns}` };
594
- }
595
- const namespace = ns;
596
- if (!Array.isArray(namespace.namespaces)) {
597
- return { valid: false, error: "Workflow namespace.namespaces must be an array" };
598
- }
599
- if (typeof namespace.name !== "string" || namespace.name.length === 0) {
600
- return { valid: false, error: "Workflow namespace.name must be a non-empty string" };
601
- }
602
- return { valid: true };
603
- }
604
- export function transformWorkflow(originalDef, modifiedSpec) {
605
- const changes = [];
606
- const errors = [];
607
- const warnings = [];
608
- try {
609
- // Validate original workflow has valid namespace (required for deployment)
610
- // This is a WARNING not an error - transformation can proceed but deployment may fail
611
- const namespaceValidation = validateWorkflowNamespace(originalDef.workflowName);
612
- if (!namespaceValidation.valid) {
613
- warnings.push(`WARNING: ${namespaceValidation.error}. Deployment may fail silently without a valid namespace.`);
614
- console.warn(`[workflow-transformer] ${namespaceValidation.error}`);
615
- }
616
- // Compile the modified spec
617
- const compiled = compileWorkflow(modifiedSpec);
618
- // Preserve workflowName from original (important for deployment)
619
- // If original has no namespace, the compiled result will also lack one (which is fine for local use)
620
- if (originalDef.workflowName) {
621
- compiled.workflow_def.workflowName = originalDef.workflowName;
622
- }
623
- // Detect what changed
624
- const originalSpec = decompileWorkflow(originalDef, modifiedSpec.personaType);
625
- changes.push(...detectChanges(originalSpec, modifiedSpec));
626
- return {
627
- success: true,
628
- spec: modifiedSpec,
629
- workflow_def: compiled.workflow_def,
630
- changes,
631
- errors,
632
- warnings: warnings.length > 0 ? warnings : undefined,
633
- };
634
- }
635
- catch (e) {
636
- errors.push(`Compilation failed: ${e instanceof Error ? e.message : String(e)}`);
637
- return {
638
- success: false,
639
- spec: modifiedSpec,
640
- workflow_def: originalDef,
641
- changes,
642
- errors,
643
- warnings: warnings.length > 0 ? warnings : undefined,
644
- };
645
- }
646
- }
647
- function detectChanges(original, modified) {
648
- const changes = [];
649
- const originalIds = new Set(original.nodes.map(n => n.id));
650
- const modifiedIds = new Set(modified.nodes.map(n => n.id));
651
- // Added nodes
652
- for (const id of modifiedIds) {
653
- if (!originalIds.has(id)) {
654
- changes.push(`Added node: ${id}`);
655
- }
656
- }
657
- // Removed nodes
658
- for (const id of originalIds) {
659
- if (!modifiedIds.has(id)) {
660
- changes.push(`Removed node: ${id}`);
661
- }
662
- }
663
- // Modified nodes (simplified check)
664
- for (const modNode of modified.nodes) {
665
- const origNode = original.nodes.find(n => n.id === modNode.id);
666
- if (origNode) {
667
- // Check if inputs changed
668
- const origInputs = JSON.stringify(origNode.inputs || {});
669
- const modInputs = JSON.stringify(modNode.inputs || {});
670
- if (origInputs !== modInputs) {
671
- changes.push(`Modified ${modNode.id} inputs`);
672
- }
673
- // Check if runIf changed
674
- if (JSON.stringify(origNode.runIf) !== JSON.stringify(modNode.runIf)) {
675
- changes.push(`Modified ${modNode.id} runIf condition`);
676
- }
677
- }
678
- }
679
- return changes;
680
- }
681
- // ═══════════════════════════════════════════════════════════════════════════
682
- // HELPERS FOR LLM CONTEXT
683
- // ═══════════════════════════════════════════════════════════════════════════
684
- /**
685
- * Generate a summary of a workflow for LLM context.
686
- */
687
- export function summarizeSpec(spec) {
688
- const lines = [
689
- `## Workflow: ${spec.name}`,
690
- `Type: ${spec.personaType}`,
691
- `Description: ${spec.description}`,
692
- ``,
693
- `### Nodes (${spec.nodes.length})`,
694
- ];
695
- for (const node of spec.nodes) {
696
- const inputs = node.inputs ? Object.keys(node.inputs).join(", ") : "none";
697
- const condition = node.runIf
698
- ? ` [if ${node.runIf.sourceAction}.${node.runIf.sourceOutput} ${node.runIf.operator} "${node.runIf.value}"]`
699
- : "";
700
- lines.push(`- ${node.id} (${node.actionType}): ${node.displayName}${condition}`);
701
- lines.push(` Inputs: ${inputs}`);
702
- }
703
- lines.push(``, `### Output Mappings`);
704
- for (const rm of spec.resultMappings) {
705
- lines.push(`- ${rm.nodeId}.${rm.output}`);
706
- }
707
- return lines.join("\n");
708
- }
709
- /**
710
- * Create an LLM prompt for workflow modification.
711
- * This should be used by the MCP handler to construct the LLM call.
712
- */
713
- export function createModificationPrompt(currentSpec, userRequest) {
714
- return `
715
- You are modifying an Ema workflow. Your output should be a valid WorkflowSpec JSON.
716
-
717
- ## Current Workflow
718
- ${summarizeSpec(currentSpec)}
719
-
720
- ## Current Spec (JSON)
721
- \`\`\`json
722
- ${JSON.stringify(currentSpec, null, 2)}
723
- \`\`\`
724
-
725
- ## Schema Reference
726
- ${WORKFLOW_SCHEMA_FOR_LLM}
727
-
728
- ## User Request
729
- ${userRequest}
730
-
731
- ## Instructions
732
- 1. Analyze the user request
733
- 2. Determine what nodes/connections need to change
734
- 3. Output the COMPLETE modified WorkflowSpec as valid JSON
735
- 4. Preserve all unchanged nodes exactly as they are
736
- 5. Only modify what's necessary for the request
737
-
738
- ## Output
739
- Return ONLY the modified WorkflowSpec JSON, no explanation needed:
740
- \`\`\`json
741
- `;
742
- }