@ema.co/mcp-toolkit 2026.3.24 → 2026.3.25-2

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.
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Dynamic Contextual Guidance Middleware
3
+ *
4
+ * Post-processes every MCP tool response to inject error-aware guidance.
5
+ *
6
+ * Resolution order:
7
+ * 1. Live DE search (errors only — semantic match on error context)
8
+ * 2. DE cache (shape-based key match + wildcards)
9
+ * 3. Universal defaults (always available)
10
+ *
11
+ * Handler-set hints are never overwritten.
12
+ *
13
+ * Template variables in guidance strings are resolved from the result context:
14
+ * {tool}, {method}, {count}, {unfiltered_count}, {persona_id}, {env}, {profile}, {error}
15
+ */
16
+ import { classifyResult } from "./classify.js";
17
+ import { getDefaultGuidance } from "./defaults.js";
18
+ /** Shapes that trigger a live DE search for contextual error guidance. */
19
+ const ERROR_SHAPES = new Set([
20
+ "error", "error_400", "error_500", "error_validation", "deploy_failed",
21
+ ]);
22
+ /**
23
+ * Enrich an MCP tool response with contextual guidance.
24
+ *
25
+ * Call this after the handler returns, before other hint injection layers.
26
+ * Only injects fields the handler didn't already set.
27
+ *
28
+ * For error responses, queries DE live with the error context to find
29
+ * relevant solutions — including agent-published fixes for similar errors.
30
+ */
31
+ export async function enrichWithGuidance(result, ctx, cache, searchFn) {
32
+ const shape = classifyResult(result, ctx.unfilteredCount ?? result._unfiltered_count);
33
+ const method = ctx.method ?? "default";
34
+ // 1. Live DE search for errors — contextual, semantic match
35
+ let liveHints;
36
+ if (ERROR_SHAPES.has(shape) && searchFn) {
37
+ liveHints = await searchDeForGuidance(ctx, shape, searchFn);
38
+ }
39
+ // 2. DE cache — exact + wildcard key match
40
+ const cacheAtom = cache?.getContextualGuidance(ctx.tool, method, shape);
41
+ const cacheHints = cacheAtom?.hints;
42
+ // 3. Universal defaults (always available)
43
+ const defaultHints = getDefaultGuidance(shape, ctx);
44
+ // Merge: live DE > cache > defaults. Handler wins over all.
45
+ const merged = mergeHints(defaultHints, mergeHints(cacheHints ?? {}, liveHints));
46
+ // Resolve template variables
47
+ const resolved = resolveTemplates(merged, ctx);
48
+ // Inject — never overwrite what the handler already set
49
+ for (const [field, value] of Object.entries(resolved)) {
50
+ if (value != null && !(field in result)) {
51
+ result[field] = value;
52
+ }
53
+ }
54
+ // Clean up internal fields
55
+ delete result._unfiltered_count;
56
+ delete result._result_shape;
57
+ }
58
+ /**
59
+ * Build a contextual search query from the error and request, then
60
+ * search DE for relevant guidance (including agent-published solutions).
61
+ */
62
+ async function searchDeForGuidance(ctx, shape, searchFn) {
63
+ // Build a query that aligns with how guidance atoms are written:
64
+ // tool + method + key error terms + shape
65
+ const error = String(ctx.result.error ?? "");
66
+ const errorTerms = extractErrorTerms(error);
67
+ const query = [
68
+ ctx.tool,
69
+ ctx.method ?? "",
70
+ shape,
71
+ errorTerms,
72
+ ].filter(Boolean).join(" ");
73
+ try {
74
+ const results = await searchFn(query, { limit: 3 });
75
+ if (!results.length)
76
+ return undefined;
77
+ return extractHintsFromSearchResults(results);
78
+ }
79
+ catch {
80
+ // Live search must never break tool dispatch
81
+ return undefined;
82
+ }
83
+ }
84
+ /**
85
+ * Extract actionable terms from an error message for DE search.
86
+ * Strips noise (HTTP codes, generic text) and keeps specific identifiers.
87
+ */
88
+ function extractErrorTerms(error) {
89
+ // Extract field names, action names, input names from error messages
90
+ // e.g., "Missing inputs: patient_scheduling_agent.named_inputs[Current_Request]"
91
+ // → "named_inputs Current_Request"
92
+ // e.g., "invalid_inputs action_name: scheduling_prompt_builder input_name: named_inputs_User_Query"
93
+ // → "invalid_inputs scheduling_prompt_builder named_inputs_User_Query"
94
+ const terms = [];
95
+ // Extract named identifiers: action_name: "X", input_name: "Y"
96
+ const namedMatches = error.matchAll(/(?:action_name|input_name|output_name):\s*"?(\w+)"?/g);
97
+ for (const m of namedMatches)
98
+ terms.push(m[1]);
99
+ // Extract bracket references: named_inputs[X]
100
+ const bracketMatches = error.matchAll(/(\w+)\[(\w+)\]/g);
101
+ for (const m of bracketMatches) {
102
+ terms.push(m[1]); // e.g., "named_inputs"
103
+ terms.push(m[2]); // e.g., "Current_Request"
104
+ }
105
+ // Extract key error keywords
106
+ const keywords = [
107
+ "named_inputs", "extraction_columns", "typeArguments", "namedResults",
108
+ "workflowName", "namespaces", "type_mismatch", "invalid_inputs",
109
+ "missing_inputs", "deprecated", "circular",
110
+ ];
111
+ for (const kw of keywords) {
112
+ if (error.toLowerCase().includes(kw.toLowerCase())) {
113
+ terms.push(kw);
114
+ }
115
+ }
116
+ // Deduplicate
117
+ return [...new Set(terms)].join(" ");
118
+ }
119
+ /**
120
+ * Extract guidance hints from DE search results.
121
+ * Looks for structData fields matching hint field names,
122
+ * or falls back to using content as _tip.
123
+ */
124
+ function extractHintsFromSearchResults(results) {
125
+ const hints = {};
126
+ for (const r of results) {
127
+ const sd = r.structData ?? {};
128
+ // Try typed_json first (structured guidance atoms)
129
+ const typedJson = sd.typed_json;
130
+ if (typedJson) {
131
+ try {
132
+ const data = JSON.parse(typedJson);
133
+ if (data.hints) {
134
+ return mergeHints(hints, data.hints);
135
+ }
136
+ }
137
+ catch { /* not a guidance atom */ }
138
+ }
139
+ // Fall back: use summary/description as tip, content as debug step
140
+ if (!hints._tip && (sd.summary || sd.description)) {
141
+ hints._tip = String(sd.summary ?? sd.description);
142
+ }
143
+ if (!hints._next_step && sd.next_step) {
144
+ hints._next_step = String(sd.next_step);
145
+ }
146
+ if (r.content && !hints._debug_steps) {
147
+ // Truncate long content to a useful snippet
148
+ const snippet = r.content.slice(0, 500);
149
+ hints._debug_steps = [snippet];
150
+ }
151
+ }
152
+ return hints;
153
+ }
154
+ /**
155
+ * Merge two hint objects. `override` fields take precedence over `base`.
156
+ */
157
+ function mergeHints(base, override) {
158
+ if (!override)
159
+ return { ...base };
160
+ return {
161
+ _next_step: override._next_step ?? base._next_step,
162
+ _required_next_steps: override._required_next_steps ?? base._required_next_steps,
163
+ _warning: override._warning ?? base._warning,
164
+ _tip: override._tip ?? base._tip,
165
+ _likely_causes: override._likely_causes ?? base._likely_causes,
166
+ _debug_steps: override._debug_steps ?? base._debug_steps,
167
+ };
168
+ }
169
+ /**
170
+ * Resolve {variable} placeholders in hint strings using result context.
171
+ */
172
+ function resolveTemplates(hints, ctx) {
173
+ const vars = {
174
+ tool: ctx.tool,
175
+ method: ctx.method ?? "default",
176
+ count: String(ctx.result.count ?? ""),
177
+ unfiltered_count: String(ctx.unfilteredCount ?? ctx.result._unfiltered_count ?? ""),
178
+ persona_id: String(ctx.result.persona_id ?? ctx.args.persona_id ?? ctx.args.id ?? ""),
179
+ env: ctx.env ?? "",
180
+ profile: ctx.profile ?? "",
181
+ error: String(ctx.result.error ?? ""),
182
+ };
183
+ const resolve = (s) => s.replace(/\{(\w+)\}/g, (_, key) => vars[key] ?? `{${key}}`);
184
+ const resolveArray = (arr) => arr.map(resolve);
185
+ return {
186
+ _next_step: hints._next_step ? resolve(hints._next_step) : undefined,
187
+ _required_next_steps: hints._required_next_steps ? resolveArray(hints._required_next_steps) : undefined,
188
+ _warning: hints._warning ? resolve(hints._warning) : undefined,
189
+ _tip: hints._tip ? resolve(hints._tip) : undefined,
190
+ _likely_causes: hints._likely_causes ? resolveArray(hints._likely_causes) : undefined,
191
+ _debug_steps: hints._debug_steps ? resolveArray(hints._debug_steps) : undefined,
192
+ };
193
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Dynamic Contextual Guidance — Type Definitions
3
+ *
4
+ * Result shapes, guidance atoms, and context types used by the
5
+ * guidance middleware to inject error-aware hints into every MCP response.
6
+ */
7
+ export {};
@@ -72,9 +72,10 @@ ${important.map((r) => `- **${r.title}**: ${r.do}`).join("\n")}
72
72
 
73
73
  ## Workflow Modification Flow
74
74
  The LLM generates the full workflow_def. MCP provides data and executes.
75
- For workflow_def format details, see \`ema://rules/workflow-constraints\` and \`ema://docs/field-constraints\`.
75
+ For workflow_def format details, search \`knowledge("workflow constraints")\` and \`knowledge("field constraints")\`.
76
76
 
77
77
  ${toolSections}## Resources
78
+ Use \`knowledge("topic")\` to search for any concept. These resources are also available for direct access:
78
79
  ${resourceList}
79
80
 
80
81
  ## Feedback Welcome
@@ -115,13 +116,15 @@ function generateDecisionFlow(tools) {
115
116
  const deployWorkflow = opExample("workflow", "Deploy");
116
117
  sections.push(`**Creating a new AI Employee?**
117
118
  1. \`${listTemplates}\` → browse templates, pick one (template_id is REQUIRED)
118
- 2. \`knowledge("search and respond pattern")\` → learn the correct workflow pattern
119
+ 2. \`knowledge("workflow patterns for <your use case>")\` → learn the correct workflow pattern
119
120
  3. \`${createPersona}\` → creates persona
120
- 4. \`${getWorkflow}\` → get starter workflow + generation schema + fingerprint
121
- 5. Build a complete workflow_def using the generation schema + knowledge patterns
121
+ 4. \`${getWorkflow}\` → get starter workflow + generation schema (FULL input/output specs from API) + fingerprint
122
+ 5. Build a complete workflow_def using the generation schema it shows ALL required inputs per action
122
123
  6. Upload data sources if needed — \`persona(id="<new_id>", data={method:"upload", path:"/path/to/doc.pdf"})\`
123
- 7. \`${deployWorkflow}\`
124
- **A persona without a deployed workflow is useless.** Template starters are incomplete — you MUST build and deploy a real workflow.`);
124
+ 7. \`workflow(mode="validate", persona_id="...", workflow_def={...})\` → catch errors BEFORE deploying
125
+ 8. \`${deployWorkflow}\`
126
+ **A persona without a deployed workflow is useless.** Template starters are incomplete — you MUST build and deploy a real workflow.
127
+ **If deployment fails:** search \`knowledge("workflow deploy <error keywords>")\` for solutions. Do NOT retry with the same workflow_def.`);
125
128
  }
