@ema.co/mcp-toolkit 2026.2.13 → 2026.2.23-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.

Files changed (67) hide show
  1. package/.context/public/guides/ema-user-guide.md +12 -16
  2. package/.context/public/guides/mcp-tools-guide.md +203 -334
  3. package/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/api-client/client/client.gen.js +3 -3
  39. package/dist/sdk/generated/api-client/client/index.js +5 -5
  40. package/dist/sdk/generated/api-client/client/utils.gen.js +4 -4
  41. package/dist/sdk/generated/api-client/client.gen.js +1 -1
  42. package/dist/sdk/generated/api-client/core/utils.gen.js +1 -1
  43. package/dist/sdk/generated/api-client/index.js +1 -1
  44. package/dist/sdk/generated/api-client/sdk.gen.js +2 -2
  45. package/dist/sdk/generated/well-known-types.js +99 -0
  46. package/dist/sdk/generated/widget-catalog.js +60 -0
  47. package/dist/sdk/grpc-client.js +115 -1
  48. package/dist/sync/sdk.js +2 -2
  49. package/dist/sync.js +4 -3
  50. package/docs/README.md +17 -9
  51. package/package.json +4 -3
  52. package/.context/public/guides/dashboard-operations.md +0 -349
  53. package/.context/public/guides/email-patterns.md +0 -125
  54. package/.context/public/guides/workflow-builder-patterns.md +0 -708
  55. package/dist/mcp/domain/intent-architect.js +0 -914
  56. package/dist/mcp/domain/quality-gates.js +0 -110
  57. package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
  58. package/dist/mcp/domain/workflow-intent.js +0 -1806
  59. package/dist/mcp/domain/workflow-merge.js +0 -449
  60. package/dist/mcp/domain/workflow-tracer.js +0 -648
  61. package/dist/mcp/domain/workflow-transformer.js +0 -742
  62. package/dist/mcp/handlers/knowledge/index.js +0 -54
  63. package/dist/mcp/handlers/persona/intent.js +0 -141
  64. package/dist/mcp/handlers/workflow/analyze.js +0 -119
  65. package/dist/mcp/handlers/workflow/compare.js +0 -70
  66. package/dist/mcp/handlers/workflow/generate.js +0 -384
  67. package/dist/mcp/handlers-consolidated.js +0 -333
@@ -21,23 +21,19 @@
21
21
  * - Path traversal is blocked
22
22
  * - Secret patterns are detected and blocked
23
23
  * - Only ema:// URI scheme is supported
24
+ *
25
+ * Module structure:
26
+ * - resources.ts (this file): Registry, types, security, static resources
27
+ * - resources-dynamic.ts: DYNAMIC_RESOURCES array + fetching helpers
28
+ * - resources-validation.ts: Validation rules generators
24
29
  */
25
30
  import * as fs from "fs";
26
31
  import * as path from "path";
27
- import { fileURLToPath } from "node:url";
28
- import { dirname } from "node:path";
29
- // ESM-compatible __dirname
30
- const __filename = fileURLToPath(import.meta.url);
31
- const __dirname = dirname(__filename);
32
- // Import knowledge catalogs for dynamic resources
33
- 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";
34
- import { INPUT_SOURCE_RULES, ANTI_PATTERNS, OPTIMIZATION_RULES } from "./domain/validation-rules.js";
35
- import { STRUCTURAL_INVARIANTS } from "./domain/structural-rules.js";
36
- import { mineConstraints, formatConstraintReport } from "./domain/proto-constraints.js";
37
- import { EmaClient } from "../sdk/client.js";
38
- import { APISchemaRegistry } from "./domain/workflow-validator.js";
39
- import { loadConfigFromJsonEnv, loadConfigOptional, resolveBearerToken, getEnvByName, getMasterEnv, } from "../sdk/config.js";
40
- import { VOICE_TEMPLATE_FALLBACK, VOICE_TEMPLATE_FIELD_DOCS, } from "../sdk/generated/template-fallbacks.js";
32
+ // Import dynamic resources and helpers
33
+ import { DYNAMIC_RESOURCES, getActionByName } from "./resources-dynamic.js";
34
+ // Re-export from split modules so external imports don't break
35
+ export { DYNAMIC_RESOURCES, getActionByName } from "./resources-dynamic.js";
36
+ export { generateValidationRulesForLLM, generateBestPracticesChecklist } from "./resources-validation.js";
41
37
  // ─────────────────────────────────────────────────────────────────────────────
42
38
  // Security Utilities
43
39
  // ─────────────────────────────────────────────────────────────────────────────
