@ema.co/mcp-toolkit 2026.1.29-9 → 2026.2.5

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

Potentially problematic release.


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

Files changed (82) hide show
  1. package/.context/public/guides/mcp-tools-guide.md +5 -3
  2. package/README.md +15 -17
  3. package/dist/cli/index.js +1 -1
  4. package/dist/{sdk → mcp}/autobuilder.js +1 -1
  5. package/dist/mcp/domain/dashboard.js +224 -0
  6. package/dist/{sdk → mcp/domain}/generation-schema.js +1 -1
  7. package/dist/{sdk → mcp/domain}/intent-architect.js +3 -3
  8. package/dist/{sdk → mcp/domain}/structural-rules.js +43 -0
  9. package/dist/{sdk → mcp/domain}/validation-rules.js +154 -9
  10. package/dist/{sdk → mcp/domain}/workflow-def-schema.js +14 -30
  11. package/dist/{sdk → mcp/domain}/workflow-execution-analyzer.js +1 -1
  12. package/dist/{sdk → mcp/domain}/workflow-generator.js +1 -1
  13. package/dist/{sdk → mcp/domain}/workflow-transformer.js +93 -8
  14. package/dist/{sdk → mcp}/guidance.js +94 -121
  15. package/dist/mcp/handlers/action/index.js +1 -1
  16. package/dist/mcp/handlers/catalog/index.js +236 -0
  17. package/dist/mcp/handlers/data/dashboard-clone.js +41 -55
  18. package/dist/mcp/handlers/data/index.js +151 -61
  19. package/dist/mcp/handlers/demo/index.js +552 -0
  20. package/dist/mcp/handlers/deprecation.js +25 -0
  21. package/dist/mcp/handlers/env/config.js +296 -0
  22. package/dist/mcp/handlers/env/index.js +2 -2
  23. package/dist/mcp/handlers/index.js +1 -1
  24. package/dist/mcp/handlers/persona/create.js +28 -2
  25. package/dist/mcp/handlers/persona/delete.js +46 -1
  26. package/dist/mcp/handlers/persona/intent.js +1 -1
  27. package/dist/mcp/handlers/persona/update.js +87 -4
  28. package/dist/mcp/handlers/persona/version.js +2 -2
  29. package/dist/mcp/handlers/reference/index.js +1 -1
  30. package/dist/mcp/handlers/sync/direct.js +229 -0
  31. package/dist/mcp/handlers/template/index.js +1 -1
  32. package/dist/mcp/handlers/utils.js +1 -1
  33. package/dist/mcp/handlers/workflow/analyze.js +1 -1
  34. package/dist/mcp/handlers/workflow/deploy.js +105 -3
  35. package/dist/mcp/handlers/workflow/fix.js +588 -0
  36. package/dist/mcp/handlers/workflow/generate.js +7 -7
  37. package/dist/mcp/handlers/workflow/index.js +47 -45
  38. package/dist/mcp/handlers/workflow/utils.js +1 -1
  39. package/dist/mcp/handlers/workflow/validate.js +3 -3
  40. package/dist/mcp/handlers/workflow/validation.js +290 -56
  41. package/dist/mcp/handlers-consolidated.js +11 -28
  42. package/dist/{sdk → mcp}/knowledge.js +166 -43
  43. package/dist/mcp/prompts.js +7 -6
  44. package/dist/mcp/resources.js +618 -14
  45. package/dist/mcp/server.js +227 -1979
  46. package/dist/mcp/tools.js +43 -21
  47. package/dist/sdk/ema-client.js +237 -15
  48. package/dist/sdk/generated/api-client/index.js +1 -1
  49. package/dist/sdk/generated/api-client/sdk.gen.js +145 -102
  50. package/dist/sdk/generated/api-types.js +5 -3
  51. package/dist/sdk/generated/protos/service/agent_qa/v1/agent_qa_pb.js +260 -5
  52. package/dist/sdk/generated/protos/service/dataingest/v1/dataingest_pb.js +1 -1
  53. package/dist/sdk/generated/protos/service/document_store/v1/rfp_response_pb.js +71 -10
  54. package/dist/sdk/generated/protos/service/persona/v1/persona_config_pb.js +1 -1
  55. package/dist/sdk/generated/protos/service/persona/v1/voicebot_widgets/widget_types_pb.js +14 -9
  56. package/dist/sdk/generated/protos/service/search/v1/search_pb.js +34 -28
  57. package/dist/sdk/generated/protos/service/transform/v1/transform_pb.js +11 -5
  58. package/dist/sdk/generated/protos/service/workflows/v1/action_type_pb.js +1 -1
  59. package/dist/sdk/generated/protos/service/workflows/v1/dashboards_pb.js +1 -1
  60. package/dist/sdk/generated/protos/service/workflows/v1/rpc/workflow_rpc_pb.js +7 -7
  61. package/dist/sdk/generated/protos/service/workflows/v1/well_known_pb.js +1 -1
  62. package/dist/sdk/generated/protos/service/workflows/v1/workflow_pb.js +1 -1
  63. package/dist/sdk/index.js +29 -126
  64. package/dist/{sdk/sync.js → sync/sdk.js} +7 -3
  65. package/package.json +1 -1
  66. package/dist/mcp/handlers/workflow/modify.js +0 -528
  67. /package/dist/{sdk → mcp}/demo-generator.js +0 -0
  68. /package/dist/{sdk → mcp/domain}/action-schema-parser.js +0 -0
  69. /package/dist/{sdk → mcp/domain}/quality-gates.js +0 -0
  70. /package/dist/{sdk → mcp/domain}/sanitizer.js +0 -0
  71. /package/dist/{sdk → mcp/domain}/workflow-intent.js +0 -0
  72. /package/dist/{sdk → mcp/domain}/workflow-merge.js +0 -0
  73. /package/dist/{sdk → mcp/domain}/workflow-optimizer.js +0 -0
  74. /package/dist/{sdk → mcp/domain}/workflow-path-enumerator.js +0 -0
  75. /package/dist/{sdk → mcp/domain}/workflow-static-validator.js +0 -0
  76. /package/dist/{sdk → mcp/domain}/workflow-tracer.js +0 -0
  77. /package/dist/{sdk → mcp/domain}/workflow-validation-types.js +0 -0
  78. /package/dist/{sdk → mcp/domain}/workflow-validator.js +0 -0
  79. /package/dist/{sdk → sync}/sync-options.js +0 -0
  80. /package/dist/{sdk → sync}/version-policy.js +0 -0
  81. /package/dist/{sdk → sync}/version-storage.js +0 -0
  82. /package/dist/{sdk → sync}/version-tracking.js +0 -0