126
129
  // Modify workflow
127
130
  if (tools.workflow) {
@@ -129,8 +132,9 @@ function generateDecisionFlow(tools) {
129
132
  const deploy = opExample("workflow", "Deploy");
130
133
  sections.push(`**Modifying an existing AI Employee's workflow?**
131
134
  1. \`${get}\` → get current workflow_def + schema + fingerprint
132
- 2. LLM modifies the workflow_def JSON
133
- 3. \`${deploy}\``);
135
+ 2. LLM modifies the workflow_def JSON (use the returned workflow_def as format reference)
136
+ 3. \`workflow(mode="validate", persona_id="...", workflow_def={...})\` → catch errors before deploying
137
+ 4. \`${deploy}\``);
134
138
  }
135
139
  // Explore
136
140
  if (tools.persona) {
@@ -321,7 +321,7 @@ export async function handleData(args, client, readFile) {
321
321
  "Fix the following issues:",
322
322
  ...validationErrors.map(e => ` - ${e.field}: ${e.message}`),
323
323
  "",
324
- "See ema://rules/nested-data-format for proper nested data structure.",
324
+ "Search knowledge(\"nested data format\") for proper nested data structure.",
325
325
  ],
326
326
  };
327
327
  }
@@ -10,8 +10,9 @@ export async function handleDebug(args, client) {
10
10
  if (!method) {
11
11
  return {
12
12
  error: "Missing required parameter: method",
13
- available_methods: ["conversations", "conversation_detail", "show_work", "action_detail", "search"],
14
- _tip: "Start with debug(method='conversations', persona_id='...') to list audit conversations.",
13
+ available_methods: ["conversations", "conversation_detail", "show_work", "action_detail", "workflow_runs", "search"],
14
+ _tip: "Start with debug(method='conversations', persona_id='...') for chat/voice personas, " +
15
+ "or debug(method='workflow_runs', persona_id='...') for dashboard personas.",
15
16
  };
16
17
  }
17
18
  switch (method) {
@@ -23,6 +24,8 @@ export async function handleDebug(args, client) {
23
24
  return handleShowWork(args, client);
24
25
  case "action_detail":
25
26
  return handleActionDetail(args, client);
27
+ case "workflow_runs":
28
+ return handleWorkflowRuns(args, client);
26
29
  case "search":
27
30
  try {
28
31
  return await handleSearch(args, client);
@@ -36,7 +39,7 @@ export async function handleDebug(args, client) {
36
39
  default:
37
40
  return {
38
41
  error: `Unknown method: ${method}`,
39
- available_methods: ["conversations", "conversation_detail", "show_work", "action_detail", "search"],
42
+ available_methods: ["conversations", "conversation_detail", "show_work", "action_detail", "workflow_runs", "search"],
40
43
  };
41
44
  }
42
45
  }
@@ -97,21 +100,36 @@ async function handleConversationDetail(args, client) {
97
100
  catch (error) {
98
101
  const msg = error instanceof Error ? error.message : String(error);
99
102
  const isNotFound = msg.includes("not_found") || msg.includes("NOT_FOUND") || msg.includes("not found");
103
+ if (isNotFound && personaId) {
104
+ // Auto-route: treat the ID as a workflow_run_id and try show_work
105
+ try {
106
+ const showWorkResponse = await client.getWorkflowLevelDebugLog(conversationId, personaId);
107
+ const formatted = formatShowWork(showWorkResponse, conversationId, personaId);
108
+ return {
109
+ ...formatted,
110
+ _auto_routed: true,
111
+ _note: `"${conversationId}" was not a conversation — auto-routed to show_work as workflow_run_id.`,
112
+ };
113
+ }
114
+ catch { /* fall through to the original not-found error */ }
115
+ }
100
116
  if (isNotFound) {
101
117
  return {
102
118
  error: `Conversation not found: ${conversationId}`,
103
- _tip: "If this ID came from a debug URL or dashboard, it may be a workflow_run_id (not a conversation_id). " +
104
- "Try: debug(method='show_work', persona_id='...', workflow_run_id='" + conversationId + "')",
119
+ _tip: personaId
120
+ ? `Auto-routing to show_work also failed. Verify the ID is correct.`
121
+ : "If this ID came from a dashboard, it may be a workflow_run_id. " +
122
+ "Try: debug(method='show_work', persona_id='...', workflow_run_id='" + conversationId + "')",
105
123
  };
106
124
  }
107
125
  throw error;
108
126
  }
109
127
  }
110
128
  async function handleShowWork(args, client) {
111
- const workflowRunId = args.workflow_run_id;
129
+ const workflowRunId = (args.workflow_run_id ?? args.conversation_id);
112
130
  const personaId = args.persona_id;
113
131
  if (!workflowRunId) {
114
- return { error: "Missing required parameter: workflow_run_id" };
132
+ return { error: "Missing required parameter: workflow_run_id (or conversation_id)" };
115
133
  }
116
134
  if (!personaId) {
117
135
  return {
@@ -144,6 +162,61 @@ async function handleActionDetail(args, client) {
144
162
  const response = await client.getActionLevelShowWorkLog(actionName, workflowRunId, personaId);
145
163
  return formatActionDetail(response, workflowRunId, actionName);
146
164
  }
165
+ async function handleWorkflowRuns(args, client) {
166
+ const personaId = args.persona_id;
167
+ if (!personaId) {
168
+ return { error: "Missing required parameter: persona_id" };
169
+ }
170
+ const limit = typeof args.limit === "number" ? args.limit : 20;
171
+ // Get persona to find dashboard ID
172
+ let persona = null;
173
+ try {
174
+ persona = await client.getPersonaById(personaId);
175
+ }
176
+ catch {
177
+ return {
178
+ error: `Could not fetch persona: ${personaId}`,
179
+ _tip: "Check that persona_id is valid and you have access.",
180
+ };
181
+ }
182
+ const dashboardId = persona?.workflow_dashboard_id;
183
+ if (!dashboardId) {
184
+ return {
185
+ error: "This persona is not a dashboard persona (no workflow_dashboard_id).",
186
+ _tip: "For chat/voice personas, use debug(method='conversations', persona_id='...') instead.",
187
+ };
188
+ }
189
+ try {
190
+ const rowsResponse = await client.getDashboardRows(dashboardId, personaId, { limit });
191
+ const rows = (rowsResponse?.rows ?? []);
192
+ const runs = rows.map((row) => {
193
+ const id = row.id ?? row.row_id ?? "";
194
+ const status = row.status ?? row.state ?? "unknown";
195
+ const createdAt = row.created_at ?? row.createdAt ?? "";
196
+ const workflowRunId = row.workflow_run_id ?? row.workflowRunId ?? id;
197
+ return { row_id: id, workflow_run_id: workflowRunId, status, created_at: createdAt };
198
+ });
199
+ return {
200
+ persona_id: personaId,
201
+ dashboard_id: dashboardId,
202
+ count: runs.length,
203
+ workflow_runs: runs,
204
+ _tip: runs.length > 0
205
+ ? `Found ${runs.length} workflow run(s). Drill into a run: debug(method='show_work', persona_id='${personaId}', workflow_run_id='<id>')`
206
+ : "No workflow runs found. Upload data or trigger a run first.",
207
+ _next_step: runs.length > 0
208
+ ? `debug(method='show_work', persona_id='${personaId}', workflow_run_id='${runs[0].workflow_run_id}')`
209
+ : undefined,
210
+ };
211
+ }
212
+ catch (err) {
213
+ const msg = err instanceof Error ? err.message : String(err);
214
+ return {
215
+ error: `Failed to fetch dashboard rows: ${msg}`,
216
+ _tip: "The dashboard API may require specific permissions. Check your access.",
217
+ };
218
+ }
219
+ }
147
220
  async function handleSearch(args, client) {
148
221
  const personaId = args.persona_id;
149
222
  const query = args.query;
@@ -167,14 +167,14 @@ function handleList(getEnvironments, toolkit) {
167
167
  workflow: [
168
168
  "1. List personas: persona(method=\"list\")",
169
169
  "2. Get workflow data: workflow(mode=\"get\", persona_id=\"...\")",
170
- "3. Analyze workflow: Fetch ema://rules/anti-patterns and check workflow_def against rules",
170
+ "3. Analyze workflow: Search knowledge(\"workflow anti-patterns\") and check workflow_def against rules",
171
171
  "4. Deploy workflow: workflow(mode=\"deploy\", persona_id=\"...\", workflow_def={...})",
172
172
  ],
173
173
  critical_rules: criticalRules,
174
174
  resources: [
175
- "ema://docs/usage-guide - Complete guide",
176
- "ema://catalog/agents-summary - Action catalog",
177
- "ema://guidance/rules - Structured rules (JSON)",
175
+ "knowledge(\"usage guide\") - Complete guide",
176
+ "knowledge(\"action catalog\") - Available actions",
177
+ "knowledge(\"workflow rules\") - Structured rules",
178
178
  ],
179
179
  prompts: [
180
180
  "toolkit_onboard - Comprehensive getting started",
@@ -22,7 +22,10 @@ export function buildDigest(entries, clientId, toolkitVersion, sessionId) {
22
22
  for (const entry of entries) {
23
23
  switch (entry.kind) {
24
24
  case "feedback":
25
- feedbackEntries.push(entry.data);
25
+ // Exclude integration test entries from digests sent to GCS
26
+ if (entry.data.context !== "integration_test") {
27
+ feedbackEntries.push(entry.data);
28
+ }
26
29
  break;
27
30
  case "telemetry":
28
31
  telemetryEntries.push(entry.data);
@@ -81,9 +81,21 @@ async function handleSubmit(args) {
81
81
  }
82
82
  // Wire up knowledge_ref from top-level param into context/quality_data
83
83
  let context = args.context ? truncate(String(args.context), MAX_CONTEXT_LENGTH) : undefined;
84
- let qualityData = (category === "quality" || category === "correction") && args.quality_data
85
- ? args.quality_data
86
- : undefined;
84
+ let qualityData;
85
+ if ((category === "quality" || category === "correction") && args.quality_data) {
86
+ const raw = args.quality_data;
87
+ if (typeof raw === "string") {
88
+ try {
89
+ qualityData = JSON.parse(raw);
90
+ }
91
+ catch {
92
+ qualityData = undefined;
93
+ }
94
+ }
95
+ else if (typeof raw === "object" && raw !== null) {
96
+ qualityData = raw;
97
+ }
98
+ }
87
99
  const knowledgeRef = args.knowledge_ref ? String(args.knowledge_ref) : undefined;
88
100
  if (knowledgeRef) {
89
101
  // Inject into context for universal correlation
@@ -272,8 +272,10 @@ export async function listTelemetry(options, rootOverride) {
272
272
  */
273
273
  export async function analyzeFeedback(rootOverride) {
274
274
  const dir = await ensureFeedbackDir(rootOverride);
275
- const feedback = await readJsonl(join(dir, FEEDBACK_FILE));
275
+ const allFeedback = await readJsonl(join(dir, FEEDBACK_FILE));
276
276
  const telemetry = await readJsonl(join(dir, TELEMETRY_FILE));
277
+ // Exclude integration test entries from analysis
278
+ const feedback = allFeedback.filter((e) => e.context !== "integration_test");
277
279
  // Category breakdown
278
280
  const categoryBreakdown = {};
279
281
  for (const entry of feedback) {
@@ -319,7 +321,7 @@ export async function analyzeFeedback(rootOverride) {
319
321
  }
320
322
  // High-severity items
321
323
  const highSeverity = feedback.filter((e) => e.severity === "high");
322
- // Actionable items
324
+ // Actionable items — deduplicated by (category, message_prefix, tool)
323
325
  const actionableItems = [];
324
326
  // Tools with high error rates
325
327
  for (const [tool, usage] of Object.entries(toolUsage)) {
@@ -328,16 +330,34 @@ export async function analyzeFeedback(rootOverride) {
328
330
  actionableItems.push(`Tool "${tool}" has ${Math.round(errorRate * 100)}% error rate (${usage.errors}/${usage.total}) - investigate error handling and documentation`);
329
331
  }
330
332
  }
331
- // Gaps are high-priority feedback
332
- const gaps = feedback.filter((e) => e.category === "gap");
333
- for (const gap of gaps) {
334
- actionableItems.push(`Documentation gap: ${gap.message}${gap.tool ? ` (tool: ${gap.tool})` : ""}`);
335
- }
336
- // Confusion items suggest unclear docs
337
- const confusions = feedback.filter((e) => e.category === "confusion");
338
- for (const confusion of confusions) {
339
- actionableItems.push(`Unclear guidance: ${confusion.message}${confusion.tool ? ` (tool: ${confusion.tool})` : ""}`);
340
- }
333
+ // Gaps and confusions — deduplicate by (message_prefix, tool) to avoid flooding
334
+ const isTestFeedback = (e) => {
335
+ const ref = e.quality_data?.knowledge_ref ?? "";
336
+ const ctx = e.context ?? "";
337
+ return ref.startsWith("test:") || ctx.includes("knowledge_ref:test:");
338
+ };
339
+ const deduplicateEntries = (entries, label) => {
340
+ const seen = new Map();
341
+ for (const e of entries) {
342
+ if (isTestFeedback(e))
343
+ continue;
344
+ const prefix = e.message.length > 120 ? e.message.slice(0, 120) : e.message;
345
+ const key = `${prefix}|${e.tool ?? ""}`;
346
+ const existing = seen.get(key);
347
+ if (existing) {
348
+ existing.count++;
349
+ }
350
+ else {
351
+ seen.set(key, { message: e.message, tool: e.tool, count: 1 });
352
+ }
353
+ }
354
+ for (const { message, tool, count } of seen.values()) {
355
+ const suffix = count > 1 ? ` (x${count})` : "";
356
+ actionableItems.push(`${label}: ${message}${tool ? ` (tool: ${tool})` : ""}${suffix}`);
357
+ }
358
+ };
359
+ deduplicateEntries(feedback.filter((e) => e.category === "gap"), "Documentation gap");
360
+ deduplicateEntries(feedback.filter((e) => e.category === "confusion"), "Unclear guidance");
341
361
  // Graph-correlated insights: group feedback by knowledge_ref
342
362
  const graphCorrelations = {};
343
363
  for (const entry of feedback) {
@@ -4,6 +4,7 @@
4
4
  * Routes template CRUD methods to the appropriate SDK calls.
5
5
  * Follows the handler pattern from debug/index.ts.
6
6
  */
7
+ import { normalizeTriggerType } from "../utils.js";
7
8
  const METHODS = ["list", "get", "create", "update", "delete"];
8
9
  /**
9
10
  * Map friendly type names to PersonaTriggerTypeEnum values.
@@ -72,21 +73,18 @@ async function handleList(args, client) {
72
73
  }
73
74
  const type = args.type;
74
75
  if (type) {
75
- // Map friendly type names to trigger_type values
76
- const typeMap = {
77
- voice: "VOICEBOT_AI_EMPLOYEE",
78
- chat: "CHATBOT",
79
- dashboard: "DOCUMENT_TRIGGER",
80
- };
81
- const triggerType = typeMap[type.toLowerCase()] ?? type;
82
- filtered = filtered.filter((t) => t.trigger_type === triggerType);
76
+ const target = type.toLowerCase();
77
+ filtered = filtered.filter((t) => {
78
+ const normalized = normalizeTriggerType(t.trigger_type);
79
+ return normalized === target;
80
+ });
83
81
  }
84
82
  const category = args.category;
85
83
  if (category) {
86
84
  const c = category.toLowerCase();
87
85
  filtered = filtered.filter((t) => t.category?.toLowerCase() === c);
88
86
  }
89
- return {
87
+ const result = {
90
88
  count: filtered.length,
91
89
  templates: filtered.map((t) => ({
92
90
  id: t.id,
@@ -94,9 +92,28 @@ async function handleList(args, client) {
94
92
  description: t.description,
95
93
  category: t.category,
96
94
  trigger_type: t.trigger_type,
95
+ type: normalizeTriggerType(t.trigger_type),
97
96
  has_project_template: t.has_project_template,
98
97
  })),
99
98
  };
99
+ // Layer 2: always provide guidance — especially on 0 results
100
+ if (filtered.length === 0 && templates.length > 0) {
101
+ const availableTypes = [...new Set(templates
102
+ .map((t) => normalizeTriggerType(t.trigger_type))
103
+ .filter(Boolean))];
104
+ result._warning = `${templates.length} templates exist but none match your filters.`;
105
+ result._tip = `Available types: ${availableTypes.join(", ")}. Try template(method='list') without filters to see all.`;
106
+ result._next_step = "template(method='list')";
107
+ }
108
+ else if (filtered.length === 0 && templates.length === 0) {
109
+ result._warning = "No templates found in this tenant.";
110
+ result._tip = "This tenant may not have templates provisioned. Try a different profile or use knowledge('templates') to search DE.";
111
+ result._next_step = "knowledge('persona templates')";
112
+ }
113
+ else {
114
+ result._next_step = "Get details: template(method='get', id='<id>')";
115
+ }
116
+ return result;
100
117
  }
101
118
  async function handleGet(args, client) {
102
119
  const id = args.id;
@@ -10,7 +10,7 @@ import { fingerprintPersona } from "../../../sync.js";
10
10
  import { createCentralStorageEngine } from "../../../sync/central-factory.js";
11
11
  import { handleWorkflow } from "./index.js";
12
12
  import { handleWorkflowOptimize } from "./optimize.js";
13
- export async function handleWorkflowAdapter(args, createClient, getDefaultEnvName) {
13
+ export async function handleWorkflowAdapter(args, createClient, getDefaultEnvName, cache) {
14
14
  const normalizedArgs = { ...(args ?? {}) };
15
15
  const personaId = normalizedArgs.persona_id ? String(normalizedArgs.persona_id) : undefined;
16
16
  let workflowDef = normalizedArgs.workflow_def;
@@ -66,7 +66,7 @@ export async function handleWorkflowAdapter(args, createClient, getDefaultEnvNam
66
66
  mode: "get",
67
67
  persona_id: personaId,
68
68
  env: normalizedArgs.env,
69
- }, client, () => undefined);
69
+ }, client, () => undefined, cache);
70
70
  }
71
71
  case "validate": {
72
72
  return handleWorkflow({
@@ -436,26 +436,26 @@ export async function handleWorkflowDeploy(args, client) {
436
436
  errorResult._likely_causes = [
437
437
  "Type mismatch: An input binding has incompatible type (e.g., STRING where TEXT_WITH_SOURCES expected). The API does NOT say which input — you must check each one.",
438
438
  "Missing typeArguments: Categorizer node missing typeArguments.categories pointing to enumType",
439
- "Invalid named_inputs format: Must use multiBinding.elements[].namedBinding structure — see ema://rules/named-inputs-format",
440
- "Wrong extraction_columns format: Must use array.values[{ wellKnown.extractionColumn: { id, name, dataType } }] — see ema://rules/extraction-column-format",
441
- "namedResults type mismatch: The type declared in namedResults (e.g. WELL_KNOWN_TYPE_ANY) must match the producer's actual output type. entity_extraction outputs aggregate 'extraction_columns', NOT individual columns.",
442
- "Empty namespaces: Action names require namespaces: ['actions', 'emainternal']",
439
+ "Invalid named_inputs format: Must use multiBinding.elements[].namedBinding structure — search knowledge(\"named inputs format\") for correct structure",
440
+ "Wrong extraction_columns format: Must use array.values[{ wellKnown.extractionColumn: { id, name, dataType } }] — search knowledge(\"extraction columns format\")",
441
+ "namedResults type mismatch: The type declared in namedResults must match the producer's actual output type. entity_extraction outputs aggregate 'extraction_columns', NOT individual columns.",
442
+ "Empty namespaces: Action names and enumTypes require non-empty namespaces: ['actions', 'emainternal']",
443
443
  "Deprecated action: Using a deprecated action version (e.g., text_categorizer/v0)",
444
- `widgetConfig binding error: ${MODEL_CONFIG.inputName} must use ${JSON.stringify(widgetBinding(MODEL_CONFIG))} — see ema://rules/widget-config-format`,
444
+ `widgetConfig binding error: ${MODEL_CONFIG.inputName} must use ${JSON.stringify(widgetBinding(MODEL_CONFIG))} — search knowledge('widget config format')`,
445
445
  ];
446
446
  errorResult._debug_steps = [
447
- "1. Compare your workflow_def against a WORKING one: workflow(mode='get', persona_id='<similar_persona>')",
448
- "2. Check EACH input binding type TEXT_WITH_SOURCES ≠ STRING ≠ JSON_VALUE (the API won't tell you which is wrong)",
449
- "3. Verify named_inputs uses multiBinding format: ema://rules/named-inputs-format",
450
- "4. Verify extraction_columns format: ema://rules/extraction-column-format (includes output wiring)",
451
- "5. Check action namespaces are ['actions', 'emainternal'], not empty []",
452
- "6. Verify categorizer nodes have typeArguments.categories pointing to a valid enumType",
453
- "7. Check namedResults producers: each MUST have BOTH 'actionName' AND 'outputName' missing actionName causes HTTP 500",
454
- "8. Check namedResults producers use correct outputName ('extraction_columns' for entity_extraction, NOT individual column names)",
455
- "9. Verify action versions are non-deprecated: knowledge(\"id=<action_name>\")",
447
+ "1. Search for solutions: knowledge(\"workflow deploy 500 <error_keywords>\") other agents may have solved this",
448
+ "2. Compare your workflow_def against a WORKING one: workflow(mode='get', persona_id='<similar_persona>')",
449
+ "3. Check EACH input binding type — TEXT_WITH_SOURCES ≠ STRING ≠ JSON_VALUE (the API won't tell you which is wrong)",
450
+ "4. Verify named_inputs format: knowledge(\"named inputs format\") for correct multiBinding structure",
451
+ "5. Verify extraction_columns format: knowledge(\"extraction columns format\") for correct structure",
452
+ "6. Check action namespaces are ['actions', 'emainternal'], not empty []",
453
+ "7. Verify categorizer nodes have typeArguments.categories pointing to a valid enumType",
454
+ "8. Check namedResults producers: each MUST have BOTH 'actionName' AND 'outputName' missing actionName causes HTTP 500",
455
+ "9. Verify action versions: knowledge(\"deprecated actions\") to check for deprecated versions",
456
456
  ];
457
457
  errorResult._next_step =
458
- "The API error lacks detail compare your workflow_def against a working one: workflow(mode='get', persona_id='<same_or_similar_persona>'). Key resources: ema://rules/named-inputs-format, ema://rules/extraction-column-format, ema://rules/widget-config-format, ema://rules/ruleset-format.";
458
+ "Search knowledge(\"workflow deploy 500\") for known solutions, then compare your workflow_def against a working one: workflow(mode='get', persona_id='<same_or_similar_persona>').";
459
459
  }
460
460
  }
461
461
  return errorResult;