@@ -67,2110 +63,75 @@ const SECRET_PATTERNS = [
67
63
  const BLOCKED_FILES = [
68
64
  ".env",
69
65
  ".env.local",
70
- ".env.development",
71
- ".env.production",
72
- ".env.test",
73
- "secrets.yaml",
74
- "secrets.json",
75
- ".npmrc",
76
- ".pypirc",
77
- "id_rsa",
78
- "id_ed25519",
79
- "*.pem",
80
- "*.key",
81
- ];
82
- function containsSecrets(content) {
83
- return SECRET_PATTERNS.some((pattern) => pattern.test(content));
84
- }
85
- function isBlockedFile(filename) {
86
- const basename = path.basename(filename);
87
- return BLOCKED_FILES.some((blocked) => {
88
- if (blocked.startsWith("*")) {
89
- return basename.endsWith(blocked.slice(1));
90
- }
91
- return basename === blocked;
92
- });
93
- }
94
- function hasPathTraversal(uriPath) {
95
- return uriPath.includes("..") || uriPath.includes("//");
96
- }
97
- // ─────────────────────────────────────────────────────────────────────────────
98
- // Resource Registry
99
- // ─────────────────────────────────────────────────────────────────────────────
100
- // Allowlisted resource mappings: uri -> relative file path
101
- // Only includes files that ACTUALLY exist in this repo (not symlinked)
102
- // and are useful for MCP consumers
103
- const RESOURCE_MAP = {
104
- // Core Documentation (public guides)
105
- "ema://docs/getting-started": {
106
- path: ".context/public/guides/mcp-tools-guide.md",
107
- description: "Quick start guide: first steps, key concepts, essential rules, common operations",
108
- mimeType: "text/markdown",
109
- },
110
- "ema://docs/readme": {
111
- path: "README.md",
112
- description: "Ema Toolkit overview: MCP tools, prompts, resources, CLI usage, SDK API, configuration",
113
- mimeType: "text/markdown",
114
- },
115
- "ema://docs/ema-user-guide": {
116
- path: ".context/public/guides/ema-user-guide.md",
117
- description: "Ema Platform guide: concepts (AI Employees, Workflows, Agents), glossary, architecture, examples, troubleshooting",
118
- mimeType: "text/markdown",
119
- },
120
- "ema://docs/mcp-tools-guide": {
121
- path: ".context/public/guides/mcp-tools-guide.md",
122
- description: "MCP tools usage guide: tool categories, best practices, example call sequences, workflow review patterns",
123
- mimeType: "text/markdown",
124
- },
125
- // Templates - REMOVED (file-backed templates removed in favor of API + generated fallbacks)
126
- // Use: client.getPersonaTemplates() for live API templates
127
- // Use: getTemplateFallback("voice") from sdk/generated/template-fallbacks.ts for offline fallbacks
128
- // See: ema://catalog/persona-templates for dynamic API-backed template listing
129
- // DEPRECATED: Use ema://docs/usage-guide (dynamic) instead
130
- // This static resource is kept for backwards compatibility but now generates from guidance module
131
- };
132
- // Legacy alias - redirect to dynamic resource
133
- const LEGACY_STATIC_REDIRECTS = {
134
- "ema://rules/mcp-usage": "ema://docs/usage-guide",
135
- };
136
- const DYNAMIC_RESOURCES = [
137
- // Agent Catalog - Dynamic list of all workflow agents
138
- {
139
- uri: "ema://catalog/agents",
140
- name: "catalog/agents",
141
- description: "Complete agent catalog: all available workflow agents with inputs, outputs, critical rules, and usage guidance",
142
- mimeType: "application/json",
143
- generate: async (ctx) => JSON.stringify(await getDynamicAgentCatalog({ env: ctx.env }), null, 2),
144
- },
145
- {
146
- uri: "ema://catalog/agents-summary",
147
- name: "catalog/agents-summary",
148
- description: "Agent catalog summary: agent names, categories, and brief descriptions for quick reference",
149
- mimeType: "text/markdown",
150
- generate: async (ctx) => {
151
- const agents = await getDynamicAgentCatalog({ env: ctx.env });
152
- const byCategory = new Map();
153
- for (const agent of agents) {
154
- const cat = agent.category || "other";
155
- if (!byCategory.has(cat))
156
- byCategory.set(cat, []);
157
- byCategory.get(cat).push(agent);
158
- }
159
- let md = "# Ema Agent Catalog\n\n";
160
- md += `> ${agents.length} agents available for workflow composition\n\n`;
161
- for (const [category, agents] of byCategory) {
162
- md += `## ${category.charAt(0).toUpperCase() + category.slice(1)}\n\n`;
163
- md += "| Agent | Description | Key Inputs | Key Outputs |\n";
164
- md += "|-------|-------------|------------|-------------|\n";
165
- for (const agent of agents) {
166
- const inputs = agent.inputs?.slice(0, 2).map((i) => i.name).join(", ") || "-";
167
- const outputs = agent.outputs?.slice(0, 2).map((o) => o.name).join(", ") || "-";
168
- md += `| \`${agent.actionName}\` | ${agent.description?.slice(0, 60) || "-"}... | ${inputs} | ${outputs} |\n`;
169
- }
170
- md += "\n";
171
- }
172
- return md;
173
- },
174
- },
175
- // Workflow Patterns - Common workflow templates
176
- {
177
- uri: "ema://catalog/patterns",
178
- name: "catalog/patterns",
179
- description: "Workflow patterns: common workflow structures (kb-search, intent-routing, tool-calling) with examples",
180
- mimeType: "application/json",
181
- generate: async () => JSON.stringify(WORKFLOW_PATTERNS, null, 2),
182
- },
183
- // Widget Catalog - UI configuration widgets
184
- {
185
- uri: "ema://catalog/widgets",
186
- name: "catalog/widgets",
187
- description: "Widget catalog: proto_config widget types for voice, chat, and dashboard personas",
188
- mimeType: "application/json",
189
- generate: async () => JSON.stringify(WIDGET_CATALOG, null, 2),
190
- },
191
- // Validation Rules - Input source and anti-pattern rules
192
- {
193
- uri: "ema://rules/input-sources",
194
- name: "rules/input-sources",
195
- description: "Input source validation rules: which agent inputs accept which data types (user_query vs chat_conversation)",
196
- mimeType: "application/json",
197
- generate: async () => JSON.stringify(INPUT_SOURCE_RULES, null, 2),
198
- },
199
- {
200
- uri: "ema://rules/anti-patterns",
201
- name: "rules/anti-patterns",
202
- description: "Anti-patterns to avoid: common workflow mistakes and how to prevent them",
203
- mimeType: "application/json",
204
- generate: async () => JSON.stringify(ANTI_PATTERNS, null, 2),
205
- },
206
- {
207
- uri: "ema://rules/optimizations",
208
- name: "rules/optimizations",
209
- description: "Optimization rules: workflow improvements for better performance and reliability",
210
- mimeType: "application/json",
211
- generate: async () => JSON.stringify(OPTIMIZATION_RULES, null, 2),
212
- },
213
- {
214
- uri: "ema://rules/structural-invariants",
215
- name: "rules/structural-invariants",
216
- description: "Structural invariants: hard constraints that workflows must satisfy (no cycles, reachability, etc.)",
217
- mimeType: "application/json",
218
- generate: async () => JSON.stringify(STRUCTURAL_INVARIANTS, null, 2),
219
- },
220
- {
221
- uri: "ema://validation/rules",
222
- name: "validation/rules",
223
- description: "Static validation rules: path enumeration, required outputs, multiple writers, response/abstain, category hierarchy. Use these rules DURING workflow generation to avoid errors.",
224
- mimeType: "text/markdown",
225
- generate: async () => generateValidationRulesForLLM(),
226
- },
227
- // Best Practices - Auto-generated from SDK rules (SINGLE SOURCE OF TRUTH)
228
- {
229
- uri: "ema://docs/best-practices",
230
- name: "docs/best-practices",
231
- description: "Auto-generated best practices checklist from SDK validation rules. Use BEFORE deploying workflows.",
232
- mimeType: "text/markdown",
233
- generate: async () => generateBestPracticesChecklist(),
234
- },
235
- // Field Constraints - Mined from proto definitions
236
- {
237
- uri: "ema://docs/field-constraints",
238
- name: "docs/field-constraints",
239
- description: "Proto field constraints: immutable fields, read-only fields, creation-only fields, conditional fields. Mined from proto definitions.",
240
- mimeType: "text/markdown",
241
- generate: async () => {
242
- const report = mineConstraints();
243
- return formatConstraintReport(report);
244
- },
245
- },
246
- // Deprecated Actions - API-first with fallback
247
- {
248
- uri: "ema://rules/deprecated-actions",
249
- name: "rules/deprecated-actions",
250
- description: "Deprecated actions list: actions to avoid in new workflows, with their replacements and migration notes",
251
- mimeType: "application/json",
252
- generate: async (ctx) => {
253
- // Try API first using existing client infrastructure
254
- let apiDeprecated = [];
255
- let source = "fallback";
256
- try {
257
- const client = getClientForEnvName(ctx.env);
258
- if (client) {
259
- const actions = await client.listActions();
260
- apiDeprecated = actions.filter(a => a.deprecated).map(a => a.id);
261
- source = "api";
262
- }
263
- }
264
- catch {
265
- // API unavailable, use fallback
266
- }
267
- // Merge API deprecated with fallback replacement info
268
- const result = {
269
- _note: "Actions marked deprecated should not be used in new workflows. Use replacements where available.",
270
- _source: source,
271
- _api_deprecated_count: apiDeprecated.length,
272
- // From API (current deprecated status)
273
- api_deprecated: apiDeprecated.length > 0 ? apiDeprecated : undefined,
274
- // From fallback (includes replacement info)
275
- with_replacement: DEPRECATED_ACTIONS_WITH_REPLACEMENT.map(d => ({
276
- action: d.action,
277
- version: d.version,
278
- replacement: d.replacement,
279
- replacement_version: d.replacementVersion,
280
- migration_notes: d.migrationNotes,
281
- })),
282
- no_replacement: DEPRECATED_ACTIONS_NO_REPLACEMENT.map(d => ({
283
- action: d.action,
284
- version: d.version,
285
- environment: d.environment,
286
- notes: d.migrationNotes,
287
- })),
288
- _tip: "Check workflow(mode='get') for deprecation_warnings on specific workflows",
289
- };
290
- return JSON.stringify(result, null, 2);
291
- },
292
- },
293
- {
294
- uri: "ema://rules/deprecated-actions-summary",
295
- name: "rules/deprecated-actions-summary",
296
- description: "Deprecated actions summary: quick reference table of deprecated actions and replacements",
297
- mimeType: "text/markdown",
298
- generate: async (ctx) => {
299
- // Try API first using existing client infrastructure
300
- let apiDeprecated = [];
301
- let source = "fallback";
302
- try {
303
- const client = getClientForEnvName(ctx.env);
304
- if (client) {
305
- const actions = await client.listActions();
306
- apiDeprecated = actions.filter(a => a.deprecated).map(a => a.id);
307
- source = "api";
308
- }
309
- }
310
- catch {
311
- // API unavailable, use fallback
312
- }
313
- let md = "# Deprecated Actions\n\n";
314
- md += "> Do NOT use these actions in new workflows. Use replacements where available.\n\n";
315
- md += `> Source: ${source} (${source === "api" ? "live" : "synced 2026-01-26"})\n\n`;
316
- if (apiDeprecated.length > 0) {
317
- md += "## From API (Current)\n\n";
318
- md += "| Action ID | Status |\n";
319
- md += "|-----------|--------|\n";
320
- for (const id of apiDeprecated) {
321
- md += `| \`${id}\` | DEPRECATED |\n`;
322
- }
323
- md += "\n";
324
- }
325
- md += "## Actions with Known Replacements\n\n";
326
- md += "| Deprecated Action | Version | Replacement | Notes |\n";
327
- md += "|-------------------|---------|-------------|-------|\n";
328
- for (const d of DEPRECATED_ACTIONS_WITH_REPLACEMENT) {
329
- const repl = Array.isArray(d.replacement) ? d.replacement.join(" or ") : d.replacement;
330
- const replVer = d.replacementVersion ? ` ${d.replacementVersion}` : "";
331
- md += `| \`${d.action}\` | ${d.version} | \`${repl}\`${replVer} | ${d.migrationNotes || "-"} |\n`;
332
- }
333
- md += "\n## Actions with No Known Replacement\n\n";
334
- md += "| Action | Environment | Notes |\n";
335
- md += "|--------|-------------|-------|\n";
336
- for (const d of DEPRECATED_ACTIONS_NO_REPLACEMENT) {
337
- md += `| \`${d.action}\` | ${d.environment || "all"} | ${d.migrationNotes || "-"} |\n`;
338
- }
339
- md += `\n**Total Known Deprecated**: ${ALL_DEPRECATED_ACTIONS.length} actions\n`;
340
- md += `\n> **Best Practice**: Use \`workflow(mode="get")\` to check for deprecation warnings in specific workflows.\n`;
341
- return md;
342
- },
343
- },
344
- // Workflow Enabling Constraints - Requirements for persona activation
345
- {
346
- uri: "ema://rules/workflow-constraints",
347
- name: "rules/workflow-constraints",
348
- description: "Workflow enabling constraints: requirements that must be met before a persona can be activated",
349
- mimeType: "application/json",
350
- generate: async () => JSON.stringify({
351
- _note: "These constraints are checked by the Ema backend when enabling a persona.",
352
- _source: "ema/ema_backend/db/models/personas_model.py:672-756",
353
- _last_synced: "2026-01-26",
354
- enabling_constraints: WORKFLOW_ENABLING_CONSTRAINTS,
355
- minimum_viable_workflows: MINIMUM_VIABLE_WORKFLOWS,
356
- }, null, 2),
357
- },
358
- {
359
- uri: "ema://rules/workflow-constraints-summary",
360
- name: "rules/workflow-constraints-summary",
361
- description: "Workflow constraints summary: checklist of requirements for enabling a persona",
362
- mimeType: "text/markdown",
363
- generate: async () => {
364
- let md = "# Workflow Enabling Constraints\n\n";
365
- md += "> These constraints must be satisfied for a persona to be activated.\n\n";
366
- md += "> Source: ema_backend/db/models/personas_model.py (synced 2026-01-26)\n\n";
367
- md += "## Required Checks\n\n";
368
- md += "| # | Constraint | Error State | Fix |\n";
369
- md += "|---|------------|-------------|-----|\n";
370
- for (const c of WORKFLOW_ENABLING_CONSTRAINTS) {
371
- const critical = c.critical ? "**" : "";
372
- md += `| ${c.id} | ${critical}${c.name}${critical} | \`${c.errorState}\` | ${c.fix} |\n`;
373
- }
374
- md += "\n## Minimum Viable Workflows by Type\n\n";
375
- for (const [type, mvw] of Object.entries(MINIMUM_VIABLE_WORKFLOWS)) {
376
- md += `### ${type}\n\n`;
377
- md += `**Structure**: \`${mvw.exampleStructure}\`\n\n`;
378
- md += `- Required nodes: ${mvw.requiredNodes.map(n => `\`${n}\``).join(", ")}\n`;
379
- md += `- Required outputs: ${mvw.requiredOutputs.map(o => `\`${o}\``).join(", ")}\n`;
380
- if (mvw.requiredWidgets) {
381
- md += `- Required widgets: ${mvw.requiredWidgets.map(w => `\`${w}\``).join(", ")}\n`;
382
- }
383
- if (mvw.notes) {
384
- md += `- Notes: ${mvw.notes}\n`;
385
- }
386
- md += "\n";
387
- }
388
- md += "## Critical Rule: results Mapping\n\n";
389
- md += "**Every workflow must have a `results` map connecting action outputs to the persona's output.**\n\n";
390
- md += "### Chat / Voice Personas\n\n";
391
- md += "Use `WORKFLOW_OUTPUT` as the key — this is the response shown to the user:\n\n";
392
- md += "```json\n";
393
- md += '{\n "results": {\n "WORKFLOW_OUTPUT": {\n "actionName": "respond_node",\n "outputName": "response_with_sources"\n }\n }\n}\n';
394
- md += "```\n\n";
395
- 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";
396
- md += "### Dashboard Personas\n\n";
397
- md += "Use `<actionName>.<outputName>` dot-notation keys — each key becomes a column in the dashboard:\n\n";
398
- md += "```json\n";
399
- 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';
400
- md += "```\n\n";
401
- md += "Common dashboard result outputs: `extraction_columns`, `ruleset_output`, `llm_output`.\n";
402
- return md;
403
- },
404
- },
405
- // JSON Output Patterns - custom_agent + json_mapper best practices
406
- {
407
- uri: "ema://rules/json-output-patterns",
408
- name: "rules/json-output-patterns",
409
- description: "How to properly wire custom_agent JSON output to json_mapper and downstream nodes like send_email_agent",
410
- mimeType: "text/markdown",
411
- generate: async () => {
412
- return `# custom_agent + json_mapper Pattern
413
-
414
- ## Problem
415
-
416
- When using \`custom_agent\` to generate JSON that will be parsed by \`json_mapper\`, the output can be misformatted if not properly configured.
417
-
418
- 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.
419
-
420
- ## Solution A: Define output_fields (RECOMMENDED)
421
-
422
- Use \`output_fields\` with extraction columns matching your JSON keys:
423
-
424
- \`\`\`json
425
- {
426
- "name": "email_gen",
427
- "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "custom_agent" }, "version": "v1" },
428
- "inputs": {
429
- "task_instructions": { "inline": { "wellKnown": { "textWithSources": {
430
- "text": "Generate notification email with To, Subject, and Body fields.",
431
- "sources": []
432
- }}}},
433
- "output_fields": {
434
- "inline": { "array": { "values": [
435
- { "wellKnown": { "extractionColumn": { "id": "To", "name": "To", "dataType": 1 }}},
436
- { "wellKnown": { "extractionColumn": { "id": "Subject", "name": "Subject", "dataType": 1 }}},
437
- { "wellKnown": { "extractionColumn": { "id": "Body", "name": "Body", "dataType": 1 }}}
438
- ]}}
439
- }
440
- }
441
- }
442
- \`\`\`
443
-
444
- ## Solution B: Strict Prompting + json_mapper
445
-
446
- If not using \`output_fields\`, instruct the LLM to output RAW JSON only (no markdown):
447
-
448
- \`\`\`
449
- task_instructions: "Output ONLY the JSON object, no markdown, no explanation. Format: {\\"To\\": \\"...\\", \\"Subject\\": \\"...\\", \\"Body\\": \\"...\\"}"
450
- \`\`\`
451
-
452
- Then wire to \`json_mapper\` with matching \`pathIndices\`:
453
-
454
- \`\`\`json
455
- {
456
- "name": "json_map",
457
- "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "json_mapper" }, "version": "v1" },
458
- "inputs": {
459
- "input_json": { "actionOutput": { "actionName": "email_gen", "output": "response_with_sources" }},
460
- "json_mapper_config": {
461
- "inline": { "wellKnown": { "jsonMapperConfig": {
462
- "rules": [
463
- { "fieldName": "to", "pathIndices": [{"stringIndex": "To"}], "type": 3, "defaultValue": {"stringValue": ""} },
464
- { "fieldName": "subject", "pathIndices": [{"stringIndex": "Subject"}], "type": 3, "defaultValue": {"stringValue": ""} },
465
- { "fieldName": "body", "pathIndices": [{"stringIndex": "Body"}], "type": 3, "defaultValue": {"stringValue": ""} }
466
- ],
467
- "sampleJson": "{\\"To\\": \\"email@example.com\\", \\"Subject\\": \\"Test\\", \\"Body\\": \\"<p>Hi</p>\\"}"
468
- }}}
469
- }
470
- }
471
- }
472
- \`\`\`
473
-
474
- ## Wiring json_mapper to fixed_response
475
-
476
- json_mapper outputs extracted fields to \`output_json\`. Use \`fixed_response\` with template variables to convert to \`TEXT_WITH_SOURCES\`:
477
-
478
- \`\`\`json
479
- {
480
- "name": "fmt_to",
481
- "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "fixed_response" }, "version": "v1" },
482
- "inputs": {
483
- "template": { "inline": { "wellKnown": { "textWithSources": { "text": "{{to}}", "sources": [] }}}},
484
- "custom_data": { "actionOutput": { "actionName": "json_map", "output": "output_json" }}
485
- }
486
- }
487
- \`\`\`
488
-
489
- ## Complete Email Chain Pattern
490
-
491
- \`\`\`
492
- custom_agent (generates JSON)
493
- → json_mapper (extracts To, Subject, Body)
494
- → fixed_response ×3 ({{to}}, {{subject}}, {{body}})
495
- → send_email_agent
496
- \`\`\`
497
-
498
- ## Critical Rules
499
-
500
- | Rule | Why |
501
- |------|-----|
502
- | Use \`output_fields\` OR strict "ONLY JSON" prompt | Prevents markdown wrapping |
503
- | json_mapper outputs to \`output_json\` | Not individual field outputs |
504
- | Use \`fixed_response\` with \`{{fieldName}}\` template | Converts to TEXT_WITH_SOURCES |
505
- | \`send_email_agent\` needs TEXT_WITH_SOURCES | Direct json_mapper output = type mismatch |
506
- | \`pathIndices\` are case-sensitive | Must match JSON keys exactly |
507
- | Include \`defaultValue\` in rules | Prevents null errors |
508
-
509
- ## Debugging
510
-
511
- If json_mapper fails to extract:
512
- 1. Check if LLM is wrapping JSON in markdown (\\\`\\\`\\\`json ... \\\`\\\`\\\`)
513
- 2. Check pathIndices match actual JSON keys (case-sensitive)
514
- 3. Check custom_agent prompt instructs "ONLY JSON, no markdown"
515
- 4. Add defaultValue to prevent null propagation
516
- `;
517
- },
518
- },
519
- // Extraction column format - canonical API shape for entity_extraction / extraction_columns
520
- {
521
- uri: "ema://rules/extraction-column-format",
522
- name: "rules/extraction-column-format",
523
- 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.",
524
- mimeType: "text/markdown",
525
- generate: async () => {
526
- return `# extraction_columns Format (API Shape)
527
-
528
- ## Rule
529
-
530
- 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}]\`).
531
-
532
- ## Required Shape
533
-
534
- \`\`\`json
535
- "extraction_columns": {
536
- "inline": {
537
- "array": {
538
- "values": [
539
- { "wellKnown": { "extractionColumn": { "id": "<column_id>", "name": "<display name>", "description": "<optional>", "dataType": 1 } } },
540
- { "wellKnown": { "extractionColumn": { "id": "...", "name": "...", "description": "...", "dataType": 1 } } }
541
- ]
542
- }
543
- }
544
- }
545
- \`\`\`
546
-
547
- - \`inline.array.values\`: array of column definitions.
548
- - Each column: \`wellKnown.extractionColumn\` with \`id\`, \`name\`, optional \`description\`, \`dataType\` (1 = string typical).
549
- - **Do not** use \`extractionColumns.columns\` or \`{ name, type, description }\` only — the API expects the \`array.values\` + \`wellKnown.extractionColumn\` shape.
550
-
551
- ## id vs name
552
-
553
- - **id**: Unique identifier for the column (stable key; used in results/downstream). Can be \`col1\`, \`col5\`, \`service_date\`, or any unique string.
554
- - **name**: Display/semantic name (LLM uses this to extract values). Can match id or differ (e.g. \`id: "col5"\`, \`name: "service_date"\` is valid).
555
- - **id and name do not have to match.** Using generic ids (\`col1\`, \`col2\`) with semantic names (\`event_id\`, \`service_date\`) is correct.
556
-
557
- ## Duplicate ids
558
-
559
- - **Within the same extraction_columns array**, every column must have a **unique \`id\`**. Duplicate \`id\` values in one array are invalid.
560
- - 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.
561
-
562
- ## dataType Reference (PrimitiveType enum)
563
-
564
- | Value | Type | Description |
565
- |-------|------|-------------|
566
- | 0 | UNKNOWN | Unspecified (avoid) |
567
- | 1 | STRING | Text values (default) |
568
- | 2 | NUMBER | Numeric values |
569
- | 3 | BOOLEAN | True/false values |
570
- | 4 | ARRAY | Array of repeated values (use with \`arrayElementType\`) |
571
- | 5 | OBJECT | Grouped/nested structure (contains sub-columns via \`value.objectValue\`) |
572
- | 6 | DATE | Date values (use with \`formattingOptions.dateFormat\`) |
573
- | 7 | DOCUMENT_VERSION_ID | Reference to a document version |
574
-
575
- **Note on enums:** To create an enumerated column, use \`dataType: 1\` (STRING) with \`isEnum: true\` and populate \`possibleValues\`.
576
-
577
- ## Object / Grouped Columns (dataType: 5)
578
-
579
- 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.
580
-
581
- 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[]\`).
582
-
583
- \`\`\`json
584
- {
585
- "wellKnown": {
586
- "extractionColumn": {
587
- "id": "address",
588
- "name": "Address",
589
- "description": "Structured address fields",
590
- "dataType": 5,
591
- "value": {
592
- "objectValue": {
593
- "values": [
594
- {
595
- "id": "street",
596
- "name": "Street Name",
597
- "description": "Street name",
598
- "dataType": 1,
599
- "detailedDescription": "",
600
- "isEnum": false,
601
- "possibleValues": [],
602
- "dependencies": [],
603
- "fileSources": [],
604
- "auxiliarySources": []
605
- },
606
- {
607
- "id": "city",
608
- "name": "City",
609
- "dataType": 1,
610
- "detailedDescription": "",
611
- "isEnum": false,
612
- "possibleValues": [],
613
- "dependencies": [],
614
- "fileSources": [],
615
- "auxiliarySources": []
616
- },
617
- {
618
- "id": "postal_code",
619
- "name": "Postal Code",
620
- "dataType": 1,
621
- "detailedDescription": "",
622
- "isEnum": false,
623
- "possibleValues": [],
624
- "dependencies": [],
625
- "fileSources": [],
626
- "auxiliarySources": []
627
- }
628
- ]
629
- }
630
- },
631
- "arrayElementType": {
632
- "id": "address",
633
- "name": "Address",
634
- "dataType": 5,
635
- "detailedDescription": "",
636
- "isEnum": false,
637
- "possibleValues": [],
638
- "dependencies": [],
639
- "fileSources": [],
640
- "auxiliarySources": []
641
- },
642
- "detailedDescription": "",
643
- "isEnum": false,
644
- "possibleValues": [],
645
- "dependencies": [],
646
- "fileSources": [],
647
- "auxiliarySources": []
648
- }
649
- }
650
- }
651
- \`\`\`
652
-
653
- **Key rules for grouped columns:**
654
- - Parent column uses \`dataType: 5\` (OBJECT)
655
- - Sub-columns live in \`value.objectValue.values\` as **bare ExtractionColumn objects** (no \`wellKnown\` wrapper)
656
- - Each sub-column needs its own unique \`id\` within the group
657
- - Sub-columns can themselves be groups (nested groups) up to the nesting depth limit
658
- - The UI shows the parent as a collapsible group header with sub-columns underneath
659
- - **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.
660
-
661
- **Default field pattern:** Live API data always includes these fields on every column, even when empty. Include them for consistency:
662
- \`\`\`
663
- "detailedDescription": "", "isEnum": false, "possibleValues": [],
664
- "dependencies": [], "fileSources": [], "auxiliarySources": []
665
- \`\`\`
666
-
667
- **IMPORTANT — wellKnown wrapping rules:**
668
- - \`extraction_columns.inline.array.values[]\` = \`Value\` proto type → each entry needs \`{ "wellKnown": { "extractionColumn": {...} } }\`
669
- - \`objectValue.values[]\` inside a column = \`ExtractionColumn[]\` proto type → bare \`{ "id": "...", "name": "...", "dataType": ... }\`
670
- - \`arrayElementType\` = \`ExtractionColumn\` proto type → bare \`{ "dataType": ..., ... }\`
671
-
672
- ## Multi-Value / Array Columns (dataType: 4)
673
-
674
- Use \`dataType: 4\` (ARRAY) with \`arrayElementType\` to extract **repeated values** from a document (e.g., multiple phone numbers, a list of names).
675
-
676
- 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\`.
677
-
678
- ### Simple Array (repeated scalar values)
679
-
680
- Extract multiple values of the same primitive type (e.g., list of invoice IDs):
681
-
682
- \`\`\`json
683
- {
684
- "wellKnown": {
685
- "extractionColumn": {
686
- "id": "invoice_ids",
687
- "name": "Invoice(s)",
688
- "description": "Invoice IDs as a list",
689
- "dataType": 4,
690
- "arrayElementType": {
691
- "id": "invoice_ids",
692
- "name": "Invoice(s)",
693
- "description": "Single invoice ID like INV-2024-1423",
694
- "dataType": 1,
695
- "isEnum": false,
696
- "possibleValues": [],
697
- "dependencies": [],
698
- "fileSources": [],
699
- "auxiliarySources": []
700
- }
701
- }
702
- }
703
- }
704
- \`\`\`
705
-
706
- ### Array of Objects (repeated grouped values)
707
-
708
- 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):
709
-
710
- \`\`\`json
711
- {
712
- "wellKnown": {
713
- "extractionColumn": {
714
- "id": "line_items",
715
- "name": "Items",
716
- "description": "All line items from the invoice",
717
- "dataType": 4,
718
- "value": {
719
- "objectValue": {
720
- "values": [
721
- {
722
- "id": "item_name",
723
- "name": "Item Name",
724
- "dataType": 1,
725
- "description": "",
726
- "detailedDescription": "",
727
- "isEnum": false,
728
- "possibleValues": [],
729
- "dependencies": [],
730
- "fileSources": [],
731
- "auxiliarySources": []
732
- },
733
- {
734
- "id": "quantity",
735
- "name": "Quantity",
736
- "dataType": 2,
737
- "description": "",
738
- "detailedDescription": "",
739
- "isEnum": false,
740
- "possibleValues": [],
741
- "dependencies": [],
742
- "fileSources": [],
743
- "auxiliarySources": []
744
- },
745
- {
746
- "id": "unit_price",
747
- "name": "Unit Price",
748
- "dataType": 2,
749
- "description": "",
750
- "detailedDescription": "",
751
- "isEnum": false,
752
- "possibleValues": [],
753
- "dependencies": [],
754
- "fileSources": [],
755
- "auxiliarySources": []
756
- }
757
- ]
758
- }
759
- },
760
- "arrayElementType": {
761
- "id": "line_items",
762
- "name": "Items",
763
- "dataType": 5,
764
- "value": {
765
- "objectValue": {
766
- "values": [
767
- {
768
- "id": "item_name",
769
- "name": "Item Name",
770
- "dataType": 1,
771
- "description": "",
772
- "detailedDescription": "",
773
- "isEnum": false,
774
- "possibleValues": [],
775
- "dependencies": [],
776
- "fileSources": [],
777
- "auxiliarySources": []
778
- },
779
- {
780
- "id": "quantity",
781
- "name": "Quantity",
782
- "dataType": 2,
783
- "description": "",
784
- "detailedDescription": "",
785
- "isEnum": false,
786
- "possibleValues": [],
787
- "dependencies": [],
788
- "fileSources": [],
789
- "auxiliarySources": []
790
- },
791
- {
792
- "id": "unit_price",
793
- "name": "Unit Price",
794
- "dataType": 2,
795
- "description": "",
796
- "detailedDescription": "",
797
- "isEnum": false,
798
- "possibleValues": [],
799
- "dependencies": [],
800
- "fileSources": [],
801
- "auxiliarySources": []
802
- }
803
- ]
804
- }
805
- },
806
- "description": "",
807
- "detailedDescription": "",
808
- "isEnum": false,
809
- "possibleValues": [],
810
- "dependencies": [],
811
- "fileSources": [],
812
- "auxiliarySources": []
813
- },
814
- "detailedDescription": "",
815
- "isEnum": false,
816
- "possibleValues": [],
817
- "dependencies": [],
818
- "fileSources": [],
819
- "auxiliarySources": []
820
- }
821
- }
822
- }
823
- \`\`\`
824
-
825
- **Key rules for array columns:**
826
- - Parent column uses \`dataType: 4\` (ARRAY)
827
- - \`arrayElementType\` is a bare \`ExtractionColumn\` (no \`wellKnown\` wrapper) — repeats the parent's \`id\` and \`name\`, plus the element \`dataType\`
828
- - For simple arrays: \`arrayElementType.dataType\` = scalar type (1=STRING, 2=NUMBER, etc.)
829
- - For arrays of objects: \`arrayElementType.dataType: 5\` with \`value.objectValue.values\` containing bare sub-columns
830
- - **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.
831
- - Sub-columns inside \`objectValue.values\` are bare ExtractionColumn objects (no \`wellKnown\` wrapper)
832
- - The UI labels this as "Multi Value" — in the API, it maps to \`dataType: 4\` + \`arrayElementType\`
833
- - **There is no \`isMultiValue\` API field** — that is a UI-only label. Always use \`dataType: 4\` + \`arrayElementType\` in workflow_def.
834
-
835
- ## Creation-Time Constraints
836
-
837
- **IMPORTANT:** The following extraction column properties are set at creation time and **cannot be changed** after the column exists:
838
-
839
- | Property | Constraint |
840
- |----------|-----------|
841
- | \`dataType\` | Cannot change after creation (e.g., STRING to ARRAY) |
842
- | \`arrayElementType\` | Cannot change after creation; defines the array element schema |
843
- | \`value.objectValue\` structure | Sub-column schema is fixed at creation |
844
-
845
- To change these properties, you must **delete the column and recreate it** with the new settings.
846
-
847
- Other properties (\`name\`, \`description\`, \`possibleValues\`, \`formattingOptions\`) can be updated after creation.
848
-
849
- ## Enum Columns
850
-
851
- To create a column with a fixed set of allowed values, use \`dataType: 1\` (STRING) with \`isEnum: true\` and \`possibleValues\`:
852
-
853
- \`\`\`json
854
- {
855
- "wellKnown": {
856
- "extractionColumn": {
857
- "id": "priority",
858
- "name": "Priority",
859
- "description": "Task priority level",
860
- "dataType": 1,
861
- "isEnum": true,
862
- "possibleValues": [
863
- { "value": { "stringValue": "High", "case": "stringValue" } },
864
- { "value": { "stringValue": "Medium", "case": "stringValue" } },
865
- { "value": { "stringValue": "Low", "case": "stringValue" } }
866
- ]
867
- }
868
- }
869
- }
870
- \`\`\`
871
-
872
- ## Document Input Wiring (Dashboard Personas)
873
-
874
- Dashboard extraction workflows receive documents through one of two patterns:
875
-
876
- ### Pattern 1: workflowInput (preferred for newer workflows)
877
-
878
- \`\`\`json
879
- "workflowInputs": {
880
- "document-mmf2": {
881
- "type": { "arrayType": { "wellKnownType": 5, "isList": false }, "isList": false },
882
- "displayName": "Document"
883
- }
884
- }
885
- \`\`\`
886
-
887
- Then in the extraction node:
888
- \`\`\`json
889
- "documents": { "workflowInput": { "inputName": "document-mmf2" } }
890
- \`\`\`
891
-
892
- ### Pattern 2: trigger output (older workflows)
893
-
894
- \`\`\`json
895
- "documents": { "actionOutput": { "actionName": "trigger", "output": "user_query" } }
896
- \`\`\`
897
-
898
- 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.
899
-
900
- ## Extraction Node Options
901
-
902
- ### disableHumanInteraction
903
-
904
- Set to \`true\` to skip the human-in-the-loop review step for extraction results. Default is \`false\` (HITL enabled):
905
-
906
- \`\`\`json
907
- {
908
- "name": "entity_extraction_node",
909
- "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "entity_extraction_with_documents" }, "version": "v0" },
910
- "inputs": { ... },
911
- "disableHumanInteraction": true
912
- }
913
- \`\`\`
914
-
915
- ## How to Use
916
-
917
- 1. Call \`workflow(mode="get", persona_id="<same or similar persona>")\` and use the returned \`workflow_def\` as your format reference.
918
- 2. For extraction_columns specifically, copy the structure from a working action node in that workflow_def, or use this resource.
919
- 3. Generate or edit workflow_def as text/JSON; do not reverse-engineer from local JSON files.
920
-
921
- See also: \`ema://rules/json-output-patterns\` for custom_agent/output_fields (same array.values + wellKnown.extractionColumn pattern).
922
- `;
923
- },
924
- },
925
- // Nested Data Format - ARRAY and OBJECT column handling
926
- {
927
- uri: "ema://rules/nested-data-format",
928
- name: "rules/nested-data-format",
929
- description: "Format guide for nested data structures in dashboard columns (ARRAY and OBJECT types). Use for uploads with sub-tables.",
930
- mimeType: "text/markdown",
931
- generate: async () => {
932
- return `# Nested Data Format Guide
933
-
934
- ## Overview
935
-
936
- Dashboard columns can contain nested data structures using \`COLUMN_TYPE_ARRAY\` (8) and \`COLUMN_TYPE_OBJECT\` (9).
937
- These are used for entity extraction results, line items, and other structured data.
938
-
939
- ## Column Types
940
-
941
- | Type | Enum | Structure | Use Case |
942
- |------|------|-----------|----------|
943
- | \`COLUMN_TYPE_ARRAY\` | 8 | \`{ arrayValues: ColumnValue[] }\` | Lists of same-type items (line items, extracted entities) |
944
- | \`COLUMN_TYPE_OBJECT\` | 9 | \`{ objectValues: { [key]: ColumnValue } }\` | Structured record with named fields |
945
-
946
- ## DashboardInput Format
947
-
948
- When uploading rows with nested data, use this format:
949
-
950
- \`\`\`typescript
951
- // ARRAY column
952
- {
953
- name: "line_items",
954
- array_value: [
955
- { name: "element", string_value: "Item 1" },
956
- { name: "element", number_value: 100 },
957
- { name: "element", object_value: {
958
- "product": { name: "product", string_value: "Widget" },
959
- "qty": { name: "qty", number_value: 5 }
960
- }}
961
- ]
962
- }
963
-
964
- // OBJECT column
965
- {
966
- name: "customer_info",
967
- object_value: {
968
- "name": { name: "name", string_value: "Acme Corp" },
969
- "email": { name: "email", string_value: "contact@acme.com" },
970
- "orders": { name: "orders", array_value: [...] } // Nested array
971
- }
972
- }
973
- \`\`\`
974
-
975
- ## Schema Information
976
-
977
- Dashboard schema includes nested structure details:
978
-
979
- \`\`\`typescript
980
- // For ARRAY columns:
981
- {
982
- columnType: "COLUMN_TYPE_ARRAY",
983
- arrayElementType: {
984
- columnType: "COLUMN_TYPE_OBJECT", // or STRING, NUMBER, etc.
985
- objectSubColumns: [...] // If element is OBJECT
986
- }
987
- }
988
-
989
- // For OBJECT columns:
990
- {
991
- columnType: "COLUMN_TYPE_OBJECT",
992
- objectSubColumns: [
993
- { name: "field1", columnType: "COLUMN_TYPE_STRING" },
994
- { name: "field2", columnType: "COLUMN_TYPE_NUMBER" },
995
- ]
996
- }
997
- \`\`\`
998
-
999
- ## Validation Rules
1000
-
1001
- | Rule | Limit | Error |
1002
- |------|-------|-------|
1003
- | Max array elements | 1000 | \`Array exceeds maximum of 1000 elements\` |
1004
- | Max nesting depth | 3 levels | \`Nesting depth exceeds maximum of 3\` |
1005
- | Type mismatch | - | \`Expected array for ARRAY column, got <type>\` |
1006
-
1007
- ## Common Mistakes
1008
-
1009
- ### ❌ Scalar value for ARRAY column
1010
- \`\`\`typescript
1011
- // WRONG - passing string where array expected
1012
- { name: "line_items", string_value: "item1, item2" }
1013
-
1014
- // CORRECT
1015
- { name: "line_items", array_value: [
1016
- { name: "element", string_value: "item1" },
1017
- { name: "element", string_value: "item2" }
1018
- ]}
1019
- \`\`\`
1020
-
1021
- ### ❌ Array value for STRING column
1022
- \`\`\`typescript
1023
- // WRONG - passing array where string expected
1024
- { name: "customer_name", array_value: [...] }
1025
-
1026
- // CORRECT
1027
- { name: "customer_name", string_value: "Acme Corp" }
1028
- \`\`\`
1029
-
1030
- ### ❌ Missing element name
1031
- \`\`\`typescript
1032
- // WRONG - missing name in array element
1033
- { name: "items", array_value: [{ string_value: "item1" }] }
1034
-
1035
- // CORRECT - always include name
1036
- { name: "items", array_value: [{ name: "element", string_value: "item1" }] }
1037
- \`\`\`
1038
-
1039
- ## Related Resources
1040
-
1041
- - \`ema://rules/extraction-column-format\` - entity_extraction output format
1042
- - \`ema://rules/json-output-patterns\` - custom_agent JSON patterns
1043
- `;
1044
- },
1045
- },
1046
- // Chat Response Wiring - how to avoid duplicate/repetitive responses
1047
- {
1048
- uri: "ema://rules/chat-response-wiring",
1049
- name: "rules/chat-response-wiring",
1050
- description: "Rules for wiring chat workflows to avoid duplicate or repetitive responses. Chat conversation must flow to chat-aware response nodes.",
1051
- mimeType: "text/markdown",
1052
- generate: async () => {
1053
- return `# Chat Response Wiring Rules
1054
-
1055
- ## The Problem
1056
-
1057
- 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:
1058
-
1059
- 1. **Repeats questions** already answered in prior turns
1060
- 2. **Loses context** from multi-turn conversations
1061
- 3. **Generates stale greetings** instead of continuing naturally
1062
-
1063
- ## Correct Patterns
1064
-
1065
- ### Pattern 1: Search + Respond (most common)
1066
- \`\`\`
1067
- chat_trigger.chat_conversation → conversation_to_search_query.conversation
1068
- conversation_to_search_query.summarized_conversation → search.query
1069
- search.search_results → respond_for_external_actions.external_action_result
1070
- chat_trigger.user_query → respond_for_external_actions.query
1071
- chat_trigger.chat_conversation → respond_for_external_actions.conversation
1072
- respond_for_external_actions.response → WORKFLOW_OUTPUT
1073
- \`\`\`
1074
-
1075
- > **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).
1076
-
1077
- ### Pattern 2: Tool/Action + Respond
1078
- \`\`\`
1079
- chat_trigger.chat_conversation → external_action_caller.conversation
1080
- external_action_caller.tool_execution_result → respond_for_external_actions.external_action_result
1081
- chat_trigger.user_query → respond_for_external_actions.query
1082
- chat_trigger.chat_conversation → respond_for_external_actions.conversation
1083
- respond_for_external_actions.response → WORKFLOW_OUTPUT
1084
- \`\`\`
1085
-
1086
- ### Pattern 3: call_llm with Conversation Context (advanced)
1087
- If you must use \`call_llm\` as the terminal responder, you **must** wire \`chat_conversation\` into its \`named_inputs\`:
1088
- \`\`\`
1089
- chat_trigger.chat_conversation → call_llm.named_inputs (key: "conversation_history")
1090
- \`\`\`
1091
- This ensures the LLM sees prior turns.
1092
-
1093
- ## Anti-Patterns
1094
-
1095
- ### ❌ Stateless call_llm as Responder
1096
- \`\`\`
1097
- chat_trigger → search → call_llm (no conversation) → WORKFLOW_OUTPUT
1098
- \`\`\`
1099
- **Problem**: call_llm only sees search results, not prior conversation. Repeats questions.
1100
-
1101
- ### ❌ Entity Extraction → call_llm (no history)
1102
- \`\`\`
1103
- chat_trigger → entity_extraction → call_llm (stateless) → WORKFLOW_OUTPUT
1104
- \`\`\`
1105
- **Problem**: LLM extracts entities but response node doesn't know what was already discussed.
1106
-
1107
- ## Key Insight
1108
-
1109
- \`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\`.
1110
-
1111
- > **Deprecated**: \`respond_with_sources/v0\` — use \`respond_for_external_actions\` for all new workflows.
1112
-
1113
- ## When to Use Each Response Node
1114
-
1115
- | Scenario | Response Node | Why |
1116
- |----------|---------------|-----|
1117
- | KB/document Q&A | \`respond_for_external_actions\` | Handles search results, conversation-aware |
1118
- | Tool/API results | \`respond_for_external_actions\` | Explains tool results in conversation context |
1119
- | Complex multi-step reasoning | \`call_llm/v2\` + named_inputs with chat_conversation | Full control but requires manual history wiring |
1120
- | Static/template response | \`fixed_response/v1\` | No LLM needed, just template + variables |
1121
- `;
1122
- },
1123
- },
1124
- // Email Input Wiring - how to wire extracted data to email fields
1125
- {
1126
- uri: "ema://rules/email-input-wiring",
1127
- name: "rules/email-input-wiring",
1128
- 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.",
1129
- mimeType: "text/markdown",
1130
- generate: async () => {
1131
- return `# Email Input Wiring Rules
1132
-
1133
- ## The Problem
1134
-
1135
- \`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:
1136
-
1137
- 1. **Type mismatch errors** at deploy time (HTTP 500)
1138
- 2. **Corrupted email addresses** — entire JSON object sent as recipient
1139
- 3. **Missing fields** — structured data not properly decomposed into individual fields
1140
-
1141
- ## Rule: Always Use Intermediary Nodes
1142
-
1143
- \`\`\`
1144
- ❌ WRONG: entity_extraction → send_email_agent.email_to
1145
- ❌ WRONG: entity_extraction → send_email_agent.email_subject
1146
- ❌ WRONG: call_llm.response → send_email_agent.email_to (sends full text as address)
1147
-
1148
- ✅ RIGHT: entity_extraction → json_mapper → fixed_response({{to}}) → send_email_agent.email_to
1149
- ✅ RIGHT: custom_agent(output_fields=[To,Subject,Body]) → send_email_agent
1150
- ✅ RIGHT: entity_extraction → fixed_response({{email_address}}) → send_email_agent.email_to
1151
- \`\`\`
1152
-
1153
- ## Pattern A: json_mapper + fixed_response (Recommended)
1154
-
1155
- Best for decomposing structured extraction results into individual email fields.
1156
-
1157
- ### Step 1: Extract fields with json_mapper
1158
- \`\`\`json
1159
- {
1160
- "name": "extract_fields",
1161
- "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "json_mapper" }, "version": "v1" },
1162
- "inputs": {
1163
- "json_input": { "actionOutput": { "actionName": "entity_extraction", "output": "extraction_result" }},
1164
- "json_mapper_config": {
1165
- "inline": { "wellKnown": { "jsonMapperConfig": {
1166
- "rules": [
1167
- { "fieldName": "to", "pathIndices": [{"stringIndex": "email_address"}], "type": 3 },
1168
- { "fieldName": "subject", "pathIndices": [{"stringIndex": "subject_line"}], "type": 3 },
1169
- { "fieldName": "body", "pathIndices": [{"stringIndex": "email_body"}], "type": 3 }
1170
- ]
1171
- }}}
1172
- }
1173
- }
1174
- }
1175
- \`\`\`
1176
-
1177
- ### Step 2: Format each field with fixed_response
1178
- \`\`\`json
1179
- {
1180
- "name": "fmt_to",
1181
- "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "fixed_response" }, "version": "v1" },
1182
- "inputs": {
1183
- "template": { "inline": { "wellKnown": { "textWithSources": { "text": "{{to}}", "sources": [] }}}},
1184
- "custom_data": { "actionOutput": { "actionName": "extract_fields", "output": "output_json" }}
1185
- }
1186
- }
1187
- \`\`\`
1188
-
1189
- Repeat for \`fmt_subject\` (using \`{{subject}}\`) and \`fmt_body\` (using \`{{body}}\`).
1190
-
1191
- ### Step 3: Wire to send_email_agent
1192
- \`\`\`json
1193
- {
1194
- "name": "send_email",
1195
- "inputs": {
1196
- "email_to": { "actionOutput": { "actionName": "fmt_to", "output": "response" }},
1197
- "email_subject": { "actionOutput": { "actionName": "fmt_subject", "output": "response" }},
1198
- "email_body": { "actionOutput": { "actionName": "fmt_body", "output": "response" }}
1199
- }
1200
- }
1201
- \`\`\`
1202
-
1203
- ## Pattern B: custom_agent with output_fields (Simpler)
1204
-
1205
- When the LLM generates email content, use \`custom_agent\` with \`output_fields\` to produce individual TEXT_WITH_SOURCES outputs directly:
1206
-
1207
- \`\`\`json
1208
- {
1209
- "name": "generate_email",
1210
- "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "custom_agent" }, "version": "v1" },
1211
- "inputs": {
1212
- "task_instructions": { "inline": { "wellKnown": { "textWithSources": { "text": "Generate an email with To, Subject, and Body fields.", "sources": [] }}}},
1213
- "output_fields": {
1214
- "inline": { "array": { "values": [
1215
- { "wellKnown": { "extractionColumn": { "id": "To", "name": "To", "dataType": 1 }}},
1216
- { "wellKnown": { "extractionColumn": { "id": "Subject", "name": "Subject", "dataType": 1 }}},
1217
- { "wellKnown": { "extractionColumn": { "id": "Body", "name": "Body", "dataType": 1 }}}
1218
- ]}}
1219
- }
1220
- }
1221
- }
1222
- \`\`\`
1223
-
1224
- Each output_field becomes a separate output (e.g., \`generate_email.To\`) that can wire directly to \`send_email_agent\`.
1225
-
1226
- ## Pattern C: fixed_response for Simple Template Formatting
1227
-
1228
- When you already have a clean extracted value and just need TEXT_WITH_SOURCES wrapping:
1229
-
1230
- \`\`\`json
1231
- {
1232
- "name": "fmt_email",
1233
- "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "fixed_response" }, "version": "v1" },
1234
- "inputs": {
1235
- "template": { "inline": { "wellKnown": { "textWithSources": { "text": "{{email_address}}", "sources": [] }}}},
1236
- "custom_data": { "actionOutput": { "actionName": "entity_extraction", "output": "extraction_result" }}
1237
- }
1238
- }
1239
- \`\`\`
1240
-
1241
- ## Common Mistakes
1242
-
1243
- | Mistake | Problem | Fix |
1244
- |---------|---------|-----|
1245
- | \`entity_extraction → email_to\` | Type mismatch (ANY → TEXT_WITH_SOURCES) | Add json_mapper + fixed_response |
1246
- | \`call_llm.response → email_to\` | Full LLM response as email address | Extract with json_mapper or use output_fields |
1247
- | \`json_mapper → email_to\` | json_mapper outputs JSON_VALUE, not TEXT_WITH_SOURCES | Add fixed_response between json_mapper and email |
1248
- | Missing fixed_response | Type still mismatched | fixed_response converts ANY → TEXT_WITH_SOURCES |
1249
-
1250
- ## See Also
1251
-
1252
- - \`ema://rules/anti-patterns\` — includes \`entity-extraction-direct-to-email\` anti-pattern
1253
- - \`ema://rules/extraction-column-format\` — for output_fields structure
1254
- `;
1255
- },
1256
- },
1257
- // ─────────────────────────────────────────────────────────────────────────────
1258
- // Workflow Requirements & Guidance
1259
- // NOT hardcoded templates - provide requirements and let LLM generate
1260
- // ─────────────────────────────────────────────────────────────────────────────
1261
- {
1262
- uri: "ema://templates/voice-ai/requirements",
1263
- name: "templates/voice-ai/requirements",
1264
- description: "Voice AI workflow requirements and guidance. Use workflow(mode='get') for schema, then generate workflow_def.",
1265
- mimeType: "application/json",
1266
- generate: async () => {
1267
- return JSON.stringify({
1268
- _note: "Requirements and guidance for Voice AI workflows. LLM generates workflow_def based on these.",
1269
- _usage: "1) workflow(mode='get', persona_id='...') for schema + fingerprint, " +
1270
- "2) Generate/modify workflow_def, " +
1271
- "3) workflow(mode='deploy', persona_id='...', base_fingerprint='<fingerprint>', workflow_def={...}) " +
1272
- "(or workflow_def_path='/path/to/wf.json' for large payloads)",
1273
- hard_requirements: {
1274
- workflow_output: {
1275
- rule: "MUST have results.WORKFLOW_OUTPUT mapped to final action output",
1276
- example: '{ "results": { "WORKFLOW_OUTPUT": { "actionName": "respond", "outputName": "response" } } }',
1277
- },
1278
- workflow_name: {
1279
- rule: "workflowName MUST be ['ema', 'personas', '<actual_persona_id>']",
1280
- },
1281
- trigger: {
1282
- rule: "Voice AI uses chat_trigger (type CHAT)",
1283
- namespace: ["triggers", "emainternal"],
1284
- },
1285
- response: {
1286
- rule: "Must produce a response output that wires to WORKFLOW_OUTPUT",
1287
- },
1288
- },
1289
- required_widgets: [
1290
- { name: "conversationSettings", type: 39, purpose: "Voice identity, welcome message, instructions" },
1291
- { name: "voiceSettings", type: 40, purpose: "Language, voice model" },
1292
- { name: "callSettings", type: 41, purpose: "Call forwarding, spam prevention" },
1293
- { name: "vadSettings", type: 42, purpose: "Voice activity detection, timeouts" },
1294
- ],
1295
- common_patterns: {
1296
- simple_qa: "chat_trigger → search → respond_for_external_actions → WORKFLOW_OUTPUT",
1297
- with_routing: "chat_trigger → chat_categorizer → [branch per intent] → respond → WORKFLOW_OUTPUT",
1298
- with_tools: "chat_trigger → categorizer → external_action_caller → respond_for_external_actions → WORKFLOW_OUTPUT",
1299
- },
1300
- best_practices: [
1301
- "Use search/v2 (NOT v0) with datastore_configs",
1302
- "Include Fallback category in every categorizer",
1303
- "Use respond_for_external_actions (NOT deprecated respond_with_sources)",
1304
- "For approval workflows: enable HITL flag on the agent (not a standalone general_hitl node)",
1305
- ],
1306
- _next_step: "Call workflow(mode='get', persona_id='...') to get full schema, then generate workflow_def",
1307
- }, null, 2);
1308
- },
1309
- },
1310
- {
1311
- uri: "ema://templates/voice-ai/config",
1312
- name: "templates/voice-ai/config",
1313
- description: "Voice AI configuration template (proto_config widgets). Customize values for your use case.",
1314
- mimeType: "application/json",
1315
- generate: async () => {
1316
- const config = {
1317
- _note: "Voice AI configuration template. Customize values for your use case.",
1318
- _usage: "persona(method='update', id='<ID>', config={widgets: [<these widgets with your values>]})",
1319
- widgets: [
1320
- {
1321
- name: "conversationSettings",
1322
- type: 39,
1323
- conversationSettings: {
1324
- ...VOICE_TEMPLATE_FALLBACK.conversationSettings,
1325
- },
1326
- },
1327
- {
1328
- name: "voiceSettings",
1329
- type: 40,
1330
- voiceSettings: {
1331
- ...VOICE_TEMPLATE_FALLBACK.voiceSettings,
1332
- },
1333
- },
1334
- {
1335
- name: "callSettings",
1336
- type: 41,
1337
- callSettings: {
1338
- ...VOICE_TEMPLATE_FALLBACK.callSettings,
1339
- },
1340
- },
1341
- {
1342
- name: "vadSettings",
1343
- type: 42,
1344
- vadSettings: {
1345
- ...VOICE_TEMPLATE_FALLBACK.vadSettings,
1346
- },
1347
- },
1348
- ],
1349
- field_docs: VOICE_TEMPLATE_FIELD_DOCS,
1350
- };
1351
- return JSON.stringify(config, null, 2);
1352
- },
1353
- },
1354
- {
1355
- uri: "ema://templates/voice-ai/guide",
1356
- name: "templates/voice-ai/guide",
1357
- description: "Voice AI creation guide with requirements and step-by-step process",
1358
- mimeType: "text/markdown",
1359
- generate: async () => {
1360
- return `# Voice AI Creation Guide
1361
-
1362
- ## Process (Follow This Order)
1363
-
1364
- ### 1. Create Persona
1365
- \`\`\`
1366
- persona(method="create", name="Your Voice AI", type="voice")
1367
- \`\`\`
1368
-
1369
- ### 2. Get Workflow Schema
1370
- \`\`\`
1371
- workflow(mode="get", persona_id="<ID>")
1372
- \`\`\`
1373
- This returns:
1374
- - Current workflow (if any)
1375
- - Deprecation warnings (fix these first!)
1376
- - Generation schema (agents, constraints)
1377
- - Requirements and guidance
1378
-
1379
- ### 3. Generate Workflow
1380
- Using the schema, generate a workflow_def that:
1381
- - Has WORKFLOW_OUTPUT in results
1382
- - Uses non-deprecated actions
1383
- - Follows the flow pattern: trigger → processing → response → OUTPUT
1384
-
1385
- ### 4. Deploy Workflow
1386
- \`\`\`
1387
- workflow(mode="deploy", persona_id="<ID>", base_fingerprint="<fingerprint>", workflow_def={...})
1388
- \n# For large payloads:\n# workflow(mode="deploy", persona_id="<ID>", base_fingerprint="<fingerprint>", workflow_def_path="/path/to/wf.json")
1389
- \`\`\`
1390
- Use \`workflow(mode="validate", ...)\` before deploy to catch structural/semantic issues.
1391
-
1392
- ### 5. Configure Settings
1393
- \`\`\`
1394
- persona(method="update", id="<ID>", config={widgets: [...]})
1395
- \`\`\`
1396
-
1397
- ### 6. Upload Knowledge
1398
- \`\`\`
1399
- persona(id="<ID>", data={method:"upload", path:"your-data.txt"})
1400
- \`\`\`
1401
-
1402
- ## Hard Requirements
1403
-
1404
- | Requirement | Why |
1405
- |-------------|-----|
1406
- | WORKFLOW_OUTPUT | Persona cannot be activated without it |
1407
- | workflowName format | API rejects invalid namespaces |
1408
- | Non-deprecated actions | Deprecated actions may fail |
1409
-
1410
- ## Check for Deprecated Actions
1411
-
1412
- \`workflow(mode="get")\` returns \`deprecation_warnings\` if any actions are deprecated.
1413
- **Fix these BEFORE deploying.**
1414
-
1415
- ## Common Deprecated Actions
1416
-
1417
- | Deprecated | Use Instead |
1418
- |------------|-------------|
1419
- | search/v0 | search/v2 (requires datastore_configs) |
1420
- | respond_with_sources | respond_for_external_actions |
1421
- | call_llm/v0 | call_llm/v2 |
1422
-
1423
- ## Anti-Patterns
1424
-
1425
- ❌ Cloning random existing persona
1426
- ❌ Skipping workflow_def
1427
- ❌ Using deprecated actions
1428
- ❌ Deploying without preview
1429
-
1430
- ${VOICE_TEMPLATE_FIELD_DOCS}
1431
- `;
1432
- },
1433
- },
1434
- // Persona Templates - Dynamic from API with fallback
1435
- {
1436
- uri: "ema://catalog/templates",
1437
- name: "catalog/templates",
1438
- description: "Persona templates from Ema API: pre-configured AI Employee templates (chatbot_starter, voicebot, dashboard, etc.)",
1439
- mimeType: "application/json",
1440
- generate: async (ctx) => {
1441
- const templates = await getDynamicPersonaTemplates({ env: ctx.env });
1442
- if (templates.length > 0) {
1443
- return JSON.stringify(templates.map(templateDtoToResource), null, 2);
1444
- }
1445
- // Fallback when API unavailable - provide guidance
1446
- return JSON.stringify({
1447
- _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.",
1448
- fallback_types: ["voice", "chat", "dashboard"],
1449
- how_to_get: "catalog(method='list', type='templates') or template(config='voice')",
1450
- }, null, 2);
1451
- },
1452
- },
1453
- {
1454
- uri: "ema://catalog/templates-summary",
1455
- name: "catalog/templates-summary",
1456
- description: "Persona templates summary: template names, categories, and descriptions for quick reference",
1457
- mimeType: "text/markdown",
1458
- generate: async (ctx) => {
1459
- const templates = await getDynamicPersonaTemplates({ env: ctx.env });
1460
- if (templates.length === 0) {
1461
- return `# Persona Templates
1462
-
1463
- > API templates not available. Use one of these alternatives:
1464
-
1465
- ## Option 1: Use the catalog tool directly
1466
- \`\`\`
1467
- catalog(method="list", type="templates")
1468
- \`\`\`
1469
- This connects to the API and may have different credentials.
1470
-
1471
- ## Option 2: Use config fallbacks
1472
- \`\`\`
1473
- template(config="voice") // Voice AI config template
1474
- template(config="chat") // Chat AI config template
1475
- template(config="dashboard") // Dashboard AI config template
1476
- \`\`\`
1477
-
1478
- ## Option 3: Create from scratch
1479
- \`\`\`
1480
- persona(method="create", name="My AI", type="voice|chat|dashboard")
1481
- \`\`\`
1482
- `;
1483
- }
1484
- const byCategory = new Map();
1485
- for (const t of templates) {
1486
- const cat = t.category || "GENERAL";
1487
- if (!byCategory.has(cat))
1488
- byCategory.set(cat, []);
1489
- byCategory.get(cat).push(t);
1490
- }
1491
- let md = "# Persona Templates (from API)\n\n";
1492
- md += `> ${templates.length} templates available for creating AI Employees\n\n`;
1493
- for (const [category, tpls] of byCategory) {
1494
- md += `## ${category}\n\n`;
1495
- md += "| Template | Type | Description |\n";
1496
- md += "|----------|------|-------------|\n";
1497
- for (const t of tpls) {
1498
- const triggerType = t.trigger_type || "-";
1499
- const desc = t.description?.slice(0, 80) || "-";
1500
- md += `| \`${t.name}\` | ${triggerType} | ${desc}... |\n`;
1501
- }
1502
- md += "\n";
1503
- }
1504
- return md;
1505
- },
1506
- },
1507
- // Action Schema - Complete action definitions from ema repo
1508
- {
1509
- uri: "ema://schema/actions",
1510
- name: "schema/actions",
1511
- description: "Complete action schema: all workflow actions with inputs, outputs, types, and documentation from ema_backend/grpc",
1512
- mimeType: "application/json",
1513
- generate: async (ctx) => {
1514
- const schema = await getDynamicActionSchema({ env: ctx.env });
1515
- return JSON.stringify(schema, null, 2);
1516
- },
1517
- },
1518
- {
1519
- uri: "ema://schema/actions-summary",
1520
- name: "schema/actions-summary",
1521
- description: "Action schema summary: action names, versions, and descriptions for quick reference",
1522
- mimeType: "text/markdown",
1523
- generate: async (ctx) => {
1524
- const schema = await getDynamicActionSchema({ env: ctx.env });
1525
- if (schema.actions.length === 0) {
1526
- return "# Action Schema\n\n> No actions loaded. Check API connectivity or bundled schema.\n";
1527
- }
1528
- let md = "# Action Schema\n\n";
1529
- md += `> ${schema.actions.length} actions loaded (source: ${schema.source})\n`;
1530
- md += `> Version: ${schema.version}\n\n`;
1531
- const byCategory = new Map();
1532
- for (const action of schema.actions) {
1533
- const cat = action.category || "OTHER";
1534
- if (!byCategory.has(cat))
1535
- byCategory.set(cat, []);
1536
- byCategory.get(cat).push(action);
1537
- }
1538
- for (const [category, actions] of byCategory) {
1539
- md += `## ${category.replace("ACTION_CATEGORY_", "")}\n\n`;
1540
- md += "| Action | Version | Description | Inputs | Outputs |\n";
1541
- md += "|--------|---------|-------------|--------|--------|\n";
1542
- for (const action of actions) {
1543
- const inputs = action.inputs?.size || 0;
1544
- const outputs = action.outputs?.size || 0;
1545
- const desc = action.description?.slice(0, 50) || "-";
1546
- md += `| \`${action.name}\` | ${action.version || "v0"} | ${desc}... | ${inputs} | ${outputs} |\n`;
1547
- }
1548
- md += "\n";
1549
- }
1550
- return md;
1551
- },
1552
- },
1553
- {
1554
- uri: "ema://schema/action/:name",
1555
- name: "schema/action",
1556
- description: "Individual action schema by name (e.g., ema://schema/action/call_llm)",
1557
- mimeType: "application/json",
1558
- generate: async (ctx) => {
1559
- // This is a template - actual resolution happens in getResource
1560
- return JSON.stringify({ error: "Use specific action name in URI" }, null, 2);
1561
- },
1562
- },
1563
- {
1564
- uri: "ema://schema/workflow-def",
1565
- name: "schema/workflow-def",
1566
- description: "Complete workflow_def schema with examples for Chat, Voice, and Dashboard personas. Essential reference for building workflows.",
1567
- mimeType: "text/markdown",
1568
- generate: async () => {
1569
- return `# Workflow Definition Schema
1570
-
1571
- This document describes the structure of \`workflow_def\` - the JSON format used to deploy AI Employee workflows.
1572
-
1573
- ## Two-Level Architecture
1574
-
1575
- 1. **WorkflowSpec** (High-level) - What YOU build:
1576
- - Nodes, inputs, categories, result mappings
1577
- - Human-readable, easy to reason about
1578
- - Gets compiled to workflow_def
1579
-
1580
- 2. **workflow_def** (Low-level) - What gets deployed:
1581
- - Compiled JSON structure
1582
- - Namespaces, action references, edges
1583
- - Generated by MCP from WorkflowSpec
1584
-
1585
- ## WorkflowSpec Structure
1586
-
1587
- \`\`\`typescript
1588
- interface WorkflowSpec {
1589
- name: string; // Workflow display name
1590
- description: string; // What it does
1591
- personaType: "voice" | "chat" | "dashboard";
1592
- nodes: Node[]; // Workflow actions
1593
- resultMappings: ResultMapping[]; // What outputs to return
1594
- }
1595
-
1596
- interface Node {
1597
- id: string; // Unique identifier (e.g., "trigger", "search", "respond")
1598
- actionType: string; // Action type (e.g., "chat_trigger", "search", "call_llm")
1599
- displayName: string; // Human-readable name
1600
- description?: string; // What this node does
1601
- inputs?: Record<string, InputBinding>; // Input configuration
1602
- categories?: Category[]; // For categorizers only
1603
- tools?: Tool[]; // For external action callers
1604
- runIf?: RunIfCondition; // Conditional execution
1605
- }
1606
-
1607
- interface InputBinding {
1608
- type: "action_output" | "literal" | "widget_config" | "llm_inferred";
1609
- actionName?: string; // Source node ID (for action_output)
1610
- output?: string; // Output name from source
1611
- value?: any; // Static value (for literal)
1612
- widgetName?: string; // Widget name (for widget_config)
1613
- }
1614
-
1615
- interface Category {
1616
- name: string; // Category name (e.g., "Sales", "Support", "Fallback")
1617
- description: string; // When to route here
1618
- examples?: string[]; // Example phrases
1619
- }
1620
-
1621
- interface ResultMapping {
1622
- nodeId: string; // Node that produces the output
1623
- output: string; // Output name
1624
- }
1625
- \`\`\`
1626
-
1627
- ## Trigger Types by Persona
1628
-
1629
- | Persona Type | Trigger ActionType | Key Outputs |
1630
- |--------------|-------------------|-------------|
1631
- | **Chat** | \`chat_trigger\` | user_query, chat_conversation |
1632
- | **Voice** | \`chat_trigger\` | user_query, chat_conversation |
1633
- | **Dashboard** | \`document_trigger\` | document_content, row_data |
1634
-
1635
- ## Example: Chat Persona (KB Search)
1636
-
1637
- \`\`\`json
1638
- {
1639
- "name": "FAQ Bot",
1640
- "description": "Answer questions from knowledge base",
1641
- "personaType": "chat",
1642
- "nodes": [
1643
- {
1644
- "id": "trigger",
1645
- "actionType": "chat_trigger",
1646
- "displayName": "Chat Input"
1647
- },
1648
- {
1649
- "id": "search",
1650
- "actionType": "search",
1651
- "displayName": "Search Knowledge Base",
1652
- "inputs": {
1653
- "query": {
1654
- "type": "action_output",
1655
- "actionName": "trigger",
1656
- "output": "user_query"
1657
- }
1658
- }
1659
- },
1660
- {
1661
- "id": "respond",
1662
- "actionType": "respond_for_external_actions",
1663
- "displayName": "Generate Response",
1664
- "inputs": {
1665
- "search_results": {
1666
- "type": "action_output",
1667
- "actionName": "search",
1668
- "output": "search_results"
1669
- },
1670
- "conversation": {
1671
- "type": "action_output",
1672
- "actionName": "trigger",
1673
- "output": "chat_conversation"
1674
- }
1675
- }
1676
- }
1677
- ],
1678
- "resultMappings": [
1679
- { "nodeId": "respond", "output": "response" }
1680
- ]
1681
- }
1682
- \`\`\`
1683
-
1684
- ## Example: Dashboard Persona (Document Extraction)
1685
-
1686
- \`\`\`json
1687
- {
1688
- "name": "Invoice Processor",
1689
- "description": "Extract data from invoices",
1690
- "personaType": "dashboard",
1691
- "nodes": [
1692
- {
1693
- "id": "trigger",
1694
- "actionType": "document_trigger",
1695
- "displayName": "Document Input"
1696
- },
1697
- {
1698
- "id": "extract",
1699
- "actionType": "entity_extraction",
1700
- "displayName": "Extract Invoice Data",
1701
- "inputs": {
1702
- "document": {
1703
- "type": "action_output",
1704
- "actionName": "trigger",
1705
- "output": "document_content"
1706
- },
1707
- "extraction_schema": {
1708
- "type": "literal",
1709
- "value": {
1710
- "fields": ["invoice_number", "amount", "date", "vendor"]
1711
- }
1712
- }
1713
- }
1714
- },
1715
- {
1716
- "id": "respond",
1717
- "actionType": "fixed_response",
1718
- "displayName": "Return Results",
1719
- "inputs": {
1720
- "data": {
1721
- "type": "action_output",
1722
- "actionName": "extract",
1723
- "output": "extracted_entities"
1724
- }
1725
- }
1726
- }
1727
- ],
1728
- "resultMappings": [
1729
- { "nodeId": "respond", "output": "response" }
1730
- ]
1731
- }
1732
- \`\`\`
1733
-
1734
- ## Example: Voice Persona (Intent Routing)
1735
-
1736
- \`\`\`json
1737
- {
1738
- "name": "IT Support Voice",
1739
- "description": "Handle IT support calls",
1740
- "personaType": "voice",
1741
- "nodes": [
1742
- {
1743
- "id": "trigger",
1744
- "actionType": "chat_trigger",
1745
- "displayName": "Voice Input"
1746
- },
1747
- {
1748
- "id": "categorize",
1749
- "actionType": "chat_categorizer",
1750
- "displayName": "Identify Intent",
1751
- "inputs": {
1752
- "conversation": {
1753
- "type": "action_output",
1754
- "actionName": "trigger",
1755
- "output": "chat_conversation"
1756
- }
1757
- },
1758
- "categories": [
1759
- { "name": "Password Reset", "description": "User needs password help" },
1760
- { "name": "Create Ticket", "description": "User wants to create a ticket" },
1761
- { "name": "Fallback", "description": "Anything else" }
1762
- ]
1763
- },
1764
- {
1765
- "id": "handle_password",
1766
- "actionType": "call_llm",
1767
- "displayName": "Password Reset Handler",
1768
- "runIf": {
1769
- "sourceAction": "categorize",
1770
- "sourceOutput": "category",
1771
- "operator": "eq",
1772
- "value": "Password Reset"
1773
- },
1774
- "inputs": {
1775
- "prompt": {
1776
- "type": "literal",
1777
- "value": "Guide user through password reset process..."
1778
- }
1779
- }
1780
- },
1781
- {
1782
- "id": "handle_ticket",
1783
- "actionType": "external_action_caller",
1784
- "displayName": "Create ServiceNow Ticket",
1785
- "runIf": {
1786
- "sourceAction": "categorize",
1787
- "sourceOutput": "category",
1788
- "operator": "eq",
1789
- "value": "Create Ticket"
1790
- },
1791
- "tools": [
1792
- { "name": "Create_Incident", "namespace": "service_now" }
1793
- ]
1794
- },
1795
- {
1796
- "id": "handle_fallback",
1797
- "actionType": "call_llm",
1798
- "displayName": "General Response",
1799
- "runIf": {
1800
- "sourceAction": "categorize",
1801
- "sourceOutput": "category",
1802
- "operator": "eq",
1803
- "value": "Fallback"
1804
- }
1805
- }
1806
- ],
1807
- "resultMappings": [
1808
- { "nodeId": "handle_password", "output": "response" },
1809
- { "nodeId": "handle_ticket", "output": "result" },
1810
- { "nodeId": "handle_fallback", "output": "response" }
1811
- ]
1812
- }
1813
- \`\`\`
1814
-
1815
- ## Critical Rules
1816
-
1817
- 1. **Always include Fallback** - Every categorizer MUST have a Fallback category
1818
- 2. **Trigger type matters** - Dashboard uses document_trigger, others use chat_trigger
1819
- 3. **Wire conversations correctly** - Categorizers need chat_conversation, search needs user_query
1820
- 4. **Map results** - At least one node must have a resultMapping to WORKFLOW_OUTPUT
1821
- 5. **Check deprecated actions** - Always verify against ema://rules/deprecated-actions-summary
1822
-
1823
- ## Common Input Bindings
1824
-
1825
- | Source | Type | Example |
1826
- |--------|------|---------|
1827
- | Previous node output | action_output | \`{ type: "action_output", actionName: "search", output: "results" }\` |
1828
- | Static value | literal | \`{ type: "literal", value: "Hello world" }\` |
1829
- | Widget config | widget_config | \`{ type: "widget_config", widgetName: "conversationSettings" }\` |
1830
- | LLM should infer | llm_inferred | \`{ type: "llm_inferred" }\` |
1831
-
1832
- ## Deployment Flow
1833
-
1834
- 1. Build your WorkflowSpec (structure above)
1835
- 2. Call \`workflow(mode="validate", workflow_spec={...})\` to check for errors
1836
- 3. Call \`workflow(mode="get", persona_id="...")\` to get the latest fingerprint (stale-state protection)
1837
- 4. Call \`workflow(mode="deploy", persona_id="...", base_fingerprint="<fingerprint>", workflow_def={...})\` to deploy
1838
-
1839
- For large payloads, use \`workflow_def_path\` instead of inline \`workflow_def\`.
1840
-
1841
- The MCP compiles your WorkflowSpec into the API's workflow_def format automatically.
1842
-
1843
- ## Raw workflow_def Input Binding Formats
1844
-
1845
- When deploying raw workflow_def (not WorkflowSpec), inputs use protobuf-compatible JSON.
1846
- These formats are CRITICAL - wrong formats cause HTTP 500 with no error details.
1847
-
1848
- ### Action Output (reference another node's output)
1849
- \`\`\`json
1850
- {
1851
- "actionOutput": { "actionName": "source_node_name", "output": "output_name" },
1852
- "autoDetectedBinding": false
1853
- }
1854
- \`\`\`
1855
-
1856
- ### Widget Config (reference a proto_config widget)
1857
- \`\`\`json
1858
- {
1859
- "widgetConfig": { "widgetName": "fusionModel" },
1860
- "autoDetectedBinding": true
1861
- }
1862
- \`\`\`
1863
-
1864
- ### Inline String (STRING type)
1865
- \`\`\`json
1866
- {
1867
- "inline": { "wellKnown": { "stringValue": "your text here" } },
1868
- "autoDetectedBinding": false
1869
- }
1870
- \`\`\`
1871
-
1872
- ### Inline Text With Sources (TEXT_WITH_SOURCES type)
1873
- \`\`\`json
1874
- {
1875
- "inline": {
1876
- "wellKnown": {
1877
- "textWithSources": {
1878
- "text": "your text here",
1879
- "sources": [],
1880
- "resultConfidence": 0,
1881
- "toolSources": [],
1882
- "resultType": 0
1883
- }
1884
- }
1885
- },
1886
- "autoDetectedBinding": false
1887
- }
1888
- \`\`\`
1889
- **WARNING**: STRING ≠ TEXT_WITH_SOURCES. Using stringValue where textWithSources is expected causes HTTP 500.
1890
-
1891
- ### Inline Enum Value
1892
- \`\`\`json
1893
- {
1894
- "inline": { "enumValue": "CategoryName" },
1895
- "autoDetectedBinding": false
66
+ ".env.development",
67
+ ".env.production",
68
+ ".env.test",
69
+ "secrets.yaml",
70
+ "secrets.json",
71
+ ".npmrc",
72
+ ".pypirc",
73
+ "id_rsa",
74
+ "id_ed25519",
75
+ "*.pem",
76
+ "*.key",
77
+ ];
78
+ function containsSecrets(content) {
79
+ return SECRET_PATTERNS.some((pattern) => pattern.test(content));
1896
80
  }
1897
- \`\`\`
1898
-
1899
- ### Named Inputs (multiBinding format - REQUIRED for named_inputs)
1900
- \`\`\`json
1901
- {
1902
- "multiBinding": {
1903
- "elements": [
1904
- {
1905
- "namedBinding": {
1906
- "name": "context_key",
1907
- "value": {
1908
- "actionOutput": { "actionName": "source_node", "output": "response_with_sources" },
1909
- "autoDetectedBinding": false
1910
- },
1911
- "description": "",
1912
- "isOptional": false
1913
- },
1914
- "autoDetectedBinding": false
1915
- }
1916
- ]
1917
- },
1918
- "autoDetectedBinding": false
81
+ function isBlockedFile(filename) {
82
+ const basename = path.basename(filename);
83
+ return BLOCKED_FILES.some((blocked) => {
84
+ if (blocked.startsWith("*")) {
85
+ return basename.endsWith(blocked.slice(1));
86
+ }
87
+ return basename === blocked;
88
+ });
1919
89
  }
1920
- \`\`\`
1921
-
1922
- ### Action Name Format
1923
- \`\`\`json
1924
- {
1925
- "action": {
1926
- "name": { "namespaces": ["actions", "emainternal"], "name": "call_llm" },
1927
- "version": "v2"
1928
- }
90
+ function hasPathTraversal(uriPath) {
91
+ return uriPath.includes("..") || uriPath.includes("//");
1929
92
  }
1930
- \`\`\`
1931
- **WARNING**: Empty namespaces [] causes deploy failure. Always use ["actions", "emainternal"].
1932
-
1933
- ### text_categorizer/v1 Requirements
1934
- - Uses \`named_inputs\` (multiBinding format), NOT \`text\` input from deprecated v0
1935
- - Requires \`typeArguments.categories\` pointing to an enumType
1936
- - \`categorization_instructions\` input type is TEXT_WITH_SOURCES (use textWithSources, NOT stringValue)
1937
- - Must define matching enumType in workflow_def.enumTypes[] with Fallback category
1938
- `;
1939
- },
1940
- },
1941
- {
1942
- uri: "ema://schema/workflow-def-json",
1943
- name: "schema/workflow-def-json",
1944
- description: "JSON Schema for workflow_def validation. Use for pre-validation before deployment.",
1945
- mimeType: "application/json",
1946
- generate: async () => {
1947
- const { getWorkflowDefSchemaJSON } = await import("./domain/workflow-def-schema.js");
1948
- return getWorkflowDefSchemaJSON();
1949
- },
1950
- },
1951
- // ─────────────────────────────────────────────────────────────────────────
1952
- // Guidance Resources - Single source of truth, multiple export formats
1953
- // ─────────────────────────────────────────────────────────────────────────
1954
- {
1955
- uri: "ema://docs/usage-guide",
1956
- name: "docs/usage-guide",
1957
- description: "Complete MCP usage guide with rules, patterns, and tool reference (markdown format)",
93
+ // ─────────────────────────────────────────────────────────────────────────────
94
+ // Resource Registry
95
+ // ─────────────────────────────────────────────────────────────────────────────
96
+ // Allowlisted resource mappings: uri -> relative file path
97
+ // Only includes files that ACTUALLY exist in this repo (not symlinked)
98
+ // and are useful for MCP consumers
99
+ const RESOURCE_MAP = {
100
+ // Core Documentation (public guides)
101
+ "ema://docs/getting-started": {
102
+ path: ".context/public/guides/mcp-tools-guide.md",
103
+ description: "Quick start guide: first steps, key concepts, essential rules, common operations",
1958
104
  mimeType: "text/markdown",
1959
- generate: async () => {
1960
- const { exportAsMarkdown } = await import("./guidance.js");
1961
- return exportAsMarkdown();
1962
- },
1963
105
  },
1964
- {
1965
- uri: "ema://guidance/rules",
1966
- name: "guidance/rules",
1967
- description: "Structured guidance rules (JSON) - for programmatic consumption by other services",
1968
- mimeType: "application/json",
1969
- generate: async () => {
1970
- const { exportAsJSON } = await import("./guidance.js");
1971
- return exportAsJSON();
1972
- },
106
+ "ema://docs/readme": {
107
+ path: "README.md",
108
+ description: "Ema Toolkit overview: MCP tools, prompts, resources, CLI usage, SDK API, configuration",
109
+ mimeType: "text/markdown",
1973
110
  },
1974
- {
1975
- uri: "ema://guidance/cursor-rule",
1976
- name: "guidance/cursor-rule",
1977
- description: "Guidance exported as Cursor .mdc rule format - for IDE integration",
111
+ "ema://docs/ema-user-guide": {
112
+ path: ".context/public/guides/ema-user-guide.md",
113
+ description: "Ema Platform guide: concepts (AI Employees, Workflows, Agents), glossary, architecture, examples, troubleshooting",
1978
114
  mimeType: "text/markdown",
1979
- generate: async () => {
1980
- const { exportAsCursorRule } = await import("./guidance.js");
1981
- return exportAsCursorRule();
1982
- },
1983
115
  },
1984
- {
1985
- uri: "ema://guidance/server-instructions",
1986
- name: "guidance/server-instructions",
1987
- description: "Server instructions (the content injected into MCP init response)",
1988
- mimeType: "text/plain",
1989
- generate: async () => {
1990
- const { generateServerInstructions } = await import("./guidance.js");
1991
- return generateServerInstructions();
1992
- },
116
+ "ema://docs/mcp-tools-guide": {
117
+ path: ".context/public/guides/mcp-tools-guide.md",
118
+ description: "MCP tools usage guide: tool categories, best practices, example call sequences, workflow review patterns",
119
+ mimeType: "text/markdown",
1993
120
  },
1994
- ];
1995
- // ─────────────────────────────────────────────────────────────────────────────
1996
- // Dynamic fetching helpers (Ema API as source of truth when available)
1997
- // ─────────────────────────────────────────────────────────────────────────────
1998
- function loadAnyConfig() {
1999
- return (loadConfigFromJsonEnv() ??
2000
- loadConfigOptional(process.env.EMA_AGENT_SYNC_CONFIG ?? "./config.yaml"));
2001
- }
2002
- function getDefaultEnvNameFromConfig(cfg) {
2003
- const fromEnv = process.env.EMA_ENV_NAME?.trim();
2004
- if (fromEnv)
2005
- return fromEnv;
2006
- const master = getMasterEnv(cfg);
2007
- return master?.name ?? cfg.environments[0]?.name ?? "demo";
2008
- }
2009
- const WK_ANY = "WELL_KNOWN_TYPE_ANY";
2010
- function actionDtoToAgentDefinition(a) {
2011
- // Handle inputs - can be array or object with inputs property
2012
- const inputsArray = Array.isArray(a.inputs) ? a.inputs : [];
2013
- const outputsArray = Array.isArray(a.outputs) ? a.outputs : [];
2014
- return {
2015
- actionName: a.name ?? a.id,
2016
- displayName: a.name ?? a.id,
2017
- category: (a.category ?? "other"),
2018
- description: a.description ?? "",
2019
- inputs: inputsArray.map((p) => ({
2020
- name: p.name ?? "input",
2021
- type: WK_ANY,
2022
- required: Boolean(p.required),
2023
- description: p.description ?? "",
2024
- })),
2025
- outputs: outputsArray.map((p) => ({
2026
- name: p.name ?? "output",
2027
- type: WK_ANY,
2028
- description: p.description ?? "",
2029
- })),
2030
- whenToUse: "",
2031
- };
2032
- }
2033
- const clientCache = new Map();
2034
- const agentCatalogCache = new Map();
2035
- function getClientForEnvName(envName) {
2036
- const cfg = loadAnyConfig();
2037
- if (!cfg)
2038
- return null;
2039
- const effectiveEnv = envName ?? getDefaultEnvNameFromConfig(cfg);
2040
- const cached = clientCache.get(effectiveEnv);
2041
- if (cached)
2042
- return cached;
2043
- const envCfg = getEnvByName(cfg, effectiveEnv) ?? getEnvByName(cfg, getDefaultEnvNameFromConfig(cfg));
2044
- if (!envCfg)
2045
- return null;
2046
- const env = {
2047
- name: envCfg.name,
2048
- baseUrl: envCfg.baseUrl,
2049
- bearerToken: resolveBearerToken(envCfg.bearerTokenEnv),
2050
- };
2051
- const client = new EmaClient(env);
2052
- clientCache.set(effectiveEnv, client);
2053
- return client;
2054
- }
2055
- async function getDynamicAgentCatalog(opts) {
2056
- const cacheKey = opts.env ?? "";
2057
- const now = Date.now();
2058
- const cached = agentCatalogCache.get(cacheKey);
2059
- if (cached && now - cached.ts < 60_000)
2060
- return cached.agents;
2061
- const client = getClientForEnvName(opts.env);
2062
- if (!client)
2063
- return AGENT_CATALOG;
2064
- try {
2065
- const actions = await client.listActions();
2066
- const agents = actions
2067
- .filter((a) => typeof a.name === "string" && a.name.trim().length > 0)
2068
- .map(actionDtoToAgentDefinition);
2069
- agentCatalogCache.set(cacheKey, { ts: now, agents });
2070
- return agents.length > 0 ? agents : AGENT_CATALOG;
2071
- }
2072
- catch {
2073
- return AGENT_CATALOG;
2074
- }
2075
- }
121
+ // Templates - REMOVED (file-backed templates removed in favor of API + generated fallbacks)
122
+ // Use: client.getPersonaTemplates() for live API templates
123
+ // Use: getTemplateFallback("voice") from sdk/generated/template-fallbacks.ts for offline fallbacks
124
+ // See: ema://catalog/persona-templates for dynamic API-backed template listing
125
+ // DEPRECATED: Use ema://docs/usage-guide (dynamic) instead
126
+ // This static resource is kept for backwards compatibility but now generates from guidance module
127
+ };
128
+ // Legacy alias - redirect to dynamic resource
129
+ const LEGACY_STATIC_REDIRECTS = {
130
+ "ema://rules/mcp-usage": "ema://docs/usage-guide",
131
+ };
2076
132
  // ─────────────────────────────────────────────────────────────────────────────
2077
- // Dynamic Persona Templates (API-first)
133
+ // Workspace Root
2078
134
  // ─────────────────────────────────────────────────────────────────────────────
2079
- const templateCatalogCache = new Map();
2080
- /**
2081
- * Fetch persona templates from API with caching.
2082
- * Falls back to empty array if API unavailable (file-backed templates still available via RESOURCE_MAP).
2083
- */
2084
- async function getDynamicPersonaTemplates(opts) {
2085
- const cacheKey = opts.env ?? "";
2086
- const now = Date.now();
2087
- const cached = templateCatalogCache.get(cacheKey);
2088
- if (cached && now - cached.ts < 60_000)
2089
- return cached.templates;
2090
- const client = getClientForEnvName(opts.env);
2091
- if (!client)
2092
- return [];
2093
- try {
2094
- const templates = await client.getPersonaTemplates();
2095
- templateCatalogCache.set(cacheKey, { ts: now, templates });
2096
- return templates;
2097
- }
2098
- catch {
2099
- return [];
2100
- }
2101
- }
2102
- const actionSchemaCache = new Map();
2103
- /**
2104
- * Get action schema with layered loading:
2105
- * 1. Try bundled schema (resources/action-schema.json)
2106
- * 2. Augment with live API data if available
2107
- */
2108
- async function getDynamicActionSchema(opts) {
2109
- const cacheKey = opts.env ?? "";
2110
- const now = Date.now();
2111
- const cached = actionSchemaCache.get(cacheKey);
2112
- if (cached && now - cached.ts < 60_000)
2113
- return cached.data;
2114
- const registry = new APISchemaRegistry();
2115
- // Try to load from bundled schema first (code source of truth)
2116
- const bundlePath = path.resolve(__dirname, "../../resources/action-schema.json");
2117
- try {
2118
- registry.loadFromBundle(bundlePath);
2119
- }
2120
- catch {
2121
- // Bundle not available - try API
2122
- const client = getClientForEnvName(opts.env);
2123
- if (client) {
2124
- try {
2125
- await registry.load(client);
2126
- }
2127
- catch {
2128
- // Neither bundle nor API available
2129
- }
2130
- }
2131
- }
2132
- const response = {
2133
- version: registry.metadata.version ?? "unknown",
2134
- source: registry.metadata.source ?? "bundle",
2135
- actions: registry.getAllActions(),
2136
- };
2137
- actionSchemaCache.set(cacheKey, { ts: now, data: response });
2138
- return response;
2139
- }
2140
- /**
2141
- * Get a specific action by name from the schema.
2142
- */
2143
- async function getActionByName(name, env) {
2144
- const schema = await getDynamicActionSchema({ env });
2145
- // Find latest version of action
2146
- const matching = schema.actions.filter(a => a.name === name);
2147
- if (matching.length === 0)
2148
- return null;
2149
- // Return latest version (sort by version desc)
2150
- return matching.sort((a, b) => (b.version || "v0").localeCompare(a.version || "v0"))[0];
2151
- }
2152
- /**
2153
- * Convert PersonaTemplateDTO to a simplified format for MCP resources.
2154
- */
2155
- function templateDtoToResource(t) {
2156
- return {
2157
- id: t.id,
2158
- name: t.name,
2159
- description: t.description,
2160
- category: t.category,
2161
- trigger_type: t.trigger_type,
2162
- has_project_template: t.has_project_template,
2163
- // Documentation fields for diagnosis/recommendations
2164
- about_template: t.about_template, // Rich description of capabilities
2165
- how_to_use: t.how_to_use, // Step-by-step usage instructions
2166
- value_prop: t.value_prop, // Value proposition sections
2167
- additional_content: t.additional_content, // Extra documentation
2168
- // Include proto_config for full template details
2169
- proto_config: t.proto_config,
2170
- // Include workflow_definition if available
2171
- workflow_definition: t.workflow_definition,
2172
- };
2173
- }
2174
135
  // Get workspace root - use centralized path resolution
2175
136
  // EMA_WORKSPACE_ROOT env var can override for testing
2176
137
  import { getToolkitRoot } from "../sdk/paths.js";
@@ -2180,6 +141,9 @@ function getWorkspaceRoot() {
2180
141
  }
2181
142
  return getToolkitRoot();
2182
143
  }
144
+ // ─────────────────────────────────────────────────────────────────────────────
145
+ // ResourceRegistry Class
146
+ // ─────────────────────────────────────────────────────────────────────────────
2183
147
  export class ResourceRegistry {
2184
148
  workspaceRoot;
2185
149
  constructor(workspaceRoot) {
@@ -2421,408 +385,8 @@ To read a resource, use the \`resources/read\` endpoint:
2421
385
  }
2422
386
  }
2423
387
  // ─────────────────────────────────────────────────────────────────────────────
2424
- // Validation Rules Generator
388
+ // Helpers
2425
389
  // ─────────────────────────────────────────────────────────────────────────────
2426
- /**
2427
- * Generate validation rules in LLM-friendly Markdown format.
2428
- * Includes rules from Go validator: required outputs, multiple writers, response/abstain, category hierarchy.
2429
- */
2430
- function generateValidationRulesForLLM() {
2431
- return `# Workflow Validation Rules
2432
-
2433
- > **Use these rules DURING workflow generation to avoid errors proactively.**
2434
-
2435
- These rules are extracted from the Go validator's static validation logic. Follow them when generating or modifying workflows.
2436
-
2437
- ## Rule 1: Required Output on All Paths
2438
-
2439
- **Rule ID**: \`required_output_all_paths\`
2440
- **Severity**: Critical
2441
- **Applies To**: Named results, execution paths
2442
-
2443
- ### Logic
2444
-
2445
- **Algorithm**: Enumerate all execution paths. For each path, check if all named results are produced.
2446
-
2447
- **Steps**:
2448
- 1. Enumerate all paths from trigger to completion
2449
- 2. For each path, collect all outputs produced
2450
- 3. For each named result, check if it's in path outputs
2451
- 4. If missing on any path → error
2452
-
2453
- **Complexity**: O(P * R) where P = paths, R = named results
2454
-
2455
- ### Rules
2456
-
2457
- **Constraint**: Every execution path must produce all named results.
2458
-
2459
- **Why**: Named results are contract guarantees. If a path doesn't produce a required output, the contract is violated.
2460
-
2461
- ### Examples
2462
-
2463
- **Bad Pattern**:
2464
- \`\`\`json
2465
- {
2466
- "path": ["trigger", "categorizer", "billing_branch", "search"],
2467
- "named_result": "response_with_sources",
2468
- "produces": [] // Missing response
2469
- }
2470
- \`\`\`
2471
-
2472
- **Why Bad**: Path ends without producing required output.
2473
-
2474
- **Good Pattern**:
2475
- \`\`\`json
2476
- {
2477
- "path": ["trigger", "categorizer", "billing_branch", "search", "respond_for_external_actions"],
2478
- "named_result": "response",
2479
- "produces": ["response"] // ✅ Produced
2480
- }
2481
- \`\`\`
2482
-
2483
- **Why Good**: All paths produce required output.
2484
-
2485
- ### Remediation
2486
-
2487
- **How to Fix**:
2488
- 1. Identify the path missing the output
2489
- 2. Add a node that produces the required output
2490
- 3. Connect the node's output to WORKFLOW_OUTPUT
2491
-
2492
- **Common Fixes**:
2493
- - Add \`respond_for_external_actions\` node after search
2494
- - Add \`call_llm\` node for text generation
2495
- - Add \`fixed_response\` for static responses
2496
-
2497
- **Prevention** (During Generation):
2498
- - Always ensure every categorizer branch has a response node
2499
- - Check that all paths end with nodes connected to WORKFLOW_OUTPUT
2500
- - Verify resultMappings include all required outputs
2501
-
2502
- ---
2503
-
2504
- ## Rule 2: Single Writer Per Output
2505
-
2506
- **Rule ID**: \`single_writer_per_output\`
2507
- **Severity**: Critical
2508
- **Applies To**: Named results, execution paths
2509
-
2510
- ### Logic
2511
-
2512
- **Algorithm**: For each named result, count how many nodes produce it on each path.
2513
-
2514
- **Steps**:
2515
- 1. For each named result
2516
- 2. For each execution path
2517
- 3. Count nodes that produce this result
2518
- 4. If count > 1 on any path → error
2519
-
2520
- ### Rules
2521
-
2522
- **Constraint**: A single named result can only have one producer per execution path.
2523
-
2524
- **Why**: Multiple producers on the same path cause ambiguity about which value to use.
2525
-
2526
- ### Examples
2527
-
2528
- **Bad Pattern**:
2529
- \`\`\`json
2530
- {
2531
- "path": ["trigger", "categorizer", "billing"],
2532
- "named_result": "response_with_sources",
2533
- "producers": ["respond_billing", "respond_general"] // ❌ Two producers
2534
- }
2535
- \`\`\`
2536
-
2537
- **Why Bad**: Two nodes produce the same output on the same path.
2538
-
2539
- **Good Pattern**:
2540
- \`\`\`json
2541
- {
2542
- "path": ["trigger", "categorizer", "billing"],
2543
- "named_result": "response_with_sources",
2544
- "producers": ["respond_billing"] // ✅ Single producer
2545
- }
2546
- \`\`\`
2547
-
2548
- **Why Good**: Only one node produces the output.
2549
-
2550
- ### Remediation
2551
-
2552
- **How to Fix**:
2553
- 1. Identify which nodes produce the same output
2554
- 2. Remove one producer or use different named results
2555
- 3. Ensure only one node produces each named result per path
2556
-
2557
- **Prevention** (During Generation):
2558
- - Use mutually exclusive runIf conditions to gate responders
2559
- - Ensure only one response node executes per category branch
2560
-
2561
- ---
2562
-
2563
- ## Rule 3: Response or Abstain Required
2564
-
2565
- **Rule ID**: \`response_or_abstain_required\`
2566
- **Severity**: Critical
2567
- **Applies To**: Execution paths
2568
-
2569
- ### Logic
2570
-
2571
- **Algorithm**: For each execution path, check if it produces a response or has an abstain reason.
2572
-
2573
- **Steps**:
2574
- 1. For each completed path
2575
- 2. Check if path produces any response output
2576
- 3. Check if path has abstain reason
2577
- 4. If neither → error
2578
-
2579
- ### Rules
2580
-
2581
- **Constraint**: Every execution path must either produce a user-visible response or explicitly abstain with a reason.
2582
-
2583
- **Why**: Users expect responses. Paths that don't respond and don't explain why create poor UX.
2584
-
2585
- ### Examples
2586
-
2587
- **Bad Pattern**:
2588
- \`\`\`json
2589
- {
2590
- "path": ["trigger", "categorizer", "billing", "search"],
2591
- "has_response": false,
2592
- "has_abstain": false // ❌ Neither
2593
- }
2594
- \`\`\`
2595
-
2596
- **Why Bad**: Path ends without response or abstain reason.
2597
-
2598
- **Good Pattern**:
2599
- \`\`\`json
2600
- {
2601
- "path": ["trigger", "categorizer", "billing", "search", "respond_for_external_actions"],
2602
- "has_response": true // ✅ Has response
2603
- }
2604
- \`\`\`
2605
-
2606
- **Why Good**: Path produces a response.
2607
-
2608
- ### Remediation
2609
-
2610
- **How to Fix**:
2611
- 1. Add a response node to the path, OR
2612
- 2. Add an abstain reason to the workflow configuration
2613
-
2614
- **Prevention** (During Generation):
2615
- - Always add response nodes to every categorizer branch
2616
- - If a path shouldn't respond, document why with abstain reason
2617
-
2618
- ---
2619
-
2620
- ## Rule 4: Category Hierarchy Valid
2621
-
2622
- **Rule ID**: \`category_hierarchy_valid\`
2623
- **Severity**: Critical
2624
- **Applies To**: Categorizers
2625
-
2626
- ### Logic
2627
-
2628
- **Algorithm**: Validate categorizer structure and category definitions.
2629
-
2630
- **Steps**:
2631
- 1. Check categorizer has Fallback category
2632
- 2. Check category hierarchy rules (TBD - need Go validator analysis)
2633
- 3. Validate category → handler mapping
2634
-
2635
- ### Rules
2636
-
2637
- **Constraint**: Categorizers must have proper structure and hierarchy.
2638
-
2639
- **Why**: Invalid category structure causes routing failures.
2640
-
2641
- ### Examples
2642
-
2643
- **Bad Pattern**:
2644
- \`\`\`json
2645
- {
2646
- "categorizer": {
2647
- "categories": ["Billing", "Technical"] // ❌ No Fallback
2648
- }
2649
- }
2650
- \`\`\`
2651
-
2652
- **Why Bad**: Missing Fallback category means unrecognized intents have no handler.
2653
-
2654
- **Good Pattern**:
2655
- \`\`\`json
2656
- {
2657
- "categorizer": {
2658
- "categories": ["Billing", "Technical", "Fallback"] // ✅ Has Fallback
2659
- }
2660
- }
2661
- \`\`\`
2662
-
2663
- **Why Good**: All intents have a handler.
2664
-
2665
- ### Remediation
2666
-
2667
- **How to Fix**:
2668
- 1. Add Fallback category to categorizer
2669
- 2. Ensure each category has a handler node
2670
- 3. Validate category hierarchy (TBD)
2671
-
2672
- **Prevention** (During Generation):
2673
- - Always include Fallback category
2674
- - Ensure every category has at least one handler node
2675
-
2676
- ---
2677
-
2678
- ## Self-Check Checklist
2679
-
2680
- After generating or modifying a workflow, verify:
2681
-
2682
- - [ ] All paths produce all named results
2683
- - [ ] No multiple writers on same path
2684
- - [ ] Every path has response or abstain
2685
- - [ ] Category hierarchy is valid
2686
- - [ ] Categorizer has Fallback category
2687
- - [ ] All categories have handler nodes
2688
-
2689
- ---
2690
-
2691
- ## Reference
2692
-
2693
- - **Source**: workflow-engine/pkg/engine/validator.go
2694
- - **Error Types**: RESULT_ERROR_TYPE_REQUIRED_OUTPUT_NOT_PRODUCED, RESULT_ERROR_TYPE_MULTIPLE_WRITERS, RESULT_ERROR_TYPE_MISSING_RESPONSE_OR_ABSTAIN_REASON, RESULT_ERROR_TYPE_CATEGORY_HIERARCHY_VIOLATION
2695
- - **Validation Tool**: Use \`workflow(mode="validate")\` to validate workflows
2696
- `;
2697
- }
2698
- /**
2699
- * Generate best practices checklist from SDK rules.
2700
- * SINGLE SOURCE OF TRUTH - all best practices come from:
2701
- * - ANTI_PATTERNS (validation-rules.ts)
2702
- * - STRUCTURAL_INVARIANTS (structural-rules.ts)
2703
- * - INPUT_SOURCE_RULES (validation-rules.ts)
2704
- * - OPTIMIZATION_RULES (validation-rules.ts)
2705
- */
2706
- function generateBestPracticesChecklist() {
2707
- // Group anti-patterns by severity
2708
- const criticalAntiPatterns = ANTI_PATTERNS.filter(ap => ap.severity === "critical");
2709
- const warningAntiPatterns = ANTI_PATTERNS.filter(ap => ap.severity === "warning");
2710
- const infoAntiPatterns = ANTI_PATTERNS.filter(ap => ap.severity === "info");
2711
- // Group structural invariants by severity
2712
- const criticalInvariants = STRUCTURAL_INVARIANTS.filter(inv => inv.severity === "critical");
2713
- const warningInvariants = STRUCTURAL_INVARIANTS.filter(inv => inv.severity === "warning");
2714
- return `# Workflow Best Practices Checklist
2715
-
2716
- > **Auto-generated from SDK rules** - This is the single source of truth.
2717
- > Check this BEFORE deploying any workflow.
2718
-
2719
- ## 🛑 CRITICAL (Must Fix Before Deploy)
2720
-
2721
- ### Anti-Patterns to Avoid
2722
-
2723
- ${criticalAntiPatterns.map(ap => `
2724
- #### ${ap.name}
2725
-
2726
- **Pattern**: ${ap.pattern}
2727
-
2728
- **Problem**: ${ap.problem}
2729
-
2730
- **Solution**: ${ap.solution}
2731
-
2732
- **Detection**: \`${ap.detection.condition}\`
2733
- `).join("\n---\n")}
2734
-
2735
- ### Structural Requirements
2736
-
2737
- ${criticalInvariants.map(inv => `
2738
- - **${inv.name}**: ${inv.rule}
2739
- - Violation: ${inv.violation}
2740
- - Fix: ${inv.fix}
2741
- `).join("")}
2742
-
2743
- ---
2744
-
2745
- ## ⚠️ WARNINGS (Should Fix)
2746
-
2747
- ### Anti-Patterns
2748
-
2749
- ${warningAntiPatterns.map(ap => `
2750
- - **${ap.name}**: ${ap.problem}
2751
- - Solution: ${ap.solution}
2752
- `).join("")}
2753
-
2754
- ### Structural Warnings
2755
-
2756
- ${warningInvariants.map(inv => `
2757
- - **${inv.name}**: ${inv.rule}
2758
- - Fix: ${inv.fix}
2759
- `).join("")}
2760
-
2761
- ---
2762
-
2763
- ## 📋 SUGGESTIONS (Good to Have)
2764
-
2765
- ${infoAntiPatterns.map(ap => `
2766
- - **${ap.name}**: ${ap.problem}
2767
- - Recommendation: ${ap.solution}
2768
- `).join("")}
2769
-
2770
- ---
2771
-
2772
- ## Input Source Rules
2773
-
2774
- | Action | Recommended Input | Avoid | Why |
2775
- |--------|-------------------|-------|-----|
2776
- ${INPUT_SOURCE_RULES.filter(r => r.severity === "critical").map(r => `| \`${r.actionPattern}\` | \`${r.recommended}\` | ${r.avoid.join(", ") || "-"} | ${r.reason.slice(0, 60)}... |`).join("\n")}
2777
-
2778
- ---
2779
-
2780
- ## Performance Optimizations
2781
-
2782
- ${OPTIMIZATION_RULES.map(opt => `
2783
- ### ${opt.name} (${opt.priority})
2784
-
2785
- - **Current State**: ${opt.currentState}
2786
- - **Recommendation**: ${opt.recommendation}
2787
- - **Benefit**: ${opt.benefit}
2788
- `).join("")}
2789
-
2790
- ---
2791
-
2792
- ## Quick Checklist
2793
-
2794
- Before deploying, verify:
2795
-
2796
- ### Critical
2797
- - [ ] Response nodes receive \`chat_conversation\` (prevents repeated questions)
2798
- - [ ] Email recipients from \`entity_extraction\` (not text outputs)
2799
- - [ ] Categorizers have Fallback category
2800
- - [ ] HITL has both success AND failure paths
2801
- - [ ] All paths reach WORKFLOW_OUTPUT
2802
- - [ ] No circular dependencies
2803
-
2804
- ### Recommended
2805
- - [ ] \`max_tokens\` set on call_llm nodes
2806
- - [ ] No orphan nodes (unused/disconnected)
2807
- - [ ] No redundant search nodes
2808
- - [ ] Search nodes have data sources uploaded
2809
-
2810
- ---
2811
-
2812
- ## Related Resources
2813
-
2814
- - \`ema://rules/anti-patterns\` - Full anti-pattern definitions (JSON)
2815
- - \`ema://rules/input-sources\` - Input source rules (JSON)
2816
- - \`ema://rules/extraction-column-format\` - extraction_columns API shape (entity_extraction nodes)
2817
- - \`ema://rules/json-output-patterns\` - custom_agent + json_mapper pattern
2818
- - \`ema://rules/chat-response-wiring\` - Chat response node wiring (avoid duplicate responses)
2819
- - \`ema://rules/email-input-wiring\` - Email input wiring (json_mapper/fixed_response patterns)
2820
- - \`ema://rules/structural-invariants\` - Structural rules (JSON)
2821
- - \`ema://rules/optimizations\` - Optimization rules (JSON)
2822
- - \`ema://validation/rules\` - Go validator rules reference
2823
- `;
2824
- }
2825
- // Helper to check if result is an error
2826
390
  export function isResourceError(result) {
2827
391
  return "code" in result;
2828
392
  }