@ema.co/mcp-toolkit 2026.2.13 → 2026.2.23

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 (60) 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/dist/cli/index.js +2 -2
  4. package/dist/mcp/domain/loop-detection.js +89 -0
  5. package/dist/mcp/domain/sanitizer.js +1 -1
  6. package/dist/mcp/domain/structural-rules.js +4 -5
  7. package/dist/mcp/domain/validation-rules.js +5 -5
  8. package/dist/mcp/domain/workflow-graph.js +3 -5
  9. package/dist/mcp/domain/workflow-path-enumerator.js +7 -4
  10. package/dist/mcp/guidance.js +62 -29
  11. package/dist/mcp/handlers/debug/adapter.js +15 -0
  12. package/dist/mcp/handlers/debug/formatters.js +282 -0
  13. package/dist/mcp/handlers/debug/index.js +133 -0
  14. package/dist/mcp/handlers/demo/adapter.js +180 -0
  15. package/dist/mcp/handlers/env/config.js +2 -2
  16. package/dist/mcp/handlers/feedback/index.js +1 -1
  17. package/dist/mcp/handlers/index.js +0 -1
  18. package/dist/mcp/handlers/persona/adapter.js +135 -0
  19. package/dist/mcp/handlers/persona/index.js +237 -8
  20. package/dist/mcp/handlers/persona/schema.js +27 -0
  21. package/dist/mcp/handlers/reference/index.js +6 -4
  22. package/dist/mcp/handlers/sync/adapter.js +200 -0
  23. package/dist/mcp/handlers/workflow/adapter.js +174 -0
  24. package/dist/mcp/handlers/workflow/fix.js +11 -12
  25. package/dist/mcp/handlers/workflow/index.js +12 -40
  26. package/dist/mcp/handlers/workflow/validation.js +1 -1
  27. package/dist/mcp/knowledge-guidance-topics.js +615 -0
  28. package/dist/mcp/knowledge-types.js +7 -0
  29. package/dist/mcp/knowledge.js +75 -1403
  30. package/dist/mcp/resources-dynamic.js +2395 -0
  31. package/dist/mcp/resources-validation.js +408 -0
  32. package/dist/mcp/resources.js +72 -2508
  33. package/dist/mcp/server.js +69 -2825
  34. package/dist/mcp/tools.js +106 -5
  35. package/dist/sdk/client-adapter.js +265 -24
  36. package/dist/sdk/ema-client.js +100 -9
  37. package/dist/sdk/generated/agent-catalog.js +615 -0
  38. package/dist/sdk/generated/well-known-types.js +99 -0
  39. package/dist/sdk/generated/widget-catalog.js +60 -0
  40. package/dist/sdk/grpc-client.js +115 -1
  41. package/dist/sync/sdk.js +2 -2
  42. package/dist/sync.js +4 -3
  43. package/docs/README.md +17 -9
  44. package/package.json +3 -2
  45. package/.context/public/guides/dashboard-operations.md +0 -349
  46. package/.context/public/guides/email-patterns.md +0 -125
  47. package/.context/public/guides/workflow-builder-patterns.md +0 -708
  48. package/dist/mcp/domain/intent-architect.js +0 -914
  49. package/dist/mcp/domain/quality-gates.js +0 -110
  50. package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
  51. package/dist/mcp/domain/workflow-intent.js +0 -1806
  52. package/dist/mcp/domain/workflow-merge.js +0 -449
  53. package/dist/mcp/domain/workflow-tracer.js +0 -648
  54. package/dist/mcp/domain/workflow-transformer.js +0 -742
  55. package/dist/mcp/handlers/knowledge/index.js +0 -54
  56. package/dist/mcp/handlers/persona/intent.js +0 -141
  57. package/dist/mcp/handlers/workflow/analyze.js +0 -119
  58. package/dist/mcp/handlers/workflow/compare.js +0 -70
  59. package/dist/mcp/handlers/workflow/generate.js +0 -384
  60. package/dist/mcp/handlers-consolidated.js +0 -333
