@ema.co/mcp-toolkit 2026.1.26-4 → 2026.1.27
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.
- package/dist/mcp/handlers/action/index.js +14 -2
- package/dist/mcp/handlers/data/index.js +72 -6
- package/dist/mcp/handlers/env/index.js +3 -3
- package/dist/mcp/handlers/reference/index.js +15 -2
- package/dist/mcp/handlers/workflow/analyze.js +53 -105
- package/dist/mcp/handlers/workflow/deploy.js +15 -0
- package/dist/mcp/handlers/workflow/generate.js +3 -6
- package/dist/mcp/handlers/workflow/index.js +18 -3
- package/dist/mcp/handlers/workflow/modify.js +9 -29
- package/dist/mcp/handlers/workflow/optimize.js +22 -108
- package/dist/mcp/prompts.js +12 -15
- package/dist/mcp/resources.js +8 -0
- package/dist/mcp/server.js +196 -250
- package/dist/mcp/tools.js +25 -8
- package/dist/sdk/action-schema-parser.js +11 -5
- package/dist/sdk/client.js +1 -1
- package/dist/sdk/guidance.js +58 -35
- package/dist/sdk/index.js +8 -7
- package/dist/sdk/knowledge.js +99 -1938
- package/dist/sdk/quality-gates.js +60 -336
- package/dist/sdk/validation-rules.js +33 -0
- package/dist/sdk/workflow-fixer.js +29 -360
- package/dist/sdk/workflow-intent.js +43 -3
- package/docs/dashboard-operations.md +35 -0
- package/docs/ema-user-guide.md +66 -0
- package/docs/mcp-tools-guide.md +40 -3
- package/package.json +1 -2
- package/resources/action-schema.json +0 -5678
|
@@ -14,10 +14,20 @@ export async function handleAction(args, client) {
|
|
|
14
14
|
const id = args.id;
|
|
15
15
|
const identifier = args.identifier; // deprecated alias for 'id'
|
|
16
16
|
const idOrName = id ?? identifier;
|
|
17
|
-
// Categories list
|
|
17
|
+
// Categories list - API-first with catalog fallback
|
|
18
18
|
if (args.categories) {
|
|
19
|
+
try {
|
|
20
|
+
const actions = await client.listActions();
|
|
21
|
+
const apiCategories = [...new Set(actions.map(a => a.category).filter(Boolean))];
|
|
22
|
+
if (apiCategories.length > 0) {
|
|
23
|
+
return { categories: apiCategories, count: apiCategories.length, source: "api" };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Fallback to catalog
|
|
28
|
+
}
|
|
19
29
|
const categories = [...new Set(Object.values(AGENT_CATALOG).map(a => a.category))];
|
|
20
|
-
return { categories, count: categories.length };
|
|
30
|
+
return { categories, count: categories.length, source: "catalog" };
|
|
21
31
|
}
|
|
22
32
|
// Suggest for use case
|
|
23
33
|
if (args.suggest) {
|
|
@@ -97,6 +107,7 @@ export async function handleAction(args, client) {
|
|
|
97
107
|
...a,
|
|
98
108
|
documentation: a.name ? getAgentByName(a.name) : undefined,
|
|
99
109
|
})),
|
|
110
|
+
source: "api",
|
|
100
111
|
};
|
|
101
112
|
}
|
|
102
113
|
return {
|
|
@@ -107,6 +118,7 @@ export async function handleAction(args, client) {
|
|
|
107
118
|
category: a.category,
|
|
108
119
|
enabled: a.enabled,
|
|
109
120
|
})),
|
|
121
|
+
source: "api",
|
|
110
122
|
};
|
|
111
123
|
}
|
|
112
124
|
catch (e) {
|
|
@@ -138,6 +138,8 @@ export async function handleData(args, client, readFile) {
|
|
|
138
138
|
// Upload file or LLM-generated content (dashboard rows)
|
|
139
139
|
const filePath = (dataArgs?.path ?? dataArgs?.file ?? args.file);
|
|
140
140
|
const items = dataArgs?.items;
|
|
141
|
+
// Widget name for targeting specific upload widgets (especially for Document Generation personas)
|
|
142
|
+
const widgetName = (dataArgs?.widget_name ?? args.widget_name);
|
|
141
143
|
if (filePath) {
|
|
142
144
|
// File upload - use provided readFile or fall back to fs
|
|
143
145
|
try {
|
|
@@ -151,11 +153,14 @@ export async function handleData(args, client, readFile) {
|
|
|
151
153
|
}
|
|
152
154
|
const path = await import("path");
|
|
153
155
|
const filename = path.basename(filePath);
|
|
154
|
-
const result = await client.uploadDataSource(personaId, fileContent, filename
|
|
156
|
+
const result = await client.uploadDataSource(personaId, fileContent, filename, {
|
|
157
|
+
widgetName: widgetName, // Pass through widget_name for doc gen personas
|
|
158
|
+
});
|
|
155
159
|
return {
|
|
156
160
|
method: "upload",
|
|
157
161
|
persona_id: personaId,
|
|
158
162
|
path: filePath,
|
|
163
|
+
widget_name: widgetName ?? "fileUpload",
|
|
159
164
|
...result,
|
|
160
165
|
};
|
|
161
166
|
}
|
|
@@ -164,14 +169,74 @@ export async function handleData(args, client, readFile) {
|
|
|
164
169
|
}
|
|
165
170
|
}
|
|
166
171
|
else if (items && items.length > 0) {
|
|
167
|
-
// Dashboard row upload (LLM-generated content)
|
|
172
|
+
// Dashboard row upload (LLM-generated content or file attachments)
|
|
168
173
|
try {
|
|
169
174
|
const results = [];
|
|
175
|
+
const path = await import("path");
|
|
176
|
+
const fs = await import("fs/promises");
|
|
170
177
|
for (const item of items) {
|
|
171
|
-
// Convert item to DashboardInput format
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
// Convert item to DashboardInput format
|
|
179
|
+
// Supports: string_value, number_value, boolean_value, document_value
|
|
180
|
+
const inputs = await Promise.all(Object.entries(item).map(async ([key, value]) => {
|
|
181
|
+
// File attachment: { file: "/path/to/file.pdf" } or { file_path: "..." }
|
|
182
|
+
if (typeof value === "object" && value !== null && ("file" in value || "file_path" in value)) {
|
|
183
|
+
const filePath = value.file ?? value.file_path;
|
|
184
|
+
if (typeof filePath === "string") {
|
|
185
|
+
let fileContent;
|
|
186
|
+
if (readFile) {
|
|
187
|
+
fileContent = await readFile(filePath);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
fileContent = await fs.readFile(filePath);
|
|
191
|
+
}
|
|
192
|
+
const filename = path.basename(filePath);
|
|
193
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
194
|
+
const mimeTypes = {
|
|
195
|
+
".pdf": "application/pdf",
|
|
196
|
+
".json": "application/json",
|
|
197
|
+
".txt": "text/plain",
|
|
198
|
+
".csv": "text/csv",
|
|
199
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
200
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
name: key,
|
|
204
|
+
document_value: [{
|
|
205
|
+
name: filename,
|
|
206
|
+
contents: fileContent.toString("base64"),
|
|
207
|
+
is_base64_encoded: true,
|
|
208
|
+
mime_type: mimeTypes[ext] ?? "application/octet-stream",
|
|
209
|
+
}],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Inline document: { contents: "...", mime_type: "..." }
|
|
214
|
+
if (typeof value === "object" && value !== null && "contents" in value) {
|
|
215
|
+
const doc = value;
|
|
216
|
+
return {
|
|
217
|
+
name: key,
|
|
218
|
+
document_value: [{
|
|
219
|
+
name: doc.name ?? `${key}.txt`,
|
|
220
|
+
contents: typeof doc.contents === "string" && doc.is_base64_encoded
|
|
221
|
+
? doc.contents
|
|
222
|
+
: Buffer.from(String(doc.contents)).toString("base64"),
|
|
223
|
+
is_base64_encoded: true,
|
|
224
|
+
mime_type: doc.mime_type ?? "text/plain",
|
|
225
|
+
}],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// Primitive values
|
|
229
|
+
if (typeof value === "number") {
|
|
230
|
+
return { name: key, number_value: value };
|
|
231
|
+
}
|
|
232
|
+
if (typeof value === "boolean") {
|
|
233
|
+
return { name: key, boolean_value: value };
|
|
234
|
+
}
|
|
235
|
+
// Default: string value
|
|
236
|
+
return {
|
|
237
|
+
name: key,
|
|
238
|
+
string_value: typeof value === "string" ? value : JSON.stringify(value),
|
|
239
|
+
};
|
|
175
240
|
}));
|
|
176
241
|
const result = await client.uploadAndRunDashboardRow(personaId, inputs);
|
|
177
242
|
results.push(result);
|
|
@@ -181,6 +246,7 @@ export async function handleData(args, client, readFile) {
|
|
|
181
246
|
persona_id: personaId,
|
|
182
247
|
uploaded_rows: results.length,
|
|
183
248
|
row_ids: results.map(r => r.row_id),
|
|
249
|
+
_tip: "Dashboard rows created with file attachments trigger workflow execution automatically",
|
|
184
250
|
};
|
|
185
251
|
}
|
|
186
252
|
catch (error) {
|
|
@@ -25,9 +25,9 @@ export async function handleEnv(_args, getEnvironments, toolkit) {
|
|
|
25
25
|
getting_started: {
|
|
26
26
|
workflow: [
|
|
27
27
|
"1. List personas: persona(method=\"list\")",
|
|
28
|
-
"2. Get
|
|
29
|
-
"3. Analyze
|
|
30
|
-
"4. Preview before deploying:
|
|
28
|
+
"2. Get workflow data: workflow(mode=\"get\", persona_id=\"...\")",
|
|
29
|
+
"3. Analyze workflow: Fetch ema://rules/anti-patterns and check workflow_def against rules",
|
|
30
|
+
"4. Preview before deploying: workflow(mode=\"deploy\", persona_id=\"...\", workflow_def={...}, preview=true)",
|
|
31
31
|
],
|
|
32
32
|
critical_rules: criticalRules,
|
|
33
33
|
resources: [
|
|
@@ -34,10 +34,22 @@ export async function handleReference(args, context) {
|
|
|
34
34
|
if (type === "actions") {
|
|
35
35
|
const client = context?.client;
|
|
36
36
|
const id = args.id;
|
|
37
|
-
// Categories list
|
|
37
|
+
// Categories list - API-first with catalog fallback
|
|
38
38
|
if (args.categories) {
|
|
39
|
+
if (client) {
|
|
40
|
+
try {
|
|
41
|
+
const actions = await client.listActions();
|
|
42
|
+
const apiCategories = [...new Set(actions.map(a => a.category).filter(Boolean))];
|
|
43
|
+
if (apiCategories.length > 0) {
|
|
44
|
+
return { categories: apiCategories, count: apiCategories.length, source: "api" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Fallback to catalog
|
|
49
|
+
}
|
|
50
|
+
}
|
|
39
51
|
const categories = [...new Set(Object.values(AGENT_CATALOG).map(a => a.category))];
|
|
40
|
-
return { categories, count: categories.length };
|
|
52
|
+
return { categories, count: categories.length, source: "catalog" };
|
|
41
53
|
}
|
|
42
54
|
// Suggest for use case
|
|
43
55
|
if (args.suggest) {
|
|
@@ -114,6 +126,7 @@ export async function handleReference(args, context) {
|
|
|
114
126
|
category: a.category,
|
|
115
127
|
enabled: a.enabled,
|
|
116
128
|
})),
|
|
129
|
+
source: "api",
|
|
117
130
|
};
|
|
118
131
|
}
|
|
119
132
|
catch {
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workflow Analyze Handler
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Returns workflow DATA for LLM to analyze using rules from ema://rules/*.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
6
|
+
* IMPORTANT: This handler does NOT pre-compute issues or fixes.
|
|
7
|
+
* The LLM applies ANTI_PATTERNS, INPUT_SOURCE_RULES, and OPTIMIZATION_RULES
|
|
8
|
+
* to reason about the workflow.
|
|
9
|
+
*
|
|
10
|
+
* Returns:
|
|
11
|
+
* - workflow_def: The raw workflow structure
|
|
12
|
+
* - connections: Type information for each edge
|
|
13
|
+
* - metrics: Basic counts (nodes, edges, etc.)
|
|
14
|
+
* - rules_hint: Pointer to ema://rules/* for analysis guidance
|
|
12
15
|
*/
|
|
13
|
-
import {
|
|
14
|
-
import { autoFixWorkflow } from "../../../sdk/workflow-fixer.js";
|
|
15
|
-
import { analyzeExecutionFlow, generateASCIIFlow } from "../../../sdk/workflow-execution-analyzer.js";
|
|
16
|
+
import { validateWorkflowConnections, parseWorkflowDef } from "../../../sdk/knowledge.js";
|
|
16
17
|
/**
|
|
17
|
-
* Calculate workflow metrics
|
|
18
|
+
* Calculate workflow metrics (pure data, no analysis)
|
|
18
19
|
*/
|
|
19
20
|
function calculateMetrics(workflow) {
|
|
20
21
|
const actions = workflow.actions;
|
|
@@ -36,15 +37,24 @@ function calculateMetrics(workflow) {
|
|
|
36
37
|
// Get trigger type
|
|
37
38
|
const trigger = workflow.trigger;
|
|
38
39
|
const triggerType = trigger?.trigger_type;
|
|
40
|
+
// Check for categorizer
|
|
41
|
+
const hasCategorizer = actions.some(a => {
|
|
42
|
+
const name = a.name?.toLowerCase() ?? "";
|
|
43
|
+
const action = a.action?.name?.name;
|
|
44
|
+
return name.includes("categorizer") || action?.includes("categorizer");
|
|
45
|
+
});
|
|
39
46
|
return {
|
|
40
47
|
node_count: actions.length,
|
|
41
48
|
connection_count: connectionCount,
|
|
42
49
|
has_hitl: hasHitl,
|
|
50
|
+
has_categorizer: hasCategorizer,
|
|
43
51
|
trigger_type: triggerType,
|
|
44
52
|
};
|
|
45
53
|
}
|
|
46
54
|
/**
|
|
47
55
|
* Handle workflow analyze mode
|
|
56
|
+
*
|
|
57
|
+
* Returns DATA for LLM to analyze - does NOT pre-compute issues.
|
|
48
58
|
*/
|
|
49
59
|
export async function handleWorkflowAnalyze(args, client) {
|
|
50
60
|
const personaId = args.persona_id;
|
|
@@ -62,110 +72,48 @@ export async function handleWorkflowAnalyze(args, client) {
|
|
|
62
72
|
if (!workflow) {
|
|
63
73
|
return { error: "No workflow to analyze. Provide workflow_def or persona_id." };
|
|
64
74
|
}
|
|
65
|
-
//
|
|
66
|
-
const
|
|
75
|
+
// Parse workflow to simpler structure
|
|
76
|
+
const nodes = parseWorkflowDef(workflow);
|
|
77
|
+
// Get connection type information
|
|
78
|
+
const connections = validateWorkflowConnections(workflow);
|
|
79
|
+
// Calculate metrics
|
|
80
|
+
const metrics = calculateMetrics(workflow);
|
|
67
81
|
const result = {
|
|
68
82
|
mode: "analyze",
|
|
69
83
|
persona_id: personaId,
|
|
70
84
|
persona_name: persona?.name,
|
|
71
|
-
|
|
85
|
+
// RAW DATA for LLM to analyze
|
|
86
|
+
workflow_def: workflow,
|
|
87
|
+
node_count: nodes.length,
|
|
88
|
+
nodes: nodes.map(n => ({
|
|
89
|
+
id: n.id,
|
|
90
|
+
action: n.action_name,
|
|
91
|
+
display_name: n.display_name,
|
|
92
|
+
inputs: n.incoming_edges?.map(e => `${e.source_node_id}.${e.source_output}`),
|
|
93
|
+
})),
|
|
94
|
+
// Type information for connections
|
|
95
|
+
connections: connections.map(c => ({
|
|
96
|
+
edge: c.edge_id,
|
|
97
|
+
source_type: c.source_type,
|
|
98
|
+
target_type: c.target_type,
|
|
99
|
+
compatible: c.compatible,
|
|
100
|
+
note: c.note,
|
|
101
|
+
})),
|
|
102
|
+
// Basic metrics
|
|
103
|
+
metrics,
|
|
104
|
+
// Tell LLM where to find analysis rules
|
|
105
|
+
_next_steps: [
|
|
106
|
+
"1. Fetch ema://rules/anti-patterns",
|
|
107
|
+
"2. Fetch ema://rules/structural-invariants",
|
|
108
|
+
"3. Check each node against the rules",
|
|
109
|
+
"4. Report issues YOU find (MCP does not pre-compute)",
|
|
110
|
+
],
|
|
72
111
|
};
|
|
73
|
-
// Issues and fixes
|
|
74
|
-
if (include.includes("issues") || include.includes("fixes")) {
|
|
75
|
-
const issues = detectWorkflowIssues(workflow);
|
|
76
|
-
if (include.includes("issues")) {
|
|
77
|
-
result.issues = issues;
|
|
78
|
-
}
|
|
79
|
-
if (include.includes("fixes")) {
|
|
80
|
-
result.fixes = suggestWorkflowFixes(issues);
|
|
81
|
-
}
|
|
82
|
-
result.issue_summary = {
|
|
83
|
-
total: issues.length,
|
|
84
|
-
critical: issues.filter((i) => i.severity === "critical").length,
|
|
85
|
-
warning: issues.filter((i) => i.severity === "warning").length,
|
|
86
|
-
info: issues.filter((i) => i.severity === "info").length,
|
|
87
|
-
};
|
|
88
|
-
result.validation_passed = issues.filter((i) => i.severity === "critical").length === 0;
|
|
89
|
-
}
|
|
90
|
-
// Connection validation
|
|
91
|
-
if (include.includes("connections")) {
|
|
92
|
-
result.connections = validateWorkflowConnections(workflow);
|
|
93
|
-
}
|
|
94
|
-
// Metrics
|
|
95
|
-
if (include.includes("metrics")) {
|
|
96
|
-
result.metrics = calculateMetrics(workflow);
|
|
97
|
-
}
|
|
98
|
-
// Execution flow analysis
|
|
99
|
-
if (include.includes("execution_flow")) {
|
|
100
|
-
const execAnalysis = analyzeExecutionFlow(workflow);
|
|
101
|
-
result.execution_flow = {
|
|
102
|
-
summary: execAnalysis.summary,
|
|
103
|
-
loops: execAnalysis.loops,
|
|
104
|
-
multiple_responder_issues: execAnalysis.multipleResponderIssues,
|
|
105
|
-
redundant_classifiers: execAnalysis.redundantClassifiers,
|
|
106
|
-
data_flow_issues: execAnalysis.dataFlowIssues,
|
|
107
|
-
dead_code_paths: execAnalysis.deadCodePaths,
|
|
108
|
-
};
|
|
109
|
-
// Include ASCII visualization if requested
|
|
110
|
-
if (args.visualize) {
|
|
111
|
-
result.execution_flow_ascii = generateASCIIFlow(execAnalysis);
|
|
112
|
-
}
|
|
113
|
-
// Add specific warnings for triple response risk
|
|
114
|
-
if (execAnalysis.summary.mayRepeatResponses) {
|
|
115
|
-
result.triple_response_warning = "⚠️ This workflow may cause duplicate/triple responses due to ungated parallel responders";
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
112
|
if (persona) {
|
|
119
113
|
result.persona = {
|
|
120
114
|
id: persona.id,
|
|
121
115
|
name: persona.name
|
|
122
116
|
};
|
|
123
117
|
}
|
|
124
|
-
// Calculate optimization guidance
|
|
125
|
-
const issues = result.issues;
|
|
126
|
-
const criticalCount = issues?.filter(i => i.severity === "critical").length ?? 0;
|
|
127
|
-
const warningCount = issues?.filter(i => i.severity === "warning").length ?? 0;
|
|
128
|
-
// AUTO-FIX: If fix=true, apply fixes and deploy
|
|
129
|
-
if (args.fix && personaId && persona && workflow) {
|
|
130
|
-
const fixResult = autoFixWorkflow(workflow);
|
|
131
|
-
result.fixes_applied = fixResult.fixesApplied;
|
|
132
|
-
result.fixes_warnings = fixResult.warnings;
|
|
133
|
-
if (fixResult.fixesApplied.length > 0 && fixResult.success) {
|
|
134
|
-
// Deploy the fixed workflow
|
|
135
|
-
try {
|
|
136
|
-
const fixedWorkflow = fixResult.workflowDef;
|
|
137
|
-
await client.updateAiEmployee({
|
|
138
|
-
persona_id: personaId,
|
|
139
|
-
workflow: fixedWorkflow,
|
|
140
|
-
proto_config: persona.proto_config,
|
|
141
|
-
});
|
|
142
|
-
result.fix_status = "deployed";
|
|
143
|
-
result.fixes_deployed = fixResult.fixesApplied.length;
|
|
144
|
-
result.status = `✅ Applied ${fixResult.fixesApplied.length} fixes and deployed`;
|
|
145
|
-
}
|
|
146
|
-
catch (deployErr) {
|
|
147
|
-
result.fix_status = "failed";
|
|
148
|
-
result.fix_error = deployErr instanceof Error ? deployErr.message : String(deployErr);
|
|
149
|
-
result.status = `⚠️ Fixes applied but deploy failed: ${result.fix_error}`;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
else if (fixResult.fixesApplied.length === 0) {
|
|
153
|
-
result.fix_status = "no_fixes_needed";
|
|
154
|
-
result.status = "✅ No auto-fixable issues found";
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
result.fix_status = "failed";
|
|
158
|
-
result.status = "⚠️ Fix generation failed";
|
|
159
|
-
}
|
|
160
|
-
return result;
|
|
161
|
-
}
|
|
162
|
-
// Provide optimization suggestion
|
|
163
|
-
if (criticalCount > 0 || warningCount > 0) {
|
|
164
|
-
result.optimization_suggestion = `This workflow has ${criticalCount} critical and ${warningCount} warning issues. ` +
|
|
165
|
-
`Use workflow(persona_id="${personaId ?? 'ID'}", optimize=true) to auto-fix.`;
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
result.status = "✅ Workflow is healthy - no issues detected";
|
|
169
|
-
}
|
|
170
118
|
return result;
|
|
171
119
|
}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Workflow Deploy Handler
|
|
3
3
|
*
|
|
4
4
|
* Deploys a workflow_def to an existing persona.
|
|
5
|
+
* Includes deprecation warnings (does not block, just warns).
|
|
5
6
|
*/
|
|
6
7
|
import { sanitizeWorkflowForDeploy } from "./utils.js";
|
|
8
|
+
import { DEPRECATED_ACTIONS_FALLBACK, checkWorkflowDeprecations } from "./index.js";
|
|
7
9
|
/**
|
|
8
10
|
* Handle workflow deploy mode
|
|
9
11
|
*/
|
|
@@ -23,6 +25,8 @@ export async function handleWorkflowDeploy(args, client) {
|
|
|
23
25
|
}
|
|
24
26
|
// Sanitize workflow before deployment
|
|
25
27
|
const sanitizedWorkflow = sanitizeWorkflowForDeploy(workflowDef);
|
|
28
|
+
// Check for deprecated actions (warn, don't block)
|
|
29
|
+
const deprecationWarnings = checkWorkflowDeprecations(workflowDef, DEPRECATED_ACTIONS_FALLBACK);
|
|
26
30
|
// Determine proto_config to use: provided > existing
|
|
27
31
|
const existingProtoConfig = persona.proto_config;
|
|
28
32
|
const providedProtoConfig = args.proto_config;
|
|
@@ -31,6 +35,11 @@ export async function handleWorkflowDeploy(args, client) {
|
|
|
31
35
|
const projectSettings = protoConfigToUse.projectSettings;
|
|
32
36
|
const isVoice = projectSettings?.projectType === 5;
|
|
33
37
|
const warnings = [];
|
|
38
|
+
// Add deprecation warning message if deprecated actions found
|
|
39
|
+
if (deprecationWarnings.length > 0) {
|
|
40
|
+
warnings.push(`Workflow uses ${deprecationWarnings.length} deprecated action(s). ` +
|
|
41
|
+
"Consider updating to prevent future breakage when deprecated actions are removed.");
|
|
42
|
+
}
|
|
34
43
|
if (isVoice && !providedProtoConfig) {
|
|
35
44
|
// Check if existing proto_config has conversationSettings populated
|
|
36
45
|
const widgets = (protoConfigToUse.widgets ?? []);
|
|
@@ -58,6 +67,12 @@ export async function handleWorkflowDeploy(args, client) {
|
|
|
58
67
|
if (warnings.length > 0) {
|
|
59
68
|
result.warnings = warnings;
|
|
60
69
|
}
|
|
70
|
+
// Include detailed deprecation info if any deprecated actions found
|
|
71
|
+
if (deprecationWarnings.length > 0) {
|
|
72
|
+
result.deprecation_warnings = deprecationWarnings;
|
|
73
|
+
result._warning = "Workflow deployed but contains deprecated actions. Update before platform removes them.";
|
|
74
|
+
result._next_step = "Use workflow(mode='get') to see current schema, then deploy updated workflow_def with non-deprecated actions.";
|
|
75
|
+
}
|
|
61
76
|
return result;
|
|
62
77
|
}
|
|
63
78
|
catch (err) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { parseInput, intentToSpec, generateWorkflow } from "../../../sdk/workflow-intent.js";
|
|
12
12
|
import { compileWorkflow } from "../../../sdk/workflow-generator.js";
|
|
13
|
-
|
|
13
|
+
// Detection removed - LLM analyzes with rules from ema://rules/*
|
|
14
14
|
import { runIntentArchitect } from "../../../sdk/intent-architect.js";
|
|
15
15
|
import { ensureActionRegistry } from "../../../sdk/action-registry.js";
|
|
16
16
|
import { ensureSchemaRegistry, validateWorkflowSpec, generateActionCatalogForLLM } from "../../../sdk/workflow-validator.js";
|
|
@@ -271,14 +271,14 @@ export async function handleWorkflowGenerate(args, client, getTemplateId) {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
const compiled = compileWorkflow(spec, { registry: actionRegistry });
|
|
274
|
-
// Validate generated workflow
|
|
275
|
-
const issues = detectWorkflowIssues(compiled.workflow_def);
|
|
276
274
|
const result = {
|
|
277
275
|
mode: "generate",
|
|
278
276
|
status: preview ? "preview" : "deployed",
|
|
279
277
|
workflow_def: compiled.workflow_def,
|
|
280
278
|
proto_config: compiled.proto_config,
|
|
281
279
|
validation: parseResult.validation,
|
|
280
|
+
// LLM should analyze with ema://rules/anti-patterns
|
|
281
|
+
_analysis_hint: "Use ema://rules/anti-patterns to check for issues",
|
|
282
282
|
};
|
|
283
283
|
if (specValidation) {
|
|
284
284
|
result.api_validation = {
|
|
@@ -287,9 +287,6 @@ export async function handleWorkflowGenerate(args, client, getTemplateId) {
|
|
|
287
287
|
action_coverage: specValidation.action_coverage,
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
|
-
if (issues.length > 0) {
|
|
291
|
-
result.issues = issues;
|
|
292
|
-
}
|
|
293
290
|
// Deploy if not preview
|
|
294
291
|
if (!preview && personaId) {
|
|
295
292
|
const persona = await client.getPersonaById(personaId);
|
|
@@ -36,7 +36,8 @@ export { handleWorkflowModify } from "./modify.js";
|
|
|
36
36
|
export { applyWorkflowModifications, buildWorkflowAction, buildInputBinding, } from "./modify.js";
|
|
37
37
|
// Fallback deprecated actions - ONLY used when API unavailable
|
|
38
38
|
// Source: ema/ema_backend/grpc/workflow_actions/registered_actions.py (synced 2026-01-26)
|
|
39
|
-
|
|
39
|
+
// Exported for use by deploy handler
|
|
40
|
+
export const DEPRECATED_ACTIONS_FALLBACK = {
|
|
40
41
|
"search/v0": { replacement: "search/v2", notes: "v2 requires datastore_configs input" },
|
|
41
42
|
"web_search/v0": { replacement: "live_web_search or ai_web_search", notes: "Split into two specialized actions" },
|
|
42
43
|
"call_llm/v0": { replacement: "call_llm/v2" },
|
|
@@ -79,8 +80,9 @@ async function getDeprecatedActions(client) {
|
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
81
82
|
* Check if workflow uses deprecated actions
|
|
83
|
+
* Exported for use by deploy handler
|
|
82
84
|
*/
|
|
83
|
-
function checkWorkflowDeprecations(workflowDef, deprecatedActions) {
|
|
85
|
+
export function checkWorkflowDeprecations(workflowDef, deprecatedActions) {
|
|
84
86
|
if (!workflowDef)
|
|
85
87
|
return [];
|
|
86
88
|
const warnings = [];
|
|
@@ -185,14 +187,27 @@ async function handleWorkflowGet(args, client) {
|
|
|
185
187
|
hitl: "For actions with side effects (email, external calls), consider general_hitl for approval",
|
|
186
188
|
search: "Use search/v2 with datastore_configs. Wire query from trigger output.",
|
|
187
189
|
},
|
|
188
|
-
//
|
|
190
|
+
// LLM-driven analysis pattern: MCP provides data, LLM does the thinking
|
|
189
191
|
_next_steps: deprecationWarnings.length > 0
|
|
190
192
|
? [
|
|
191
193
|
"WARNING: Workflow uses deprecated actions. Update them first:",
|
|
192
194
|
...deprecationWarnings.map(w => ` - ${w.action}: ${w.replacement || "check catalog for replacement"}`),
|
|
195
|
+
"",
|
|
196
|
+
"To analyze workflow health:",
|
|
197
|
+
"1. Fetch ema://rules/anti-patterns",
|
|
198
|
+
"2. Fetch ema://rules/structural-invariants",
|
|
199
|
+
"3. Check workflow_def nodes against the rules",
|
|
200
|
+
"4. Report issues YOU find (MCP does not pre-compute)",
|
|
201
|
+
"",
|
|
193
202
|
"Then deploy with: workflow(mode='deploy', persona_id='...', workflow_def={...})",
|
|
194
203
|
]
|
|
195
204
|
: [
|
|
205
|
+
"To analyze workflow health:",
|
|
206
|
+
"1. Fetch ema://rules/anti-patterns",
|
|
207
|
+
"2. Fetch ema://rules/structural-invariants",
|
|
208
|
+
"3. Check workflow_def nodes against the rules",
|
|
209
|
+
"4. Report issues YOU find (MCP does not pre-compute)",
|
|
210
|
+
"",
|
|
196
211
|
"Generate or modify workflow_def based on requirements above",
|
|
197
212
|
"Deploy with: workflow(mode='deploy', persona_id='...', workflow_def={...})",
|
|
198
213
|
],
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
*
|
|
9
9
|
* See: src/mcp/AGENTS.md for anti-patterns to avoid.
|
|
10
10
|
*/
|
|
11
|
-
import { detectWorkflowIssues, suggestWorkflowFixes } from "../../../sdk/knowledge.js";
|
|
12
11
|
import { sanitizeWorkflowForDeploy } from "./utils.js";
|
|
13
12
|
import { ensureSchemaRegistry } from "../../../sdk/workflow-validator.js";
|
|
14
13
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -323,9 +322,6 @@ export async function handleWorkflowModify(args, client) {
|
|
|
323
322
|
return { error: "Persona has no workflow_def" };
|
|
324
323
|
}
|
|
325
324
|
const actions = (workflow.actions || []);
|
|
326
|
-
// Run analysis to give Agent context
|
|
327
|
-
const issues = detectWorkflowIssues(workflow);
|
|
328
|
-
const suggestions = suggestWorkflowFixes(issues);
|
|
329
325
|
// Load schema registry for action catalog
|
|
330
326
|
const schemaRegistry = await ensureSchemaRegistry(client);
|
|
331
327
|
const availableActions = schemaRegistry.isLoaded()
|
|
@@ -335,20 +331,21 @@ export async function handleWorkflowModify(args, client) {
|
|
|
335
331
|
message: "No operations provided. Returning workflow context for Agent to build operations.",
|
|
336
332
|
persona_id: personaId,
|
|
337
333
|
persona_name: persona.name,
|
|
338
|
-
// Current workflow state
|
|
334
|
+
// Current workflow state (DATA for Agent)
|
|
339
335
|
current_nodes: actions.map(a => ({
|
|
340
336
|
name: a.name,
|
|
341
337
|
display_name: a.displaySettings?.displayName,
|
|
342
338
|
action_type: a.action?.name?.name,
|
|
343
339
|
})),
|
|
344
|
-
// Analysis
|
|
345
|
-
issues: issues.map(i => ({ severity: i.severity, type: i.type, node: i.node, reason: i.reason })),
|
|
346
|
-
suggestions,
|
|
347
340
|
// Available actions from registry
|
|
348
341
|
available_actions: availableActions.slice(0, 20), // Top 20 for context
|
|
349
342
|
// Guidance for Agent
|
|
350
|
-
|
|
351
|
-
|
|
343
|
+
_next_steps: [
|
|
344
|
+
"1. Review current_nodes and available_actions above",
|
|
345
|
+
"2. Fetch ema://rules/anti-patterns to check for issues",
|
|
346
|
+
"3. Build ModificationOperation[] based on your analysis",
|
|
347
|
+
"4. Pass operations as 'operations' parameter",
|
|
348
|
+
],
|
|
352
349
|
// Example operation structures
|
|
353
350
|
example_operations: {
|
|
354
351
|
insert_hitl: {
|
|
@@ -392,9 +389,6 @@ export async function handleWorkflowModify(args, client) {
|
|
|
392
389
|
const result = applyWorkflowModifications(workflow, operations, schemaRegistry);
|
|
393
390
|
// Sanitize before deploy
|
|
394
391
|
const sanitized = sanitizeWorkflowForDeploy(result.workflow);
|
|
395
|
-
// Run validation
|
|
396
|
-
const postIssues = detectWorkflowIssues(sanitized);
|
|
397
|
-
const errors = postIssues.filter(i => i.severity === "critical");
|
|
398
392
|
if (preview) {
|
|
399
393
|
return {
|
|
400
394
|
preview: true,
|
|
@@ -406,23 +400,9 @@ export async function handleWorkflowModify(args, client) {
|
|
|
406
400
|
nodes_removed: result.nodesRemoved,
|
|
407
401
|
nodes_modified: result.nodesModified,
|
|
408
402
|
connections_changed: result.connectionsChanged,
|
|
409
|
-
//
|
|
410
|
-
validation_errors: errors.map(e => e.reason || e.type),
|
|
411
|
-
validation_warnings: postIssues.filter(i => i.severity === "warning").map(w => w.reason || w.type),
|
|
412
|
-
// The modified workflow
|
|
403
|
+
// The modified workflow (LLM should analyze with ema://rules/*)
|
|
413
404
|
modified_workflow: sanitized,
|
|
414
|
-
_tip:
|
|
415
|
-
? "Fix validation errors before deploying"
|
|
416
|
-
: "Preview looks good. Call without preview=true to deploy",
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
// Deploy if validation passes
|
|
420
|
-
if (errors.length > 0) {
|
|
421
|
-
return {
|
|
422
|
-
error: "Validation failed - cannot deploy",
|
|
423
|
-
validation_errors: errors.map(e => e.reason || e.type),
|
|
424
|
-
changes_attempted: result.changesApplied,
|
|
425
|
-
_tip: "Fix validation errors and try again",
|
|
405
|
+
_tip: "Preview complete. Use ema://rules/anti-patterns to check for issues. Call without preview=true to deploy.",
|
|
426
406
|
};
|
|
427
407
|
}
|
|
428
408
|
// Deploy the workflow
|