@ema.co/mcp-toolkit 2026.2.19 → 2026.2.23

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

Potentially problematic release.


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

@@ -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,2325 +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 Node Reference - per-node I/O types, semantics, categorizer patterns
1259
- // Consolidated from .context/public/guides/workflow-builder-patterns.md
1260
- // ─────────────────────────────────────────────────────────────────────────────
1261
- {
1262
- uri: "ema://docs/workflow-node-reference",
1263
- name: "docs/workflow-node-reference",
1264
- description: "Complete workflow node reference: per-node I/O types, categorizer patterns (runIf/enumTypes), " +
1265
- "LLM named_inputs convention, temperature guidelines, data types between nodes",
1266
- mimeType: "text/markdown",
1267
- generate: async () => {
1268
- let md = `# Workflow Node Reference\n\n`;
1269
- md += `Practical reference for building Ema workflows. Covers per-node I/O semantics, `;
1270
- md += `categorizer configuration, LLM patterns, and data type compatibility.\n\n`;
1271
- md += `## Data Types Between Nodes\n\n`;
1272
- md += `| Type | Code Constant | Description | Example |\n`;
1273
- md += `|------|---------------|-------------|---------|\n`;
1274
- md += `| **Plain Text** | \`WELL_KNOWN_TYPE_TEXT_WITH_SOURCES\` | Text with optional citation metadata | User's message, LLM response |\n`;
1275
- md += `| **Conversation** | \`WELL_KNOWN_TYPE_CHAT_CONVERSATION\` | Structured message history (role + content) | Full chat thread |\n`;
1276
- md += `| **Search Results** | \`WELL_KNOWN_TYPE_SEARCH_RESULT\` | Retrieved document chunks with citations | KB search results |\n`;
1277
- md += `| **Enum** | \`WELL_KNOWN_TYPE_ENUM\` | Category/classification signal for routing | \`category::Schedule Appointment\` |\n`;
1278
- md += `| **Document** | \`WELL_KNOWN_TYPE_DOCUMENT\` | Uploaded file content | PDF, DOCX for extraction |\n`;
1279
- md += `| **Any** | \`WELL_KNOWN_TYPE_ANY\` | Untyped — needs intermediary for type-safe wiring | entity_extraction output |\n\n`;
1280
- md += `**Critical**: Types are NOT interchangeable. \`CHAT_CONVERSATION\` into a \`TEXT_WITH_SOURCES\` input causes type mismatch errors. Use converter nodes when needed.\n\n`;
1281
- md += `---\n\n`;
1282
- md += `## Input Semantics by Context\n\n`;
1283
- md += `The same input name means different things depending on the node:\n\n`;
1284
- md += `| Input Name | In Search Nodes | In Respond Nodes | In Extract Nodes | In Categorizers |\n`;
1285
- md += `|------------|-----------------|-------------------|-------------------|------------------|\n`;
1286
- md += `| \`query\` | Search term to look up | User's question to answer | Source text to analyze | N/A |\n`;
1287
- md += `| \`conversation\` | N/A | N/A (use named_inputs) | N/A | Full history for classification |\n`;
1288
- md += `| \`trigger_when\` | N/A | "Should I run?" | "Should I run?" | N/A |\n`;
1289
- md += `| \`named_inputs_*\` | N/A | Additional context | Additional context | N/A |\n\n`;
1290
- md += `**Mental model:** \`query\` = "What to process?" · \`conversation\` = "Full context" · \`trigger_when\` = "Should I run?" · \`named_inputs_*\` = "Extra context"\n\n`;
1291
- md += `---\n\n`;
1292
- md += `## Node Type Reference\n\n`;
1293
- md += `### chat_trigger\n`;
1294
- md += `Entry point for chat/voice workflows.\n`;
1295
- md += `- **Inputs**: None (system event)\n`;
1296
- md += `- **Outputs**: \`user_query\` (TEXT_WITH_SOURCES), \`chat_conversation\` (CHAT_CONVERSATION)\n`;
1297
- md += `- **Pairs with**: Intent routers, search nodes, LLM nodes\n\n`;
1298
- md += `### document_trigger\n`;
1299
- md += `Entry point for dashboard workflows (file upload per row).\n`;
1300
- md += `- **Inputs**: None (system event — triggered when row created/file uploaded)\n`;
1301
- md += `- **Outputs**: \`document_content\` (DOCUMENT), \`row_data\` (ANY — column values)\n`;
1302
- md += `- **Pairs with**: entity_extraction_with_documents, call_llm, search\n`;
1303
- md += `- **Critical**: Dashboard personas only. Each row triggers one workflow execution.\n\n`;
1304
- md += `### chat_categorizer\n`;
1305
- md += `Classify conversation into intent categories for routing.\n`;
1306
- md += `- **Inputs**: \`conversation\` (CHAT_CONVERSATION) — must be full history, NOT user_query\n`;
1307
- md += `- **Outputs**: \`category\` (ENUM) — one per configured category\n`;
1308
- md += `- **Critical**: Always include Fallback category. Every category needs a handler. Must have \`typeArguments.categories\` pointing to enumType.\n\n`;
1309
- md += `### text_categorizer/v1\n`;
1310
- md += `Classify text content (not conversation) for routing.\n`;
1311
- md += `- **Inputs**: \`named_inputs\` (multiBinding)\n`;
1312
- md += `- **Outputs**: \`category\` (ENUM)\n`;
1313
- md += `- **Critical**: \`text_categorizer/v0\` is deprecated — use v1 with \`named_inputs\`.\n\n`;
1314
- md += `### search/v2\n`;
1315
- md += `Retrieve relevant documents from uploaded knowledge base.\n`;
1316
- md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES), \`datastore_configs\` (from widget)\n`;
1317
- md += `- **Outputs**: \`search_results\` (SEARCH_RESULT)\n`;
1318
- md += `- **Critical**: NOT an LLM node — do NOT include \`model_config\`. Data must be uploaded first. \`search/v0\` is deprecated.\n\n`;
1319
- md += `### conversation_to_search_query\n`;
1320
- md += `Convert multi-turn conversation to a search-optimized query.\n`;
1321
- md += `- **Inputs**: \`conversation\` (CHAT_CONVERSATION)\n`;
1322
- md += `- **Outputs**: \`summarized_conversation\` (TEXT_WITH_SOURCES)\n`;
1323
- md += `- **Critical**: Required for multi-turn chat search. Direct CHAT_CONVERSATION → search causes type mismatch.\n\n`;
1324
- md += `### call_llm/v2\n`;
1325
- md += `Generate natural language response using LLM.\n`;
1326
- md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES), \`named_inputs_*\` (ANY — flexible), \`trigger_when\` (ENUM)\n`;
1327
- md += `- **Outputs**: \`response_with_sources\` (TEXT_WITH_SOURCES)\n`;
1328
- md += `- **Critical**: Must wire to WORKFLOW_OUTPUT or response is lost. \`call_llm/v0\` is deprecated.\n\n`;
1329
- md += `### respond_for_external_actions\n`;
1330
- md += `Generate conversation-aware response from search results or tool outputs.\n`;
1331
- md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES), \`conversation\` (CHAT_CONVERSATION), \`external_action_result\` (TEXT_WITH_SOURCES/SEARCH_RESULT)\n`;
1332
- md += `- **Outputs**: \`response\` (TEXT_WITH_SOURCES)\n`;
1333
- md += `- **Critical**: Replaces deprecated \`respond_with_sources/v0\`. Has built-in citation handling and conversation awareness.\n\n`;
1334
- md += `### fixed_response/v1\n`;
1335
- md += `Return a static predefined message.\n`;
1336
- md += `- **Inputs**: \`trigger_when\` (ENUM), \`named_inputs_*\` for template variables\n`;
1337
- md += `- **Outputs**: \`fixed_response_with_sources\` (TEXT_WITH_SOURCES)\n`;
1338
- md += `- **Pairs with**: Categorizers (as fallback), type conversion with \`{{variables}}\` templates, send_email_agent\n\n`;
1339
- md += `### external_action_caller\n`;
1340
- md += `Call external APIs/tools (ServiceNow, Salesforce, calendars).\n`;
1341
- md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES), \`conversation\` (CHAT_CONVERSATION), tool configuration\n`;
1342
- md += `- **Outputs**: \`tool_execution_result\` (TEXT_WITH_SOURCES)\n`;
1343
- md += `- **Pairs with**: Entity extractors (for parameters), respond nodes (for result formatting)\n\n`;
1344
- md += `### entity_extraction_with_documents\n`;
1345
- md += `Extract structured entities grounded in provided documents.\n`;
1346
- md += `- **Inputs**: \`documents\` (DOCUMENT), extraction column config\n`;
1347
- md += `- **Outputs**: \`extraction_columns\` (ANY — structured extraction results)\n`;
1348
- md += `- **Critical**: Output type is ANY — needs intermediary (json_mapper + fixed_response) before send_email inputs.\n`;
1349
- md += `- See \`ema://rules/extraction-column-format\` for column schema.\n\n`;
1350
- md += `### send_email_agent\n`;
1351
- md += `Send email with specified recipient, subject, and body.\n`;
1352
- md += `- **Inputs**: \`email_to\` (TEXT_WITH_SOURCES), \`email_subject\` (ANY), \`email_body\` (TEXT_WITH_SOURCES)\n`;
1353
- md += `- **Outputs**: \`send_status\` (ANY)\n`;
1354
- md += `- **Critical**: Inputs must be TEXT_WITH_SOURCES — use intermediary chain from ANY-typed sources.\n`;
1355
- md += `- See \`ema://rules/email-input-wiring\` for wiring patterns.\n\n`;
1356
- md += `### json_mapper\n`;
1357
- md += `Extract specific fields from JSON/structured data into individual outputs.\n`;
1358
- md += `- **Inputs**: \`input_json\` (ANY), mapping rules\n`;
1359
- md += `- **Outputs**: \`output_json\` per mapped field\n`;
1360
- md += `- **Pairs with**: entity_extraction (field extraction), fixed_response (type conversion)\n\n`;
1361
- md += `### rule_validation_with_documents\n`;
1362
- md += `Check extracted data against business rules (compliance, thresholds).\n`;
1363
- md += `- **Inputs**: \`primary_docs\` (DOCUMENT), \`map_of_extracted_columns\` (ANY)\n`;
1364
- md += `- **Outputs**: \`ruleset_output\` (ANY — validation results)\n`;
1365
- md += `- **Critical**: Rules configured in UI settings panel, not via workflow_def inputs.\n\n`;
1366
- md += `### live_web_search\n`;
1367
- md += `Real-time web search for current information.\n`;
1368
- md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES)\n`;
1369
- md += `- **Outputs**: \`web_search_results\` (SEARCH_RESULT)\n`;
1370
- md += `- **Critical**: No data upload needed (searches the web).\n\n`;
1371
- md += `### combine_search_results\n`;
1372
- md += `Merge results from multiple search sources with deduplication.\n`;
1373
- md += `- **Inputs**: \`search_results_1\` (SEARCH_RESULT), \`search_results_2\` (SEARCH_RESULT)\n`;
1374
- md += `- **Outputs**: \`combined_results\` (SEARCH_RESULT)\n`;
1375
- md += `- **Pairs with**: search/v2 + live_web_search, or any two search sources\n`;
1376
- md += `- **Note**: \`combine_search_results/v0\` is deprecated. For new workflows, prefer \`call_llm\` with multiple \`named_inputs\` to combine search results.\n\n`;
1377
- md += `### response_validator\n`;
1378
- md += `Validate LLM output against quality/compliance criteria.\n`;
1379
- md += `- **Inputs**: \`reference_query\` (TEXT_WITH_SOURCES), \`response_to_validate\` (TEXT_WITH_SOURCES)\n`;
1380
- md += `- **Outputs**: \`abstain_reason\` (TEXT_WITH_SOURCES — reason for rejection, empty if valid)\n\n`;
1381
- md += `### abstain_action\n`;
1382
- md += `Provide a safe decline response when AI should not answer.\n`;
1383
- md += `- **Inputs**: \`abstain_reason\` (TEXT_WITH_SOURCES)\n`;
1384
- md += `- **Outputs**: \`abstain_reason\` (TEXT_WITH_SOURCES — decline message)\n\n`;
1385
- md += `---\n\n`;
1386
- md += `## Categorizer Configuration\n\n`;
1387
- md += `### Defining enumTypes\n\n`;
1388
- md += `Categories must be defined in \`workflow_def.enumTypes\`:\n\n`;
1389
- md += `\`\`\`json\n`;
1390
- md += `"enumTypes": [{\n`;
1391
- md += ` "name": { "name": "intent_categories", "namespaces": [] },\n`;
1392
- md += ` "options": [\n`;
1393
- md += ` { "name": "Sales", "description": "Sales inquiries" },\n`;
1394
- md += ` { "name": "Support", "description": "Technical support" },\n`;
1395
- md += ` { "name": "General", "description": "General questions" },\n`;
1396
- md += ` { "name": "Fallback", "description": "Unclear or unmatched intents" }\n`;
1397
- md += ` ]\n`;
1398
- md += `}]\n`;
1399
- md += `\`\`\`\n\n`;
1400
- md += `### typeArguments\n\n`;
1401
- md += `The categorizer's \`typeArguments\` must reference the enum:\n\n`;
1402
- md += `\`\`\`json\n`;
1403
- md += `"typeArguments": {\n`;
1404
- md += ` "categories": {\n`;
1405
- md += ` "enumType": { "name": { "name": "intent_categories", "namespaces": [] } },\n`;
1406
- md += ` "isList": false\n`;
1407
- md += ` }\n`;
1408
- md += `}\n`;
1409
- md += `\`\`\`\n\n`;
1410
- md += `**Critical**: Empty \`typeArguments\` causes deploy failure.\n\n`;
1411
- md += `### runIf Condition Format\n\n`;
1412
- md += `\`\`\`json\n`;
1413
- md += `{\n`;
1414
- md += ` "lhs": {\n`;
1415
- md += ` "actionOutput": { "actionName": "chat_categorizer", "output": "category" },\n`;
1416
- md += ` "autoDetectedBinding": false\n`;
1417
- md += ` },\n`;
1418
- md += ` "operator": 1,\n`;
1419
- md += ` "rhs": {\n`;
1420
- md += ` "inline": { "enumValue": "Market_Impact" },\n`;
1421
- md += ` "autoDetectedBinding": false\n`;
1422
- md += ` }\n`;
1423
- md += `}\n`;
1424
- md += `\`\`\`\n\n`;
1425
- md += `| Operator | Meaning | Use Case |\n`;
1426
- md += `|----------|---------|----------|\n`;
1427
- md += `| \`1\` | Equals (\`==\`) | Route to handler when category matches |\n`;
1428
- md += `| \`2\` | Not equals (\`!=\`) | Run for all categories except one |\n\n`;
1429
- md += `For OR conditions (run for Sales OR General), use \`operator: 2\` with Fallback: \`category != Fallback\`.\n\n`;
1430
- md += `### Nested Categorizers\n\n`;
1431
- md += `For complex routing, chain categorizers:\n`;
1432
- md += `\`\`\`\n`;
1433
- md += `chat_categorizer (Level 1: HR, IT, General, Fallback)\n`;
1434
- md += ` └─→ text_categorizer (Level 2 for HR: Benefits, Leave, Payroll, Fallback)\n`;
1435
- md += `\`\`\`\n\n`;
1436
- md += `---\n\n`;
1437
- md += `## LLM Configuration (call_llm)\n\n`;
1438
- md += `### named_inputs Convention\n\n`;
1439
- md += `\`call_llm\` accepts additional context via suffix pattern \`named_inputs_<Descriptive_Name>\`:\n\n`;
1440
- md += `| Named Input | Type | Purpose |\n`;
1441
- md += `|-------------|------|----------|\n`;
1442
- md += `| \`named_inputs_Search_Results\` | SEARCH_RESULT | KB search results for RAG |\n`;
1443
- md += `| \`named_inputs_Conversation\` | CHAT_CONVERSATION | Full conversation history |\n`;
1444
- md += `| \`named_inputs_Intent\` | ENUM | Detected category from categorizer |\n`;
1445
- md += `| \`named_inputs_Current_Message\` | TEXT_WITH_SOURCES | Current user message |\n`;
1446
- md += `| \`named_inputs_Tool_Result\` | TEXT_WITH_SOURCES | External action output |\n\n`;
1447
- md += `\`named_inputs\` accepts **ANY** type — this is how you pass CHAT_CONVERSATION and SEARCH_RESULT into LLM nodes.\n\n`;
1448
- md += `### Temperature Guidelines\n\n`;
1449
- md += `| Use Case | Temperature | Why |\n`;
1450
- md += `|----------|-------------|-----|\n`;
1451
- md += `| Entity extraction | 0.0-0.3 | Accuracy over creativity |\n`;
1452
- md += `| Document generation | 0.3-0.5 | Consistent formatting |\n`;
1453
- md += `| General Q&A / chat | 0.5-0.7 | Balanced creativity and accuracy |\n`;
1454
- md += `| Creative writing | 0.7-1.0 | More varied output |\n\n`;
1455
- md += `### Consolidation Rule\n\n`;
1456
- md += `If multiple \`call_llm\` nodes share the same inputs and differ only by \`trigger_when\` gate, consolidate them:\n`;
1457
- md += `1. Create one \`call_llm\` that runs when not Fallback\n`;
1458
- md += `2. Wire \`categorizer.category → call_llm.named_inputs_Intent\`\n`;
1459
- md += `3. Update prompt: "Based on the detected intent ({{Intent}}), respond accordingly"\n`;
1460
- md += `4. Keep separate nodes only when intents require different tools, search sources, or safety constraints.\n\n`;
1461
- md += `---\n\n`;
1462
- md += `## See Also\n\n`;
1463
- md += `- \`ema://rules/input-sources\` — Input source validation rules\n`;
1464
- md += `- \`ema://rules/extraction-column-format\` — Entity extraction column schema\n`;
1465
- md += `- \`ema://rules/email-input-wiring\` — Email field wiring patterns\n`;
1466
- md += `- \`ema://rules/json-output-patterns\` — custom_agent + json_mapper patterns\n`;
1467
- md += `- \`ema://rules/chat-response-wiring\` — Chat response wiring rules\n`;
1468
- md += `- \`ema://rules/anti-patterns\` — Common workflow anti-patterns\n`;
1469
- return md;
1470
- },
1471
- },
1472
- // ─────────────────────────────────────────────────────────────────────────────
1473
- // Workflow Requirements & Guidance
1474
- // NOT hardcoded templates - provide requirements and let LLM generate
1475
- // ─────────────────────────────────────────────────────────────────────────────
1476
- {
1477
- uri: "ema://templates/voice-ai/requirements",
1478
- name: "templates/voice-ai/requirements",
1479
- description: "Voice AI workflow requirements and guidance. Use workflow(mode='get') for schema, then generate workflow_def.",
1480
- mimeType: "application/json",
1481
- generate: async () => {
1482
- return JSON.stringify({
1483
- _note: "Requirements and guidance for Voice AI workflows. LLM generates workflow_def based on these.",
1484
- _usage: "1) workflow(mode='get', persona_id='...') for schema + fingerprint, " +
1485
- "2) Generate/modify workflow_def, " +
1486
- "3) workflow(mode='deploy', persona_id='...', base_fingerprint='<fingerprint>', workflow_def={...}) " +
1487
- "(or workflow_def_path='/path/to/wf.json' for large payloads)",
1488
- hard_requirements: {
1489
- workflow_output: {
1490
- rule: "MUST have results.WORKFLOW_OUTPUT mapped to final action output",
1491
- example: '{ "results": { "WORKFLOW_OUTPUT": { "actionName": "respond", "outputName": "response" } } }',
1492
- },
1493
- workflow_name: {
1494
- rule: "workflowName MUST be ['ema', 'personas', '<actual_persona_id>']",
1495
- },
1496
- trigger: {
1497
- rule: "Voice AI uses chat_trigger (type CHAT)",
1498
- namespace: ["triggers", "emainternal"],
1499
- },
1500
- response: {
1501
- rule: "Must produce a response output that wires to WORKFLOW_OUTPUT",
1502
- },
1503
- },
1504
- required_widgets: [
1505
- { name: "conversationSettings", type: 39, purpose: "Voice identity, welcome message, instructions" },
1506
- { name: "voiceSettings", type: 40, purpose: "Language, voice model" },
1507
- { name: "callSettings", type: 41, purpose: "Call forwarding, spam prevention" },
1508
- { name: "vadSettings", type: 42, purpose: "Voice activity detection, timeouts" },
1509
- ],
1510
- common_patterns: {
1511
- simple_qa: "chat_trigger → search → respond_for_external_actions → WORKFLOW_OUTPUT",
1512
- with_routing: "chat_trigger → chat_categorizer → [branch per intent] → respond → WORKFLOW_OUTPUT",
1513
- with_tools: "chat_trigger → categorizer → external_action_caller → respond_for_external_actions → WORKFLOW_OUTPUT",
1514
- },
1515
- best_practices: [
1516
- "Use search/v2 (NOT v0) with datastore_configs",
1517
- "Include Fallback category in every categorizer",
1518
- "Use respond_for_external_actions (NOT deprecated respond_with_sources)",
1519
- "For approval workflows: enable HITL flag on send_email_agent or entity_extraction_with_documents (only nodes that support HITL). general_hitl is NOT deployable.",
1520
- ],
1521
- _next_step: "Call workflow(mode='get', persona_id='...') to get full schema, then generate workflow_def",
1522
- }, null, 2);
1523
- },
1524
- },
1525
- {
1526
- uri: "ema://templates/voice-ai/config",
1527
- name: "templates/voice-ai/config",
1528
- description: "Voice AI configuration template (proto_config widgets). Customize values for your use case.",
1529
- mimeType: "application/json",
1530
- generate: async () => {
1531
- const config = {
1532
- _note: "Voice AI configuration template. Customize values for your use case.",
1533
- _usage: "persona(method='update', id='<ID>', config={widgets: [<these widgets with your values>]})",
1534
- widgets: [
1535
- {
1536
- name: "conversationSettings",
1537
- type: 39,
1538
- conversationSettings: {
1539
- ...VOICE_TEMPLATE_FALLBACK.conversationSettings,
1540
- },
1541
- },
1542
- {
1543
- name: "voiceSettings",
1544
- type: 40,
1545
- voiceSettings: {
1546
- ...VOICE_TEMPLATE_FALLBACK.voiceSettings,
1547
- },
1548
- },
1549
- {
1550
- name: "callSettings",
1551
- type: 41,
1552
- callSettings: {
1553
- ...VOICE_TEMPLATE_FALLBACK.callSettings,
1554
- },
1555
- },
1556
- {
1557
- name: "vadSettings",
1558
- type: 42,
1559
- vadSettings: {
1560
- ...VOICE_TEMPLATE_FALLBACK.vadSettings,
1561
- },
1562
- },
1563
- ],
1564
- field_docs: VOICE_TEMPLATE_FIELD_DOCS,
1565
- };
1566
- return JSON.stringify(config, null, 2);
1567
- },
1568
- },
1569
- {
1570
- uri: "ema://templates/voice-ai/guide",
1571
- name: "templates/voice-ai/guide",
1572
- description: "Voice AI creation guide with requirements and step-by-step process",
1573
- mimeType: "text/markdown",
1574
- generate: async () => {
1575
- return `# Voice AI Creation Guide
1576
-
1577
- ## Process (Follow This Order)
1578
-
1579
- ### 1. Create Persona
1580
- \`\`\`
1581
- persona(method="create", name="Your Voice AI", type="voice")
1582
- \`\`\`
1583
-
1584
- ### 2. Get Workflow Schema
1585
- \`\`\`
1586
- workflow(mode="get", persona_id="<ID>")
1587
- \`\`\`
1588
- This returns:
1589
- - Current workflow (if any)
1590
- - Deprecation warnings (fix these first!)
1591
- - Generation schema (agents, constraints)
1592
- - Requirements and guidance
1593
-
1594
- ### 3. Generate Workflow
1595
- Using the schema, generate a workflow_def that:
1596
- - Has WORKFLOW_OUTPUT in results
1597
- - Uses non-deprecated actions
1598
- - Follows the flow pattern: trigger → processing → response → OUTPUT
1599
-
1600
- ### 4. Deploy Workflow
1601
- \`\`\`
1602
- workflow(mode="deploy", persona_id="<ID>", base_fingerprint="<fingerprint>", workflow_def={...})
1603
- \n# For large payloads:\n# workflow(mode="deploy", persona_id="<ID>", base_fingerprint="<fingerprint>", workflow_def_path="/path/to/wf.json")
1604
- \`\`\`
1605
- Use \`workflow(mode="validate", ...)\` before deploy to catch structural/semantic issues.
1606
-
1607
- ### 5. Configure Settings
1608
- \`\`\`
1609
- persona(method="update", id="<ID>", config={widgets: [...]})
1610
- \`\`\`
1611
-
1612
- ### 6. Upload Knowledge
1613
- \`\`\`
1614
- persona(id="<ID>", data={method:"upload", path:"your-data.txt"})
1615
- \`\`\`
1616
-
1617
- ## Hard Requirements
1618
-
1619
- | Requirement | Why |
1620
- |-------------|-----|
1621
- | WORKFLOW_OUTPUT | Persona cannot be activated without it |
1622
- | workflowName format | API rejects invalid namespaces |
1623
- | Non-deprecated actions | Deprecated actions may fail |
1624
-
1625
- ## Check for Deprecated Actions
1626
-
1627
- \`workflow(mode="get")\` returns \`deprecation_warnings\` if any actions are deprecated.
1628
- **Fix these BEFORE deploying.**
1629
-
1630
- ## Common Deprecated Actions
1631
-
1632
- | Deprecated | Use Instead |
1633
- |------------|-------------|
1634
- | search/v0 | search/v2 (requires datastore_configs) |
1635
- | respond_with_sources | respond_for_external_actions |
1636
- | call_llm/v0 | call_llm/v2 |
1637
-
1638
- ## Anti-Patterns
1639
-
1640
- ❌ Cloning random existing persona
1641
- ❌ Skipping workflow_def
1642
- ❌ Using deprecated actions
1643
- ❌ Deploying without preview
1644
-
1645
- ${VOICE_TEMPLATE_FIELD_DOCS}
1646
- `;
1647
- },
1648
- },
1649
- // Persona Templates - Dynamic from API with fallback
1650
- {
1651
- uri: "ema://catalog/templates",
1652
- name: "catalog/templates",
1653
- description: "Persona templates from Ema API: pre-configured AI Employee templates (chatbot_starter, voicebot, dashboard, etc.)",
1654
- mimeType: "application/json",
1655
- generate: async (ctx) => {
1656
- const templates = await getDynamicPersonaTemplates({ env: ctx.env });
1657
- if (templates.length > 0) {
1658
- return JSON.stringify(templates.map(templateDtoToResource), null, 2);
1659
- }
1660
- // Fallback when API unavailable - provide guidance
1661
- return JSON.stringify({
1662
- _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.",
1663
- fallback_types: ["voice", "chat", "dashboard"],
1664
- how_to_get: "catalog(method='list', type='templates') or template(config='voice')",
1665
- }, null, 2);
1666
- },
1667
- },
1668
- {
1669
- uri: "ema://catalog/templates-summary",
1670
- name: "catalog/templates-summary",
1671
- description: "Persona templates summary: template names, categories, and descriptions for quick reference",
1672
- mimeType: "text/markdown",
1673
- generate: async (ctx) => {
1674
- const templates = await getDynamicPersonaTemplates({ env: ctx.env });
1675
- if (templates.length === 0) {
1676
- return `# Persona Templates
1677
-
1678
- > API templates not available. Use one of these alternatives:
1679
-
1680
- ## Option 1: Use the catalog tool directly
1681
- \`\`\`
1682
- catalog(method="list", type="templates")
1683
- \`\`\`
1684
- This connects to the API and may have different credentials.
1685
-
1686
- ## Option 2: Use config fallbacks
1687
- \`\`\`
1688
- template(config="voice") // Voice AI config template
1689
- template(config="chat") // Chat AI config template
1690
- template(config="dashboard") // Dashboard AI config template
1691
- \`\`\`
1692
-
1693
- ## Option 3: Create from scratch
1694
- \`\`\`
1695
- persona(method="create", name="My AI", type="voice|chat|dashboard")
1696
- \`\`\`
1697
- `;
1698
- }
1699
- const byCategory = new Map();
1700
- for (const t of templates) {
1701
- const cat = t.category || "GENERAL";
1702
- if (!byCategory.has(cat))
1703
- byCategory.set(cat, []);
1704
- byCategory.get(cat).push(t);
1705
- }
1706
- let md = "# Persona Templates (from API)\n\n";
1707
- md += `> ${templates.length} templates available for creating AI Employees\n\n`;
1708
- for (const [category, tpls] of byCategory) {
1709
- md += `## ${category}\n\n`;
1710
- md += "| Template | Type | Description |\n";
1711
- md += "|----------|------|-------------|\n";
1712
- for (const t of tpls) {
1713
- const triggerType = t.trigger_type || "-";
1714
- const desc = t.description?.slice(0, 80) || "-";
1715
- md += `| \`${t.name}\` | ${triggerType} | ${desc}... |\n`;
1716
- }
1717
- md += "\n";
1718
- }
1719
- return md;
1720
- },
1721
- },
1722
- // Action Schema - Complete action definitions from ema repo
1723
- {
1724
- uri: "ema://schema/actions",
1725
- name: "schema/actions",
1726
- description: "Complete action schema: all workflow actions with inputs, outputs, types, and documentation from ema_backend/grpc",
1727
- mimeType: "application/json",
1728
- generate: async (ctx) => {
1729
- const schema = await getDynamicActionSchema({ env: ctx.env });
1730
- return JSON.stringify(schema, null, 2);
1731
- },
1732
- },
1733
- {
1734
- uri: "ema://schema/actions-summary",
1735
- name: "schema/actions-summary",
1736
- description: "Action schema summary: action names, versions, and descriptions for quick reference",
1737
- mimeType: "text/markdown",
1738
- generate: async (ctx) => {
1739
- const schema = await getDynamicActionSchema({ env: ctx.env });
1740
- if (schema.actions.length === 0) {
1741
- return "# Action Schema\n\n> No actions loaded. Check API connectivity or bundled schema.\n";
1742
- }
1743
- let md = "# Action Schema\n\n";
1744
- md += `> ${schema.actions.length} actions loaded (source: ${schema.source})\n`;
1745
- md += `> Version: ${schema.version}\n\n`;
1746
- const byCategory = new Map();
1747
- for (const action of schema.actions) {
1748
- const cat = action.category || "OTHER";
1749
- if (!byCategory.has(cat))
1750
- byCategory.set(cat, []);
1751
- byCategory.get(cat).push(action);
1752
- }
1753
- for (const [category, actions] of byCategory) {
1754
- md += `## ${category.replace("ACTION_CATEGORY_", "")}\n\n`;
1755
- md += "| Action | Version | Description | Inputs | Outputs |\n";
1756
- md += "|--------|---------|-------------|--------|--------|\n";
1757
- for (const action of actions) {
1758
- const inputs = action.inputs?.size || 0;
1759
- const outputs = action.outputs?.size || 0;
1760
- const desc = action.description?.slice(0, 50) || "-";
1761
- md += `| \`${action.name}\` | ${action.version || "v0"} | ${desc}... | ${inputs} | ${outputs} |\n`;
1762
- }
1763
- md += "\n";
1764
- }
1765
- return md;
1766
- },
1767
- },
1768
- {
1769
- uri: "ema://schema/action/:name",
1770
- name: "schema/action",
1771
- description: "Individual action schema by name (e.g., ema://schema/action/call_llm)",
1772
- mimeType: "application/json",
1773
- generate: async (ctx) => {
1774
- // This is a template - actual resolution happens in getResource
1775
- return JSON.stringify({ error: "Use specific action name in URI" }, null, 2);
1776
- },
1777
- },
1778
- {
1779
- uri: "ema://schema/workflow-def",
1780
- name: "schema/workflow-def",
1781
- description: "Complete workflow_def schema with examples for Chat, Voice, and Dashboard personas. Essential reference for building workflows.",
1782
- mimeType: "text/markdown",
1783
- generate: async () => {
1784
- return `# Workflow Definition Schema
1785
-
1786
- This document describes the structure of \`workflow_def\` - the JSON format used to deploy AI Employee workflows.
1787
-
1788
- ## Two-Level Architecture
1789
-
1790
- 1. **WorkflowSpec** (High-level) - What YOU build:
1791
- - Nodes, inputs, categories, result mappings
1792
- - Human-readable, easy to reason about
1793
- - Gets compiled to workflow_def
1794
-
1795
- 2. **workflow_def** (Low-level) - What gets deployed:
1796
- - Compiled JSON structure
1797
- - Namespaces, action references, edges
1798
- - Generated by MCP from WorkflowSpec
1799
-
1800
- ## WorkflowSpec Structure
1801
-
1802
- \`\`\`typescript
1803
- interface WorkflowSpec {
1804
- name: string; // Workflow display name
1805
- description: string; // What it does
1806
- personaType: "voice" | "chat" | "dashboard";
1807
- nodes: Node[]; // Workflow actions
1808
- resultMappings: ResultMapping[]; // What outputs to return
1809
- }
1810
-
1811
- interface Node {
1812
- id: string; // Unique identifier (e.g., "trigger", "search", "respond")
1813
- actionType: string; // Action type (e.g., "chat_trigger", "search", "call_llm")
1814
- displayName: string; // Human-readable name
1815
- description?: string; // What this node does
1816
- inputs?: Record<string, InputBinding>; // Input configuration
1817
- categories?: Category[]; // For categorizers only
1818
- tools?: Tool[]; // For external action callers
1819
- runIf?: RunIfCondition; // Conditional execution
1820
- }
1821
-
1822
- interface InputBinding {
1823
- type: "action_output" | "literal" | "widget_config" | "llm_inferred";
1824
- actionName?: string; // Source node ID (for action_output)
1825
- output?: string; // Output name from source
1826
- value?: any; // Static value (for literal)
1827
- widgetName?: string; // Widget name (for widget_config)
1828
- }
1829
-
1830
- interface Category {
1831
- name: string; // Category name (e.g., "Sales", "Support", "Fallback")
1832
- description: string; // When to route here
1833
- examples?: string[]; // Example phrases
1834
- }
1835
-
1836
- interface ResultMapping {
1837
- nodeId: string; // Node that produces the output
1838
- output: string; // Output name
1839
- }
1840
- \`\`\`
1841
-
1842
- ## Trigger Types by Persona
1843
-
1844
- | Persona Type | Trigger ActionType | Key Outputs |
1845
- |--------------|-------------------|-------------|
1846
- | **Chat** | \`chat_trigger\` | user_query, chat_conversation |
1847
- | **Voice** | \`chat_trigger\` | user_query, chat_conversation |
1848
- | **Dashboard** | \`document_trigger\` | document_content, row_data |
1849
-
1850
- ## Example: Chat Persona (KB Search)
1851
-
1852
- \`\`\`json
1853
- {
1854
- "name": "FAQ Bot",
1855
- "description": "Answer questions from knowledge base",
1856
- "personaType": "chat",
1857
- "nodes": [
1858
- {
1859
- "id": "trigger",
1860
- "actionType": "chat_trigger",
1861
- "displayName": "Chat Input"
1862
- },
1863
- {
1864
- "id": "search",
1865
- "actionType": "search",
1866
- "displayName": "Search Knowledge Base",
1867
- "inputs": {
1868
- "query": {
1869
- "type": "action_output",
1870
- "actionName": "trigger",
1871
- "output": "user_query"
1872
- }
1873
- }
1874
- },
1875
- {
1876
- "id": "respond",
1877
- "actionType": "respond_for_external_actions",
1878
- "displayName": "Generate Response",
1879
- "inputs": {
1880
- "search_results": {
1881
- "type": "action_output",
1882
- "actionName": "search",
1883
- "output": "search_results"
1884
- },
1885
- "conversation": {
1886
- "type": "action_output",
1887
- "actionName": "trigger",
1888
- "output": "chat_conversation"
1889
- }
1890
- }
1891
- }
1892
- ],
1893
- "resultMappings": [
1894
- { "nodeId": "respond", "output": "response" }
1895
- ]
1896
- }
1897
- \`\`\`
1898
-
1899
- ## Example: Dashboard Persona (Document Extraction)
1900
-
1901
- \`\`\`json
1902
- {
1903
- "name": "Invoice Processor",
1904
- "description": "Extract data from invoices",
1905
- "personaType": "dashboard",
1906
- "nodes": [
1907
- {
1908
- "id": "trigger",
1909
- "actionType": "document_trigger",
1910
- "displayName": "Document Input"
1911
- },
1912
- {
1913
- "id": "extract",
1914
- "actionType": "entity_extraction",
1915
- "displayName": "Extract Invoice Data",
1916
- "inputs": {
1917
- "document": {
1918
- "type": "action_output",
1919
- "actionName": "trigger",
1920
- "output": "document_content"
1921
- },
1922
- "extraction_schema": {
1923
- "type": "literal",
1924
- "value": {
1925
- "fields": ["invoice_number", "amount", "date", "vendor"]
1926
- }
1927
- }
1928
- }
1929
- },
1930
- {
1931
- "id": "respond",
1932
- "actionType": "fixed_response",
1933
- "displayName": "Return Results",
1934
- "inputs": {
1935
- "data": {
1936
- "type": "action_output",
1937
- "actionName": "extract",
1938
- "output": "extracted_entities"
1939
- }
1940
- }
1941
- }
1942
- ],
1943
- "resultMappings": [
1944
- { "nodeId": "respond", "output": "response" }
1945
- ]
1946
- }
1947
- \`\`\`
1948
-
1949
- ## Example: Voice Persona (Intent Routing)
1950
-
1951
- \`\`\`json
1952
- {
1953
- "name": "IT Support Voice",
1954
- "description": "Handle IT support calls",
1955
- "personaType": "voice",
1956
- "nodes": [
1957
- {
1958
- "id": "trigger",
1959
- "actionType": "chat_trigger",
1960
- "displayName": "Voice Input"
1961
- },
1962
- {
1963
- "id": "categorize",
1964
- "actionType": "chat_categorizer",
1965
- "displayName": "Identify Intent",
1966
- "inputs": {
1967
- "conversation": {
1968
- "type": "action_output",
1969
- "actionName": "trigger",
1970
- "output": "chat_conversation"
1971
- }
1972
- },
1973
- "categories": [
1974
- { "name": "Password Reset", "description": "User needs password help" },
1975
- { "name": "Create Ticket", "description": "User wants to create a ticket" },
1976
- { "name": "Fallback", "description": "Anything else" }
1977
- ]
1978
- },
1979
- {
1980
- "id": "handle_password",
1981
- "actionType": "call_llm",
1982
- "displayName": "Password Reset Handler",
1983
- "runIf": {
1984
- "sourceAction": "categorize",
1985
- "sourceOutput": "category",
1986
- "operator": "eq",
1987
- "value": "Password Reset"
1988
- },
1989
- "inputs": {
1990
- "prompt": {
1991
- "type": "literal",
1992
- "value": "Guide user through password reset process..."
1993
- }
1994
- }
1995
- },
1996
- {
1997
- "id": "handle_ticket",
1998
- "actionType": "external_action_caller",
1999
- "displayName": "Create ServiceNow Ticket",
2000
- "runIf": {
2001
- "sourceAction": "categorize",
2002
- "sourceOutput": "category",
2003
- "operator": "eq",
2004
- "value": "Create Ticket"
2005
- },
2006
- "tools": [
2007
- { "name": "Create_Incident", "namespace": "service_now" }
2008
- ]
2009
- },
2010
- {
2011
- "id": "handle_fallback",
2012
- "actionType": "call_llm",
2013
- "displayName": "General Response",
2014
- "runIf": {
2015
- "sourceAction": "categorize",
2016
- "sourceOutput": "category",
2017
- "operator": "eq",
2018
- "value": "Fallback"
2019
- }
2020
- }
2021
- ],
2022
- "resultMappings": [
2023
- { "nodeId": "handle_password", "output": "response" },
2024
- { "nodeId": "handle_ticket", "output": "result" },
2025
- { "nodeId": "handle_fallback", "output": "response" }
2026
- ]
2027
- }
2028
- \`\`\`
2029
-
2030
- ## Critical Rules
2031
-
2032
- 1. **Always include Fallback** - Every categorizer MUST have a Fallback category
2033
- 2. **Trigger type matters** - Dashboard uses document_trigger, others use chat_trigger
2034
- 3. **Wire conversations correctly** - Categorizers need chat_conversation, search needs user_query
2035
- 4. **Map results** - At least one node must have a resultMapping to WORKFLOW_OUTPUT
2036
- 5. **Check deprecated actions** - Always verify against ema://rules/deprecated-actions-summary
2037
-
2038
- ## Common Input Bindings
2039
-
2040
- | Source | Type | Example |
2041
- |--------|------|---------|
2042
- | Previous node output | action_output | \`{ type: "action_output", actionName: "search", output: "results" }\` |
2043
- | Static value | literal | \`{ type: "literal", value: "Hello world" }\` |
2044
- | Widget config | widget_config | \`{ type: "widget_config", widgetName: "conversationSettings" }\` |
2045
- | LLM should infer | llm_inferred | \`{ type: "llm_inferred" }\` |
2046
-
2047
- ## Deployment Flow
2048
-
2049
- 1. Build your WorkflowSpec (structure above)
2050
- 2. Call \`workflow(mode="validate", workflow_spec={...})\` to check for errors
2051
- 3. Call \`workflow(mode="get", persona_id="...")\` to get the latest fingerprint (stale-state protection)
2052
- 4. Call \`workflow(mode="deploy", persona_id="...", base_fingerprint="<fingerprint>", workflow_def={...})\` to deploy
2053
-
2054
- For large payloads, use \`workflow_def_path\` instead of inline \`workflow_def\`.
2055
-
2056
- The MCP compiles your WorkflowSpec into the API's workflow_def format automatically.
2057
-
2058
- ## Raw workflow_def Input Binding Formats
2059
-
2060
- When deploying raw workflow_def (not WorkflowSpec), inputs use protobuf-compatible JSON.
2061
- These formats are CRITICAL - wrong formats cause HTTP 500 with no error details.
2062
-
2063
- ### Action Output (reference another node's output)
2064
- \`\`\`json
2065
- {
2066
- "actionOutput": { "actionName": "source_node_name", "output": "output_name" },
2067
- "autoDetectedBinding": false
2068
- }
2069
- \`\`\`
2070
-
2071
- ### Widget Config (reference a proto_config widget)
2072
- \`\`\`json
2073
- {
2074
- "widgetConfig": { "widgetName": "fusionModel" },
2075
- "autoDetectedBinding": true
2076
- }
2077
- \`\`\`
2078
-
2079
- ### Inline String (STRING type)
2080
- \`\`\`json
2081
- {
2082
- "inline": { "wellKnown": { "stringValue": "your text here" } },
2083
- "autoDetectedBinding": false
2084
- }
2085
- \`\`\`
2086
-
2087
- ### Inline Text With Sources (TEXT_WITH_SOURCES type)
2088
- \`\`\`json
2089
- {
2090
- "inline": {
2091
- "wellKnown": {
2092
- "textWithSources": {
2093
- "text": "your text here",
2094
- "sources": [],
2095
- "resultConfidence": 0,
2096
- "toolSources": [],
2097
- "resultType": 0
2098
- }
2099
- }
2100
- },
2101
- "autoDetectedBinding": false
2102
- }
2103
- \`\`\`
2104
- **WARNING**: STRING ≠ TEXT_WITH_SOURCES. Using stringValue where textWithSources is expected causes HTTP 500.
2105
-
2106
- ### Inline Enum Value
2107
- \`\`\`json
2108
- {
2109
- "inline": { "enumValue": "CategoryName" },
2110
- "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));
2111
80
  }
2112
- \`\`\`
2113
-
2114
- ### Named Inputs (multiBinding format - REQUIRED for named_inputs)
2115
- \`\`\`json
2116
- {
2117
- "multiBinding": {
2118
- "elements": [
2119
- {
2120
- "namedBinding": {
2121
- "name": "context_key",
2122
- "value": {
2123
- "actionOutput": { "actionName": "source_node", "output": "response_with_sources" },
2124
- "autoDetectedBinding": false
2125
- },
2126
- "description": "",
2127
- "isOptional": false
2128
- },
2129
- "autoDetectedBinding": false
2130
- }
2131
- ]
2132
- },
2133
- "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
+ });
2134
89
  }
2135
- \`\`\`
2136
-
2137
- ### Action Name Format
2138
- \`\`\`json
2139
- {
2140
- "action": {
2141
- "name": { "namespaces": ["actions", "emainternal"], "name": "call_llm" },
2142
- "version": "v2"
2143
- }
90
+ function hasPathTraversal(uriPath) {
91
+ return uriPath.includes("..") || uriPath.includes("//");
2144
92
  }
2145
- \`\`\`
2146
- **WARNING**: Empty namespaces [] causes deploy failure. Always use ["actions", "emainternal"].
2147
-
2148
- ### text_categorizer/v1 Requirements
2149
- - Uses \`named_inputs\` (multiBinding format), NOT \`text\` input from deprecated v0
2150
- - Requires \`typeArguments.categories\` pointing to an enumType
2151
- - \`categorization_instructions\` input type is TEXT_WITH_SOURCES (use textWithSources, NOT stringValue)
2152
- - Must define matching enumType in workflow_def.enumTypes[] with Fallback category
2153
- `;
2154
- },
2155
- },
2156
- {
2157
- uri: "ema://schema/workflow-def-json",
2158
- name: "schema/workflow-def-json",
2159
- description: "JSON Schema for workflow_def validation. Use for pre-validation before deployment.",
2160
- mimeType: "application/json",
2161
- generate: async () => {
2162
- const { getWorkflowDefSchemaJSON } = await import("./domain/workflow-def-schema.js");
2163
- return getWorkflowDefSchemaJSON();
2164
- },
2165
- },
2166
- // ─────────────────────────────────────────────────────────────────────────
2167
- // Guidance Resources - Single source of truth, multiple export formats
2168
- // ─────────────────────────────────────────────────────────────────────────
2169
- {
2170
- uri: "ema://docs/usage-guide",
2171
- name: "docs/usage-guide",
2172
- 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",
2173
104
  mimeType: "text/markdown",
2174
- generate: async () => {
2175
- const { exportAsMarkdown } = await import("./guidance.js");
2176
- return exportAsMarkdown();
2177
- },
2178
105
  },