@@ -0,0 +1,2395 @@
1
+ /**
2
+ * Dynamic MCP Resources
3
+ *
4
+ * Contains the DYNAMIC_RESOURCES array (~50 dynamic resource generators)
5
+ * with their inline markdown templates, plus the dynamic fetching helpers
6
+ * (API clients, caches, DTO converters) that the generators use.
7
+ *
8
+ * Extracted from resources.ts for maintainability.
9
+ */
10
+ import * as path from "path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { dirname } from "node:path";
13
+ // ESM-compatible __dirname
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+ import { AGENT_CATALOG, WORKFLOW_PATTERNS, WIDGET_CATALOG, ALL_DEPRECATED_ACTIONS, DEPRECATED_ACTIONS_WITH_REPLACEMENT, DEPRECATED_ACTIONS_NO_REPLACEMENT, WORKFLOW_ENABLING_CONSTRAINTS, MINIMUM_VIABLE_WORKFLOWS, } from "./knowledge.js";
17
+ import { INPUT_SOURCE_RULES, ANTI_PATTERNS, OPTIMIZATION_RULES } from "./domain/validation-rules.js";
18
+ import { STRUCTURAL_INVARIANTS } from "./domain/structural-rules.js";
19
+ import { mineConstraints, formatConstraintReport } from "./domain/proto-constraints.js";
20
+ import { EmaClientAdapter } from "../sdk/client-adapter.js";
21
+ import { APISchemaRegistry } from "./domain/workflow-validator.js";
22
+ import { loadConfigFromJsonEnv, loadConfigOptional, resolveBearerToken, getEnvByName, getMasterEnv, } from "../sdk/config.js";
23
+ import { VOICE_TEMPLATE_FALLBACK, VOICE_TEMPLATE_FIELD_DOCS, } from "../sdk/generated/template-fallbacks.js";
24
+ import { generateValidationRulesForLLM, generateBestPracticesChecklist } from "./resources-validation.js";
25
+ const DYNAMIC_RESOURCES = [
26
+ // Agent Catalog - Dynamic list of all workflow agents
27
+ {
28
+ uri: "ema://catalog/agents",
29
+ name: "catalog/agents",
30
+ description: "Complete agent catalog: all available workflow agents with inputs, outputs, critical rules, and usage guidance",
31
+ mimeType: "application/json",
32
+ generate: async (ctx) => JSON.stringify(await getDynamicAgentCatalog({ env: ctx.env }), null, 2),
33
+ },
34
+ {
35
+ uri: "ema://catalog/agents-summary",
36
+ name: "catalog/agents-summary",
37
+ description: "Agent catalog summary: agent names, categories, and brief descriptions for quick reference",
38
+ mimeType: "text/markdown",
39
+ generate: async (ctx) => {
40
+ const agents = await getDynamicAgentCatalog({ env: ctx.env });
41
+ const byCategory = new Map();
42
+ for (const agent of agents) {
43
+ const cat = agent.category || "other";
44
+ if (!byCategory.has(cat))
45
+ byCategory.set(cat, []);
46
+ byCategory.get(cat).push(agent);
47
+ }
48
+ let md = "# Ema Agent Catalog\n\n";
49
+ md += `> ${agents.length} agents available for workflow composition\n\n`;
50
+ for (const [category, agents] of byCategory) {
51
+ md += `## ${category.charAt(0).toUpperCase() + category.slice(1)}\n\n`;
52
+ md += "| Agent | Description | Key Inputs | Key Outputs |\n";
53
+ md += "|-------|-------------|------------|-------------|\n";
54
+ for (const agent of agents) {
55
+ const inputs = agent.inputs?.slice(0, 2).map((i) => i.name).join(", ") || "-";
56
+ const outputs = agent.outputs?.slice(0, 2).map((o) => o.name).join(", ") || "-";
57
+ md += `| \`${agent.actionName}\` | ${agent.description?.slice(0, 60) || "-"}... | ${inputs} | ${outputs} |\n`;
58
+ }
59
+ md += "\n";
60
+ }
61
+ return md;
62
+ },
63
+ },
64
+ // Workflow Patterns - Common workflow templates
65
+ {
66
+ uri: "ema://catalog/patterns",
67
+ name: "catalog/patterns",
68
+ description: "Workflow patterns: common workflow structures (kb-search, intent-routing, tool-calling) with examples",
69
+ mimeType: "application/json",
70
+ generate: async () => JSON.stringify(WORKFLOW_PATTERNS, null, 2),
71
+ },
72
+ // Widget Catalog - UI configuration widgets
73
+ {
74
+ uri: "ema://catalog/widgets",
75
+ name: "catalog/widgets",
76
+ description: "Widget catalog: proto_config widget types for voice, chat, and dashboard personas",
77
+ mimeType: "application/json",
78
+ generate: async () => JSON.stringify(WIDGET_CATALOG, null, 2),
79
+ },
80
+ // Validation Rules - Input source and anti-pattern rules
81
+ {
82
+ uri: "ema://rules/input-sources",
83
+ name: "rules/input-sources",
84
+ description: "Input source validation rules: which agent inputs accept which data types (user_query vs chat_conversation)",
85
+ mimeType: "application/json",
86
+ generate: async () => JSON.stringify(INPUT_SOURCE_RULES, null, 2),
87
+ },
88
+ {
89
+ uri: "ema://rules/anti-patterns",
90
+ name: "rules/anti-patterns",
91
+ description: "Anti-patterns to avoid: common workflow mistakes and how to prevent them",
92
+ mimeType: "application/json",
93
+ generate: async () => JSON.stringify(ANTI_PATTERNS, null, 2),
94
+ },
95
+ {
96
+ uri: "ema://rules/optimizations",
97
+ name: "rules/optimizations",
98
+ description: "Optimization rules: workflow improvements for better performance and reliability",
99
+ mimeType: "application/json",
100
+ generate: async () => JSON.stringify(OPTIMIZATION_RULES, null, 2),
101
+ },
102
+ {
103
+ uri: "ema://rules/structural-invariants",
104
+ name: "rules/structural-invariants",
105
+ description: "Structural invariants: hard constraints that workflows must satisfy (no cycles, reachability, etc.)",
106
+ mimeType: "application/json",
107
+ generate: async () => JSON.stringify(STRUCTURAL_INVARIANTS, null, 2),
108
+ },
109
+ {
110
+ uri: "ema://validation/rules",
111
+ name: "validation/rules",
112
+ description: "Static validation rules: path enumeration, required outputs, multiple writers, response/abstain, category hierarchy. Use these rules DURING workflow generation to avoid errors.",
113
+ mimeType: "text/markdown",
114
+ generate: async () => generateValidationRulesForLLM(),
115
+ },
116
+ // Best Practices - Auto-generated from SDK rules (SINGLE SOURCE OF TRUTH)
117
+ {
118
+ uri: "ema://docs/best-practices",
119
+ name: "docs/best-practices",
120
+ description: "Auto-generated best practices checklist from SDK validation rules. Use BEFORE deploying workflows.",
121
+ mimeType: "text/markdown",
122
+ generate: async () => generateBestPracticesChecklist(),
123
+ },
124
+ // Field Constraints - Mined from proto definitions
125
+ {
126
+ uri: "ema://docs/field-constraints",
127
+ name: "docs/field-constraints",
128
+ description: "Proto field constraints: immutable fields, read-only fields, creation-only fields, conditional fields. Mined from proto definitions.",
129
+ mimeType: "text/markdown",
130
+ generate: async () => {
131
+ const report = mineConstraints();
132
+ return formatConstraintReport(report);
133
+ },
134
+ },
135
+ // Deprecated Actions - API-first with fallback
136
+ {
137
+ uri: "ema://rules/deprecated-actions",
138
+ name: "rules/deprecated-actions",
139
+ description: "Deprecated actions list: actions to avoid in new workflows, with their replacements and migration notes",
140
+ mimeType: "application/json",
141
+ generate: async (ctx) => {
142
+ // Try API first using existing client infrastructure
143
+ let apiDeprecated = [];
144
+ let source = "fallback";
145
+ try {
146
+ const client = getClientForEnvName(ctx.env);
147
+ if (client) {
148
+ const actions = await client.listActions();
149
+ apiDeprecated = actions.filter(a => a.deprecated).map(a => a.id);
150
+ source = "api";
151
+ }
152
+ }
153
+ catch {
154
+ // API unavailable, use fallback
155
+ }
156
+ // Merge API deprecated with fallback replacement info
157
+ const result = {
158
+ _note: "Actions marked deprecated should not be used in new workflows. Use replacements where available.",
159
+ _source: source,
160
+ _api_deprecated_count: apiDeprecated.length,
161
+ // From API (current deprecated status)
162
+ api_deprecated: apiDeprecated.length > 0 ? apiDeprecated : undefined,
163
+ // From fallback (includes replacement info)
164
+ with_replacement: DEPRECATED_ACTIONS_WITH_REPLACEMENT.map(d => ({
165
+ action: d.action,
166
+ version: d.version,
167
+ replacement: d.replacement,
168
+ replacement_version: d.replacementVersion,
169
+ migration_notes: d.migrationNotes,
170
+ })),
171
+ no_replacement: DEPRECATED_ACTIONS_NO_REPLACEMENT.map(d => ({
172
+ action: d.action,
173
+ version: d.version,
174
+ environment: d.environment,
175
+ notes: d.migrationNotes,
176
+ })),
177
+ _tip: "Check workflow(mode='get') for deprecation_warnings on specific workflows",
178
+ };
179
+ return JSON.stringify(result, null, 2);
180
+ },
181
+ },
182
+ {
183
+ uri: "ema://rules/deprecated-actions-summary",
184
+ name: "rules/deprecated-actions-summary",
185
+ description: "Deprecated actions summary: quick reference table of deprecated actions and replacements",
186
+ mimeType: "text/markdown",
187
+ generate: async (ctx) => {
188
+ // Try API first using existing client infrastructure
189
+ let apiDeprecated = [];
190
+ let source = "fallback";
191
+ try {
192
+ const client = getClientForEnvName(ctx.env);
193
+ if (client) {
194
+ const actions = await client.listActions();
195
+ apiDeprecated = actions.filter(a => a.deprecated).map(a => a.id);
196
+ source = "api";
197
+ }
198
+ }
199
+ catch {
200
+ // API unavailable, use fallback
201
+ }
202
+ let md = "# Deprecated Actions\n\n";
203
+ md += "> Do NOT use these actions in new workflows. Use replacements where available.\n\n";
204
+ md += `> Source: ${source} (${source === "api" ? "live" : "synced 2026-01-26"})\n\n`;
205
+ if (apiDeprecated.length > 0) {
206
+ md += "## From API (Current)\n\n";
207
+ md += "| Action ID | Status |\n";
208
+ md += "|-----------|--------|\n";
209
+ for (const id of apiDeprecated) {
210
+ md += `| \`${id}\` | DEPRECATED |\n`;
211
+ }
212
+ md += "\n";
213
+ }
214
+ md += "## Actions with Known Replacements\n\n";
215
+ md += "| Deprecated Action | Version | Replacement | Notes |\n";
216
+ md += "|-------------------|---------|-------------|-------|\n";
217
+ for (const d of DEPRECATED_ACTIONS_WITH_REPLACEMENT) {
218
+ const repl = Array.isArray(d.replacement) ? d.replacement.join(" or ") : d.replacement;
219
+ const replVer = d.replacementVersion ? ` ${d.replacementVersion}` : "";
220
+ md += `| \`${d.action}\` | ${d.version} | \`${repl}\`${replVer} | ${d.migrationNotes || "-"} |\n`;
221
+ }
222
+ md += "\n## Actions with No Known Replacement\n\n";
223
+ md += "| Action | Environment | Notes |\n";
224
+ md += "|--------|-------------|-------|\n";
225
+ for (const d of DEPRECATED_ACTIONS_NO_REPLACEMENT) {
226
+ md += `| \`${d.action}\` | ${d.environment || "all"} | ${d.migrationNotes || "-"} |\n`;
227
+ }
228
+ md += `\n**Total Known Deprecated**: ${ALL_DEPRECATED_ACTIONS.length} actions\n`;
229
+ md += `\n> **Best Practice**: Use \`workflow(mode="get")\` to check for deprecation warnings in specific workflows.\n`;
230
+ return md;
231
+ },
232
+ },
233
+ // Workflow Enabling Constraints - Requirements for persona activation
234
+ {
235
+ uri: "ema://rules/workflow-constraints",
236
+ name: "rules/workflow-constraints",
237
+ description: "Workflow enabling constraints: requirements that must be met before a persona can be activated",
238
+ mimeType: "application/json",
239
+ generate: async () => JSON.stringify({
240
+ _note: "These constraints are checked by the Ema backend when enabling a persona.",
241
+ _source: "ema/ema_backend/db/models/personas_model.py:672-756",
242
+ _last_synced: "2026-01-26",
243
+ enabling_constraints: WORKFLOW_ENABLING_CONSTRAINTS,
244
+ minimum_viable_workflows: MINIMUM_VIABLE_WORKFLOWS,
245
+ }, null, 2),
246
+ },
247
+ {
248
+ uri: "ema://rules/workflow-constraints-summary",
249
+ name: "rules/workflow-constraints-summary",
250
+ description: "Workflow constraints summary: checklist of requirements for enabling a persona",
251
+ mimeType: "text/markdown",
252
+ generate: async () => {
253
+ let md = "# Workflow Enabling Constraints\n\n";
254
+ md += "> These constraints must be satisfied for a persona to be activated.\n\n";
255
+ md += "> Source: ema_backend/db/models/personas_model.py (synced 2026-01-26)\n\n";
256
+ md += "## Required Checks\n\n";
257
+ md += "| # | Constraint | Error State | Fix |\n";
258
+ md += "|---|------------|-------------|-----|\n";
259
+ for (const c of WORKFLOW_ENABLING_CONSTRAINTS) {
260
+ const critical = c.critical ? "**" : "";
261
+ md += `| ${c.id} | ${critical}${c.name}${critical} | \`${c.errorState}\` | ${c.fix} |\n`;
262
+ }
263
+ md += "\n## Minimum Viable Workflows by Type\n\n";
264
+ for (const [type, mvw] of Object.entries(MINIMUM_VIABLE_WORKFLOWS)) {
265
+ md += `### ${type}\n\n`;
266
+ md += `**Structure**: \`${mvw.exampleStructure}\`\n\n`;
267
+ md += `- Required nodes: ${mvw.requiredNodes.map(n => `\`${n}\``).join(", ")}\n`;
268
+ md += `- Required outputs: ${mvw.requiredOutputs.map(o => `\`${o}\``).join(", ")}\n`;
269
+ if (mvw.requiredWidgets) {
270
+ md += `- Required widgets: ${mvw.requiredWidgets.map(w => `\`${w}\``).join(", ")}\n`;
271
+ }
272
+ if (mvw.notes) {
273
+ md += `- Notes: ${mvw.notes}\n`;
274
+ }
275
+ md += "\n";
276
+ }
277
+ md += "## Critical Rule: results Mapping\n\n";
278
+ md += "**Every workflow must have a `results` map connecting action outputs to the persona's output.**\n\n";
279
+ md += "### Chat / Voice Personas\n\n";
280
+ md += "Use `WORKFLOW_OUTPUT` as the key — this is the response shown to the user:\n\n";
281
+ md += "```json\n";
282
+ md += '{\n "results": {\n "WORKFLOW_OUTPUT": {\n "actionName": "respond_node",\n "outputName": "response_with_sources"\n }\n }\n}\n';
283
+ md += "```\n\n";
284
+ md += "Multiple response nodes (e.g., one per categorizer branch) can all map to `WORKFLOW_OUTPUT` — only the one that executes via `runIf` will produce output.\n\n";
285
+ md += "### Dashboard Personas\n\n";
286
+ md += "Use `<actionName>.<outputName>` dot-notation keys — each key becomes a column in the dashboard:\n\n";
287
+ md += "```json\n";
288
+ md += '{\n "results": {\n "entity_extraction_node.extraction_columns": {\n "actionName": "entity_extraction_node",\n "outputName": "extraction_columns"\n },\n "rule_validation_node.ruleset_output": {\n "actionName": "rule_validation_node",\n "outputName": "ruleset_output"\n }\n }\n}\n';
289
+ md += "```\n\n";
290
+ md += "Common dashboard result outputs: `extraction_columns`, `ruleset_output`, `llm_output`.\n";
291
+ return md;
292
+ },
293
+ },
294
+ // JSON Output Patterns - custom_agent + json_mapper best practices
295
+ {
296
+ uri: "ema://rules/json-output-patterns",
297
+ name: "rules/json-output-patterns",
298
+ description: "How to properly wire custom_agent JSON output to json_mapper and downstream nodes like send_email_agent",
299
+ mimeType: "text/markdown",
300
+ generate: async () => {
301
+ return `# custom_agent + json_mapper Pattern
302
+
303
+ ## Problem
304
+
305
+ When using \`custom_agent\` to generate JSON that will be parsed by \`json_mapper\`, the output can be misformatted if not properly configured.
306
+
307
+ Without explicit \`output_fields\`, custom_agent returns the entire JSON as a string blob in \`response_with_sources\`, causing json_mapper to fail or produce incorrect results.
308
+
309
+ ## Solution A: Define output_fields (RECOMMENDED)
310
+
311
+ Use \`output_fields\` with extraction columns matching your JSON keys:
312
+
313
+ \`\`\`json
314
+ {
315
+ "name": "email_gen",
316
+ "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "custom_agent" }, "version": "v1" },
317
+ "inputs": {
318
+ "task_instructions": { "inline": { "wellKnown": { "textWithSources": {
319
+ "text": "Generate notification email with To, Subject, and Body fields.",
320
+ "sources": []
321
+ }}}},
322
+ "output_fields": {
323
+ "inline": { "array": { "values": [
324
+ { "wellKnown": { "extractionColumn": { "id": "To", "name": "To", "dataType": 1 }}},
325
+ { "wellKnown": { "extractionColumn": { "id": "Subject", "name": "Subject", "dataType": 1 }}},
326
+ { "wellKnown": { "extractionColumn": { "id": "Body", "name": "Body", "dataType": 1 }}}
327
+ ]}}
328
+ }
329
+ }
330
+ }
331
+ \`\`\`
332
+
333
+ ## Solution B: Strict Prompting + json_mapper
334
+
335
+ If not using \`output_fields\`, instruct the LLM to output RAW JSON only (no markdown):
336
+
337
+ \`\`\`
338
+ task_instructions: "Output ONLY the JSON object, no markdown, no explanation. Format: {\\"To\\": \\"...\\", \\"Subject\\": \\"...\\", \\"Body\\": \\"...\\"}"
339
+ \`\`\`
340
+
341
+ Then wire to \`json_mapper\` with matching \`pathIndices\`:
342
+
343
+ \`\`\`json
344
+ {
345
+ "name": "json_map",
346
+ "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "json_mapper" }, "version": "v1" },
347
+ "inputs": {
348
+ "input_json": { "actionOutput": { "actionName": "email_gen", "output": "response_with_sources" }},
349
+ "json_mapper_config": {
350
+ "inline": { "wellKnown": { "jsonMapperConfig": {
351
+ "rules": [
352
+ { "fieldName": "to", "pathIndices": [{"stringIndex": "To"}], "type": 3, "defaultValue": {"stringValue": ""} },
353
+ { "fieldName": "subject", "pathIndices": [{"stringIndex": "Subject"}], "type": 3, "defaultValue": {"stringValue": ""} },
354
+ { "fieldName": "body", "pathIndices": [{"stringIndex": "Body"}], "type": 3, "defaultValue": {"stringValue": ""} }
355
+ ],
356
+ "sampleJson": "{\\"To\\": \\"email@example.com\\", \\"Subject\\": \\"Test\\", \\"Body\\": \\"<p>Hi</p>\\"}"
357
+ }}}
358
+ }
359
+ }
360
+ }
361
+ \`\`\`
362
+
363
+ ## Wiring json_mapper to fixed_response
364
+
365
+ json_mapper outputs extracted fields to \`output_json\`. Use \`fixed_response\` with template variables to convert to \`TEXT_WITH_SOURCES\`:
366
+
367
+ \`\`\`json
368
+ {
369
+ "name": "fmt_to",
370
+ "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "fixed_response" }, "version": "v1" },
371
+ "inputs": {
372
+ "template": { "inline": { "wellKnown": { "textWithSources": { "text": "{{to}}", "sources": [] }}}},
373
+ "custom_data": { "actionOutput": { "actionName": "json_map", "output": "output_json" }}
374
+ }
375
+ }
376
+ \`\`\`
377
+
378
+ ## Complete Email Chain Pattern
379
+
380
+ \`\`\`
381
+ custom_agent (generates JSON)
382
+ → json_mapper (extracts To, Subject, Body)
383
+ → fixed_response ×3 ({{to}}, {{subject}}, {{body}})
384
+ → send_email_agent
385
+ \`\`\`
386
+
387
+ ## Critical Rules
388
+
389
+ | Rule | Why |
390
+ |------|-----|
391
+ | Use \`output_fields\` OR strict "ONLY JSON" prompt | Prevents markdown wrapping |
392
+ | json_mapper outputs to \`output_json\` | Not individual field outputs |
393
+ | Use \`fixed_response\` with \`{{fieldName}}\` template | Converts to TEXT_WITH_SOURCES |
394
+ | \`send_email_agent\` needs TEXT_WITH_SOURCES | Direct json_mapper output = type mismatch |
395
+ | \`pathIndices\` are case-sensitive | Must match JSON keys exactly |
396
+ | Include \`defaultValue\` in rules | Prevents null errors |
397
+
398
+ ## Debugging
399
+
400
+ If json_mapper fails to extract:
401
+ 1. Check if LLM is wrapping JSON in markdown (\\\`\\\`\\\`json ... \\\`\\\`\\\`)
402
+ 2. Check pathIndices match actual JSON keys (case-sensitive)
403
+ 3. Check custom_agent prompt instructs "ONLY JSON, no markdown"
404
+ 4. Add defaultValue to prevent null propagation
405
+ `;
406
+ },
407
+ },
408
+ // Extraction column format - canonical API shape for entity_extraction / extraction_columns
409
+ {
410
+ uri: "ema://rules/extraction-column-format",
411
+ name: "rules/extraction-column-format",
412
+ description: "Canonical API format for extraction_columns (entity_extraction_with_documents, etc.). Use this shape when generating or fixing workflow_def; do not guess from catalog or local files.",
413
+ mimeType: "text/markdown",
414
+ generate: async () => {
415
+ return `# extraction_columns Format (API Shape)
416
+
417
+ ## Rule
418
+
419
+ When generating or fixing workflow_def that includes \`entity_extraction_with_documents\` (or similar) nodes, the \`extraction_columns\` input **must** use this exact structure. The API rejects other shapes (e.g. \`extractionColumns.columns[{name, type}]\`).
420
+
421
+ ## Required Shape
422
+
423
+ \`\`\`json
424
+ "extraction_columns": {
425
+ "inline": {
426
+ "array": {
427
+ "values": [
428
+ { "wellKnown": { "extractionColumn": { "id": "<column_id>", "name": "<display name>", "description": "<optional>", "dataType": 1 } } },
429
+ { "wellKnown": { "extractionColumn": { "id": "...", "name": "...", "description": "...", "dataType": 1 } } }
430
+ ]
431
+ }
432
+ }
433
+ }
434
+ \`\`\`
435
+
436
+ - \`inline.array.values\`: array of column definitions.
437
+ - Each column: \`wellKnown.extractionColumn\` with \`id\`, \`name\`, optional \`description\`, \`dataType\` (1 = string typical).
438
+ - **Do not** use \`extractionColumns.columns\` or \`{ name, type, description }\` only — the API expects the \`array.values\` + \`wellKnown.extractionColumn\` shape.
439
+
440
+ ## id vs name
441
+
442
+ - **id**: Unique identifier for the column (stable key; used in results/downstream). Can be \`col1\`, \`col5\`, \`service_date\`, or any unique string.
443
+ - **name**: Display/semantic name (LLM uses this to extract values). Can match id or differ (e.g. \`id: "col5"\`, \`name: "service_date"\` is valid).
444
+ - **id and name do not have to match.** Using generic ids (\`col1\`, \`col2\`) with semantic names (\`event_id\`, \`service_date\`) is correct.
445
+
446
+ ## Emoji in Column Names (Dashboard Polish)
447
+
448
+ Column \`name\` values support **emoji prefixes**, which render as dashboard column headers. This makes dashboards significantly more scannable and visually polished.
449
+
450
+ \`\`\`json
451
+ { "wellKnown": { "extractionColumn": { "id": "customer", "name": "🧑 Customer", "dataType": 1 } } },
452
+ { "wellKnown": { "extractionColumn": { "id": "segment", "name": "🟠 Segment", "dataType": 1 } } },
453
+ { "wellKnown": { "extractionColumn": { "id": "balance", "name": "💰 Balance", "dataType": 1 } } },
454
+ { "wellKnown": { "extractionColumn": { "id": "days_past", "name": "📅 Days Past Due", "dataType": 1 } } },
455
+ { "wellKnown": { "extractionColumn": { "id": "dispute", "name": "⚖️ Dispute", "dataType": 1 } } },
456
+ { "wellKnown": { "extractionColumn": { "id": "ptp", "name": "🤝 PTP", "dataType": 1 } } },
457
+ { "wellKnown": { "extractionColumn": { "id": "invoices", "name": "📄 Invoices", "dataType": 1 } } }
458
+ \`\`\`
459
+
460
+ **Emoji in cell values**: The extraction \`description\` (instructions) can tell the LLM to prefix extracted values with emoji for visual status indicators. These render in dashboard cells:
461
+
462
+ - \`"description": "Total outstanding balance. Prefix with ❗ if over $500,000"\`
463
+ - \`"description": "Yes or No. Prefix Yes with 🚫 for active disputes"\`
464
+ - \`"description": "Promise-to-pay date. Prefix with 🤝 if present"\`
465
+ - \`"description": "Pass or Fail. Prefix Pass with ✅ and Fail with ❌"\`
466
+
467
+ This creates rich, at-a-glance dashboards where status is conveyed by color and symbol without reading every cell.
468
+
469
+ ## Duplicate ids
470
+
471
+ - **Within the same extraction_columns array**, every column must have a **unique \`id\`**. Duplicate \`id\` values in one array are invalid.
472
+ - Different actions can reuse the same id strings (e.g. \`event_extraction\` has \`col1\`..\`col6\`, \`validation_results\` has \`col1\`..\`col10\`) — each action has its own array.
473
+
474
+ ## dataType Reference (PrimitiveType enum)
475
+
476
+ | Value | Type | Description |
477
+ |-------|------|-------------|
478
+ | 0 | UNKNOWN | Unspecified (avoid) |
479
+ | 1 | STRING | Text values (default) |
480
+ | 2 | NUMBER | Numeric values |
481
+ | 3 | BOOLEAN | True/false values |
482
+ | 4 | ARRAY | Array of repeated values (use with \`arrayElementType\`) |
483
+ | 5 | OBJECT | Grouped/nested structure (contains sub-columns via \`value.objectValue\`) |
484
+ | 6 | DATE | Date values (use with \`formattingOptions.dateFormat\`) |
485
+ | 7 | DOCUMENT_VERSION_ID | Reference to a document version |
486
+
487
+ **Note on enums:** To create an enumerated column, use \`dataType: 1\` (STRING) with \`isEnum: true\` and populate \`possibleValues\`.
488
+
489
+ ## Object / Grouped Columns (dataType: 5)
490
+
491
+ Use \`dataType: 5\` to create a **Group** column with sub-columns (nested structure). In the UI, this appears as a parent column with expandable sub-columns.
492
+
493
+ The sub-columns are defined as a nested array in the column's \`value.objectValue.values\`. Each sub-column is a **bare \`ExtractionColumn\`** (no \`wellKnown\` wrapper — that wrapper only appears at the top-level \`extraction_columns.inline.array.values[]\`).
494
+
495
+ \`\`\`json
496
+ {
497
+ "wellKnown": {
498
+ "extractionColumn": {
499
+ "id": "address",
500
+ "name": "Address",
501
+ "description": "Structured address fields",
502
+ "dataType": 5,
503
+ "value": {
504
+ "objectValue": {
505
+ "values": [
506
+ {
507
+ "id": "street",
508
+ "name": "Street Name",
509
+ "description": "Street name",
510
+ "dataType": 1,
511
+ "detailedDescription": "",
512
+ "isEnum": false,
513
+ "possibleValues": [],
514
+ "dependencies": [],
515
+ "fileSources": [],
516
+ "auxiliarySources": []
517
+ },
518
+ {
519
+ "id": "city",
520
+ "name": "City",
521
+ "dataType": 1,
522
+ "detailedDescription": "",
523
+ "isEnum": false,
524
+ "possibleValues": [],
525
+ "dependencies": [],
526
+ "fileSources": [],
527
+ "auxiliarySources": []
528
+ },
529
+ {
530
+ "id": "postal_code",
531
+ "name": "Postal Code",
532
+ "dataType": 1,
533
+ "detailedDescription": "",
534
+ "isEnum": false,
535
+ "possibleValues": [],
536
+ "dependencies": [],
537
+ "fileSources": [],
538
+ "auxiliarySources": []
539
+ }
540
+ ]
541
+ }
542
+ },
543
+ "arrayElementType": {
544
+ "id": "address",
545
+ "name": "Address",
546
+ "dataType": 5,
547
+ "detailedDescription": "",
548
+ "isEnum": false,
549
+ "possibleValues": [],
550
+ "dependencies": [],
551
+ "fileSources": [],
552
+ "auxiliarySources": []
553
+ },
554
+ "detailedDescription": "",
555
+ "isEnum": false,
556
+ "possibleValues": [],
557
+ "dependencies": [],
558
+ "fileSources": [],
559
+ "auxiliarySources": []
560
+ }
561
+ }
562
+ }
563
+ \`\`\`
564
+
565
+ **Key rules for grouped columns:**
566
+ - Parent column uses \`dataType: 5\` (OBJECT)
567
+ - Sub-columns live in \`value.objectValue.values\` as **bare ExtractionColumn objects** (no \`wellKnown\` wrapper)
568
+ - Each sub-column needs its own unique \`id\` within the group
569
+ - Sub-columns can themselves be groups (nested groups) up to the nesting depth limit
570
+ - The UI shows the parent as a collapsible group header with sub-columns underneath
571
+ - **Auto-populated \`arrayElementType\`:** The API may auto-populate a sparse \`arrayElementType\` on OBJECT columns (with \`id\`, \`name\`, \`dataType: 5\` but NO sub-columns). This is normal — include it when copying from a working persona but it is not required when creating new columns.
572
+
573
+ **Default field pattern:** Live API data always includes these fields on every column, even when empty. Include them for consistency:
574
+ \`\`\`
575
+ "detailedDescription": "", "isEnum": false, "possibleValues": [],
576
+ "dependencies": [], "fileSources": [], "auxiliarySources": []
577
+ \`\`\`
578
+
579
+ **IMPORTANT — wellKnown wrapping rules:**
580
+ - \`extraction_columns.inline.array.values[]\` = \`Value\` proto type → each entry needs \`{ "wellKnown": { "extractionColumn": {...} } }\`
581
+ - \`objectValue.values[]\` inside a column = \`ExtractionColumn[]\` proto type → bare \`{ "id": "...", "name": "...", "dataType": ... }\`
582
+ - \`arrayElementType\` = \`ExtractionColumn\` proto type → bare \`{ "dataType": ..., ... }\`
583
+
584
+ ## Multi-Value / Array Columns (dataType: 4)
585
+
586
+ Use \`dataType: 4\` (ARRAY) with \`arrayElementType\` to extract **repeated values** from a document (e.g., multiple phone numbers, a list of names).
587
+
588
+ The \`arrayElementType\` field is a bare \`ExtractionColumn\` (no \`wellKnown\` wrapper). It typically includes \`id\` and \`name\` (often repeating the parent's values) plus the element \`dataType\`.
589
+
590
+ ### Simple Array (repeated scalar values)
591
+
592
+ Extract multiple values of the same primitive type (e.g., list of invoice IDs):
593
+
594
+ \`\`\`json
595
+ {
596
+ "wellKnown": {
597
+ "extractionColumn": {
598
+ "id": "invoice_ids",
599
+ "name": "Invoice(s)",
600
+ "description": "Invoice IDs as a list",
601
+ "dataType": 4,
602
+ "arrayElementType": {
603
+ "id": "invoice_ids",
604
+ "name": "Invoice(s)",
605
+ "description": "Single invoice ID like INV-2024-1423",
606
+ "dataType": 1,
607
+ "isEnum": false,
608
+ "possibleValues": [],
609
+ "dependencies": [],
610
+ "fileSources": [],
611
+ "auxiliarySources": []
612
+ }
613
+ }
614
+ }
615
+ }
616
+ \`\`\`
617
+
618
+ ### Array of Objects (repeated grouped values)
619
+
620
+ Extract multiple structured items (e.g., line items from an invoice). Combine \`dataType: 4\` with an \`arrayElementType\` of \`dataType: 5\` (OBJECT). Sub-columns inside \`objectValue.values\` are **bare ExtractionColumn objects** (no \`wellKnown\` wrapper):
621
+
622
+ \`\`\`json
623
+ {
624
+ "wellKnown": {
625
+ "extractionColumn": {
626
+ "id": "line_items",
627
+ "name": "Items",
628
+ "description": "All line items from the invoice",
629
+ "dataType": 4,
630
+ "value": {
631
+ "objectValue": {
632
+ "values": [
633
+ {
634
+ "id": "item_name",
635
+ "name": "Item Name",
636
+ "dataType": 1,
637
+ "description": "",
638
+ "detailedDescription": "",
639
+ "isEnum": false,
640
+ "possibleValues": [],
641
+ "dependencies": [],
642
+ "fileSources": [],
643
+ "auxiliarySources": []
644
+ },
645
+ {
646
+ "id": "quantity",
647
+ "name": "Quantity",
648
+ "dataType": 2,
649
+ "description": "",
650
+ "detailedDescription": "",
651
+ "isEnum": false,
652
+ "possibleValues": [],
653
+ "dependencies": [],
654
+ "fileSources": [],
655
+ "auxiliarySources": []
656
+ },
657
+ {
658
+ "id": "unit_price",
659
+ "name": "Unit Price",
660
+ "dataType": 2,
661
+ "description": "",
662
+ "detailedDescription": "",
663
+ "isEnum": false,
664
+ "possibleValues": [],
665
+ "dependencies": [],
666
+ "fileSources": [],
667
+ "auxiliarySources": []
668
+ }
669
+ ]
670
+ }
671
+ },
672
+ "arrayElementType": {
673
+ "id": "line_items",
674
+ "name": "Items",
675
+ "dataType": 5,
676
+ "value": {
677
+ "objectValue": {
678
+ "values": [
679
+ {
680
+ "id": "item_name",
681
+ "name": "Item Name",
682
+ "dataType": 1,
683
+ "description": "",
684
+ "detailedDescription": "",
685
+ "isEnum": false,
686
+ "possibleValues": [],
687
+ "dependencies": [],
688
+ "fileSources": [],
689
+ "auxiliarySources": []
690
+ },
691
+ {
692
+ "id": "quantity",
693
+ "name": "Quantity",
694
+ "dataType": 2,
695
+ "description": "",
696
+ "detailedDescription": "",
697
+ "isEnum": false,
698
+ "possibleValues": [],
699
+ "dependencies": [],
700
+ "fileSources": [],
701
+ "auxiliarySources": []
702
+ },
703
+ {
704
+ "id": "unit_price",
705
+ "name": "Unit Price",
706
+ "dataType": 2,
707
+ "description": "",
708
+ "detailedDescription": "",
709
+ "isEnum": false,
710
+ "possibleValues": [],
711
+ "dependencies": [],
712
+ "fileSources": [],
713
+ "auxiliarySources": []
714
+ }
715
+ ]
716
+ }
717
+ },
718
+ "description": "",
719
+ "detailedDescription": "",
720
+ "isEnum": false,
721
+ "possibleValues": [],
722
+ "dependencies": [],
723
+ "fileSources": [],
724
+ "auxiliarySources": []
725
+ },
726
+ "detailedDescription": "",
727
+ "isEnum": false,
728
+ "possibleValues": [],
729
+ "dependencies": [],
730
+ "fileSources": [],
731
+ "auxiliarySources": []
732
+ }
733
+ }
734
+ }
735
+ \`\`\`
736
+
737
+ **Key rules for array columns:**
738
+ - Parent column uses \`dataType: 4\` (ARRAY)
739
+ - \`arrayElementType\` is a bare \`ExtractionColumn\` (no \`wellKnown\` wrapper) — repeats the parent's \`id\` and \`name\`, plus the element \`dataType\`
740
+ - For simple arrays: \`arrayElementType.dataType\` = scalar type (1=STRING, 2=NUMBER, etc.)
741
+ - For arrays of objects: \`arrayElementType.dataType: 5\` with \`value.objectValue.values\` containing bare sub-columns
742
+ - **Dual storage pattern:** For array-of-objects, the sub-columns appear in BOTH \`value.objectValue.values\` (on the parent) AND \`arrayElementType.value.objectValue.values\`. Both sets must match. The \`arrayElementType\` is the canonical schema; the parent \`value.objectValue\` may be auto-populated by the API.
743
+ - Sub-columns inside \`objectValue.values\` are bare ExtractionColumn objects (no \`wellKnown\` wrapper)
744
+ - The UI labels this as "Multi Value" — in the API, it maps to \`dataType: 4\` + \`arrayElementType\`
745
+ - **There is no \`isMultiValue\` API field** — that is a UI-only label. Always use \`dataType: 4\` + \`arrayElementType\` in workflow_def.
746
+
747
+ ## Creation-Time Constraints
748
+
749
+ **IMPORTANT:** The following extraction column properties are set at creation time and **cannot be changed** after the column exists:
750
+
751
+ | Property | Constraint |
752
+ |----------|-----------|
753
+ | \`dataType\` | Cannot change after creation (e.g., STRING to ARRAY) |
754
+ | \`arrayElementType\` | Cannot change after creation; defines the array element schema |
755
+ | \`value.objectValue\` structure | Sub-column schema is fixed at creation |
756
+
757
+ To change these properties, you must **delete the column and recreate it** with the new settings.
758
+
759
+ Other properties (\`name\`, \`description\`, \`possibleValues\`, \`formattingOptions\`) can be updated after creation.
760
+
761
+ ## Enum Columns
762
+
763
+ To create a column with a fixed set of allowed values, use \`dataType: 1\` (STRING) with \`isEnum: true\` and \`possibleValues\`:
764
+
765
+ \`\`\`json
766
+ {
767
+ "wellKnown": {
768
+ "extractionColumn": {
769
+ "id": "priority",
770
+ "name": "Priority",
771
+ "description": "Task priority level",
772
+ "dataType": 1,
773
+ "isEnum": true,
774
+ "possibleValues": [
775
+ { "value": { "stringValue": "High", "case": "stringValue" } },
776
+ { "value": { "stringValue": "Medium", "case": "stringValue" } },
777
+ { "value": { "stringValue": "Low", "case": "stringValue" } }
778
+ ]
779
+ }
780
+ }
781
+ }
782
+ \`\`\`
783
+
784
+ ## Document Input Wiring (Dashboard Personas)
785
+
786
+ Dashboard extraction workflows receive documents through one of two patterns:
787
+
788
+ ### Pattern 1: workflowInput (preferred for newer workflows)
789
+
790
+ \`\`\`json
791
+ "workflowInputs": {
792
+ "document-mmf2": {
793
+ "type": { "arrayType": { "wellKnownType": 5, "isList": false }, "isList": false },
794
+ "displayName": "Document"
795
+ }
796
+ }
797
+ \`\`\`
798
+
799
+ Then in the extraction node:
800
+ \`\`\`json
801
+ "documents": { "workflowInput": { "inputName": "document-mmf2" } }
802
+ \`\`\`
803
+
804
+ ### Pattern 2: trigger output (older workflows)
805
+
806
+ \`\`\`json
807
+ "documents": { "actionOutput": { "actionName": "trigger", "output": "user_query" } }
808
+ \`\`\`
809
+
810
+ Note: The output name \`user_query\` is used for documents in older \`document_trigger\` workflows — despite the misleading name, it carries the uploaded document content.
811
+
812
+ ## Extraction Node Options
813
+
814
+ ### disableHumanInteraction
815
+
816
+ Set to \`true\` to skip the human-in-the-loop review step for extraction results. Default is \`false\` (HITL enabled):
817
+
818
+ \`\`\`json
819
+ {
820
+ "name": "entity_extraction_node",
821
+ "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "entity_extraction_with_documents" }, "version": "v0" },
822
+ "inputs": { ... },
823
+ "disableHumanInteraction": true
824
+ }
825
+ \`\`\`
826
+
827
+ ## How to Use
828
+
829
+ 1. Call \`workflow(mode="get", persona_id="<same or similar persona>")\` and use the returned \`workflow_def\` as your format reference.
830
+ 2. For extraction_columns specifically, copy the structure from a working action node in that workflow_def, or use this resource.
831
+ 3. Generate or edit workflow_def as text/JSON; do not reverse-engineer from local JSON files.
832
+
833
+ See also: \`ema://rules/json-output-patterns\` for custom_agent/output_fields (same array.values + wellKnown.extractionColumn pattern).
834
+ `;
835
+ },
836
+ },
837
+ // Nested Data Format - ARRAY and OBJECT column handling
838
+ {
839
+ uri: "ema://rules/nested-data-format",
840
+ name: "rules/nested-data-format",
841
+ description: "Format guide for nested data structures in dashboard columns (ARRAY and OBJECT types). Use for uploads with sub-tables.",
842
+ mimeType: "text/markdown",
843
+ generate: async () => {
844
+ return `# Nested Data Format Guide
845
+
846
+ ## Overview
847
+
848
+ Dashboard columns can contain nested data structures using \`COLUMN_TYPE_ARRAY\` (8) and \`COLUMN_TYPE_OBJECT\` (9).
849
+ These are used for entity extraction results, line items, and other structured data.
850
+
851
+ ## Column Types
852
+
853
+ | Type | Enum | Structure | Use Case |
854
+ |------|------|-----------|----------|
855
+ | \`COLUMN_TYPE_ARRAY\` | 8 | \`{ arrayValues: ColumnValue[] }\` | Lists of same-type items (line items, extracted entities) |
856
+ | \`COLUMN_TYPE_OBJECT\` | 9 | \`{ objectValues: { [key]: ColumnValue } }\` | Structured record with named fields |
857
+
858
+ ## DashboardInput Format
859
+
860
+ When uploading rows with nested data, use this format:
861
+
862
+ \`\`\`typescript
863
+ // ARRAY column
864
+ {
865
+ name: "line_items",
866
+ array_value: [
867
+ { name: "element", string_value: "Item 1" },
868
+ { name: "element", number_value: 100 },
869
+ { name: "element", object_value: {
870
+ "product": { name: "product", string_value: "Widget" },
871
+ "qty": { name: "qty", number_value: 5 }
872
+ }}
873
+ ]
874
+ }
875
+
876
+ // OBJECT column
877
+ {
878
+ name: "customer_info",
879
+ object_value: {
880
+ "name": { name: "name", string_value: "Acme Corp" },
881
+ "email": { name: "email", string_value: "contact@acme.com" },
882
+ "orders": { name: "orders", array_value: [...] } // Nested array
883
+ }
884
+ }
885
+ \`\`\`
886
+
887
+ ## Schema Information
888
+
889
+ Dashboard schema includes nested structure details:
890
+
891
+ \`\`\`typescript
892
+ // For ARRAY columns:
893
+ {
894
+ columnType: "COLUMN_TYPE_ARRAY",
895
+ arrayElementType: {
896
+ columnType: "COLUMN_TYPE_OBJECT", // or STRING, NUMBER, etc.
897
+ objectSubColumns: [...] // If element is OBJECT
898
+ }
899
+ }
900
+
901
+ // For OBJECT columns:
902
+ {
903
+ columnType: "COLUMN_TYPE_OBJECT",
904
+ objectSubColumns: [
905
+ { name: "field1", columnType: "COLUMN_TYPE_STRING" },
906
+ { name: "field2", columnType: "COLUMN_TYPE_NUMBER" },
907
+ ]
908
+ }
909
+ \`\`\`
910
+
911
+ ## Validation Rules
912
+
913
+ | Rule | Limit | Error |
914
+ |------|-------|-------|
915
+ | Max array elements | 1000 | \`Array exceeds maximum of 1000 elements\` |
916
+ | Max nesting depth | 3 levels | \`Nesting depth exceeds maximum of 3\` |
917
+ | Type mismatch | - | \`Expected array for ARRAY column, got <type>\` |
918
+
919
+ ## Common Mistakes
920
+
921
+ ### ❌ Scalar value for ARRAY column
922
+ \`\`\`typescript
923
+ // WRONG - passing string where array expected
924
+ { name: "line_items", string_value: "item1, item2" }
925
+
926
+ // CORRECT
927
+ { name: "line_items", array_value: [
928
+ { name: "element", string_value: "item1" },
929
+ { name: "element", string_value: "item2" }
930
+ ]}
931
+ \`\`\`
932
+
933
+ ### ❌ Array value for STRING column
934
+ \`\`\`typescript
935
+ // WRONG - passing array where string expected
936
+ { name: "customer_name", array_value: [...] }
937
+
938
+ // CORRECT
939
+ { name: "customer_name", string_value: "Acme Corp" }
940
+ \`\`\`
941
+
942
+ ### ❌ Missing element name
943
+ \`\`\`typescript
944
+ // WRONG - missing name in array element
945
+ { name: "items", array_value: [{ string_value: "item1" }] }
946
+
947
+ // CORRECT - always include name
948
+ { name: "items", array_value: [{ name: "element", string_value: "item1" }] }
949
+ \`\`\`
950
+
951
+ ## Related Resources
952
+
953
+ - \`ema://rules/extraction-column-format\` - entity_extraction output format
954
+ - \`ema://rules/json-output-patterns\` - custom_agent JSON patterns
955
+ `;
956
+ },
957
+ },
958
+ // Chat Response Wiring - how to avoid duplicate/repetitive responses
959
+ {
960
+ uri: "ema://rules/chat-response-wiring",
961
+ name: "rules/chat-response-wiring",
962
+ description: "Rules for wiring chat workflows to avoid duplicate or repetitive responses. Chat conversation must flow to chat-aware response nodes.",
963
+ mimeType: "text/markdown",
964
+ generate: async () => {
965
+ return `# Chat Response Wiring Rules
966
+
967
+ ## The Problem
968
+
969
+ Chat workflows that process \`chat_conversation\` through search, extraction, or tool calls **must** wire the results to a **chat-aware response node** — not a generic \`call_llm\`. Without conversation history in the response node, the LLM:
970
+
971
+ 1. **Repeats questions** already answered in prior turns
972
+ 2. **Loses context** from multi-turn conversations
973
+ 3. **Generates stale greetings** instead of continuing naturally
974
+
975
+ ## Correct Patterns
976
+
977
+ ### Pattern 1: Search + Respond (most common)
978
+ \`\`\`
979
+ chat_trigger.chat_conversation → conversation_to_search_query.conversation
980
+ conversation_to_search_query.summarized_conversation → search.query
981
+ search.search_results → respond_for_external_actions.external_action_result
982
+ chat_trigger.user_query → respond_for_external_actions.query
983
+ chat_trigger.chat_conversation → respond_for_external_actions.conversation
984
+ respond_for_external_actions.response → WORKFLOW_OUTPUT
985
+ \`\`\`
986
+
987
+ > **Note**: \`respond_with_sources/v0\` is DEPRECATED. Use \`respond_for_external_actions\` for all new workflows (search results, tool results, or any context-aware response).
988
+
989
+ ### Pattern 2: Tool/Action + Respond
990
+ \`\`\`
991
+ chat_trigger.chat_conversation → external_action_caller.conversation
992
+ external_action_caller.tool_execution_result → respond_for_external_actions.external_action_result
993
+ chat_trigger.user_query → respond_for_external_actions.query
994
+ chat_trigger.chat_conversation → respond_for_external_actions.conversation
995
+ respond_for_external_actions.response → WORKFLOW_OUTPUT
996
+ \`\`\`
997
+
998
+ ### Pattern 3: call_llm with Conversation Context (advanced)
999
+ If you must use \`call_llm\` as the terminal responder, you **must** wire \`chat_conversation\` into its \`named_inputs\`:
1000
+ \`\`\`
1001
+ chat_trigger.chat_conversation → call_llm.named_inputs (key: "conversation_history")
1002
+ \`\`\`
1003
+ This ensures the LLM sees prior turns.
1004
+
1005
+ ## Anti-Patterns
1006
+
1007
+ ### ❌ Stateless call_llm as Responder
1008
+ \`\`\`
1009
+ chat_trigger → search → call_llm (no conversation) → WORKFLOW_OUTPUT
1010
+ \`\`\`
1011
+ **Problem**: call_llm only sees search results, not prior conversation. Repeats questions.
1012
+
1013
+ ### ❌ Entity Extraction → call_llm (no history)
1014
+ \`\`\`
1015
+ chat_trigger → entity_extraction → call_llm (stateless) → WORKFLOW_OUTPUT
1016
+ \`\`\`
1017
+ **Problem**: LLM extracts entities but response node doesn't know what was already discussed.
1018
+
1019
+ ## Key Insight
1020
+
1021
+ \`respond_for_external_actions\` is **conversation-aware by design** — it incorporates conversation context via its \`conversation\` input. Generic \`call_llm\` does not — you must manually wire \`chat_conversation\` via \`named_inputs\`.
1022
+
1023
+ > **Deprecated**: \`respond_with_sources/v0\` — use \`respond_for_external_actions\` for all new workflows.
1024
+
1025
+ ## When to Use Each Response Node
1026
+
1027
+ | Scenario | Response Node | Why |
1028
+ |----------|---------------|-----|
1029
+ | KB/document Q&A | \`respond_for_external_actions\` | Handles search results, conversation-aware |
1030
+ | Tool/API results | \`respond_for_external_actions\` | Explains tool results in conversation context |
1031
+ | Complex multi-step reasoning | \`call_llm/v2\` + named_inputs with chat_conversation | Full control but requires manual history wiring |
1032
+ | Static/template response | \`fixed_response/v1\` | No LLM needed, just template + variables |
1033
+ `;
1034
+ },
1035
+ },
1036
+ // Email Input Wiring - how to wire extracted data to email fields
1037
+ {
1038
+ uri: "ema://rules/email-input-wiring",
1039
+ name: "rules/email-input-wiring",
1040
+ description: "Rules for wiring data to send_email_agent inputs. Entity extraction outputs cannot go directly to email fields — use json_mapper/fixed_response intermediaries.",
1041
+ mimeType: "text/markdown",
1042
+ generate: async () => {
1043
+ return `# Email Input Wiring Rules
1044
+
1045
+ ## The Problem
1046
+
1047
+ \`send_email_agent\` inputs (\`email_to\`, \`email_subject\`, \`email_body\`) require **TEXT_WITH_SOURCES** type. But entity_extraction outputs are **ANY** type (structured JSON). Direct wiring causes:
1048
+
1049
+ 1. **Type mismatch errors** at deploy time (HTTP 500)
1050
+ 2. **Corrupted email addresses** — entire JSON object sent as recipient
1051
+ 3. **Missing fields** — structured data not properly decomposed into individual fields
1052
+
1053
+ ## Rule: Always Use Intermediary Nodes
1054
+
1055
+ \`\`\`
1056
+ ❌ WRONG: entity_extraction → send_email_agent.email_to
1057
+ ❌ WRONG: entity_extraction → send_email_agent.email_subject
1058
+ ❌ WRONG: call_llm.response → send_email_agent.email_to (sends full text as address)
1059
+
1060
+ ✅ RIGHT: entity_extraction → json_mapper → fixed_response({{to}}) → send_email_agent.email_to
1061
+ ✅ RIGHT: custom_agent(output_fields=[To,Subject,Body]) → send_email_agent
1062
+ ✅ RIGHT: entity_extraction → fixed_response({{email_address}}) → send_email_agent.email_to
1063
+ \`\`\`
1064
+
1065
+ ## Pattern A: json_mapper + fixed_response (Recommended)
1066
+
1067
+ Best for decomposing structured extraction results into individual email fields.
1068
+
1069
+ ### Step 1: Extract fields with json_mapper
1070
+ \`\`\`json
1071
+ {
1072
+ "name": "extract_fields",
1073
+ "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "json_mapper" }, "version": "v1" },
1074
+ "inputs": {
1075
+ "json_input": { "actionOutput": { "actionName": "entity_extraction", "output": "extraction_result" }},
1076
+ "json_mapper_config": {
1077
+ "inline": { "wellKnown": { "jsonMapperConfig": {
1078
+ "rules": [
1079
+ { "fieldName": "to", "pathIndices": [{"stringIndex": "email_address"}], "type": 3 },
1080
+ { "fieldName": "subject", "pathIndices": [{"stringIndex": "subject_line"}], "type": 3 },
1081
+ { "fieldName": "body", "pathIndices": [{"stringIndex": "email_body"}], "type": 3 }
1082
+ ]
1083
+ }}}
1084
+ }
1085
+ }
1086
+ }
1087
+ \`\`\`
1088
+
1089
+ ### Step 2: Format each field with fixed_response
1090
+ \`\`\`json
1091
+ {
1092
+ "name": "fmt_to",
1093
+ "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "fixed_response" }, "version": "v1" },
1094
+ "inputs": {
1095
+ "template": { "inline": { "wellKnown": { "textWithSources": { "text": "{{to}}", "sources": [] }}}},
1096
+ "custom_data": { "actionOutput": { "actionName": "extract_fields", "output": "output_json" }}
1097
+ }
1098
+ }
1099
+ \`\`\`
1100
+
1101
+ Repeat for \`fmt_subject\` (using \`{{subject}}\`) and \`fmt_body\` (using \`{{body}}\`).
1102
+
1103
+ ### Step 3: Wire to send_email_agent
1104
+ \`\`\`json
1105
+ {
1106
+ "name": "send_email",
1107
+ "inputs": {
1108
+ "email_to": { "actionOutput": { "actionName": "fmt_to", "output": "response" }},
1109
+ "email_subject": { "actionOutput": { "actionName": "fmt_subject", "output": "response" }},
1110
+ "email_body": { "actionOutput": { "actionName": "fmt_body", "output": "response" }}
1111
+ }
1112
+ }
1113
+ \`\`\`
1114
+
1115
+ ## Pattern B: custom_agent with output_fields (Simpler)
1116
+
1117
+ When the LLM generates email content, use \`custom_agent\` with \`output_fields\` to produce individual TEXT_WITH_SOURCES outputs directly:
1118
+
1119
+ \`\`\`json
1120
+ {
1121
+ "name": "generate_email",
1122
+ "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "custom_agent" }, "version": "v1" },
1123
+ "inputs": {
1124
+ "task_instructions": { "inline": { "wellKnown": { "textWithSources": { "text": "Generate an email with To, Subject, and Body fields.", "sources": [] }}}},
1125
+ "output_fields": {
1126
+ "inline": { "array": { "values": [
1127
+ { "wellKnown": { "extractionColumn": { "id": "To", "name": "To", "dataType": 1 }}},
1128
+ { "wellKnown": { "extractionColumn": { "id": "Subject", "name": "Subject", "dataType": 1 }}},
1129
+ { "wellKnown": { "extractionColumn": { "id": "Body", "name": "Body", "dataType": 1 }}}
1130
+ ]}}
1131
+ }
1132
+ }
1133
+ }
1134
+ \`\`\`
1135
+
1136
+ Each output_field becomes a separate output (e.g., \`generate_email.To\`) that can wire directly to \`send_email_agent\`.
1137
+
1138
+ ## Pattern C: fixed_response for Simple Template Formatting
1139
+
1140
+ When you already have a clean extracted value and just need TEXT_WITH_SOURCES wrapping:
1141
+
1142
+ \`\`\`json
1143
+ {
1144
+ "name": "fmt_email",
1145
+ "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "fixed_response" }, "version": "v1" },
1146
+ "inputs": {
1147
+ "template": { "inline": { "wellKnown": { "textWithSources": { "text": "{{email_address}}", "sources": [] }}}},
1148
+ "custom_data": { "actionOutput": { "actionName": "entity_extraction", "output": "extraction_result" }}
1149
+ }
1150
+ }
1151
+ \`\`\`
1152
+
1153
+ ## Common Mistakes
1154
+
1155
+ | Mistake | Problem | Fix |
1156
+ |---------|---------|-----|
1157
+ | \`entity_extraction → email_to\` | Type mismatch (ANY → TEXT_WITH_SOURCES) | Add json_mapper + fixed_response |
1158
+ | \`call_llm.response → email_to\` | Full LLM response as email address | Extract with json_mapper or use output_fields |
1159
+ | \`json_mapper → email_to\` | json_mapper outputs JSON_VALUE, not TEXT_WITH_SOURCES | Add fixed_response between json_mapper and email |
1160
+ | Missing fixed_response | Type still mismatched | fixed_response converts ANY → TEXT_WITH_SOURCES |
1161
+
1162
+ ## See Also
1163
+
1164
+ - \`ema://rules/anti-patterns\` — includes \`entity-extraction-direct-to-email\` anti-pattern
1165
+ - \`ema://rules/extraction-column-format\` — for output_fields structure
1166
+ `;
1167
+ },
1168
+ },
1169
+ // ─────────────────────────────────────────────────────────────────────────────
1170
+ // Workflow Node Reference - per-node I/O types, semantics, categorizer patterns
1171
+ // Consolidated from .context/public/guides/workflow-builder-patterns.md
1172
+ // ─────────────────────────────────────────────────────────────────────────────
1173
+ {
1174
+ uri: "ema://docs/workflow-node-reference",
1175
+ name: "docs/workflow-node-reference",
1176
+ description: "Complete workflow node reference: per-node I/O types, categorizer patterns (runIf/enumTypes), " +
1177
+ "LLM named_inputs convention, temperature guidelines, data types between nodes",
1178
+ mimeType: "text/markdown",
1179
+ generate: async () => {
1180
+ let md = `# Workflow Node Reference\n\n`;
1181
+ md += `Practical reference for building Ema workflows. Covers per-node I/O semantics, `;
1182
+ md += `categorizer configuration, LLM patterns, and data type compatibility.\n\n`;
1183
+ md += `## Data Types Between Nodes\n\n`;
1184
+ md += `| Type | Code Constant | Description | Example |\n`;
1185
+ md += `|------|---------------|-------------|---------|\n`;
1186
+ md += `| **Plain Text** | \`WELL_KNOWN_TYPE_TEXT_WITH_SOURCES\` | Text with optional citation metadata | User's message, LLM response |\n`;
1187
+ md += `| **Conversation** | \`WELL_KNOWN_TYPE_CHAT_CONVERSATION\` | Structured message history (role + content) | Full chat thread |\n`;
1188
+ md += `| **Search Results** | \`WELL_KNOWN_TYPE_SEARCH_RESULT\` | Retrieved document chunks with citations | KB search results |\n`;
1189
+ md += `| **Enum** | \`WELL_KNOWN_TYPE_ENUM\` | Category/classification signal for routing | \`category::Schedule Appointment\` |\n`;
1190
+ md += `| **Document** | \`WELL_KNOWN_TYPE_DOCUMENT\` | Uploaded file content | PDF, DOCX for extraction |\n`;
1191
+ md += `| **Any** | \`WELL_KNOWN_TYPE_ANY\` | Untyped — needs intermediary for type-safe wiring | entity_extraction output |\n\n`;
1192
+ md += `**Critical**: Types are NOT interchangeable. \`CHAT_CONVERSATION\` into a \`TEXT_WITH_SOURCES\` input causes type mismatch errors. Use converter nodes when needed.\n\n`;
1193
+ md += `---\n\n`;
1194
+ md += `## Input Semantics by Context\n\n`;
1195
+ md += `The same input name means different things depending on the node:\n\n`;
1196
+ md += `| Input Name | In Search Nodes | In Respond Nodes | In Extract Nodes | In Categorizers |\n`;
1197
+ md += `|------------|-----------------|-------------------|-------------------|------------------|\n`;
1198
+ md += `| \`query\` | Search term to look up | User's question to answer | Source text to analyze | N/A |\n`;
1199
+ md += `| \`conversation\` | N/A | N/A (use named_inputs) | N/A | Full history for classification |\n`;
1200
+ md += `| \`trigger_when\` | N/A | "Should I run?" | "Should I run?" | N/A |\n`;
1201
+ md += `| \`named_inputs_*\` | N/A | Additional context | Additional context | N/A |\n\n`;
1202
+ md += `**Mental model:** \`query\` = "What to process?" · \`conversation\` = "Full context" · \`trigger_when\` = "Should I run?" · \`named_inputs_*\` = "Extra context"\n\n`;
1203
+ md += `---\n\n`;
1204
+ md += `## Node Type Reference\n\n`;
1205
+ md += `### chat_trigger\n`;
1206
+ md += `Entry point for chat/voice workflows.\n`;
1207
+ md += `- **Inputs**: None (system event)\n`;
1208
+ md += `- **Outputs**: \`user_query\` (TEXT_WITH_SOURCES), \`chat_conversation\` (CHAT_CONVERSATION)\n`;
1209
+ md += `- **Pairs with**: Intent routers, search nodes, LLM nodes\n\n`;
1210
+ md += `### document_trigger\n`;
1211
+ md += `Entry point for dashboard workflows (file upload per row).\n`;
1212
+ md += `- **Inputs**: None (system event — triggered when row created/file uploaded)\n`;
1213
+ md += `- **Outputs**: \`document_content\` (DOCUMENT), \`row_data\` (ANY — column values)\n`;
1214
+ md += `- **Pairs with**: entity_extraction_with_documents, call_llm, search\n`;
1215
+ md += `- **Critical**: Dashboard personas only. Each row triggers one workflow execution.\n\n`;
1216
+ md += `### chat_categorizer\n`;
1217
+ md += `Classify conversation into intent categories for routing.\n`;
1218
+ md += `- **Inputs**: \`conversation\` (CHAT_CONVERSATION) — must be full history, NOT user_query\n`;
1219
+ md += `- **Outputs**: \`category\` (ENUM) — one per configured category\n`;
1220
+ md += `- **Critical**: Always include Fallback category. Every category needs a handler. Must have \`typeArguments.categories\` pointing to enumType.\n\n`;
1221
+ md += `### text_categorizer/v1\n`;
1222
+ md += `Classify text content (not conversation) for routing.\n`;
1223
+ md += `- **Inputs**: \`named_inputs\` (multiBinding)\n`;
1224
+ md += `- **Outputs**: \`category\` (ENUM)\n`;
1225
+ md += `- **Critical**: \`text_categorizer/v0\` is deprecated — use v1 with \`named_inputs\`.\n\n`;
1226
+ md += `### search/v2\n`;
1227
+ md += `Retrieve relevant documents from uploaded knowledge base.\n`;
1228
+ md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES), \`datastore_configs\` (from widget)\n`;
1229
+ md += `- **Outputs**: \`search_results\` (SEARCH_RESULT)\n`;
1230
+ md += `- **Critical**: NOT an LLM node — do NOT include \`model_config\`. Data must be uploaded first. \`search/v0\` is deprecated.\n\n`;
1231
+ md += `### conversation_to_search_query\n`;
1232
+ md += `Convert multi-turn conversation to a search-optimized query.\n`;
1233
+ md += `- **Inputs**: \`conversation\` (CHAT_CONVERSATION)\n`;
1234
+ md += `- **Outputs**: \`summarized_conversation\` (TEXT_WITH_SOURCES)\n`;
1235
+ md += `- **Critical**: Required for multi-turn chat search. Direct CHAT_CONVERSATION → search causes type mismatch.\n\n`;
1236
+ md += `### call_llm/v2\n`;
1237
+ md += `Generate natural language response using LLM.\n`;
1238
+ md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES), \`named_inputs_*\` (ANY — flexible), \`trigger_when\` (ENUM)\n`;
1239
+ md += `- **Outputs**: \`response_with_sources\` (TEXT_WITH_SOURCES)\n`;
1240
+ md += `- **Critical**: Must wire to WORKFLOW_OUTPUT or response is lost. \`call_llm/v0\` is deprecated.\n\n`;
1241
+ md += `### respond_for_external_actions\n`;
1242
+ md += `Generate conversation-aware response from search results or tool outputs.\n`;
1243
+ md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES), \`conversation\` (CHAT_CONVERSATION), \`external_action_result\` (TEXT_WITH_SOURCES/SEARCH_RESULT)\n`;
1244
+ md += `- **Outputs**: \`response\` (TEXT_WITH_SOURCES)\n`;
1245
+ md += `- **Critical**: Replaces deprecated \`respond_with_sources/v0\`. Has built-in citation handling and conversation awareness.\n\n`;
1246
+ md += `### fixed_response/v1\n`;
1247
+ md += `Return a static predefined message.\n`;
1248
+ md += `- **Inputs**: \`trigger_when\` (ENUM), \`named_inputs_*\` for template variables\n`;
1249
+ md += `- **Outputs**: \`fixed_response_with_sources\` (TEXT_WITH_SOURCES)\n`;
1250
+ md += `- **Pairs with**: Categorizers (as fallback), type conversion with \`{{variables}}\` templates, send_email_agent\n\n`;
1251
+ md += `### external_action_caller\n`;
1252
+ md += `Call external APIs/tools (ServiceNow, Salesforce, calendars).\n`;
1253
+ md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES), \`conversation\` (CHAT_CONVERSATION), tool configuration\n`;
1254
+ md += `- **Outputs**: \`tool_execution_result\` (TEXT_WITH_SOURCES)\n`;
1255
+ md += `- **Pairs with**: Entity extractors (for parameters), respond nodes (for result formatting)\n\n`;
1256
+ md += `### entity_extraction_with_documents\n`;
1257
+ md += `Extract structured entities grounded in provided documents.\n`;
1258
+ md += `- **Inputs**: \`documents\` (DOCUMENT), extraction column config\n`;
1259
+ md += `- **Outputs**: \`extraction_columns\` (ANY — structured extraction results)\n`;
1260
+ md += `- **Critical**: Output type is ANY — needs intermediary (json_mapper + fixed_response) before send_email inputs.\n`;
1261
+ md += `- See \`ema://rules/extraction-column-format\` for column schema.\n\n`;
1262
+ md += `### send_email_agent\n`;
1263
+ md += `Send email with specified recipient, subject, and body.\n`;
1264
+ md += `- **Inputs**: \`email_to\` (TEXT_WITH_SOURCES), \`email_subject\` (ANY), \`email_body\` (TEXT_WITH_SOURCES)\n`;
1265
+ md += `- **Outputs**: \`send_status\` (ANY)\n`;
1266
+ md += `- **Critical**: Inputs must be TEXT_WITH_SOURCES — use intermediary chain from ANY-typed sources.\n`;
1267
+ md += `- See \`ema://rules/email-input-wiring\` for wiring patterns.\n\n`;
1268
+ md += `### json_mapper\n`;
1269
+ md += `Extract specific fields from JSON/structured data into individual outputs.\n`;
1270
+ md += `- **Inputs**: \`input_json\` (ANY), mapping rules\n`;
1271
+ md += `- **Outputs**: \`output_json\` per mapped field\n`;
1272
+ md += `- **Pairs with**: entity_extraction (field extraction), fixed_response (type conversion)\n\n`;
1273
+ md += `### rule_validation_with_documents\n`;
1274
+ md += `Check extracted data against business rules (compliance, thresholds).\n`;
1275
+ md += `- **Inputs**: \`primary_docs\` (DOCUMENT), \`map_of_extracted_columns\` (ANY)\n`;
1276
+ md += `- **Outputs**: \`ruleset_output\` (ANY — validation results)\n`;
1277
+ md += `- **Critical**: Rules configured in UI settings panel, not via workflow_def inputs.\n\n`;
1278
+ md += `### live_web_search\n`;
1279
+ md += `Real-time web search for current information.\n`;
1280
+ md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES)\n`;
1281
+ md += `- **Outputs**: \`web_search_results\` (SEARCH_RESULT)\n`;
1282
+ md += `- **Critical**: No data upload needed (searches the web).\n\n`;
1283
+ md += `### combine_search_results\n`;
1284
+ md += `Merge results from multiple search sources with deduplication.\n`;
1285
+ md += `- **Inputs**: \`search_results_1\` (SEARCH_RESULT), \`search_results_2\` (SEARCH_RESULT)\n`;
1286
+ md += `- **Outputs**: \`combined_results\` (SEARCH_RESULT)\n`;
1287
+ md += `- **Pairs with**: search/v2 + live_web_search, or any two search sources\n`;
1288
+ md += `- **Note**: \`combine_search_results/v0\` is deprecated. For new workflows, prefer \`call_llm\` with multiple \`named_inputs\` to combine search results.\n\n`;
1289
+ md += `### response_validator\n`;
1290
+ md += `Validate LLM output against quality/compliance criteria.\n`;
1291
+ md += `- **Inputs**: \`reference_query\` (TEXT_WITH_SOURCES), \`response_to_validate\` (TEXT_WITH_SOURCES)\n`;
1292
+ md += `- **Outputs**: \`abstain_reason\` (TEXT_WITH_SOURCES — reason for rejection, empty if valid)\n\n`;
1293
+ md += `### abstain_action\n`;
1294
+ md += `Provide a safe decline response when AI should not answer.\n`;
1295
+ md += `- **Inputs**: \`abstain_reason\` (TEXT_WITH_SOURCES)\n`;
1296
+ md += `- **Outputs**: \`abstain_reason\` (TEXT_WITH_SOURCES — decline message)\n\n`;
1297
+ md += `---\n\n`;
1298
+ md += `## Categorizer Configuration\n\n`;
1299
+ md += `### Defining enumTypes\n\n`;
1300
+ md += `Categories must be defined in \`workflow_def.enumTypes\`:\n\n`;
1301
+ md += `\`\`\`json\n`;
1302
+ md += `"enumTypes": [{\n`;
1303
+ md += ` "name": { "name": "intent_categories", "namespaces": [] },\n`;
1304
+ md += ` "options": [\n`;
1305
+ md += ` { "name": "Sales", "description": "Sales inquiries" },\n`;
1306
+ md += ` { "name": "Support", "description": "Technical support" },\n`;
1307
+ md += ` { "name": "General", "description": "General questions" },\n`;
1308
+ md += ` { "name": "Fallback", "description": "Unclear or unmatched intents" }\n`;
1309
+ md += ` ]\n`;
1310
+ md += `}]\n`;
1311
+ md += `\`\`\`\n\n`;
1312
+ md += `### typeArguments\n\n`;
1313
+ md += `The categorizer's \`typeArguments\` must reference the enum:\n\n`;
1314
+ md += `\`\`\`json\n`;
1315
+ md += `"typeArguments": {\n`;
1316
+ md += ` "categories": {\n`;
1317
+ md += ` "enumType": { "name": { "name": "intent_categories", "namespaces": [] } },\n`;
1318
+ md += ` "isList": false\n`;
1319
+ md += ` }\n`;
1320
+ md += `}\n`;
1321
+ md += `\`\`\`\n\n`;
1322
+ md += `**Critical**: Empty \`typeArguments\` causes deploy failure.\n\n`;
1323
+ md += `### runIf Condition Format\n\n`;
1324
+ md += `\`\`\`json\n`;
1325
+ md += `{\n`;
1326
+ md += ` "lhs": {\n`;
1327
+ md += ` "actionOutput": { "actionName": "chat_categorizer", "output": "category" },\n`;
1328
+ md += ` "autoDetectedBinding": false\n`;
1329
+ md += ` },\n`;
1330
+ md += ` "operator": 1,\n`;
1331
+ md += ` "rhs": {\n`;
1332
+ md += ` "inline": { "enumValue": "Market_Impact" },\n`;
1333
+ md += ` "autoDetectedBinding": false\n`;
1334
+ md += ` }\n`;
1335
+ md += `}\n`;
1336
+ md += `\`\`\`\n\n`;
1337
+ md += `| Operator | Meaning | Use Case |\n`;
1338
+ md += `|----------|---------|----------|\n`;
1339
+ md += `| \`1\` | Equals (\`==\`) | Route to handler when category matches |\n`;
1340
+ md += `| \`2\` | Not equals (\`!=\`) | Run for all categories except one |\n\n`;
1341
+ md += `For OR conditions (run for Sales OR General), use \`operator: 2\` with Fallback: \`category != Fallback\`.\n\n`;
1342
+ md += `### Nested Categorizers\n\n`;
1343
+ md += `For complex routing, chain categorizers:\n`;
1344
+ md += `\`\`\`\n`;
1345
+ md += `chat_categorizer (Level 1: HR, IT, General, Fallback)\n`;
1346
+ md += ` └─→ text_categorizer (Level 2 for HR: Benefits, Leave, Payroll, Fallback)\n`;
1347
+ md += `\`\`\`\n\n`;
1348
+ md += `---\n\n`;
1349
+ md += `## LLM Configuration (call_llm)\n\n`;
1350
+ md += `### named_inputs Convention\n\n`;
1351
+ md += `\`call_llm\` accepts additional context via suffix pattern \`named_inputs_<Descriptive_Name>\`:\n\n`;
1352
+ md += `| Named Input | Type | Purpose |\n`;
1353
+ md += `|-------------|------|----------|\n`;
1354
+ md += `| \`named_inputs_Search_Results\` | SEARCH_RESULT | KB search results for RAG |\n`;
1355
+ md += `| \`named_inputs_Conversation\` | CHAT_CONVERSATION | Full conversation history |\n`;
1356
+ md += `| \`named_inputs_Intent\` | ENUM | Detected category from categorizer |\n`;
1357
+ md += `| \`named_inputs_Current_Message\` | TEXT_WITH_SOURCES | Current user message |\n`;
1358
+ md += `| \`named_inputs_Tool_Result\` | TEXT_WITH_SOURCES | External action output |\n\n`;
1359
+ md += `\`named_inputs\` accepts **ANY** type — this is how you pass CHAT_CONVERSATION and SEARCH_RESULT into LLM nodes.\n\n`;
1360
+ md += `### Temperature Guidelines\n\n`;
1361
+ md += `| Use Case | Temperature | Why |\n`;
1362
+ md += `|----------|-------------|-----|\n`;
1363
+ md += `| Entity extraction | 0.0-0.3 | Accuracy over creativity |\n`;
1364
+ md += `| Document generation | 0.3-0.5 | Consistent formatting |\n`;
1365
+ md += `| General Q&A / chat | 0.5-0.7 | Balanced creativity and accuracy |\n`;
1366
+ md += `| Creative writing | 0.7-1.0 | More varied output |\n\n`;
1367
+ md += `### Consolidation Rule\n\n`;
1368
+ md += `If multiple \`call_llm\` nodes share the same inputs and differ only by \`trigger_when\` gate, consolidate them:\n`;
1369
+ md += `1. Create one \`call_llm\` that runs when not Fallback\n`;
1370
+ md += `2. Wire \`categorizer.category → call_llm.named_inputs_Intent\`\n`;
1371
+ md += `3. Update prompt: "Based on the detected intent ({{Intent}}), respond accordingly"\n`;
1372
+ md += `4. Keep separate nodes only when intents require different tools, search sources, or safety constraints.\n\n`;
1373
+ md += `---\n\n`;
1374
+ md += `## See Also\n\n`;
1375
+ md += `- \`ema://rules/input-sources\` — Input source validation rules\n`;
1376
+ md += `- \`ema://rules/extraction-column-format\` — Entity extraction column schema\n`;
1377
+ md += `- \`ema://rules/email-input-wiring\` — Email field wiring patterns\n`;
1378
+ md += `- \`ema://rules/json-output-patterns\` — custom_agent + json_mapper patterns\n`;
1379
+ md += `- \`ema://rules/chat-response-wiring\` — Chat response wiring rules\n`;
1380
+ md += `- \`ema://rules/anti-patterns\` — Common workflow anti-patterns\n`;
1381
+ return md;
1382
+ },
1383
+ },
1384
+ // ─────────────────────────────────────────────────────────────────────────────
1385
+ // Workflow Requirements & Guidance
1386
+ // NOT hardcoded templates - provide requirements and let LLM generate
1387
+ // ─────────────────────────────────────────────────────────────────────────────
1388
+ {
1389
+ uri: "ema://templates/voice-ai/requirements",
1390
+ name: "templates/voice-ai/requirements",
1391
+ description: "Voice AI workflow requirements and guidance. Use workflow(mode='get') for schema, then generate workflow_def.",
1392
+ mimeType: "application/json",
1393
+ generate: async () => {
1394
+ return JSON.stringify({
1395
+ _note: "Requirements and guidance for Voice AI workflows. LLM generates workflow_def based on these.",
1396
+ _usage: "1) workflow(mode='get', persona_id='...') for schema + fingerprint, " +
1397
+ "2) Generate/modify workflow_def, " +
1398
+ "3) workflow(mode='deploy', persona_id='...', base_fingerprint='<fingerprint>', workflow_def={...}) " +
1399
+ "(or workflow_def_path='/path/to/wf.json' for large payloads)",
1400
+ hard_requirements: {
1401
+ workflow_output: {
1402
+ rule: "MUST have results.WORKFLOW_OUTPUT mapped to final action output",
1403
+ example: '{ "results": { "WORKFLOW_OUTPUT": { "actionName": "respond", "outputName": "response" } } }',
1404
+ },
1405
+ workflow_name: {
1406
+ rule: "workflowName MUST be ['ema', 'personas', '<actual_persona_id>']",
1407
+ },
1408
+ trigger: {
1409
+ rule: "Voice AI uses chat_trigger (type CHAT)",
1410
+ namespace: ["triggers", "emainternal"],
1411
+ },
1412
+ response: {
1413
+ rule: "Must produce a response output that wires to WORKFLOW_OUTPUT",
1414
+ },
1415
+ },
1416
+ required_widgets: [
1417
+ { name: "conversationSettings", type: 39, purpose: "Voice identity, welcome message, instructions" },
1418
+ { name: "voiceSettings", type: 40, purpose: "Language, voice model" },
1419
+ { name: "callSettings", type: 41, purpose: "Call forwarding, spam prevention" },
1420
+ { name: "vadSettings", type: 42, purpose: "Voice activity detection, timeouts" },
1421
+ ],
1422
+ common_patterns: {
1423
+ simple_qa: "chat_trigger → search → respond_for_external_actions → WORKFLOW_OUTPUT",
1424
+ with_routing: "chat_trigger → chat_categorizer → [branch per intent] → respond → WORKFLOW_OUTPUT",
1425
+ with_tools: "chat_trigger → categorizer → external_action_caller → respond_for_external_actions → WORKFLOW_OUTPUT",
1426
+ },
1427
+ best_practices: [
1428
+ "Use search/v2 (NOT v0) with datastore_configs",
1429
+ "Include Fallback category in every categorizer",
1430
+ "Use respond_for_external_actions (NOT deprecated respond_with_sources)",
1431
+ "For approval workflows: enable HITL flag on send_email_agent or entity_extraction_with_documents (only nodes that support HITL). general_hitl is NOT deployable.",
1432
+ ],
1433
+ _next_step: "Call workflow(mode='get', persona_id='...') to get full schema, then generate workflow_def",
1434
+ }, null, 2);
1435
+ },
1436
+ },
1437
+ {
1438
+ uri: "ema://templates/voice-ai/config",
1439
+ name: "templates/voice-ai/config",
1440
+ description: "Voice AI configuration template (proto_config widgets). Customize values for your use case.",
1441
+ mimeType: "application/json",
1442
+ generate: async () => {
1443
+ const config = {
1444
+ _note: "Voice AI configuration template. Customize values for your use case.",
1445
+ _usage: "persona(method='update', id='<ID>', config={widgets: [<these widgets with your values>]})",
1446
+ widgets: [
1447
+ {
1448
+ name: "conversationSettings",
1449
+ type: 39,
1450
+ conversationSettings: {
1451
+ ...VOICE_TEMPLATE_FALLBACK.conversationSettings,
1452
+ },
1453
+ },
1454
+ {
1455
+ name: "voiceSettings",
1456
+ type: 40,
1457
+ voiceSettings: {
1458
+ ...VOICE_TEMPLATE_FALLBACK.voiceSettings,
1459
+ },
1460
+ },
1461
+ {
1462
+ name: "callSettings",
1463
+ type: 41,
1464
+ callSettings: {
1465
+ ...VOICE_TEMPLATE_FALLBACK.callSettings,
1466
+ },
1467
+ },
1468
+ {
1469
+ name: "vadSettings",
1470
+ type: 42,
1471
+ vadSettings: {
1472
+ ...VOICE_TEMPLATE_FALLBACK.vadSettings,
1473
+ },
1474
+ },
1475
+ ],
1476
+ field_docs: VOICE_TEMPLATE_FIELD_DOCS,
1477
+ };
1478
+ return JSON.stringify(config, null, 2);
1479
+ },
1480
+ },
1481
+ {
1482
+ uri: "ema://templates/voice-ai/guide",
1483
+ name: "templates/voice-ai/guide",
1484
+ description: "Voice AI creation guide with requirements and step-by-step process",
1485
+ mimeType: "text/markdown",
1486
+ generate: async () => {
1487
+ return `# Voice AI Creation Guide
1488
+
1489
+ ## Process (Follow This Order)
1490
+
1491
+ ### 1. Create Persona
1492
+ \`\`\`
1493
+ persona(method="create", name="Your Voice AI", type="voice")
1494
+ \`\`\`
1495
+
1496
+ ### 2. Get Workflow Schema
1497
+ \`\`\`
1498
+ workflow(mode="get", persona_id="<ID>")
1499
+ \`\`\`
1500
+ This returns:
1501
+ - Current workflow (if any)
1502
+ - Deprecation warnings (fix these first!)
1503
+ - Generation schema (agents, constraints)
1504
+ - Requirements and guidance
1505
+
1506
+ ### 3. Generate Workflow
1507
+ Using the schema, generate a workflow_def that:
1508
+ - Has WORKFLOW_OUTPUT in results
1509
+ - Uses non-deprecated actions
1510
+ - Follows the flow pattern: trigger → processing → response → OUTPUT
1511
+
1512
+ ### 4. Deploy Workflow
1513
+ \`\`\`
1514
+ workflow(mode="deploy", persona_id="<ID>", base_fingerprint="<fingerprint>", workflow_def={...})
1515
+ \n# For large payloads:\n# workflow(mode="deploy", persona_id="<ID>", base_fingerprint="<fingerprint>", workflow_def_path="/path/to/wf.json")
1516
+ \`\`\`
1517
+ Use \`workflow(mode="validate", ...)\` before deploy to catch structural/semantic issues.
1518
+
1519
+ ### 5. Configure Settings
1520
+ \`\`\`
1521
+ persona(method="update", id="<ID>", config={widgets: [...]})
1522
+ \`\`\`
1523
+
1524
+ ### 6. Upload Knowledge
1525
+ \`\`\`
1526
+ persona(id="<ID>", data={method:"upload", path:"your-data.txt"})
1527
+ \`\`\`
1528
+
1529
+ ## Hard Requirements
1530
+
1531
+ | Requirement | Why |
1532
+ |-------------|-----|
1533
+ | WORKFLOW_OUTPUT | Persona cannot be activated without it |
1534
+ | workflowName format | API rejects invalid namespaces |
1535
+ | Non-deprecated actions | Deprecated actions may fail |
1536
+
1537
+ ## Check for Deprecated Actions
1538
+
1539
+ \`workflow(mode="get")\` returns \`deprecation_warnings\` if any actions are deprecated.
1540
+ **Fix these BEFORE deploying.**
1541
+
1542
+ ## Common Deprecated Actions
1543
+
1544
+ | Deprecated | Use Instead |
1545
+ |------------|-------------|
1546
+ | search/v0 | search/v2 (requires datastore_configs) |
1547
+ | respond_with_sources | respond_for_external_actions |
1548
+ | call_llm/v0 | call_llm/v2 |
1549
+
1550
+ ## Anti-Patterns
1551
+
1552
+ ❌ Cloning random existing persona
1553
+ ❌ Skipping workflow_def
1554
+ ❌ Using deprecated actions
1555
+ ❌ Deploying without preview
1556
+
1557
+ ${VOICE_TEMPLATE_FIELD_DOCS}
1558
+ `;
1559
+ },
1560
+ },
1561
+ // Persona Templates - Dynamic from API with fallback
1562
+ {
1563
+ uri: "ema://catalog/templates",
1564
+ name: "catalog/templates",
1565
+ description: "Persona templates from Ema API: pre-configured AI Employee templates (chatbot_starter, voicebot, dashboard, etc.)",
1566
+ mimeType: "application/json",
1567
+ generate: async (ctx) => {
1568
+ const templates = await getDynamicPersonaTemplates({ env: ctx.env });
1569
+ if (templates.length > 0) {
1570
+ return JSON.stringify(templates.map(templateDtoToResource), null, 2);
1571
+ }
1572
+ // Fallback when API unavailable - provide guidance
1573
+ return JSON.stringify({
1574
+ _note: "API templates unavailable. Use catalog(method='list', type='templates') tool which connects to API directly, or use template(config='voice|chat|dashboard') for config fallbacks.",
1575
+ fallback_types: ["voice", "chat", "dashboard"],
1576
+ how_to_get: "catalog(method='list', type='templates') or template(config='voice')",
1577
+ }, null, 2);
1578
+ },
1579
+ },
1580
+ {
1581
+ uri: "ema://catalog/templates-summary",
1582
+ name: "catalog/templates-summary",
1583
+ description: "Persona templates summary: template names, categories, and descriptions for quick reference",
1584
+ mimeType: "text/markdown",
1585
+ generate: async (ctx) => {
1586
+ const templates = await getDynamicPersonaTemplates({ env: ctx.env });
1587
+ if (templates.length === 0) {
1588
+ return `# Persona Templates
1589
+
1590
+ > API templates not available. Use one of these alternatives:
1591
+
1592
+ ## Option 1: Use the catalog tool directly
1593
+ \`\`\`
1594
+ catalog(method="list", type="templates")
1595
+ \`\`\`
1596
+ This connects to the API and may have different credentials.
1597
+
1598
+ ## Option 2: Use config fallbacks
1599
+ \`\`\`
1600
+ template(config="voice") // Voice AI config template
1601
+ template(config="chat") // Chat AI config template
1602
+ template(config="dashboard") // Dashboard AI config template
1603
+ \`\`\`
1604
+
1605
+ ## Option 3: Create from scratch
1606
+ \`\`\`
1607
+ persona(method="create", name="My AI", type="voice|chat|dashboard")
1608
+ \`\`\`
1609
+ `;
1610
+ }
1611
+ const byCategory = new Map();
1612
+ for (const t of templates) {
1613
+ const cat = t.category || "GENERAL";
1614
+ if (!byCategory.has(cat))
1615
+ byCategory.set(cat, []);
1616
+ byCategory.get(cat).push(t);
1617
+ }
1618
+ let md = "# Persona Templates (from API)\n\n";
1619
+ md += `> ${templates.length} templates available for creating AI Employees\n\n`;
1620
+ for (const [category, tpls] of byCategory) {
1621
+ md += `## ${category}\n\n`;
1622
+ md += "| Template | Type | Description |\n";
1623
+ md += "|----------|------|-------------|\n";
1624
+ for (const t of tpls) {
1625
+ const triggerType = t.trigger_type || "-";
1626
+ const desc = t.description?.slice(0, 80) || "-";
1627
+ md += `| \`${t.name}\` | ${triggerType} | ${desc}... |\n`;
1628
+ }
1629
+ md += "\n";
1630
+ }
1631
+ return md;
1632
+ },
1633
+ },
1634
+ // Action Schema - Complete action definitions from ema repo
1635
+ {
1636
+ uri: "ema://schema/actions",
1637
+ name: "schema/actions",
1638
+ description: "Complete action schema: all workflow actions with inputs, outputs, types, and documentation from ema_backend/grpc",
1639
+ mimeType: "application/json",
1640
+ generate: async (ctx) => {
1641
+ const schema = await getDynamicActionSchema({ env: ctx.env });
1642
+ return JSON.stringify(schema, null, 2);
1643
+ },
1644
+ },
1645
+ {
1646
+ uri: "ema://schema/actions-summary",
1647
+ name: "schema/actions-summary",
1648
+ description: "Action schema summary: action names, versions, and descriptions for quick reference",
1649
+ mimeType: "text/markdown",
1650
+ generate: async (ctx) => {
1651
+ const schema = await getDynamicActionSchema({ env: ctx.env });
1652
+ if (schema.actions.length === 0) {
1653
+ return "# Action Schema\n\n> No actions loaded. Check API connectivity or bundled schema.\n";
1654
+ }
1655
+ let md = "# Action Schema\n\n";
1656
+ md += `> ${schema.actions.length} actions loaded (source: ${schema.source})\n`;
1657
+ md += `> Version: ${schema.version}\n\n`;
1658
+ const byCategory = new Map();
1659
+ for (const action of schema.actions) {
1660
+ const cat = action.category || "OTHER";
1661
+ if (!byCategory.has(cat))
1662
+ byCategory.set(cat, []);
1663
+ byCategory.get(cat).push(action);
1664
+ }
1665
+ for (const [category, actions] of byCategory) {
1666
+ md += `## ${category.replace("ACTION_CATEGORY_", "")}\n\n`;
1667
+ md += "| Action | Version | Description | Inputs | Outputs |\n";
1668
+ md += "|--------|---------|-------------|--------|--------|\n";
1669
+ for (const action of actions) {
1670
+ const inputs = action.inputs?.size || 0;
1671
+ const outputs = action.outputs?.size || 0;
1672
+ const desc = action.description?.slice(0, 50) || "-";
1673
+ md += `| \`${action.name}\` | ${action.version || "v0"} | ${desc}... | ${inputs} | ${outputs} |\n`;
1674
+ }
1675
+ md += "\n";
1676
+ }
1677
+ return md;
1678
+ },
1679
+ },
1680
+ {
1681
+ uri: "ema://schema/action/:name",
1682
+ name: "schema/action",
1683
+ description: "Individual action schema by name (e.g., ema://schema/action/call_llm)",
1684
+ mimeType: "application/json",
1685
+ generate: async (ctx) => {
1686
+ // This is a template - actual resolution happens in getResource
1687
+ return JSON.stringify({ error: "Use specific action name in URI" }, null, 2);
1688
+ },
1689
+ },
1690
+ {
1691
+ uri: "ema://schema/workflow-def",
1692
+ name: "schema/workflow-def",
1693
+ description: "Complete workflow_def schema with examples for Chat, Voice, and Dashboard personas. Essential reference for building workflows.",
1694
+ mimeType: "text/markdown",
1695
+ generate: async () => {
1696
+ return `# Workflow Definition Schema
1697
+
1698
+ This document describes the structure of \`workflow_def\` - the JSON format used to deploy AI Employee workflows.
1699
+
1700
+ ## Two-Level Architecture
1701
+
1702
+ 1. **WorkflowSpec** (High-level) - What YOU build:
1703
+ - Nodes, inputs, categories, result mappings
1704
+ - Human-readable, easy to reason about
1705
+ - Gets compiled to workflow_def
1706
+
1707
+ 2. **workflow_def** (Low-level) - What gets deployed:
1708
+ - Compiled JSON structure
1709
+ - Namespaces, action references, edges
1710
+ - Generated by MCP from WorkflowSpec
1711
+
1712
+ ## WorkflowSpec Structure
1713
+
1714
+ \`\`\`typescript
1715
+ interface WorkflowSpec {
1716
+ name: string; // Workflow display name
1717
+ description: string; // What it does
1718
+ personaType: "voice" | "chat" | "dashboard";
1719
+ nodes: Node[]; // Workflow actions
1720
+ resultMappings: ResultMapping[]; // What outputs to return
1721
+ }
1722
+
1723
+ interface Node {
1724
+ id: string; // Unique identifier (e.g., "trigger", "search", "respond")
1725
+ actionType: string; // Action type (e.g., "chat_trigger", "search", "call_llm")
1726
+ displayName: string; // Human-readable name
1727
+ description?: string; // What this node does
1728
+ inputs?: Record<string, InputBinding>; // Input configuration
1729
+ categories?: Category[]; // For categorizers only
1730
+ tools?: Tool[]; // For external action callers
1731
+ runIf?: RunIfCondition; // Conditional execution
1732
+ }
1733
+
1734
+ interface InputBinding {
1735
+ type: "action_output" | "literal" | "widget_config" | "llm_inferred";
1736
+ actionName?: string; // Source node ID (for action_output)
1737
+ output?: string; // Output name from source
1738
+ value?: any; // Static value (for literal)
1739
+ widgetName?: string; // Widget name (for widget_config)
1740
+ }
1741
+
1742
+ interface Category {
1743
+ name: string; // Category name (e.g., "Sales", "Support", "Fallback")
1744
+ description: string; // When to route here
1745
+ examples?: string[]; // Example phrases
1746
+ }
1747
+
1748
+ interface ResultMapping {
1749
+ nodeId: string; // Node that produces the output
1750
+ output: string; // Output name
1751
+ }
1752
+ \`\`\`
1753
+
1754
+ ## Trigger Types by Persona
1755
+
1756
+ | Persona Type | Trigger ActionType | Key Outputs |
1757
+ |--------------|-------------------|-------------|
1758
+ | **Chat** | \`chat_trigger\` | user_query, chat_conversation |
1759
+ | **Voice** | \`chat_trigger\` | user_query, chat_conversation |
1760
+ | **Dashboard** | \`document_trigger\` | document_content, row_data |
1761
+
1762
+ ## Example: Chat Persona (KB Search)
1763
+
1764
+ \`\`\`json
1765
+ {
1766
+ "name": "FAQ Bot",
1767
+ "description": "Answer questions from knowledge base",
1768
+ "personaType": "chat",
1769
+ "nodes": [
1770
+ {
1771
+ "id": "trigger",
1772
+ "actionType": "chat_trigger",
1773
+ "displayName": "Chat Input"
1774
+ },
1775
+ {
1776
+ "id": "search",
1777
+ "actionType": "search",
1778
+ "displayName": "Search Knowledge Base",
1779
+ "inputs": {
1780
+ "query": {
1781
+ "type": "action_output",
1782
+ "actionName": "trigger",
1783
+ "output": "user_query"
1784
+ }
1785
+ }
1786
+ },
1787
+ {
1788
+ "id": "respond",
1789
+ "actionType": "respond_for_external_actions",
1790
+ "displayName": "Generate Response",
1791
+ "inputs": {
1792
+ "search_results": {
1793
+ "type": "action_output",
1794
+ "actionName": "search",
1795
+ "output": "search_results"
1796
+ },
1797
+ "conversation": {
1798
+ "type": "action_output",
1799
+ "actionName": "trigger",
1800
+ "output": "chat_conversation"
1801
+ }
1802
+ }
1803
+ }
1804
+ ],
1805
+ "resultMappings": [
1806
+ { "nodeId": "respond", "output": "response" }
1807
+ ]
1808
+ }
1809
+ \`\`\`
1810
+
1811
+ ## Example: Dashboard Persona (Document Extraction)
1812
+
1813
+ \`\`\`json
1814
+ {
1815
+ "name": "Invoice Processor",
1816
+ "description": "Extract data from invoices",
1817
+ "personaType": "dashboard",
1818
+ "nodes": [
1819
+ {
1820
+ "id": "trigger",
1821
+ "actionType": "document_trigger",
1822
+ "displayName": "Document Input"
1823
+ },
1824
+ {
1825
+ "id": "extract",
1826
+ "actionType": "entity_extraction",
1827
+ "displayName": "Extract Invoice Data",
1828
+ "inputs": {
1829
+ "document": {
1830
+ "type": "action_output",
1831
+ "actionName": "trigger",
1832
+ "output": "document_content"
1833
+ },
1834
+ "extraction_schema": {
1835
+ "type": "literal",
1836
+ "value": {
1837
+ "fields": ["invoice_number", "amount", "date", "vendor"]
1838
+ }
1839
+ }
1840
+ }
1841
+ },
1842
+ {
1843
+ "id": "respond",
1844
+ "actionType": "fixed_response",
1845
+ "displayName": "Return Results",
1846
+ "inputs": {
1847
+ "data": {
1848
+ "type": "action_output",
1849
+ "actionName": "extract",
1850
+ "output": "extracted_entities"
1851
+ }
1852
+ }
1853
+ }
1854
+ ],
1855
+ "resultMappings": [
1856
+ { "nodeId": "respond", "output": "response" }
1857
+ ]
1858
+ }
1859
+ \`\`\`
1860
+
1861
+ ## Example: Voice Persona (Intent Routing)
1862
+
1863
+ \`\`\`json
1864
+ {
1865
+ "name": "IT Support Voice",
1866
+ "description": "Handle IT support calls",
1867
+ "personaType": "voice",
1868
+ "nodes": [
1869
+ {
1870
+ "id": "trigger",
1871
+ "actionType": "chat_trigger",
1872
+ "displayName": "Voice Input"
1873
+ },
1874
+ {
1875
+ "id": "categorize",
1876
+ "actionType": "chat_categorizer",
1877
+ "displayName": "Identify Intent",
1878
+ "inputs": {
1879
+ "conversation": {
1880
+ "type": "action_output",
1881
+ "actionName": "trigger",
1882
+ "output": "chat_conversation"
1883
+ }
1884
+ },
1885
+ "categories": [
1886
+ { "name": "Password Reset", "description": "User needs password help" },
1887
+ { "name": "Create Ticket", "description": "User wants to create a ticket" },
1888
+ { "name": "Fallback", "description": "Anything else" }
1889
+ ]
1890
+ },
1891
+ {
1892
+ "id": "handle_password",
1893
+ "actionType": "call_llm",
1894
+ "displayName": "Password Reset Handler",
1895
+ "runIf": {
1896
+ "sourceAction": "categorize",
1897
+ "sourceOutput": "category",
1898
+ "operator": "eq",
1899
+ "value": "Password Reset"
1900
+ },
1901
+ "inputs": {
1902
+ "prompt": {
1903
+ "type": "literal",
1904
+ "value": "Guide user through password reset process..."
1905
+ }
1906
+ }
1907
+ },
1908
+ {
1909
+ "id": "handle_ticket",
1910
+ "actionType": "external_action_caller",
1911
+ "displayName": "Create ServiceNow Ticket",
1912
+ "runIf": {
1913
+ "sourceAction": "categorize",
1914
+ "sourceOutput": "category",
1915
+ "operator": "eq",
1916
+ "value": "Create Ticket"
1917
+ },
1918
+ "tools": [
1919
+ { "name": "Create_Incident", "namespace": "service_now" }
1920
+ ]
1921
+ },
1922
+ {
1923
+ "id": "handle_fallback",
1924
+ "actionType": "call_llm",
1925
+ "displayName": "General Response",
1926
+ "runIf": {
1927
+ "sourceAction": "categorize",
1928
+ "sourceOutput": "category",
1929
+ "operator": "eq",
1930
+ "value": "Fallback"
1931
+ }
1932
+ }
1933
+ ],
1934
+ "resultMappings": [
1935
+ { "nodeId": "handle_password", "output": "response" },
1936
+ { "nodeId": "handle_ticket", "output": "result" },
1937
+ { "nodeId": "handle_fallback", "output": "response" }
1938
+ ]
1939
+ }
1940
+ \`\`\`
1941
+
1942
+ ## Critical Rules
1943
+
1944
+ 1. **Always include Fallback** - Every categorizer MUST have a Fallback category
1945
+ 2. **Trigger type matters** - Dashboard uses document_trigger, others use chat_trigger
1946
+ 3. **Wire conversations correctly** - Categorizers need chat_conversation, search needs user_query
1947
+ 4. **Map results** - At least one node must have a resultMapping to WORKFLOW_OUTPUT
1948
+ 5. **Check deprecated actions** - Always verify against ema://rules/deprecated-actions-summary
1949
+
1950
+ ## Common Input Bindings
1951
+
1952
+ | Source | Type | Example |
1953
+ |--------|------|---------|
1954
+ | Previous node output | action_output | \`{ type: "action_output", actionName: "search", output: "results" }\` |
1955
+ | Static value | literal | \`{ type: "literal", value: "Hello world" }\` |
1956
+ | Widget config | widget_config | \`{ type: "widget_config", widgetName: "conversationSettings" }\` |
1957
+ | LLM should infer | llm_inferred | \`{ type: "llm_inferred" }\` |
1958
+
1959
+ ## Deployment Flow
1960
+
1961
+ 1. Build your WorkflowSpec (structure above)
1962
+ 2. Call \`workflow(mode="validate", workflow_spec={...})\` to check for errors
1963
+ 3. Call \`workflow(mode="get", persona_id="...")\` to get the latest fingerprint (stale-state protection)
1964
+ 4. Call \`workflow(mode="deploy", persona_id="...", base_fingerprint="<fingerprint>", workflow_def={...})\` to deploy
1965
+
1966
+ For large payloads, use \`workflow_def_path\` instead of inline \`workflow_def\`.
1967
+
1968
+ The MCP compiles your WorkflowSpec into the API's workflow_def format automatically.
1969
+
1970
+ ## Raw workflow_def Input Binding Formats
1971
+
1972
+ When deploying raw workflow_def (not WorkflowSpec), inputs use protobuf-compatible JSON.
1973
+ These formats are CRITICAL - wrong formats cause HTTP 500 with no error details.
1974
+
1975
+ ### Action Output (reference another node's output)
1976
+ \`\`\`json
1977
+ {
1978
+ "actionOutput": { "actionName": "source_node_name", "output": "output_name" },
1979
+ "autoDetectedBinding": false
1980
+ }
1981
+ \`\`\`
1982
+
1983
+ ### Widget Config (reference a proto_config widget)
1984
+ \`\`\`json
1985
+ {
1986
+ "widgetConfig": { "widgetName": "fusionModel" },
1987
+ "autoDetectedBinding": true
1988
+ }
1989
+ \`\`\`
1990
+
1991
+ ### Inline String (STRING type)
1992
+ \`\`\`json
1993
+ {
1994
+ "inline": { "wellKnown": { "stringValue": "your text here" } },
1995
+ "autoDetectedBinding": false
1996
+ }
1997
+ \`\`\`
1998
+
1999
+ ### Inline Text With Sources (TEXT_WITH_SOURCES type)
2000
+ \`\`\`json
2001
+ {
2002
+ "inline": {
2003
+ "wellKnown": {
2004
+ "textWithSources": {
2005
+ "text": "your text here",
2006
+ "sources": [],
2007
+ "resultConfidence": 0,
2008
+ "toolSources": [],
2009
+ "resultType": 0
2010
+ }
2011
+ }
2012
+ },
2013
+ "autoDetectedBinding": false
2014
+ }
2015
+ \`\`\`
2016
+ **WARNING**: STRING ≠ TEXT_WITH_SOURCES. Using stringValue where textWithSources is expected causes HTTP 500.
2017
+
2018
+ ### Inline Enum Value
2019
+ \`\`\`json
2020
+ {
2021
+ "inline": { "enumValue": "CategoryName" },
2022
+ "autoDetectedBinding": false
2023
+ }
2024
+ \`\`\`
2025
+
2026
+ ### Named Inputs (multiBinding format - REQUIRED for named_inputs)
2027
+ \`\`\`json
2028
+ {
2029
+ "multiBinding": {
2030
+ "elements": [
2031
+ {
2032
+ "namedBinding": {
2033
+ "name": "context_key",
2034
+ "value": {
2035
+ "actionOutput": { "actionName": "source_node", "output": "response_with_sources" },
2036
+ "autoDetectedBinding": false
2037
+ },
2038
+ "description": "",
2039
+ "isOptional": false
2040
+ },
2041
+ "autoDetectedBinding": false
2042
+ }
2043
+ ]
2044
+ },
2045
+ "autoDetectedBinding": false
2046
+ }
2047
+ \`\`\`
2048
+
2049
+ ### Action Name Format
2050
+ \`\`\`json
2051
+ {
2052
+ "action": {
2053
+ "name": { "namespaces": ["actions", "emainternal"], "name": "call_llm" },
2054
+ "version": "v2"
2055
+ }
2056
+ }
2057
+ \`\`\`
2058
+ **WARNING**: Empty namespaces [] causes deploy failure. Always use ["actions", "emainternal"].
2059
+
2060
+ ### text_categorizer/v1 Requirements
2061
+ - Uses \`named_inputs\` (multiBinding format), NOT \`text\` input from deprecated v0
2062
+ - Requires \`typeArguments.categories\` pointing to an enumType
2063
+ - \`categorization_instructions\` input type is TEXT_WITH_SOURCES (use textWithSources, NOT stringValue)
2064
+ - Must define matching enumType in workflow_def.enumTypes[] with Fallback category
2065
+ `;
2066
+ },
2067
+ },
2068
+ {
2069
+ uri: "ema://schema/workflow-def-json",
2070
+ name: "schema/workflow-def-json",
2071
+ description: "JSON Schema for workflow_def validation. Use for pre-validation before deployment.",
2072
+ mimeType: "application/json",
2073
+ generate: async () => {
2074
+ const { getWorkflowDefSchemaJSON } = await import("./domain/workflow-def-schema.js");
2075
+ return getWorkflowDefSchemaJSON();
2076
+ },
2077
+ },
2078
+ // ─────────────────────────────────────────────────────────────────────────
2079
+ // Guidance Resources - Single source of truth, multiple export formats
2080
+ // ─────────────────────────────────────────────────────────────────────────
2081
+ {
2082
+ uri: "ema://docs/usage-guide",
2083
+ name: "docs/usage-guide",
2084
+ description: "Complete MCP usage guide with rules, patterns, and tool reference (markdown format)",
2085
+ mimeType: "text/markdown",
2086
+ generate: async () => {
2087
+ const { exportAsMarkdown } = await import("./guidance.js");
2088
+ return exportAsMarkdown();
2089
+ },
2090
+ },
2091
+ {
2092
+ uri: "ema://guidance/rules",
2093
+ name: "guidance/rules",
2094
+ description: "Structured guidance rules (JSON) - for programmatic consumption by other services",
2095
+ mimeType: "application/json",
2096
+ generate: async () => {
2097
+ const { exportAsJSON } = await import("./guidance.js");
2098
+ return exportAsJSON();
2099
+ },
2100
+ },
2101
+ {
2102
+ uri: "ema://guidance/cursor-rule",
2103
+ name: "guidance/cursor-rule",
2104
+ description: "Guidance exported as Cursor .mdc rule format - for IDE integration",
2105
+ mimeType: "text/markdown",
2106
+ generate: async () => {
2107
+ const { exportAsCursorRule } = await import("./guidance.js");
2108
+ return exportAsCursorRule();
2109
+ },
2110
+ },
2111
+ {
2112
+ uri: "ema://guidance/server-instructions",
2113
+ name: "guidance/server-instructions",
2114
+ description: "Server instructions (the content injected into MCP init response)",
2115
+ mimeType: "text/plain",
2116
+ generate: async () => {
2117
+ const { generateServerInstructions } = await import("./guidance.js");
2118
+ return generateServerInstructions();
2119
+ },
2120
+ },
2121
+ {
2122
+ uri: "ema://docs/debugging-guide",
2123
+ name: "docs/debugging-guide",
2124
+ description: "Guide for debugging workflow executions — drill-down flow, status meanings, metrics, and common failure patterns",
2125
+ mimeType: "text/markdown",
2126
+ generate: async () => {
2127
+ return `# Debugging Workflow Executions
2128
+
2129
+ ## Typical Debugging Flow
2130
+
2131
+ The debug tool follows a drill-down pattern that mirrors the web UI's Audit tab:
2132
+
2133
+ \`\`\`
2134
+ 1. debug(method="conversations", persona_id="...")
2135
+ → List audit conversations with optional filters (date, channel, rating)
2136
+
2137
+ 2. debug(method="conversation_detail", conversation_id="...")
2138
+ → See messages with workflow_run_ids linking to execution traces
2139
+
2140
+ 3. debug(method="show_work", persona_id="...", workflow_run_id="...")
2141
+ → Overview of ALL actions in the workflow run: status, cost, latency, I/O
2142
+
2143
+ 4. debug(method="action_detail", persona_id="...", workflow_run_id="...", action_name="...")
2144
+ → Deep trace: full inputs/outputs, LLM prompts/responses, steps, HITL rounds
2145
+ \`\`\`
2146
+
2147
+ Each response includes \`_next_step\` hints to guide you through this flow.
2148
+
2149
+ ## Action Run Status Values
2150
+
2151
+ | Status | Meaning |
2152
+ |--------|---------|
2153
+ | SUCCESS | Action completed without errors |
2154
+ | ERRORED | Action failed — check error_info and steps for details |
2155
+ | WARNING | Action completed but with warnings (e.g., partial results) |
2156
+ | NOT_RUN | Action was skipped (branch not taken, condition not met) |
2157
+ | PAUSED | Action is waiting (e.g., HITL approval pending) |
2158
+
2159
+ ## Key Metrics
2160
+
2161
+ - **llm_call_count**: Number of LLM invocations in the action
2162
+ - **llm_cost_usd**: Total LLM cost across all calls (in USD)
2163
+ - **llm_latency_ms**: Total LLM processing time
2164
+ - **step_count**: Number of execution steps (sub-operations within the action)
2165
+ - **hitl_round_count**: Number of human-in-the-loop interaction rounds
2166
+
2167
+ ## Search
2168
+
2169
+ \`debug(method="search", persona_id="...", query="...")\` performs full-text search
2170
+ across conversation messages. Useful for finding specific user queries or bot responses.
2171
+
2172
+ **Note**: The search method relies on DebuggerService which may not be deployed in all environments.
2173
+ If you get a 404 or "unimplemented" error, use the drill-down flow instead (conversations → conversation_detail → show_work).
2174
+
2175
+ ## Common Failure Patterns
2176
+
2177
+ ### Empty search results
2178
+ - **Symptom**: Search/knowledge action returns no results
2179
+ - **Check**: \`show_work\` → look at the search action's inputs (query, data source)
2180
+ - **Common cause**: No data sources uploaded, or embedding not completed
2181
+
2182
+ ### LLM cost spike
2183
+ - **Symptom**: High \`llm_cost_usd\` on an action
2184
+ - **Check**: \`action_detail\` → examine \`llm_calls[].prompt\` for oversized prompts
2185
+ - **Common cause**: Large documents passed as context, excessive search results
2186
+
2187
+ ### HITL timeout
2188
+ - **Symptom**: Action status is PAUSED indefinitely
2189
+ - **Check**: \`action_detail\` → look at \`hitl_rounds\` for pending requests
2190
+ - **Common cause**: Approval request sent but never responded to
2191
+
2192
+ ### Missing inputs
2193
+ - **Symptom**: Action ERRORED with empty inputs
2194
+ - **Check**: \`action_detail\` → examine \`inputs\` array
2195
+ - **Common cause**: Upstream action didn't produce expected output, wiring issue
2196
+
2197
+ ### Wrong response
2198
+ - **Symptom**: Bot gives incorrect/irrelevant answer
2199
+ - **Check**: Trace the full chain: search inputs → search results → LLM prompt → LLM response
2200
+ - **Use**: \`action_detail\` on each action in sequence to find where data goes wrong
2201
+
2202
+ ## Also Available As Persona Sub-Resource
2203
+
2204
+ \`\`\`
2205
+ persona(id="abc", debug={method:"conversations"})
2206
+ persona(id="abc", debug={method:"show_work", workflow_run_id:"..."})
2207
+ persona(id="abc", debug={method:"action_detail", workflow_run_id:"...", action_name:"..."})
2208
+ \`\`\`
2209
+ `;
2210
+ },
2211
+ },
2212
+ ];
2213
+ // ─────────────────────────────────────────────────────────────────────────────
2214
+ // Dynamic fetching helpers (Ema API as source of truth when available)
2215
+ // ─────────────────────────────────────────────────────────────────────────────
2216
+ function loadAnyConfig() {
2217
+ return (loadConfigFromJsonEnv() ??
2218
+ loadConfigOptional(process.env.EMA_AGENT_SYNC_CONFIG ?? "./config.yaml"));
2219
+ }
2220
+ function getDefaultEnvNameFromConfig(cfg) {
2221
+ const fromEnv = process.env.EMA_ENV_NAME?.trim();
2222
+ if (fromEnv)
2223
+ return fromEnv;
2224
+ const master = getMasterEnv(cfg);
2225
+ return master?.name ?? cfg.environments[0]?.name ?? "demo";
2226
+ }
2227
+ const WK_ANY = "WELL_KNOWN_TYPE_ANY";
2228
+ function actionDtoToAgentDefinition(a) {
2229
+ // Handle inputs - can be array or object with inputs property
2230
+ const inputsArray = Array.isArray(a.inputs) ? a.inputs : [];
2231
+ const outputsArray = Array.isArray(a.outputs) ? a.outputs : [];
2232
+ return {
2233
+ actionName: a.name ?? a.id,
2234
+ displayName: a.name ?? a.id,
2235
+ category: (a.category ?? "other"),
2236
+ description: a.description ?? "",
2237
+ inputs: inputsArray.map((p) => ({
2238
+ name: p.name ?? "input",
2239
+ type: WK_ANY,
2240
+ required: Boolean(p.required),
2241
+ description: p.description ?? "",
2242
+ })),
2243
+ outputs: outputsArray.map((p) => ({
2244
+ name: p.name ?? "output",
2245
+ type: WK_ANY,
2246
+ description: p.description ?? "",
2247
+ })),
2248
+ whenToUse: "",
2249
+ };
2250
+ }
2251
+ const clientCache = new Map();
2252
+ const agentCatalogCache = new Map();
2253
+ function getClientForEnvName(envName) {
2254
+ const cfg = loadAnyConfig();
2255
+ if (!cfg)
2256
+ return null;
2257
+ const effectiveEnv = envName ?? getDefaultEnvNameFromConfig(cfg);
2258
+ const cached = clientCache.get(effectiveEnv);
2259
+ if (cached)
2260
+ return cached;
2261
+ const envCfg = getEnvByName(cfg, effectiveEnv) ?? getEnvByName(cfg, getDefaultEnvNameFromConfig(cfg));
2262
+ if (!envCfg)
2263
+ return null;
2264
+ const env = {
2265
+ name: envCfg.name,
2266
+ baseUrl: envCfg.baseUrl,
2267
+ bearerToken: resolveBearerToken(envCfg.bearerTokenEnv),
2268
+ };
2269
+ const client = new EmaClientAdapter(env);
2270
+ clientCache.set(effectiveEnv, client);
2271
+ return client;
2272
+ }
2273
+ async function getDynamicAgentCatalog(opts) {
2274
+ const cacheKey = opts.env ?? "";
2275
+ const now = Date.now();
2276
+ const cached = agentCatalogCache.get(cacheKey);
2277
+ if (cached && now - cached.ts < 60_000)
2278
+ return cached.agents;
2279
+ const client = getClientForEnvName(opts.env);
2280
+ if (!client)
2281
+ return AGENT_CATALOG;
2282
+ try {
2283
+ const actions = await client.listActions();
2284
+ const agents = actions
2285
+ .filter((a) => typeof a.name === "string" && a.name.trim().length > 0)
2286
+ .map(actionDtoToAgentDefinition);
2287
+ agentCatalogCache.set(cacheKey, { ts: now, agents });
2288
+ return agents.length > 0 ? agents : AGENT_CATALOG;
2289
+ }
2290
+ catch {
2291
+ return AGENT_CATALOG;
2292
+ }
2293
+ }
2294
+ // ─────────────────────────────────────────────────────────────────────────────
2295
+ // Dynamic Persona Templates (API-first)
2296
+ // ─────────────────────────────────────────────────────────────────────────────
2297
+ const templateCatalogCache = new Map();
2298
+ /**
2299
+ * Fetch persona templates from API with caching.
2300
+ * Falls back to empty array if API unavailable (file-backed templates still available via RESOURCE_MAP).
2301
+ */
2302
+ async function getDynamicPersonaTemplates(opts) {
2303
+ const cacheKey = opts.env ?? "";
2304
+ const now = Date.now();
2305
+ const cached = templateCatalogCache.get(cacheKey);
2306
+ if (cached && now - cached.ts < 60_000)
2307
+ return cached.templates;
2308
+ const client = getClientForEnvName(opts.env);
2309
+ if (!client)
2310
+ return [];
2311
+ try {
2312
+ const templates = await client.getPersonaTemplates();
2313
+ templateCatalogCache.set(cacheKey, { ts: now, templates });
2314
+ return templates;
2315
+ }
2316
+ catch {
2317
+ return [];
2318
+ }
2319
+ }
2320
+ const actionSchemaCache = new Map();
2321
+ /**
2322
+ * Get action schema with layered loading:
2323
+ * 1. Try bundled schema (resources/action-schema.json)
2324
+ * 2. Augment with live API data if available
2325
+ */
2326
+ async function getDynamicActionSchema(opts) {
2327
+ const cacheKey = opts.env ?? "";
2328
+ const now = Date.now();
2329
+ const cached = actionSchemaCache.get(cacheKey);
2330
+ if (cached && now - cached.ts < 60_000)
2331
+ return cached.data;
2332
+ const registry = new APISchemaRegistry();
2333
+ // Try to load from bundled schema first (code source of truth)
2334
+ const bundlePath = path.resolve(__dirname, "../../resources/action-schema.json");
2335
+ try {
2336
+ registry.loadFromBundle(bundlePath);
2337
+ }
2338
+ catch {
2339
+ // Bundle not available - try API
2340
+ const client = getClientForEnvName(opts.env);
2341
+ if (client) {
2342
+ try {
2343
+ await registry.load(client);
2344
+ }
2345
+ catch {
2346
+ // Neither bundle nor API available
2347
+ }
2348
+ }
2349
+ }
2350
+ const response = {
2351
+ version: registry.metadata.version ?? "unknown",
2352
+ source: registry.metadata.source ?? "bundle",
2353
+ actions: registry.getAllActions(),
2354
+ };
2355
+ actionSchemaCache.set(cacheKey, { ts: now, data: response });
2356
+ return response;
2357
+ }
2358
+ /**
2359
+ * Get a specific action by name from the schema.
2360
+ */
2361
+ async function getActionByName(name, env) {
2362
+ const schema = await getDynamicActionSchema({ env });
2363
+ // Find latest version of action
2364
+ const matching = schema.actions.filter(a => a.name === name);
2365
+ if (matching.length === 0)
2366
+ return null;
2367
+ // Return latest version (sort by version desc)
2368
+ return matching.sort((a, b) => (b.version || "v0").localeCompare(a.version || "v0"))[0];
2369
+ }
2370
+ /**
2371
+ * Convert PersonaTemplateDTO to a simplified format for MCP resources.
2372
+ */
2373
+ function templateDtoToResource(t) {
2374
+ return {
2375
+ id: t.id,
2376
+ name: t.name,
2377
+ description: t.description,
2378
+ category: t.category,
2379
+ trigger_type: t.trigger_type,
2380
+ has_project_template: t.has_project_template,
2381
+ // Documentation fields for diagnosis/recommendations
2382
+ about_template: t.about_template, // Rich description of capabilities
2383
+ how_to_use: t.how_to_use, // Step-by-step usage instructions
2384
+ value_prop: t.value_prop, // Value proposition sections
2385
+ additional_content: t.additional_content, // Extra documentation
2386
+ // Include proto_config for full template details
2387
+ proto_config: t.proto_config,
2388
+ // Include workflow_definition if available
2389
+ workflow_definition: t.workflow_definition,
2390
+ };
2391
+ }
2392
+ // ─────────────────────────────────────────────────────────────────────────────
2393
+ // Exports
2394
+ // ─────────────────────────────────────────────────────────────────────────────
2395
+ export { DYNAMIC_RESOURCES, getActionByName };