@ema.co/mcp-toolkit 2026.1.26 → 2026.1.27-1
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 +17 -20
- package/dist/mcp/handlers/data/index.js +72 -6
- package/dist/mcp/handlers/deprecation.js +50 -0
- package/dist/mcp/handlers/env/index.js +3 -3
- package/dist/mcp/handlers/knowledge/index.js +44 -237
- package/dist/mcp/handlers/persona/create.js +47 -18
- package/dist/mcp/handlers/persona/index.js +9 -10
- package/dist/mcp/handlers/persona/update.js +4 -2
- package/dist/mcp/handlers/reference/index.js +15 -2
- package/dist/mcp/handlers/sync/index.js +3 -18
- package/dist/mcp/handlers/workflow/analyze.js +53 -105
- package/dist/mcp/handlers/workflow/deploy.js +129 -0
- package/dist/mcp/handlers/workflow/generate.js +8 -28
- package/dist/mcp/handlers/workflow/index.js +258 -85
- package/dist/mcp/handlers/workflow/modify.js +9 -29
- package/dist/mcp/handlers/workflow/optimize.js +22 -108
- package/dist/mcp/handlers/workflow/utils.js +0 -102
- package/dist/mcp/handlers-consolidated.js +15 -38
- package/dist/mcp/prompts.js +82 -44
- package/dist/mcp/resources.js +335 -3
- package/dist/mcp/server.js +242 -457
- package/dist/mcp/tools.js +44 -61
- package/dist/sdk/action-schema-parser.js +11 -5
- package/dist/sdk/client.js +46 -17
- package/dist/sdk/ema-client.js +11 -0
- package/dist/sdk/generated/deprecated-actions.js +171 -0
- package/dist/sdk/guidance.js +58 -35
- package/dist/sdk/index.js +8 -7
- package/dist/sdk/knowledge.js +216 -1932
- 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/dist/sdk/workflow-transformer.js +0 -342
- package/docs/dashboard-operations.md +35 -0
- package/docs/ema-user-guide.md +66 -0
- package/docs/mcp-tools-guide.md +74 -45
- package/package.json +2 -2
- package/dist/mcp/handlers/persona/analyze.js +0 -275
- package/dist/mcp/handlers/persona/compare.js +0 -32
- package/dist/mcp/handlers/workflow/compile.js +0 -39
- package/docs/DEBUG-ANALYSIS-unused-category-type-mismatch.md +0 -481
- package/docs/TODO-fix-analyzer-and-modify.md +0 -182
- package/resources/action-schema.json +0 -5678
|
@@ -1,43 +1,228 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workflow Handler
|
|
2
|
+
* Workflow Handler
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* - analyze: Return current state + issues
|
|
8
|
-
* - deploy: Deploy workflow to persona
|
|
9
|
-
* - compare: Compare two workflow versions
|
|
10
|
-
* - compile: Compile WorkflowSpec to workflow_def
|
|
11
|
-
* - optimize: Auto-fix issues
|
|
4
|
+
* PUBLIC TOOL MODES (workflow tool):
|
|
5
|
+
* - get: Return workflow data + schema for LLM to generate/modify
|
|
6
|
+
* - deploy: Deploy LLM-generated workflow_def
|
|
12
7
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* INTERNAL MODES (called from persona tool, not exposed):
|
|
9
|
+
* - modify: Used internally by persona(method="update", input="...")
|
|
10
|
+
* - generate: Used internally for workflow generation
|
|
11
|
+
* - analyze: Used internally for workflow analysis
|
|
12
|
+
* - optimize: Used internally for auto-fix
|
|
13
|
+
* - compare: Used internally for comparison
|
|
14
|
+
*
|
|
15
|
+
* THE LLM DOES ALL THE THINKING. MCP provides data and executes.
|
|
15
16
|
*/
|
|
17
|
+
import { generateSchema } from "../../../sdk/generation-schema.js";
|
|
16
18
|
// Re-export types
|
|
17
19
|
export * from "./types.js";
|
|
18
|
-
// Re-export utilities
|
|
20
|
+
// Re-export utilities
|
|
19
21
|
export * from "./utils.js";
|
|
20
|
-
// Import
|
|
22
|
+
// Import internal handlers (not exposed as tool modes, but used internally)
|
|
21
23
|
import { handleWorkflowAnalyze } from "./analyze.js";
|
|
22
24
|
import { handleWorkflowCompare } from "./compare.js";
|
|
23
|
-
import { handleWorkflowCompile } from "./compile.js";
|
|
24
25
|
import { handleWorkflowOptimize } from "./optimize.js";
|
|
25
26
|
import { handleWorkflowDeploy } from "./deploy.js";
|
|
26
27
|
import { handleWorkflowGenerate } from "./generate.js";
|
|
27
28
|
import { handleWorkflowModify } from "./modify.js";
|
|
28
|
-
// Re-export
|
|
29
|
+
// Re-export for internal use (persona handler routes to these)
|
|
29
30
|
export { handleWorkflowAnalyze } from "./analyze.js";
|
|
30
31
|
export { handleWorkflowCompare } from "./compare.js";
|
|
31
|
-
export { handleWorkflowCompile } from "./compile.js";
|
|
32
32
|
export { handleWorkflowOptimize } from "./optimize.js";
|
|
33
33
|
export { handleWorkflowDeploy } from "./deploy.js";
|
|
34
34
|
export { handleWorkflowGenerate } from "./generate.js";
|
|
35
35
|
export { handleWorkflowModify } from "./modify.js";
|
|
36
36
|
export { applyWorkflowModifications, buildWorkflowAction, buildInputBinding, } from "./modify.js";
|
|
37
|
+
// Fallback deprecated actions - ONLY used when API unavailable
|
|
38
|
+
// Source: ema/ema_backend/grpc/workflow_actions/registered_actions.py (synced 2026-01-26)
|
|
39
|
+
// Exported for use by deploy handler
|
|
40
|
+
export const DEPRECATED_ACTIONS_FALLBACK = {
|
|
41
|
+
"search/v0": { replacement: "search/v2", notes: "v2 requires datastore_configs input" },
|
|
42
|
+
"web_search/v0": { replacement: "live_web_search or ai_web_search", notes: "Split into two specialized actions" },
|
|
43
|
+
"call_llm/v0": { replacement: "call_llm/v2" },
|
|
44
|
+
"human_collaboration/v0": { replacement: "general_hitl" },
|
|
45
|
+
"fixed_response/v0": { replacement: "fixed_response/v1" },
|
|
46
|
+
"custom_agent/v0": { replacement: "custom_agent/v1" },
|
|
47
|
+
"json_mapper/v0": { replacement: "json_mapper/v1" },
|
|
48
|
+
"text_categorizer/v0": { replacement: "text_categorizer/v1" },
|
|
49
|
+
"respond_with_sources/v0": { replacement: "respond_for_external_actions", notes: "Handles tool results better" },
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Get deprecated actions from API (with fallback)
|
|
53
|
+
* Returns map of action/version -> replacement info
|
|
54
|
+
*/
|
|
55
|
+
async function getDeprecatedActions(client) {
|
|
56
|
+
try {
|
|
57
|
+
const actions = await client.listActions();
|
|
58
|
+
const deprecated = {};
|
|
59
|
+
for (const action of actions) {
|
|
60
|
+
if (action.deprecated) {
|
|
61
|
+
// Use action name as key (API doesn't include version in deprecated flag)
|
|
62
|
+
deprecated[action.id] = {
|
|
63
|
+
notes: "Deprecated - check catalog for current version",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Merge with fallback to get replacement info
|
|
68
|
+
for (const [key, info] of Object.entries(DEPRECATED_ACTIONS_FALLBACK)) {
|
|
69
|
+
const actionName = key.split("/")[0];
|
|
70
|
+
if (deprecated[actionName] || !deprecated[key]) {
|
|
71
|
+
deprecated[key] = { ...info, ...deprecated[key] };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { deprecated, source: "api" };
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// API unavailable, use fallback
|
|
78
|
+
return { deprecated: DEPRECATED_ACTIONS_FALLBACK, source: "fallback" };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check if workflow uses deprecated actions
|
|
83
|
+
* Exported for use by deploy handler
|
|
84
|
+
*/
|
|
85
|
+
export function checkWorkflowDeprecations(workflowDef, deprecatedActions) {
|
|
86
|
+
if (!workflowDef)
|
|
87
|
+
return [];
|
|
88
|
+
const warnings = [];
|
|
89
|
+
const actions = (workflowDef.actions ?? []);
|
|
90
|
+
for (const action of actions) {
|
|
91
|
+
const actionName = action.action?.name?.name;
|
|
92
|
+
const version = action.action?.name?.version || "v0";
|
|
93
|
+
if (!actionName)
|
|
94
|
+
continue;
|
|
95
|
+
const key = `${actionName}/${version}`;
|
|
96
|
+
const deprecation = deprecatedActions[key] || deprecatedActions[actionName];
|
|
97
|
+
if (deprecation) {
|
|
98
|
+
warnings.push({
|
|
99
|
+
action: action.name,
|
|
100
|
+
version,
|
|
101
|
+
replacement: deprecation.replacement,
|
|
102
|
+
notes: deprecation.notes,
|
|
103
|
+
severity: "warning",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return warnings;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Handle workflow(mode="get") - return data + schema + guidance for LLM
|
|
111
|
+
*
|
|
112
|
+
* Returns:
|
|
113
|
+
* - Current workflow_def (if exists)
|
|
114
|
+
* - Deprecation warnings for current workflow
|
|
115
|
+
* - Generation schema (agents, types, constraints)
|
|
116
|
+
* - Requirements and guidance (NOT hardcoded templates)
|
|
117
|
+
* - Available widgets
|
|
118
|
+
*
|
|
119
|
+
* LLM uses this to generate or modify workflow_def
|
|
120
|
+
*/
|
|
121
|
+
async function handleWorkflowGet(args, client) {
|
|
122
|
+
const personaId = args.persona_id;
|
|
123
|
+
if (!personaId) {
|
|
124
|
+
return { error: "persona_id required" };
|
|
125
|
+
}
|
|
126
|
+
const persona = await client.getPersonaById(personaId);
|
|
127
|
+
if (!persona) {
|
|
128
|
+
return { error: `Persona not found: ${personaId}` };
|
|
129
|
+
}
|
|
130
|
+
const protoConfig = persona.proto_config;
|
|
131
|
+
const widgets = (protoConfig?.widgets ?? []);
|
|
132
|
+
const workflowDef = persona.workflow_def;
|
|
133
|
+
// Extract available widget names for LLM
|
|
134
|
+
const availableWidgets = widgets
|
|
135
|
+
.filter(w => typeof w.name === "string")
|
|
136
|
+
.map(w => ({
|
|
137
|
+
name: w.name,
|
|
138
|
+
type: w.type,
|
|
139
|
+
}));
|
|
140
|
+
// Get generation schema for LLM
|
|
141
|
+
const schema = generateSchema();
|
|
142
|
+
// Get deprecated actions (API-first, with fallback)
|
|
143
|
+
const { deprecated: deprecatedActions, source: deprecationSource } = await getDeprecatedActions(client);
|
|
144
|
+
// Check if current workflow uses deprecated actions
|
|
145
|
+
const deprecationWarnings = checkWorkflowDeprecations(workflowDef, deprecatedActions);
|
|
146
|
+
// Build response
|
|
147
|
+
const result = {
|
|
148
|
+
persona_id: persona.id,
|
|
149
|
+
persona_name: persona.name,
|
|
150
|
+
persona_type: persona.type,
|
|
151
|
+
workflow_def: workflowDef ?? null,
|
|
152
|
+
available_widgets: availableWidgets,
|
|
153
|
+
// Deprecation warnings (severity: warning)
|
|
154
|
+
deprecation_warnings: deprecationWarnings.length > 0 ? deprecationWarnings : undefined,
|
|
155
|
+
deprecated_actions_source: deprecationWarnings.length > 0 ? deprecationSource : undefined,
|
|
156
|
+
// Schema for LLM to generate valid workflow_def
|
|
157
|
+
generation_schema: {
|
|
158
|
+
agents: schema.agents,
|
|
159
|
+
constraints: schema.constraints,
|
|
160
|
+
input_rules: schema.inputRules,
|
|
161
|
+
},
|
|
162
|
+
// HARD REQUIREMENTS (must be satisfied)
|
|
163
|
+
hard_requirements: {
|
|
164
|
+
workflow_output: {
|
|
165
|
+
rule: "Every workflow MUST have results.WORKFLOW_OUTPUT mapped to a final action output",
|
|
166
|
+
format: '{ "results": { "WORKFLOW_OUTPUT": { "actionName": "...", "outputName": "..." } } }',
|
|
167
|
+
failure: "Persona cannot be activated without this",
|
|
168
|
+
},
|
|
169
|
+
workflow_name: {
|
|
170
|
+
rule: "workflowName must be ['ema', 'personas', '<persona_id>']",
|
|
171
|
+
format: '{ "workflowName": ["ema", "personas", "actual-persona-id"] }',
|
|
172
|
+
},
|
|
173
|
+
action_structure: {
|
|
174
|
+
rule: "Each action needs: name (unique ID), action.name (namespaces, name, version), action.inputs",
|
|
175
|
+
namespaces: "Most actions use ['actions', 'emainternal'], triggers use ['triggers', 'emainternal']",
|
|
176
|
+
},
|
|
177
|
+
no_deprecated_actions: deprecationWarnings.length > 0 ? {
|
|
178
|
+
rule: "Update deprecated actions before deploying",
|
|
179
|
+
warnings: deprecationWarnings,
|
|
180
|
+
} : undefined,
|
|
181
|
+
},
|
|
182
|
+
// GUIDANCE (best practices, not hard requirements)
|
|
183
|
+
guidance: {
|
|
184
|
+
flow_pattern: "trigger → categorizer (optional) → processing → response → WORKFLOW_OUTPUT",
|
|
185
|
+
categorizer: "For multi-intent workflows, use chat_categorizer early with a Fallback category",
|
|
186
|
+
fallback: "Every categorizer MUST have a Fallback category for unmatched intents",
|
|
187
|
+
hitl: "For actions with side effects (email, external calls), consider general_hitl for approval",
|
|
188
|
+
search: "Use search/v2 with datastore_configs. Wire query from trigger output.",
|
|
189
|
+
},
|
|
190
|
+
// LLM-driven analysis pattern: MCP provides data, LLM does the thinking
|
|
191
|
+
_next_steps: deprecationWarnings.length > 0
|
|
192
|
+
? [
|
|
193
|
+
"WARNING: Workflow uses deprecated actions. Update them first:",
|
|
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
|
+
"",
|
|
202
|
+
"Then deploy with: workflow(mode='deploy', persona_id='...', workflow_def={...})",
|
|
203
|
+
]
|
|
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
|
+
"",
|
|
211
|
+
"Generate or modify workflow_def based on requirements above",
|
|
212
|
+
"Deploy with: workflow(mode='deploy', persona_id='...', workflow_def={...})",
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
// Include deprecated actions reference if there are warnings or on request
|
|
216
|
+
if (deprecationWarnings.length > 0 || args.include_deprecated) {
|
|
217
|
+
result.deprecated_actions_reference = deprecatedActions;
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
37
221
|
/**
|
|
38
222
|
* Main workflow handler with mode-based dispatch
|
|
39
223
|
*
|
|
40
|
-
*
|
|
224
|
+
* PUBLIC modes: get, deploy
|
|
225
|
+
* INTERNAL modes: modify, generate, analyze, optimize, compare (called from persona tool)
|
|
41
226
|
*/
|
|
42
227
|
export async function handleWorkflow(args, client, getTemplateId) {
|
|
43
228
|
const personaId = args.persona_id;
|
|
@@ -46,90 +231,78 @@ export async function handleWorkflow(args, client, getTemplateId) {
|
|
|
46
231
|
const optimize = args.optimize;
|
|
47
232
|
const compareTo = args.compare_to;
|
|
48
233
|
const operations = args.operations;
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
effectiveMode = "deploy";
|
|
84
|
-
// Route to extracted handler
|
|
85
|
-
switch (effectiveMode) {
|
|
86
|
-
case "generate":
|
|
87
|
-
return handleWorkflowGenerate(args, client, getTemplateId);
|
|
88
|
-
case "modify":
|
|
89
|
-
case "extend":
|
|
90
|
-
return handleWorkflowModify(args, client);
|
|
91
|
-
case "analyze":
|
|
92
|
-
return handleWorkflowAnalyze(args, client);
|
|
93
|
-
case "deploy":
|
|
94
|
-
return handleWorkflowDeploy(args, client);
|
|
95
|
-
case "compare":
|
|
96
|
-
return handleWorkflowCompare(args, client);
|
|
97
|
-
case "compile":
|
|
98
|
-
return handleWorkflowCompile(args, client);
|
|
99
|
-
case "optimize":
|
|
100
|
-
return handleWorkflowOptimize(args, client);
|
|
101
|
-
default:
|
|
102
|
-
return { error: `Unknown mode: ${effectiveMode}` };
|
|
234
|
+
// Explicit mode takes priority
|
|
235
|
+
const mode = args.mode;
|
|
236
|
+
// PUBLIC MODES (from workflow tool)
|
|
237
|
+
if (mode === "get") {
|
|
238
|
+
return handleWorkflowGet(args, client);
|
|
239
|
+
}
|
|
240
|
+
if (mode === "deploy") {
|
|
241
|
+
return handleWorkflowDeploy(args, client);
|
|
242
|
+
}
|
|
243
|
+
// INTERNAL MODES (called from persona tool, not workflow tool)
|
|
244
|
+
if (mode === "modify" || mode === "extend") {
|
|
245
|
+
return handleWorkflowModify(args, client);
|
|
246
|
+
}
|
|
247
|
+
if (mode === "generate") {
|
|
248
|
+
return handleWorkflowGenerate(args, client, getTemplateId);
|
|
249
|
+
}
|
|
250
|
+
if (mode === "analyze") {
|
|
251
|
+
return handleWorkflowAnalyze(args, client);
|
|
252
|
+
}
|
|
253
|
+
if (mode === "optimize") {
|
|
254
|
+
return handleWorkflowOptimize(args, client);
|
|
255
|
+
}
|
|
256
|
+
if (mode === "compare") {
|
|
257
|
+
return handleWorkflowCompare(args, client);
|
|
258
|
+
}
|
|
259
|
+
// Auto-detect mode (for backwards compatibility with internal routing)
|
|
260
|
+
if (compareTo) {
|
|
261
|
+
return handleWorkflowCompare(args, client);
|
|
262
|
+
}
|
|
263
|
+
if (optimize && personaId) {
|
|
264
|
+
return handleWorkflowOptimize(args, client);
|
|
265
|
+
}
|
|
266
|
+
if (personaId && workflowDef) {
|
|
267
|
+
return handleWorkflowDeploy(args, client);
|
|
103
268
|
}
|
|
269
|
+
if (personaId && (input || operations)) {
|
|
270
|
+
return handleWorkflowModify(args, client);
|
|
271
|
+
}
|
|
272
|
+
if (input && !personaId) {
|
|
273
|
+
return handleWorkflowGenerate(args, client, getTemplateId);
|
|
274
|
+
}
|
|
275
|
+
if (personaId) {
|
|
276
|
+
return handleWorkflowAnalyze(args, client);
|
|
277
|
+
}
|
|
278
|
+
// Invalid mode
|
|
279
|
+
return {
|
|
280
|
+
error: `Invalid or missing mode: ${mode}`,
|
|
281
|
+
public_modes: ["get", "deploy"],
|
|
282
|
+
hint: "MCP provides data (get) and executes (deploy). LLM does all thinking.",
|
|
283
|
+
};
|
|
104
284
|
}
|
|
105
285
|
/**
|
|
106
286
|
* Check if a workflow mode has been extracted
|
|
107
287
|
*/
|
|
108
288
|
export function hasExtractedWorkflowHandler(mode) {
|
|
109
|
-
const extractedModes = ["
|
|
289
|
+
const extractedModes = ["get", "deploy", "modify", "extend", "generate", "analyze", "optimize", "compare"];
|
|
110
290
|
return extractedModes.includes(mode);
|
|
111
291
|
}
|
|
112
292
|
/**
|
|
113
293
|
* Get handler for a specific workflow mode
|
|
114
|
-
* Note: generate handler has a different signature (requires getTemplateId)
|
|
115
294
|
*/
|
|
116
295
|
export function getWorkflowModeHandler(mode) {
|
|
117
296
|
switch (mode) {
|
|
118
|
-
case "
|
|
119
|
-
return handleWorkflowAnalyze;
|
|
120
|
-
case "compare":
|
|
121
|
-
return handleWorkflowCompare;
|
|
122
|
-
case "compile":
|
|
123
|
-
return handleWorkflowCompile;
|
|
124
|
-
case "optimize":
|
|
125
|
-
return handleWorkflowOptimize;
|
|
297
|
+
case "get":
|
|
126
298
|
case "deploy":
|
|
127
|
-
return handleWorkflowDeploy;
|
|
128
|
-
case "generate":
|
|
129
|
-
return handleWorkflowGenerate;
|
|
130
299
|
case "modify":
|
|
131
300
|
case "extend":
|
|
132
|
-
|
|
301
|
+
case "generate":
|
|
302
|
+
case "analyze":
|
|
303
|
+
case "optimize":
|
|
304
|
+
case "compare":
|
|
305
|
+
return handleWorkflow;
|
|
133
306
|
default:
|
|
134
307
|
return undefined;
|
|
135
308
|
}
|
|
@@ -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
|
|
@@ -1,80 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workflow Optimize Handler
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* DEPRECATED: This handler returned pre-computed fixes which violates
|
|
5
|
+
* the "MCP = data, LLM = logic" principle.
|
|
6
|
+
*
|
|
7
|
+
* The LLM should:
|
|
8
|
+
* 1. Get workflow with workflow(mode="get")
|
|
9
|
+
* 2. Fetch rules from ema://rules/anti-patterns
|
|
10
|
+
* 3. Apply rules and propose fixes
|
|
11
|
+
* 4. Deploy via workflow(mode="deploy")
|
|
5
12
|
*/
|
|
6
|
-
import { detectWorkflowIssues, suggestWorkflowFixes } from "../../../sdk/knowledge.js";
|
|
7
|
-
/**
|
|
8
|
-
* Apply simple workflow fixes for auto_fixable issues
|
|
9
|
-
* Handles orphan removal with cascading dependency cleanup
|
|
10
|
-
*/
|
|
11
|
-
function applySimpleFixes(workflow, issues) {
|
|
12
|
-
// Deep clone to avoid mutating original
|
|
13
|
-
const fixed = JSON.parse(JSON.stringify(workflow));
|
|
14
|
-
// Get nodes array from workflow (handle different structures)
|
|
15
|
-
const getNodes = (w) => {
|
|
16
|
-
if (Array.isArray(w.nodes))
|
|
17
|
-
return w.nodes;
|
|
18
|
-
const wd = w.workflow_def;
|
|
19
|
-
if (wd && Array.isArray(wd.nodes))
|
|
20
|
-
return wd.nodes;
|
|
21
|
-
return undefined;
|
|
22
|
-
};
|
|
23
|
-
const nodes = getNodes(fixed);
|
|
24
|
-
if (!nodes)
|
|
25
|
-
return fixed;
|
|
26
|
-
// Collect all orphan node IDs to remove
|
|
27
|
-
const nodesToRemove = new Set();
|
|
28
|
-
for (const issue of issues) {
|
|
29
|
-
if (issue.type === "orphan" && issue.auto_fixable && issue.node) {
|
|
30
|
-
nodesToRemove.add(issue.node);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (nodesToRemove.size === 0)
|
|
34
|
-
return fixed;
|
|
35
|
-
// Step 1: Remove orphan nodes
|
|
36
|
-
const filteredNodes = nodes.filter(n => {
|
|
37
|
-
const nodeId = n.id;
|
|
38
|
-
return nodeId && !nodesToRemove.has(nodeId);
|
|
39
|
-
});
|
|
40
|
-
// Step 2: Clean up dangling references in remaining nodes
|
|
41
|
-
for (const node of filteredNodes) {
|
|
42
|
-
const incomingEdges = node.incoming_edges;
|
|
43
|
-
if (incomingEdges && Array.isArray(incomingEdges)) {
|
|
44
|
-
// Filter out edges that reference removed nodes
|
|
45
|
-
node.incoming_edges = incomingEdges.filter(edge => {
|
|
46
|
-
const sourceNodeId = edge.source_node_id;
|
|
47
|
-
return sourceNodeId && !nodesToRemove.has(sourceNodeId);
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
// Also clean up inputBindings if they exist (different workflow format)
|
|
51
|
-
const inputBindings = node.inputBindings;
|
|
52
|
-
if (inputBindings && Array.isArray(inputBindings)) {
|
|
53
|
-
node.inputBindings = inputBindings.filter(binding => {
|
|
54
|
-
const actionOutput = binding.actionOutput;
|
|
55
|
-
const actionName = actionOutput?.actionName;
|
|
56
|
-
return !actionName || !nodesToRemove.has(actionName);
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
// Update nodes in the workflow
|
|
61
|
-
if (Array.isArray(fixed.nodes)) {
|
|
62
|
-
fixed.nodes = filteredNodes;
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
const wd = fixed.workflow_def;
|
|
66
|
-
if (wd) {
|
|
67
|
-
wd.nodes = filteredNodes;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return fixed;
|
|
71
|
-
}
|
|
72
13
|
/**
|
|
73
14
|
* Handle workflow optimize mode
|
|
15
|
+
*
|
|
16
|
+
* NOW DEPRECATED - Returns guidance for LLM to do the analysis.
|
|
74
17
|
*/
|
|
75
18
|
export async function handleWorkflowOptimize(args, client) {
|
|
76
19
|
const personaId = args.persona_id;
|
|
77
|
-
const preview = args.preview !== false;
|
|
78
20
|
if (!personaId) {
|
|
79
21
|
return { error: "persona_id required for optimize mode" };
|
|
80
22
|
}
|
|
@@ -89,48 +31,20 @@ export async function handleWorkflowOptimize(args, client) {
|
|
|
89
31
|
hint: "Use mode='generate' to create a workflow first",
|
|
90
32
|
};
|
|
91
33
|
}
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
const fixes = suggestWorkflowFixes(issues);
|
|
95
|
-
if (issues.length === 0) {
|
|
96
|
-
return {
|
|
97
|
-
mode: "optimize",
|
|
98
|
-
status: "✅ No issues found",
|
|
99
|
-
persona_id: personaId,
|
|
100
|
-
persona_name: persona.name,
|
|
101
|
-
workflow_healthy: true,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
// Apply fixes
|
|
105
|
-
const fixedWorkflow = applySimpleFixes(existingWorkflow, issues);
|
|
106
|
-
const result = {
|
|
34
|
+
// Return the workflow and guidance for LLM to do the analysis
|
|
35
|
+
return {
|
|
107
36
|
mode: "optimize",
|
|
108
|
-
status:
|
|
37
|
+
status: "DEPRECATED - Use LLM analysis instead",
|
|
109
38
|
persona_id: personaId,
|
|
110
39
|
persona_name: persona.name,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
40
|
+
// Return the workflow for LLM to analyze
|
|
41
|
+
workflow_def: existingWorkflow,
|
|
42
|
+
_next_steps: [
|
|
43
|
+
"1. Use workflow(mode='get') to get workflow data",
|
|
44
|
+
"2. Fetch ema://rules/anti-patterns",
|
|
45
|
+
"3. Apply rules to find issues (LLM does this, not MCP)",
|
|
46
|
+
"4. Modify workflow based on analysis",
|
|
47
|
+
"5. Deploy via workflow(mode='deploy', workflow_def={...})",
|
|
48
|
+
],
|
|
119
49
|
};
|
|
120
|
-
// If preview=false, deploy the fixed workflow
|
|
121
|
-
if (!preview) {
|
|
122
|
-
await client.updateAiEmployee({
|
|
123
|
-
persona_id: personaId,
|
|
124
|
-
workflow: fixedWorkflow,
|
|
125
|
-
proto_config: args.proto_config || persona.proto_config,
|
|
126
|
-
});
|
|
127
|
-
result.deployed = true;
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
result.next_steps = [
|
|
131
|
-
"Review the suggested fixes and fixed_workflow",
|
|
132
|
-
`Deploy with: workflow(mode="optimize", persona_id="${personaId}", preview=false)`,
|
|
133
|
-
];
|
|
134
|
-
}
|
|
135
|
-
return result;
|
|
136
50
|
}
|