2179
- {
2180
- uri: "ema://guidance/rules",
2181
- name: "guidance/rules",
2182
- description: "Structured guidance rules (JSON) - for programmatic consumption by other services",
2183
- mimeType: "application/json",
2184
- generate: async () => {
2185
- const { exportAsJSON } = await import("./guidance.js");
2186
- return exportAsJSON();
2187
- },
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",
2188
110
  },
2189
- {
2190
- uri: "ema://guidance/cursor-rule",
2191
- name: "guidance/cursor-rule",
2192
- 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",
2193
114
  mimeType: "text/markdown",
2194
- generate: async () => {
2195
- const { exportAsCursorRule } = await import("./guidance.js");
2196
- return exportAsCursorRule();
2197
- },
2198
115
  },
2199
- {
2200
- uri: "ema://guidance/server-instructions",
2201
- name: "guidance/server-instructions",
2202
- description: "Server instructions (the content injected into MCP init response)",
2203
- mimeType: "text/plain",
2204
- generate: async () => {
2205
- const { generateServerInstructions } = await import("./guidance.js");
2206
- return generateServerInstructions();
2207
- },
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",
2208
120
  },
2209
- ];
2210
- // ─────────────────────────────────────────────────────────────────────────────
2211
- // Dynamic fetching helpers (Ema API as source of truth when available)
2212
- // ─────────────────────────────────────────────────────────────────────────────
2213
- function loadAnyConfig() {
2214
- return (loadConfigFromJsonEnv() ??
2215
- loadConfigOptional(process.env.EMA_AGENT_SYNC_CONFIG ?? "./config.yaml"));
2216
- }
2217
- function getDefaultEnvNameFromConfig(cfg) {
2218
- const fromEnv = process.env.EMA_ENV_NAME?.trim();
2219
- if (fromEnv)
2220
- return fromEnv;
2221
- const master = getMasterEnv(cfg);
2222
- return master?.name ?? cfg.environments[0]?.name ?? "demo";
2223
- }
2224
- const WK_ANY = "WELL_KNOWN_TYPE_ANY";
2225
- function actionDtoToAgentDefinition(a) {
2226
- // Handle inputs - can be array or object with inputs property
2227
- const inputsArray = Array.isArray(a.inputs) ? a.inputs : [];
2228
- const outputsArray = Array.isArray(a.outputs) ? a.outputs : [];
2229
- return {
2230
- actionName: a.name ?? a.id,
2231
- displayName: a.name ?? a.id,
2232
- category: (a.category ?? "other"),
2233
- description: a.description ?? "",
2234
- inputs: inputsArray.map((p) => ({
2235
- name: p.name ?? "input",
2236
- type: WK_ANY,
2237
- required: Boolean(p.required),
2238
- description: p.description ?? "",
2239
- })),
2240
- outputs: outputsArray.map((p) => ({
2241
- name: p.name ?? "output",
2242
- type: WK_ANY,
2243
- description: p.description ?? "",
2244
- })),
2245
- whenToUse: "",
2246
- };
2247
- }
2248
- const clientCache = new Map();
2249
- const agentCatalogCache = new Map();
2250
- function getClientForEnvName(envName) {
2251
- const cfg = loadAnyConfig();
2252
- if (!cfg)
2253
- return null;
2254
- const effectiveEnv = envName ?? getDefaultEnvNameFromConfig(cfg);
2255
- const cached = clientCache.get(effectiveEnv);
2256
- if (cached)
2257
- return cached;
2258
- const envCfg = getEnvByName(cfg, effectiveEnv) ?? getEnvByName(cfg, getDefaultEnvNameFromConfig(cfg));
2259
- if (!envCfg)
2260
- return null;
2261
- const env = {
2262
- name: envCfg.name,
2263
- baseUrl: envCfg.baseUrl,
2264
- bearerToken: resolveBearerToken(envCfg.bearerTokenEnv),
2265
- };
2266
- const client = new EmaClient(env);
2267
- clientCache.set(effectiveEnv, client);
2268
- return client;
2269
- }
2270
- async function getDynamicAgentCatalog(opts) {
2271
- const cacheKey = opts.env ?? "";
2272
- const now = Date.now();
2273
- const cached = agentCatalogCache.get(cacheKey);
2274
- if (cached && now - cached.ts < 60_000)
2275
- return cached.agents;
2276
- const client = getClientForEnvName(opts.env);
2277
- if (!client)
2278
- return AGENT_CATALOG;
2279
- try {
2280
- const actions = await client.listActions();
2281
- const agents = actions
2282
- .filter((a) => typeof a.name === "string" && a.name.trim().length > 0)
2283
- .map(actionDtoToAgentDefinition);
2284
- agentCatalogCache.set(cacheKey, { ts: now, agents });
2285
- return agents.length > 0 ? agents : AGENT_CATALOG;
2286
- }
2287
- catch {
2288
- return AGENT_CATALOG;
2289
- }
2290
- }
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
+ };
2291
132
  // ─────────────────────────────────────────────────────────────────────────────