@@ -510,9 +510,11 @@ call_llm/v0 → Use call_llm/v2
510
510
 
511
511
  | Scenario | Default | How to Add HITL |
512
512
  |----------|---------|-----------------|
513
- | Send email | ❌ No HITL | `persona(mode="update", id="...", input="add approval before sending")` |
514
- | External API | ❌ No HITL | Request explicitly in input |
515
- | Create records | ❌ No HITL | Request explicitly in input |
513
+ | Send email | ❌ No HITL | Get workflow add general_hitl node before email → deploy |
514
+ | External API | ❌ No HITL | Get workflow add general_hitl node → deploy |
515
+ | Create records | ❌ No HITL | Get workflow add general_hitl node → deploy |
516
+
517
+ **Pattern:** `workflow(mode="get")` → modify workflow_def → `workflow(mode="deploy")`
516
518
 
517
519
  ## Auto Builder Prompt Length Limits
518
520
 
package/README.md CHANGED
@@ -223,27 +223,25 @@ persona(input="IT helpdesk with KB search", type="chat", name="IT Support", prev
223
223
 
224
224
  **Modify existing AI Employee (LLM-Driven):**
225
225
  ```typescript
226
- // Step 1: Get workflow context
227
- persona(mode="modify", id="abc-123")
228
- // Returns: current_nodes, available_actions, example_operations
229
-
230
- // Step 2: Build and execute structured operations
231
- persona(mode="modify", id="abc-123", operations=[
232
- { type: "insert", insert: { action_type: "hitl", insert_before: "send_email" }},
233
- { type: "remove", remove: { nodes: ["unused_node"] }}
234
- ], preview=true)
235
- ```
226
+ // Step 1: Get current workflow
227
+ workflow(mode="get", persona_id="abc-123")
228
+ // Returns: workflow_def, schema, guidance, fingerprint
236
229
 
237
- **Fix issues automatically:**
238
- ```typescript
239
- persona(id="abc-123", optimize=true, preview=false)
230
+ // Step 2: LLM modifies the workflow_def JSON
231
+ // (add nodes, remove nodes, rewire connections)
232
+
233
+ // Step 3: Deploy modified workflow
234
+ // IMPORTANT: pass base_fingerprint from the get() response to prevent overwriting out-of-band changes
235
+ workflow(mode="deploy", persona_id="abc-123", base_fingerprint="<fingerprint>", workflow_def={...})
236
+
237
+ // If the workflow_def is large (near transport limits), deploy from a file instead:
238
+ // workflow(mode="deploy", persona_id="abc-123", base_fingerprint="<fingerprint>", workflow_def_path="/path/to/wf.json")
240
239
  ```
241
240
 
242
241
  **Get reference info:**
243
242
  ```typescript
244
- reference(type="envs") // List environments
245
- reference(type="actions", id="send_email") // Get action details
246
- reference(type="patterns", pattern="intent-routing") // Get pattern
243
+ catalog(type="actions", id="send_email") // Get action details
244
+ catalog(type="templates") // List templates
247
245
  ```
248
246
 
249
247
  ## Dynamic Resources
@@ -276,7 +274,7 @@ The MCP is fully self-contained—no external configuration files needed.
276
274
  - Read `ema://docs/usage-guide` (markdown) for documentation
277
275
  - Read `ema://guidance/cursor-rule` (.mdc) for IDE integration
278
276
 
279
- All guidance flows from a single source (`src/sdk/guidance.ts`).
277
+ All guidance flows from a single source (`src/mcp/guidance.ts`).
280
278
 
281
279
  ---
282
280
 
package/dist/cli/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { loadConfig } from "../sdk/config.js";
14
14
  import { EmaClient } from "../sdk/client.js";
15
- import { SyncSDK } from "../sdk/sync.js";
15
+ import { SyncSDK } from "../sync/sdk.js";
16
16
  function printUsage() {
17
17
  console.log(`
18
18
  Ema Agent Sync CLI
@@ -274,7 +274,7 @@ export function generateAutobuilderPrompt(description, personaType) {
274
274
  prompt += "- Use external_action_caller or send_email_agent as needed\n";
275
275
  }
276
276
  if (hasHITL) {
277
- prompt += "- Include general_hitl with success/failure paths\n";
277
+ prompt += "- Enable HITL flag on agents that need approval (do NOT add standalone general_hitl nodes)\n";
278
278
  }
279
279
  prompt += `- ${config.outputNote}\n`;
280
280
  }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Dashboard Domain Logic
3
+ *
4
+ * Business logic for dashboard operations including:
5
+ * - Sanitization of PII in column values
6
+ * - Nested data transformation with sanitization
7
+ * - Validation helpers
8
+ *
9
+ * Note: Pure format translation (without sanitization) is in SDK's columnValueToInput().
10
+ */
11
+ import { columnValueToInput } from "../../sdk/ema-client.js";
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+ // Sanitization
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+ /**
16
+ * Sanitization session for PII replacement.
17
+ * Maintains consistent replacements across multiple values.
18
+ */
19
+ export class SanitizationSession {
20
+ replacements = new Map();
21
+ counters = {};
22
+ /**
23
+ * Get or create a replacement for a detected PII value.
24
+ * Returns consistent replacements for the same value.
25
+ */
26
+ getOrCreateReplacement(value, type) {
27
+ const existing = this.replacements.get(value);
28
+ if (existing)
29
+ return existing;
30
+ this.counters[type] = (this.counters[type] ?? 0) + 1;
31
+ const replacement = `[${type.toUpperCase()}_${this.counters[type]}]`;
32
+ this.replacements.set(value, replacement);
33
+ return replacement;
34
+ }
35
+ /** Get all replacements made in this session */
36
+ getReplacements() {
37
+ return new Map(this.replacements);
38
+ }
39
+ }
40
+ /**
41
+ * Detect PII patterns in text using regex.
42
+ */
43
+ export function detectPiiPatterns(text) {
44
+ const detected = [];
45
+ // Email pattern
46
+ const emailRegex = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
47
+ for (const match of text.matchAll(emailRegex)) {
48
+ detected.push({ value: match[0], type: "email" });
49
+ }
50
+ // Phone pattern (various formats)
51
+ const phoneRegex = /\b(?:\+?1[-.\s]?)?\(?[0-9]{3}\)?[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}\b/g;
52
+ for (const match of text.matchAll(phoneRegex)) {
53
+ detected.push({ value: match[0], type: "phone" });
54
+ }
55
+ return detected;
56
+ }
57
+ /**
58
+ * Sanitize a string value by replacing detected PII.
59
+ */
60
+ export function sanitizeString(value, session, additionalPatterns) {
61
+ let result = value;
62
+ // Pattern-based sanitization
63
+ const detected = detectPiiPatterns(result);
64
+ for (const entity of detected) {
65
+ const replacement = session.getOrCreateReplacement(entity.value, entity.type);
66
+ result = result.split(entity.value).join(replacement);
67
+ }
68
+ // User-provided patterns
69
+ if (additionalPatterns) {
70
+ for (const pattern of additionalPatterns) {
71
+ if (result.includes(pattern)) {
72
+ const replacement = session.getOrCreateReplacement(pattern, "custom");
73
+ result = result.split(pattern).join(replacement);
74
+ }
75
+ }
76
+ }
77
+ return result;
78
+ }
79
+ // ─────────────────────────────────────────────────────────────────────────────
80
+ // Column Value Conversion with Sanitization
81
+ // ─────────────────────────────────────────────────────────────────────────────
82
+ /**
83
+ * Convert a column value to DashboardInput with optional sanitization.
84
+ *
85
+ * This wraps the SDK's pure conversion function and adds sanitization.
86
+ * Use this in MCP handlers when sanitization is needed.
87
+ *
88
+ * @param name - The column/field name
89
+ * @param value - The column value from API response
90
+ * @param session - Sanitization session (null to skip sanitization)
91
+ * @param additionalPatterns - Additional strings to sanitize
92
+ * @returns DashboardInput for upload, or null if value is empty/unsupported
93
+ */
94
+ export function columnValueToInputWithSanitization(name, value, session, additionalPatterns) {
95
+ if (!value)
96
+ return null;
97
+ // If no sanitization needed, delegate to SDK
98
+ if (!session) {
99
+ return columnValueToInput(name, value);
100
+ }
101
+ // Handle string value with sanitization
102
+ if (value.stringValue !== undefined) {
103
+ const sanitized = sanitizeString(value.stringValue, session, additionalPatterns);
104
+ return { name, string_value: sanitized };
105
+ }
106
+ // Handle number value (no sanitization needed)
107
+ if (value.numberValue !== undefined) {
108
+ return { name, number_value: value.numberValue };
109
+ }
110
+ // Handle boolean value (no sanitization needed)
111
+ if (value.booleanValue !== undefined) {
112
+ return { name, boolean_value: value.booleanValue };
113
+ }
114
+ // Handle array value (recursively with sanitization)
115
+ if (value.arrayValue) {
116
+ const elements = value.arrayValue.arrayValues ?? [];
117
+ if (elements.length === 0)
118
+ return null;
119
+ const arrayInputs = [];
120
+ for (const elem of elements) {
121
+ const converted = columnValueToInputWithSanitization("element", elem, session, additionalPatterns);
122
+ if (converted)
123
+ arrayInputs.push(converted);
124
+ }
125
+ return arrayInputs.length > 0 ? { name, array_value: arrayInputs } : null;
126
+ }
127
+ // Handle object value (recursively with sanitization)
128
+ if (value.objectValue) {
129
+ const objectVals = value.objectValue.objectValues ?? {};
130
+ const keys = Object.keys(objectVals);
131
+ if (keys.length === 0)
132
+ return null;
133
+ const objectInput = {};
134
+ for (const key of keys) {
135
+ const converted = columnValueToInputWithSanitization(key, objectVals[key], session, additionalPatterns);
136
+ if (converted)
137
+ objectInput[key] = converted;
138
+ }
139
+ return Object.keys(objectInput).length > 0 ? { name, object_value: objectInput } : null;
140
+ }
141
+ // Unsupported types - delegate to SDK (will return null)
142
+ return columnValueToInput(name, value);
143
+ }
144
+ // ─────────────────────────────────────────────────────────────────────────────
145
+ // Nested Data Validation
146
+ // ─────────────────────────────────────────────────────────────────────────────
147
+ /** Maximum array elements to prevent oversized payloads */
148
+ export const MAX_ARRAY_ELEMENTS = 1000;
149
+ /** Maximum nesting depth for arrays/objects */
150
+ export const MAX_NESTING_DEPTH = 3;
151
+ /**
152
+ * Validate nested data structure.
153
+ * Returns errors/warnings for issues found.
154
+ */
155
+ export function validateNestedData(value, schemaType, path = "", depth = 0) {
156
+ const errors = [];
157
+ // Check nesting depth
158
+ if (depth > MAX_NESTING_DEPTH) {
159
+ errors.push({
160
+ path,
161
+ message: `Nesting depth exceeds maximum of ${MAX_NESTING_DEPTH}`,
162
+ severity: "error",
163
+ });
164
+ return errors;
165
+ }
166
+ // Validate based on schema type
167
+ if (schemaType === "COLUMN_TYPE_ARRAY" || schemaType === "ARRAY") {
168
+ if (!Array.isArray(value)) {
169
+ errors.push({
170
+ path,
171
+ message: `Expected array for ARRAY column, got ${typeof value}`,
172
+ severity: "error",
173
+ });
174
+ }
175
+ else if (value.length > MAX_ARRAY_ELEMENTS) {
176
+ errors.push({
177
+ path,
178
+ message: `Array has ${value.length} elements, exceeds maximum of ${MAX_ARRAY_ELEMENTS}`,
179
+ severity: "error",
180
+ });
181
+ }
182
+ }
183
+ else if (schemaType === "COLUMN_TYPE_OBJECT" || schemaType === "OBJECT") {
184
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
185
+ errors.push({
186
+ path,
187
+ message: `Expected object for OBJECT column, got ${Array.isArray(value) ? "array" : typeof value}`,
188
+ severity: "error",
189
+ });
190
+ }
191
+ }
192
+ else if (schemaType === "COLUMN_TYPE_STRING" || schemaType === "STRING") {
193
+ if (typeof value !== "string") {
194
+ if (Array.isArray(value)) {
195
+ errors.push({
196
+ path,
197
+ message: `Column expects scalar STRING value, got array. Did you mean to use an ARRAY column?`,
198
+ severity: "error",
199
+ });
200
+ }
201
+ else if (typeof value === "object" && value !== null) {
202
+ errors.push({
203
+ path,
204
+ message: `Column expects scalar STRING value, got object. Did you mean to use an OBJECT column?`,
205
+ severity: "error",
206
+ });
207
+ }
208
+ }
209
+ }
210
+ return errors;
211
+ }
212
+ /**
213
+ * Check if a value contains nested data (array or object).
214
+ */
215
+ export function hasNestedData(value) {
216
+ if (Array.isArray(value))
217
+ return true;
218
+ if (typeof value === "object" && value !== null) {
219
+ // Check if it looks like a nested structure vs a simple object
220
+ const keys = Object.keys(value);
221
+ return keys.some(k => !["name", "string_value", "number_value", "boolean_value", "document_value"].includes(k));
222
+ }
223
+ return false;
224
+ }
@@ -7,7 +7,7 @@
7
7
  * The auto-builder sends ~70K tokens of documentation per request.
8
8
  * This schema reduces that to ~5K tokens of actionable constraints.
9
9
  */
10
- import { AGENT_CATALOG } from "./knowledge.js";
10
+ import { AGENT_CATALOG } from "../knowledge.js";
11
11
  import { INPUT_SOURCE_RULES } from "./validation-rules.js";
12
12
  // ─────────────────────────────────────────────────────────────────────────────
13
13
  // Schema Generation
@@ -34,7 +34,7 @@ import * as fs from "fs";
34
34
  import * as path from "path";
35
35
  import { fileURLToPath } from "node:url";
36
36
  import { dirname } from "node:path";
37
- import { getResourcePath } from "./paths.js";
37
+ import { getResourcePath } from "../../sdk/paths.js";
38
38
  // ESM-compatible __dirname
39
39
  const __filename = fileURLToPath(import.meta.url);
40
40
  const __dirname = dirname(__filename);
@@ -54,8 +54,8 @@ export function loadGateConfig(forceReload = false) {
54
54
  }
55
55
  // Try multiple paths (works in both src and dist)
56
56
  const possiblePaths = [
57
- path.resolve(__dirname, "../../resources/config/gates.json"),
58
57
  path.resolve(__dirname, "../../../resources/config/gates.json"),
58
+ path.resolve(__dirname, "../../../../resources/config/gates.json"),
59
59
  getResourcePath("resources/config/gates.json"),
60
60
  ];
61
61
  for (const configPath of possiblePaths) {
@@ -131,7 +131,7 @@ let _cachedPrompt = null;
131
131
  export function getIntentArchitectPrompt() {
132
132
  if (_cachedPrompt)
133
133
  return _cachedPrompt;
134
- const promptPath = path.join(__dirname, "../prompts/intent-architect.md");
134
+ const promptPath = path.join(__dirname, "../../prompts/intent-architect.md");
135
135
  try {
136
136
  _cachedPrompt = fs.readFileSync(promptPath, "utf-8");
137
137
  return _cachedPrompt;
@@ -129,6 +129,25 @@ export const STRUCTURAL_INVARIANTS = [
129
129
  fix: "Use entity_extraction to extract email_address, then connect to to_email",
130
130
  severity: "critical",
131
131
  },
132
+ {
133
+ id: "inline_type_format",
134
+ name: "Inline Values Must Use Correct Type Format",
135
+ rule: "When providing inline values, the format must match the expected input type. " +
136
+ "stringValue → STRING, textWithSources → TEXT_WITH_SOURCES, enumValue → ENUM. " +
137
+ "These are NOT interchangeable - the Go workflow engine validates types at deploy time.",
138
+ violation: "Node input expects TEXT_WITH_SOURCES but inline uses { wellKnown: { stringValue: '...' } } (STRING type)",
139
+ fix: "Change to { wellKnown: { textWithSources: { text: '...', sources: [], resultConfidence: 0, toolSources: [], resultType: 0 } } }",
140
+ severity: "critical",
141
+ },
142
+ {
143
+ id: "named_inputs_binding_format",
144
+ name: "Named Inputs Must Use multiBinding Format",
145
+ rule: "The named_inputs field in workflow_def requires protobuf-compatible multiBinding.elements[].namedBinding structure. " +
146
+ "Plain objects or arrays will cause deploy failure.",
147
+ violation: "named_inputs: { key: value } or named_inputs: [{ name: 'key', value: ... }]",
148
+ fix: "Use: { multiBinding: { elements: [{ namedBinding: { name: 'key', value: <binding>, description: '', isOptional: false }, autoDetectedBinding: false }] }, autoDetectedBinding: false }",
149
+ severity: "critical",
150
+ },
132
151
  {
133
152
  id: "no_redundant_classifiers",
134
153
  name: "No Redundant Classifiers",
@@ -313,6 +332,25 @@ BEFORE finalizing any workflow modification, verify these rules:
313
332
 
314
333
  10. **Both Paths Required**: general_hitl needs handlers for both approval AND rejection.
315
334
 
335
+ ### Raw workflow_def Format Rules (CRITICAL)
336
+
337
+ 11. **Inline Type Format** (wrong format → HTTP 500 with no error details):
338
+ | Input Expects | Inline Format |
339
+ |---------------|---------------|
340
+ | STRING | \`{ wellKnown: { stringValue: "..." } }\` |
341
+ | TEXT_WITH_SOURCES | \`{ wellKnown: { textWithSources: { text: "...", sources: [], resultConfidence: 0, toolSources: [], resultType: 0 } } }\` |
342
+ | ENUM | \`{ enumValue: "CategoryName" }\` |
343
+
344
+ **WARNING**: STRING ≠ TEXT_WITH_SOURCES. Wrong format → HTTP 500 with no error details.
345
+
346
+ 12. **named_inputs Format**: Must use \`multiBinding.elements[].namedBinding\` structure (protobuf JSON format). Plain objects or arrays cause deploy failure.
347
+
348
+ 13. **Action Namespaces**: Must be \`["actions", "emainternal"]\`, not empty \`[]\`. Empty namespaces cause deploy failure.
349
+
350
+ 14. **Categorizer typeArguments**: Categorizers require \`typeArguments.categories\` pointing to an enumType. Empty \`typeArguments: {}\` causes deploy failure.
351
+
352
+ 15. **Dashboard Document Upload Order**: For dashboard personas, documents must be uploaded AFTER the persona is enabled. The workflow must be active to process input documents. Order: create → deploy workflow → enable → upload documents.
353
+
316
354
  ### Self-Check Checklist
317
355
 
318
356
  After generating/modifying a workflow, verify:
@@ -325,6 +363,11 @@ After generating/modifying a workflow, verify:
325
363
  - [ ] Response nodes are mutually exclusive (gated by runIf)
326
364
  - [ ] Email recipients come from entity_extraction
327
365
  - [ ] No circular dependencies
366
+ - [ ] Inline values use correct type format (stringValue vs textWithSources)
367
+ - [ ] named_inputs uses multiBinding.elements[].namedBinding format
368
+ - [ ] Action namespaces are ["actions", "emainternal"], not empty []
369
+ - [ ] Categorizers have typeArguments.categories pointing to enumType
370
+ - [ ] Dashboard personas: enable persona BEFORE uploading documents
328
371
  `;
329
372
  export const GRAPH_ANALYSIS_RULES = [
330
373
  // ═══════════════════════════════════════════════════════════════════════════
@@ -32,11 +32,17 @@ export const INPUT_SOURCE_RULES = [
32
32
  },
33
33
  {
34
34
  actionPattern: "text_categorizer",
35
- recommended: "user_query",
36
- avoid: ["chat_conversation"],
37
- reason: "text_categorizer expects TEXT_WITH_SOURCES, not CHAT_CONVERSATION. Use chat_categorizer instead for conversations.",
35
+ recommended: "named_inputs (v1) or user_query (v0 deprecated)",
36
+ avoid: ["chat_conversation", "stringValue for categorization_instructions"],
37
+ reason: "text_categorizer/v1 uses named_inputs (multiBinding format) for input context. " +
38
+ "categorization_instructions must be TEXT_WITH_SOURCES (textWithSources), not STRING (stringValue). " +
39
+ "v0 is DEPRECATED - use v1 with named_inputs. " +
40
+ "For conversation routing, use chat_categorizer instead.",
38
41
  severity: "critical",
39
- fix: "Use chat_categorizer for conversation routing, or use user_query/summarized_conversation for text_categorizer",
42
+ fix: "Use text_categorizer/v1 with: named_inputs (multiBinding format), " +
43
+ "categorization_instructions (textWithSources inline), " +
44
+ "typeArguments.categories pointing to enumType, " +
45
+ "and ensure enumType has Fallback category",
40
46
  },
41
47
  {
42
48
  actionPattern: "respond_with_sources",
@@ -126,12 +132,13 @@ export const ANTI_PATTERNS = [
126
132
  {
127
133
  id: "email-without-validation",
128
134
  name: "Email Without Input Validation",
129
- pattern: "send_email_agent without entity_extraction and HITL confirmation",
135
+ pattern: "send_email_agent without entity_extraction to validate recipient data",
130
136
  problem: "Sending emails without extracting and validating recipient data risks sending to wrong people or with wrong content. Emails are high-impact actions with external side effects.",
131
- solution: "Always: 1) Extract required fields (email_address, subject) via entity_extraction, 2) Validate completeness via categorizer, 3) Ask user if missing, 4) Confirm via HITL before sending.",
137
+ solution: "Always: 1) Extract required fields (email_address, subject) via entity_extraction, 2) Validate completeness via categorizer, 3) Ask user if missing. " +
138
+ "For approval: enable the HITL flag on send_email_agent (do NOT add a standalone general_hitl node).",
132
139
  detection: {
133
- issueType: "incomplete_hitl",
134
- condition: "send_email_agent without preceding entity_extraction AND hitl nodes",
140
+ issueType: "incomplete_email_validation",
141
+ condition: "send_email_agent without preceding entity_extraction node to extract/validate recipient data",
135
142
  },
136
143
  severity: "critical",
137
144
  },
@@ -188,12 +195,15 @@ export const ANTI_PATTERNS = [
188
195
  name: "Incomplete HITL Paths",
189
196
  pattern: "HITL with only success path",
190
197
  problem: "Rejected requests have no handling, leaving users without response.",
191
- solution: "ALWAYS implement both success AND failure paths for general_hitl.",
198
+ solution: "If workflow already has general_hitl node: ALWAYS implement both success AND failure paths. For NEW workflows: use HITL flag on the agent instead of standalone general_hitl nodes (see hitl-patterns guidance).",
192
199
  detection: {
193
200
  issueType: "incomplete_hitl",
194
201
  condition: "HITL node missing 'hitl_status_HITL Success' or 'hitl_status_HITL Failure' edge (note: space, not underscore)",
195
202
  },
196
203
  severity: "critical",
204
+ // TODO: Revisit if general_hitl is re-enabled as a standalone node.
205
+ // Currently HITL is a flag on agents (send_email_agent, external_action_caller).
206
+ // This anti-pattern still fires for existing workflows that use general_hitl.
197
207
  },
198
208
  {
199
209
  id: "orphan-nodes",
@@ -317,6 +327,83 @@ export const ANTI_PATTERNS = [
317
327
  },
318
328
  severity: "critical",
319
329
  },
330
+ {
331
+ id: "type-mismatch-inline",
332
+ name: "Inline Type Mismatch",
333
+ pattern: "Using stringValue inline where TEXT_WITH_SOURCES input expected, or vice versa",
334
+ problem: "The Go workflow engine validates type compatibility at deploy time. " +
335
+ "stringValue (STRING type) is NOT interchangeable with textWithSources (TEXT_WITH_SOURCES type). " +
336
+ "Mismatches cause HTTP 500 with no error details.",
337
+ solution: "Match inline value format to expected input type:\n" +
338
+ "- STRING input → { inline: { wellKnown: { stringValue: '...' } } }\n" +
339
+ "- TEXT_WITH_SOURCES input → { inline: { wellKnown: { textWithSources: { text: '...', sources: [], resultConfidence: 0, toolSources: [], resultType: 0 } } } }\n" +
340
+ "- ENUM input → { inline: { enumValue: 'CategoryName' } }",
341
+ detection: {
342
+ issueType: "type_mismatch_inline",
343
+ condition: "Inline value format doesn't match expected input type",
344
+ },
345
+ severity: "critical",
346
+ },
347
+ {
348
+ id: "wrong-named-inputs-format",
349
+ name: "Wrong named_inputs Format",
350
+ pattern: "Using plain objects or arrays for named_inputs instead of multiBinding format",
351
+ problem: "named_inputs expects protobuf-compatible multiBinding.elements[].namedBinding structure. " +
352
+ "Plain JSON objects cause deploy failure (HTTP 500).",
353
+ solution: "Use the correct multiBinding format:\n" +
354
+ '{ "multiBinding": { "elements": [{ "namedBinding": { "name": "key", "value": <binding>, "description": "", "isOptional": false }, "autoDetectedBinding": false }] }, "autoDetectedBinding": false }',
355
+ detection: {
356
+ issueType: "wrong_named_inputs_format",
357
+ condition: "named_inputs value is not a multiBinding structure",
358
+ },
359
+ severity: "critical",
360
+ },
361
+ {
362
+ id: "categorizer-missing-type-arguments",
363
+ name: "Categorizer Missing typeArguments",
364
+ pattern: "text_categorizer or chat_categorizer with empty typeArguments ({})",
365
+ problem: "Categorizers require typeArguments.categories pointing to the enum type. " +
366
+ "Empty typeArguments causes deploy failure.",
367
+ solution: "Add typeArguments:\n" +
368
+ '{ "typeArguments": { "categories": { "enumType": { "name": { "name": "my_enum", "namespaces": [] } }, "isList": false } } }\n' +
369
+ "And ensure a matching enumType exists in workflow_def.enumTypes[].",
370
+ detection: {
371
+ issueType: "categorizer_missing_type_arguments",
372
+ condition: "Categorizer node has empty typeArguments or missing categories key",
373
+ },
374
+ severity: "critical",
375
+ },
376
+ {
377
+ id: "empty-action-namespaces",
378
+ name: "Empty Action Namespaces",
379
+ pattern: "Action with namespaces: [] instead of proper namespace path",
380
+ problem: "Actions require namespaces: ['actions', 'emainternal']. " +
381
+ "Empty namespaces cause deploy failure.",
382
+ solution: "Use the correct namespace format:\n" +
383
+ '{ "action": { "name": { "namespaces": ["actions", "emainternal"], "name": "call_llm" }, "version": "v2" } }',
384
+ detection: {
385
+ issueType: "empty_action_namespaces",
386
+ condition: "Action has empty namespaces array []",
387
+ },
388
+ severity: "critical",
389
+ },
390
+ {
391
+ id: "dashboard-upload-before-enable",
392
+ name: "Dashboard Documents Uploaded Before Persona Enabled",
393
+ pattern: "Uploading input documents to a dashboard persona before the persona is enabled",
394
+ problem: "For dashboard personas, the workflow must be active (persona enabled) before uploading input documents. " +
395
+ "If documents are uploaded before enabling, the workflow isn't active to process them and the upload will fail.",
396
+ solution: "Follow this order:\n" +
397
+ "1. Create persona\n" +
398
+ "2. Deploy workflow: workflow(mode='deploy', persona_id='...', workflow_def={...})\n" +
399
+ "3. Enable persona (activate it)\n" +
400
+ "4. Upload documents: persona(id='...', data={method:'upload', items:[...]})",
401
+ detection: {
402
+ issueType: "dashboard_upload_before_enable",
403
+ condition: "Dashboard persona receives document upload while persona is not enabled/active",
404
+ },
405
+ severity: "critical",
406
+ },
320
407
  {
321
408
  id: "dashboard-input-columns",
322
409
  name: "Dashboard Input Columns From Trigger Outputs",
@@ -349,6 +436,64 @@ export const ANTI_PATTERNS = [
349
436
  },
350
437
  severity: "critical",
351
438
  },
439
+ {
440
+ id: "chat-conversation-without-chat-response",
441
+ name: "Chat Conversation Not Wired to Chat Response Node",
442
+ pattern: "Chat workflow where chat_conversation is consumed by processing nodes but never wired to a chat-aware response node (respond_with_sources or respond_for_external_actions)",
443
+ problem: "When chat_conversation is processed by nodes like entity_extraction, call_llm, or search, " +
444
+ "but the final response is generated by a generic call_llm (not a chat-aware response node), " +
445
+ "the response node lacks conversation history context. This causes:\n" +
446
+ "1. Duplicate/repetitive responses — the LLM re-asks questions already answered in conversation\n" +
447
+ "2. Lost context — follow-up messages lose prior context\n" +
448
+ "3. Hallucinated greetings — the LLM generates its own greeting instead of continuing the conversation\n\n" +
449
+ "The chat-aware response nodes (respond_with_sources, respond_for_external_actions) are specifically " +
450
+ "designed to handle conversation history and produce contextually appropriate responses.",
451
+ solution: "Wire chat_conversation through to a chat-aware response node:\n" +
452
+ "• For search-based responses: use respond_with_sources (takes query + search_results)\n" +
453
+ "• For tool/action results: use respond_for_external_actions (takes query + external_action_result)\n" +
454
+ "• If using call_llm as responder: wire chat_conversation into named_inputs so the LLM sees full history\n\n" +
455
+ "Correct patterns:\n" +
456
+ " chat_trigger → search → respond_with_sources → WORKFLOW_OUTPUT\n" +
457
+ " chat_trigger → external_action_caller → respond_for_external_actions → WORKFLOW_OUTPUT\n" +
458
+ " chat_trigger → call_llm(named_inputs includes chat_conversation) → WORKFLOW_OUTPUT\n\n" +
459
+ "Anti-patterns:\n" +
460
+ " ❌ chat_trigger → search → call_llm (no conversation context) → WORKFLOW_OUTPUT\n" +
461
+ " ❌ chat_trigger → entity_extraction → call_llm (stateless) → WORKFLOW_OUTPUT",
462
+ detection: {
463
+ issueType: "chat_conversation_not_wired_to_response",
464
+ condition: "Chat workflow (chat_trigger) where terminal response node is call_llm without chat_conversation in named_inputs, " +
465
+ "and respond_with_sources / respond_for_external_actions are not used",
466
+ },
467
+ severity: "critical",
468
+ },
469
+ {
470
+ id: "entity-extraction-direct-to-email",
471
+ name: "Entity Extraction Wired Directly to Email Inputs",
472
+ pattern: "entity_extraction outputs connected directly to send_email_agent.email_to, email_subject, or email_body",
473
+ problem: "entity_extraction outputs are typed as WELL_KNOWN_TYPE_ANY (structured extraction results), " +
474
+ "but send_email_agent inputs (email_to, email_subject, email_body) require WELL_KNOWN_TYPE_TEXT_WITH_SOURCES. " +
475
+ "Direct wiring causes type mismatch errors or corrupted email fields.\n\n" +
476
+ "Additionally, entity_extraction outputs may contain multiple extracted values in a structured format — " +
477
+ "wiring the entire output to email_to would send an object/JSON as the recipient address, not a clean email string.",
478
+ solution: "Use intermediary nodes to extract and format individual fields:\n\n" +
479
+ "Pattern A — json_mapper + fixed_response (recommended for multiple fields):\n" +
480
+ " entity_extraction → json_mapper (extract 'to', 'subject', 'body') → fixed_response({{to}}) → send_email_agent.email_to\n" +
481
+ " Same json_mapper → fixed_response({{subject}}) → send_email_agent.email_subject\n" +
482
+ " Same json_mapper → fixed_response({{body}}) → send_email_agent.email_body\n\n" +
483
+ "Pattern B — custom_agent with output_fields (simpler for LLM-generated content):\n" +
484
+ " custom_agent(output_fields=[To,Subject,Body]) → send_email_agent\n" +
485
+ " (custom_agent with output_fields produces individual TEXT_WITH_SOURCES outputs)\n\n" +
486
+ "Pattern C — fixed_response with template variables (for simple extraction):\n" +
487
+ " entity_extraction → fixed_response(template='{{email_address}}', custom_data=entity_extraction) → send_email_agent.email_to\n\n" +
488
+ "Key principle: send_email_agent inputs need TEXT_WITH_SOURCES — always use fixed_response or " +
489
+ "custom_agent(output_fields) as the adapter between extraction results and email fields.",
490
+ detection: {
491
+ issueType: "entity_extraction_direct_to_email",
492
+ condition: "entity_extraction output connected directly to send_email_agent.email_to, email_subject, or email_body " +
493
+ "without an intermediary json_mapper/fixed_response/custom_agent node",
494
+ },
495
+ severity: "critical",
496
+ },
352
497
  ];
353
498
  export const OPTIMIZATION_RULES = [
354
499
  {