@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.
- package/dist/knowledge/guidance-cache.js +60 -3
- package/dist/knowledge/intelligence/staleness.js +4 -0
- package/dist/knowledge/search-client.js +89 -1
- package/dist/mcp/domain/generation-schema.js +244 -31
- package/dist/mcp/domain/workflow-def-validator.js +321 -1
- package/dist/mcp/domain/workflow-static-validator.js +1 -1
- package/dist/mcp/guidance/classify.js +74 -0
- package/dist/mcp/guidance/defaults.js +114 -0
- package/dist/mcp/guidance/middleware.js +193 -0
- package/dist/mcp/guidance/types.js +7 -0
- package/dist/mcp/guidance.js +12 -8
- package/dist/mcp/handlers/data/index.js +1 -1
- package/dist/mcp/handlers/debug/index.js +80 -7
- package/dist/mcp/handlers/env/index.js +4 -4
- package/dist/mcp/handlers/feedback/coalesce.js +4 -1
- package/dist/mcp/handlers/feedback/index.js +15 -3
- package/dist/mcp/handlers/feedback/store.js +32 -12
- package/dist/mcp/handlers/template/crud.js +26 -9
- package/dist/mcp/handlers/workflow/adapter.js +2 -2
- package/dist/mcp/handlers/workflow/deploy.js +15 -15
- package/dist/mcp/handlers/workflow/index.js +23 -11
- package/dist/mcp/handlers/workflow/validate.js +30 -1
- package/dist/mcp/handlers/workflow/validation.js +4 -4
- package/dist/mcp/server.js +57 -5
- package/dist/mcp/tools.js +18 -11
- package/dist/sdk/client.js +7 -7
- package/dist/sdk/ema-client.js +16 -1
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/mcp/guidance.js
CHANGED
|
@@ -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,
|
|
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("
|
|
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
|
|
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.
|
|
124
|
-
|
|
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.
|
|
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
|
-
"
|
|
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='...')
|
|
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:
|
|
104
|
-
|
|
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:
|
|
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
|
-
"
|
|
176
|
-
"
|
|
177
|
-
"
|
|
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
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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 —
|
|
440
|
-
"Wrong extraction_columns format: Must use array.values[{ wellKnown.extractionColumn: { id, name, dataType } }] —
|
|
441
|
-
"namedResults type mismatch: The type declared in namedResults
|
|
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))} —
|
|
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.
|
|
448
|
-
"2.
|
|
449
|
-
"3.
|
|
450
|
-
"4. Verify
|
|
451
|
-
"5.
|
|
452
|
-
"6.
|
|
453
|
-
"7.
|
|
454
|
-
"8. Check namedResults producers
|
|
455
|
-
"9. Verify action versions
|
|
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
|
-
"
|
|
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;
|