2292
- // Dynamic Persona Templates (API-first)
133
+ // Workspace Root
2293
134
  // ─────────────────────────────────────────────────────────────────────────────
2294
- const templateCatalogCache = new Map();
2295
- /**
2296
- * Fetch persona templates from API with caching.
2297
- * Falls back to empty array if API unavailable (file-backed templates still available via RESOURCE_MAP).
2298
- */
2299
- async function getDynamicPersonaTemplates(opts) {
2300
- const cacheKey = opts.env ?? "";
2301
- const now = Date.now();
2302
- const cached = templateCatalogCache.get(cacheKey);
2303
- if (cached && now - cached.ts < 60_000)
2304
- return cached.templates;
2305
- const client = getClientForEnvName(opts.env);
2306
- if (!client)
2307
- return [];
2308
- try {
2309
- const templates = await client.getPersonaTemplates();
2310
- templateCatalogCache.set(cacheKey, { ts: now, templates });
2311
- return templates;
2312
- }
2313
- catch {
2314
- return [];
2315
- }
2316
- }
2317
- const actionSchemaCache = new Map();
2318
- /**
2319
- * Get action schema with layered loading:
2320
- * 1. Try bundled schema (resources/action-schema.json)
2321
- * 2. Augment with live API data if available
2322
- */
2323
- async function getDynamicActionSchema(opts) {
2324
- const cacheKey = opts.env ?? "";
2325
- const now = Date.now();
2326
- const cached = actionSchemaCache.get(cacheKey);
2327
- if (cached && now - cached.ts < 60_000)
2328
- return cached.data;
2329
- const registry = new APISchemaRegistry();
2330
- // Try to load from bundled schema first (code source of truth)
2331
- const bundlePath = path.resolve(__dirname, "../../resources/action-schema.json");
2332
- try {
2333
- registry.loadFromBundle(bundlePath);
2334
- }
2335
- catch {
2336
- // Bundle not available - try API
2337
- const client = getClientForEnvName(opts.env);
2338
- if (client) {
2339
- try {
2340
- await registry.load(client);
2341
- }
2342
- catch {
2343
- // Neither bundle nor API available
2344
- }
2345
- }
2346
- }
2347
- const response = {
2348
- version: registry.metadata.version ?? "unknown",
2349
- source: registry.metadata.source ?? "bundle",
2350
- actions: registry.getAllActions(),
2351
- };
2352
- actionSchemaCache.set(cacheKey, { ts: now, data: response });
2353
- return response;
2354
- }
2355
- /**
2356
- * Get a specific action by name from the schema.
2357
- */
2358
- async function getActionByName(name, env) {
2359
- const schema = await getDynamicActionSchema({ env });
2360
- // Find latest version of action
2361
- const matching = schema.actions.filter(a => a.name === name);
2362
- if (matching.length === 0)
2363
- return null;
2364
- // Return latest version (sort by version desc)
2365
- return matching.sort((a, b) => (b.version || "v0").localeCompare(a.version || "v0"))[0];
2366
- }
2367
- /**
2368
- * Convert PersonaTemplateDTO to a simplified format for MCP resources.
2369
- */
2370
- function templateDtoToResource(t) {
2371
- return {
2372
- id: t.id,
2373
- name: t.name,
2374
- description: t.description,
2375
- category: t.category,
2376
- trigger_type: t.trigger_type,
2377
- has_project_template: t.has_project_template,
2378
- // Documentation fields for diagnosis/recommendations
2379
- about_template: t.about_template, // Rich description of capabilities
2380
- how_to_use: t.how_to_use, // Step-by-step usage instructions
2381
- value_prop: t.value_prop, // Value proposition sections
2382
- additional_content: t.additional_content, // Extra documentation
2383
- // Include proto_config for full template details
2384
- proto_config: t.proto_config,
2385
- // Include workflow_definition if available
2386
- workflow_definition: t.workflow_definition,
2387
- };
2388
- }
2389
135
  // Get workspace root - use centralized path resolution
2390
136
  // EMA_WORKSPACE_ROOT env var can override for testing
2391
137
  import { getToolkitRoot } from "../sdk/paths.js";
@@ -2395,6 +141,9 @@ function getWorkspaceRoot() {
2395
141
  }
2396
142
  return getToolkitRoot();
2397
143
  }
144
+ // ─────────────────────────────────────────────────────────────────────────────
145
+ // ResourceRegistry Class
146
+ // ─────────────────────────────────────────────────────────────────────────────
2398
147
  export class ResourceRegistry {
2399
148
  workspaceRoot;
2400
149
  constructor(workspaceRoot) {
@@ -2636,409 +385,8 @@ To read a resource, use the \`resources/read\` endpoint:
2636
385
  }
2637
386
  }
2638
387
  // ─────────────────────────────────────────────────────────────────────────────
2639
- // Validation Rules Generator
388
+ // Helpers
2640
389
  // ─────────────────────────────────────────────────────────────────────────────
2641
- /**
2642
- * Generate validation rules in LLM-friendly Markdown format.
2643
- * Includes rules from Go validator: required outputs, multiple writers, response/abstain, category hierarchy.
2644
- */
2645
- function generateValidationRulesForLLM() {
2646
- return `# Workflow Validation Rules
2647
-
2648
- > **Use these rules DURING workflow generation to avoid errors proactively.**
2649
-
2650
- These rules are extracted from the Go validator's static validation logic. Follow them when generating or modifying workflows.
2651
-
2652
- ## Rule 1: Required Output on All Paths
2653
-
2654
- **Rule ID**: \`required_output_all_paths\`
2655
- **Severity**: Critical
2656
- **Applies To**: Named results, execution paths
2657
-
2658
- ### Logic
2659
-
2660
- **Algorithm**: Enumerate all execution paths. For each path, check if all named results are produced.
2661
-
2662
- **Steps**:
2663
- 1. Enumerate all paths from trigger to completion
2664
- 2. For each path, collect all outputs produced
2665
- 3. For each named result, check if it's in path outputs
2666
- 4. If missing on any path → error
2667
-
2668
- **Complexity**: O(P * R) where P = paths, R = named results
2669
-
2670
- ### Rules
2671
-
2672
- **Constraint**: Every execution path must produce all named results.
2673
-
2674
- **Why**: Named results are contract guarantees. If a path doesn't produce a required output, the contract is violated.
2675
-
2676
- ### Examples
2677
-
2678
- **Bad Pattern**:
2679
- \`\`\`json
2680
- {
2681
- "path": ["trigger", "categorizer", "billing_branch", "search"],
2682
- "named_result": "response_with_sources",
2683
- "produces": [] // Missing response
2684
- }
2685
- \`\`\`
2686
-
2687
- **Why Bad**: Path ends without producing required output.
2688
-
2689
- **Good Pattern**:
2690
- \`\`\`json
2691
- {
2692
- "path": ["trigger", "categorizer", "billing_branch", "search", "respond_for_external_actions"],
2693
- "named_result": "response",
2694
- "produces": ["response"] // ✅ Produced
2695
- }
2696
- \`\`\`
2697
-
2698
- **Why Good**: All paths produce required output.
2699
-
2700
- ### Remediation
2701
-
2702
- **How to Fix**:
2703
- 1. Identify the path missing the output
2704
- 2. Add a node that produces the required output
2705
- 3. Connect the node's output to WORKFLOW_OUTPUT
2706
-
2707
- **Common Fixes**:
2708
- - Add \`respond_for_external_actions\` node after search
2709
- - Add \`call_llm\` node for text generation
2710
- - Add \`fixed_response\` for static responses
2711
-
2712
- **Prevention** (During Generation):
2713
- - Always ensure every categorizer branch has a response node
2714
- - Check that all paths end with nodes connected to WORKFLOW_OUTPUT
2715
- - Verify resultMappings include all required outputs
2716
-
2717
- ---
2718
-
2719
- ## Rule 2: Single Writer Per Output
2720
-
2721
- **Rule ID**: \`single_writer_per_output\`
2722
- **Severity**: Critical
2723
- **Applies To**: Named results, execution paths
2724
-
2725
- ### Logic
2726
-
2727
- **Algorithm**: For each named result, count how many nodes produce it on each path.
2728
-
2729
- **Steps**:
2730
- 1. For each named result
2731
- 2. For each execution path
2732
- 3. Count nodes that produce this result
2733
- 4. If count > 1 on any path → error
2734
-
2735
- ### Rules
2736
-
2737
- **Constraint**: A single named result can only have one producer per execution path.
2738
-
2739
- **Why**: Multiple producers on the same path cause ambiguity about which value to use.
2740
-
2741
- ### Examples
2742
-
2743
- **Bad Pattern**:
2744
- \`\`\`json
2745
- {
2746
- "path": ["trigger", "categorizer", "billing"],
2747
- "named_result": "response_with_sources",
2748
- "producers": ["respond_billing", "respond_general"] // ❌ Two producers
2749
- }
2750
- \`\`\`
2751
-
2752
- **Why Bad**: Two nodes produce the same output on the same path.
2753
-
2754
- **Good Pattern**:
2755
- \`\`\`json
2756
- {
2757
- "path": ["trigger", "categorizer", "billing"],
2758
- "named_result": "response_with_sources",
2759
- "producers": ["respond_billing"] // ✅ Single producer
2760
- }
2761
- \`\`\`
2762
-
2763
- **Why Good**: Only one node produces the output.
2764
-
2765
- ### Remediation
2766
-
2767
- **How to Fix**:
2768
- 1. Identify which nodes produce the same output
2769
- 2. Remove one producer or use different named results
2770
- 3. Ensure only one node produces each named result per path
2771
-
2772
- **Prevention** (During Generation):
2773
- - Use mutually exclusive runIf conditions to gate responders
2774
- - Ensure only one response node executes per category branch
2775
-
2776
- ---
2777
-
2778
- ## Rule 3: Response or Abstain Required
2779
-
2780
- **Rule ID**: \`response_or_abstain_required\`
2781
- **Severity**: Critical
2782
- **Applies To**: Execution paths
2783
-
2784
- ### Logic
2785
-
2786
- **Algorithm**: For each execution path, check if it produces a response or has an abstain reason.
2787
-
2788
- **Steps**:
2789
- 1. For each completed path
2790
- 2. Check if path produces any response output
2791
- 3. Check if path has abstain reason
2792
- 4. If neither → error
2793
-
2794
- ### Rules
2795
-
2796
- **Constraint**: Every execution path must either produce a user-visible response or explicitly abstain with a reason.
2797
-
2798
- **Why**: Users expect responses. Paths that don't respond and don't explain why create poor UX.
2799
-
2800
- ### Examples
2801
-
2802
- **Bad Pattern**:
2803
- \`\`\`json
2804
- {
2805
- "path": ["trigger", "categorizer", "billing", "search"],
2806
- "has_response": false,
2807
- "has_abstain": false // ❌ Neither
2808
- }
2809
- \`\`\`
2810
-
2811
- **Why Bad**: Path ends without response or abstain reason.
2812
-
2813
- **Good Pattern**:
2814
- \`\`\`json
2815
- {
2816
- "path": ["trigger", "categorizer", "billing", "search", "respond_for_external_actions"],
2817
- "has_response": true // ✅ Has response
2818
- }
2819
- \`\`\`
2820
-
2821
- **Why Good**: Path produces a response.
2822
-
2823
- ### Remediation
2824
-
2825
- **How to Fix**:
2826
- 1. Add a response node to the path, OR
2827
- 2. Add an abstain reason to the workflow configuration
2828
-
2829
- **Prevention** (During Generation):
2830
- - Always add response nodes to every categorizer branch
2831
- - If a path shouldn't respond, document why with abstain reason
2832
-
2833
- ---
2834
-
2835
- ## Rule 4: Category Hierarchy Valid
2836
-
2837
- **Rule ID**: \`category_hierarchy_valid\`
2838
- **Severity**: Critical
2839
- **Applies To**: Categorizers
2840
-
2841
- ### Logic
2842
-
2843
- **Algorithm**: Validate categorizer structure and category definitions.
2844
-
2845
- **Steps**:
2846
- 1. Check categorizer has Fallback category
2847
- 2. Check category hierarchy rules (TBD - need Go validator analysis)
2848
- 3. Validate category → handler mapping
2849
-
2850
- ### Rules
2851
-
2852
- **Constraint**: Categorizers must have proper structure and hierarchy.
2853
-
2854
- **Why**: Invalid category structure causes routing failures.
2855
-
2856
- ### Examples
2857
-
2858
- **Bad Pattern**:
2859
- \`\`\`json
2860
- {
2861
- "categorizer": {
2862
- "categories": ["Billing", "Technical"] // ❌ No Fallback
2863
- }
2864
- }
2865
- \`\`\`
2866
-
2867
- **Why Bad**: Missing Fallback category means unrecognized intents have no handler.
2868
-
2869
- **Good Pattern**:
2870
- \`\`\`json
2871
- {
2872
- "categorizer": {
2873
- "categories": ["Billing", "Technical", "Fallback"] // ✅ Has Fallback
2874
- }
2875
- }
2876
- \`\`\`
2877
-
2878
- **Why Good**: All intents have a handler.
2879
-
2880
- ### Remediation
2881
-
2882
- **How to Fix**:
2883
- 1. Add Fallback category to categorizer
2884
- 2. Ensure each category has a handler node
2885
- 3. Validate category hierarchy (TBD)
2886
-
2887
- **Prevention** (During Generation):
2888
- - Always include Fallback category
2889
- - Ensure every category has at least one handler node
2890
-
2891
- ---
2892
-
2893
- ## Self-Check Checklist
2894
-
2895
- After generating or modifying a workflow, verify:
2896
-
2897
- - [ ] All paths produce all named results
2898
- - [ ] No multiple writers on same path
2899
- - [ ] Every path has response or abstain
2900
- - [ ] Category hierarchy is valid
2901
- - [ ] Categorizer has Fallback category
2902
- - [ ] All categories have handler nodes
2903
-
2904
- ---
2905
-
2906
- ## Reference
2907
-
2908
- - **Source**: workflow-engine/pkg/engine/validator.go
2909
- - **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
2910
- - **Validation Tool**: Use \`workflow(mode="validate")\` to validate workflows
2911
- `;
2912
- }
2913
- /**
2914
- * Generate best practices checklist from SDK rules.
2915
- * SINGLE SOURCE OF TRUTH - all best practices come from:
2916
- * - ANTI_PATTERNS (validation-rules.ts)
2917
- * - STRUCTURAL_INVARIANTS (structural-rules.ts)
2918
- * - INPUT_SOURCE_RULES (validation-rules.ts)
2919
- * - OPTIMIZATION_RULES (validation-rules.ts)
2920
- */
2921
- function generateBestPracticesChecklist() {
2922
- // Group anti-patterns by severity
2923
- const criticalAntiPatterns = ANTI_PATTERNS.filter(ap => ap.severity === "critical");
2924
- const warningAntiPatterns = ANTI_PATTERNS.filter(ap => ap.severity === "warning");
2925
- const infoAntiPatterns = ANTI_PATTERNS.filter(ap => ap.severity === "info");
2926
- // Group structural invariants by severity
2927
- const criticalInvariants = STRUCTURAL_INVARIANTS.filter(inv => inv.severity === "critical");
2928
- const warningInvariants = STRUCTURAL_INVARIANTS.filter(inv => inv.severity === "warning");
2929
- return `# Workflow Best Practices Checklist
2930
-
2931
- > **Auto-generated from SDK rules** - This is the single source of truth.
2932
- > Check this BEFORE deploying any workflow.
2933
-
2934
- ## 🛑 CRITICAL (Must Fix Before Deploy)
2935
-
2936
- ### Anti-Patterns to Avoid
2937
-
2938
- ${criticalAntiPatterns.map(ap => `
2939
- #### ${ap.name}
2940
-
2941
- **Pattern**: ${ap.pattern}
2942
-
2943
- **Problem**: ${ap.problem}
2944
-
2945
- **Solution**: ${ap.solution}
2946
-
2947
- **Detection**: \`${ap.detection.condition}\`
2948
- `).join("\n---\n")}
2949
-
2950
- ### Structural Requirements
2951
-
2952
- ${criticalInvariants.map(inv => `
2953
- - **${inv.name}**: ${inv.rule}
2954
- - Violation: ${inv.violation}
2955
- - Fix: ${inv.fix}
2956
- `).join("")}
2957
-
2958
- ---
2959
-
2960
- ## ⚠️ WARNINGS (Should Fix)
2961
-
2962
- ### Anti-Patterns
2963
-
2964
- ${warningAntiPatterns.map(ap => `
2965
- - **${ap.name}**: ${ap.problem}
2966
- - Solution: ${ap.solution}
2967
- `).join("")}
2968
-
2969
- ### Structural Warnings
2970
-
2971
- ${warningInvariants.map(inv => `
2972
- - **${inv.name}**: ${inv.rule}
2973
- - Fix: ${inv.fix}
2974
- `).join("")}
2975
-
2976
- ---
2977
-
2978
- ## 📋 SUGGESTIONS (Good to Have)
2979
-
2980
- ${infoAntiPatterns.map(ap => `
2981
- - **${ap.name}**: ${ap.problem}
2982
- - Recommendation: ${ap.solution}
2983
- `).join("")}
2984
-
2985
- ---
2986
-
2987
- ## Input Source Rules
2988
-
2989
- | Action | Recommended Input | Avoid | Why |
2990
- |--------|-------------------|-------|-----|
2991
- ${INPUT_SOURCE_RULES.filter(r => r.severity === "critical").map(r => `| \`${r.actionPattern}\` | \`${r.recommended}\` | ${r.avoid.join(", ") || "-"} | ${r.reason.slice(0, 60)}... |`).join("\n")}
2992
-
2993
- ---
2994
-
2995
- ## Performance Optimizations
2996
-
2997
- ${OPTIMIZATION_RULES.map(opt => `
2998
- ### ${opt.name} (${opt.priority})
2999
-
3000
- - **Current State**: ${opt.currentState}
3001
- - **Recommendation**: ${opt.recommendation}
3002
- - **Benefit**: ${opt.benefit}
3003
- `).join("")}
3004
-
3005
- ---
3006
-
3007
- ## Quick Checklist
3008
-
3009
- Before deploying, verify:
3010
-
3011
- ### Critical
3012
- - [ ] Response nodes receive \`chat_conversation\` (prevents repeated questions)
3013
- - [ ] Email recipients from \`entity_extraction\` (not text outputs)
3014
- - [ ] Categorizers have Fallback category
3015
- - [ ] HITL has both success AND failure paths
3016
- - [ ] All paths reach WORKFLOW_OUTPUT
3017
- - [ ] No circular dependencies
3018
-
3019
- ### Recommended
3020
- - [ ] \`max_tokens\` set on call_llm nodes
3021
- - [ ] No orphan nodes (unused/disconnected)
3022
- - [ ] No redundant search nodes
3023
- - [ ] Search nodes have data sources uploaded
3024
-
3025
- ---
3026
-
3027
- ## Related Resources
3028
-
3029
- - \`ema://rules/anti-patterns\` - Full anti-pattern definitions (JSON)
3030
- - \`ema://rules/input-sources\` - Input source rules (JSON)
3031
- - \`ema://rules/extraction-column-format\` - extraction_columns API shape (entity_extraction nodes)
3032
- - \`ema://rules/json-output-patterns\` - custom_agent + json_mapper pattern
3033
- - \`ema://rules/chat-response-wiring\` - Chat response node wiring (avoid duplicate responses)
3034
- - \`ema://rules/email-input-wiring\` - Email input wiring (json_mapper/fixed_response patterns)
3035
- - \`ema://docs/workflow-node-reference\` - Per-node I/O types, categorizer patterns, LLM config
3036
- - \`ema://rules/structural-invariants\` - Structural rules (JSON)
3037
- - \`ema://rules/optimizations\` - Optimization rules (JSON)
3038
- - \`ema://validation/rules\` - Go validator rules reference
3039
- `;
3040
- }
3041
- // Helper to check if result is an error
3042
390
  export function isResourceError(result) {
3043
391
  return "code" in result;
3044
392
  }