@ema.co/mcp-toolkit 2026.1.25 → 2026.1.26-4
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/README.md +10 -2
- package/dist/mcp/handlers/action/index.js +3 -18
- package/dist/mcp/handlers/data/index.js +385 -41
- package/dist/mcp/handlers/data/templates.js +107 -0
- package/dist/mcp/handlers/deprecation.js +50 -0
- package/dist/mcp/handlers/env/index.js +8 -4
- 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 +14 -11
- package/dist/mcp/handlers/persona/update.js +4 -2
- package/dist/mcp/handlers/persona/version.js +234 -0
- package/dist/mcp/handlers/sync/index.js +3 -18
- package/dist/mcp/handlers/template/index.js +75 -10
- package/dist/mcp/handlers/workflow/analyze.js +171 -0
- package/dist/mcp/handlers/workflow/compare.js +70 -0
- package/dist/mcp/handlers/workflow/deploy.js +73 -0
- package/dist/mcp/handlers/workflow/generate.js +350 -0
- package/dist/mcp/handlers/workflow/index.js +294 -0
- package/dist/mcp/handlers/workflow/modify.js +456 -0
- package/dist/mcp/handlers/workflow/optimize.js +136 -0
- package/dist/mcp/handlers/workflow/types.js +4 -0
- package/dist/mcp/handlers/workflow/utils.js +30 -0
- package/dist/mcp/handlers-consolidated.js +73 -2696
- package/dist/mcp/prompts.js +83 -43
- package/dist/mcp/resources.js +382 -57
- package/dist/mcp/server.js +199 -391
- package/dist/mcp/{tools-v2.js → tools.js} +20 -54
- package/dist/mcp/workflow-operations.js +2 -2
- package/dist/sdk/client-adapter.js +267 -32
- package/dist/sdk/client.js +45 -16
- package/dist/sdk/ema-client.js +183 -0
- package/dist/sdk/generated/deprecated-actions.js +171 -0
- package/dist/sdk/generated/template-fallbacks.js +123 -0
- package/dist/sdk/guidance.js +65 -11
- package/dist/sdk/index.js +3 -1
- package/dist/sdk/knowledge.js +139 -86
- package/dist/sdk/workflow-intent.js +27 -0
- package/dist/sdk/workflow-transformer.js +0 -342
- package/docs/mcp-tools-guide.md +37 -45
- package/package.json +10 -4
- package/dist/mcp/handlers/persona/analyze.js +0 -275
- package/dist/mcp/handlers/persona/compare.js +0 -32
- package/dist/mcp/tools-consolidated.js +0 -875
- package/dist/mcp/tools-legacy.js +0 -736
- package/docs/CODEBASE-ANALYSIS-2026-01-23.md +0 -936
- package/docs/CODEBASE-ANALYSIS-PRIORITIZED.md +0 -774
- package/docs/api-contracts.md +0 -216
- package/docs/auto-builder-analysis.md +0 -271
- package/docs/blog/mcp-tool-design-lessons.md +0 -309
- package/docs/data-architecture.md +0 -166
- package/docs/demos/ap-invoice-generation.md +0 -347
- package/docs/demos/ap-invoice-processing.md +0 -271
- package/docs/ema-auto-builder-guide.html +0 -394
- package/docs/lessons-learned.md +0 -209
- package/docs/llm-native-workflow-design.md +0 -252
- package/docs/local-generation.md +0 -508
- package/docs/mcp-flow-diagram.md +0 -135
- package/docs/migration/action-composition-migration.md +0 -270
- package/docs/naming-conventions.md +0 -278
- package/docs/proposals/HANDOFF-tool-restructure.md +0 -526
- package/docs/proposals/action-composition.md +0 -490
- package/docs/proposals/explicit-method-restructure.md +0 -328
- package/docs/proposals/mcp-tool-restructure-2026-01.md +0 -366
- package/docs/proposals/self-contained-guidance.md +0 -427
- package/docs/proto-sdk-generation.md +0 -242
- package/docs/release-impact.md +0 -102
- package/docs/release-process.md +0 -157
- package/docs/staging.RULE.md +0 -142
- package/docs/test-persona-creation.md +0 -196
- package/docs/tool-consolidation-v2.md +0 -225
- package/docs/tool-response-standards.md +0 -256
- package/resources/demo-kits/README.md +0 -175
- package/resources/demo-kits/finance-ap/manifest.json +0 -150
- package/resources/demo-kits/tags.json +0 -91
- package/resources/docs/getting-started.md +0 -97
- package/resources/templates/auto-builder-rules.md +0 -224
- package/resources/templates/chat-ai/README.md +0 -119
- package/resources/templates/chat-ai/persona-config.json +0 -111
- package/resources/templates/dashboard-ai/README.md +0 -156
- package/resources/templates/dashboard-ai/persona-config.json +0 -180
- package/resources/templates/demo-scenarios/README.md +0 -63
- package/resources/templates/demo-scenarios/test-published-package.md +0 -116
- package/resources/templates/document-gen-ai/README.md +0 -132
- package/resources/templates/document-gen-ai/persona-config.json +0 -316
- package/resources/templates/voice-ai/README.md +0 -123
- package/resources/templates/voice-ai/persona-config.json +0 -74
- package/resources/templates/voice-ai/workflow-prompt.md +0 -121
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Modify Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles brownfield workflow modifications via STRUCTURED OPERATIONS.
|
|
5
|
+
*
|
|
6
|
+
* CRITICAL: This handler does NOT parse natural language.
|
|
7
|
+
* The Agent (LLM) builds structured operations and passes them here.
|
|
8
|
+
*
|
|
9
|
+
* See: src/mcp/AGENTS.md for anti-patterns to avoid.
|
|
10
|
+
*/
|
|
11
|
+
import { detectWorkflowIssues, suggestWorkflowFixes } from "../../../sdk/knowledge.js";
|
|
12
|
+
import { sanitizeWorkflowForDeploy } from "./utils.js";
|
|
13
|
+
import { ensureSchemaRegistry } from "../../../sdk/workflow-validator.js";
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
15
|
+
// Action Building Helpers (use ActionRegistry for metadata)
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* Get action metadata from registry.
|
|
19
|
+
* Falls back to defaults if action not found.
|
|
20
|
+
*/
|
|
21
|
+
function getActionMetadata(actionType, registry) {
|
|
22
|
+
// Try to get from registry first
|
|
23
|
+
if (registry?.isLoaded()) {
|
|
24
|
+
const action = registry.getAction(actionType);
|
|
25
|
+
if (action) {
|
|
26
|
+
return {
|
|
27
|
+
namespaces: ["actions", "emainternal"], // Standard namespace
|
|
28
|
+
version: action.version,
|
|
29
|
+
apiName: actionType,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Fallback defaults (registry should have these, but just in case)
|
|
34
|
+
return {
|
|
35
|
+
namespaces: ["actions", "emainternal"],
|
|
36
|
+
version: "v0",
|
|
37
|
+
apiName: actionType,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build an input binding for a workflow action.
|
|
42
|
+
*/
|
|
43
|
+
function buildInputBinding(config) {
|
|
44
|
+
switch (config.type) {
|
|
45
|
+
case "action_output":
|
|
46
|
+
return {
|
|
47
|
+
actionOutput: {
|
|
48
|
+
actionName: config.actionName,
|
|
49
|
+
output: config.output,
|
|
50
|
+
},
|
|
51
|
+
autoDetectedBinding: false,
|
|
52
|
+
};
|
|
53
|
+
case "inline_string":
|
|
54
|
+
return {
|
|
55
|
+
inline: { wellKnown: { stringValue: config.value } },
|
|
56
|
+
autoDetectedBinding: false,
|
|
57
|
+
};
|
|
58
|
+
case "inline_number":
|
|
59
|
+
return {
|
|
60
|
+
inline: { number: config.value },
|
|
61
|
+
autoDetectedBinding: false,
|
|
62
|
+
};
|
|
63
|
+
case "llm_inferred":
|
|
64
|
+
default:
|
|
65
|
+
return { llmInferred: {} };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build a complete workflow action structure from InsertNodeConfig.
|
|
70
|
+
*/
|
|
71
|
+
function buildWorkflowAction(config, registry) {
|
|
72
|
+
const nodeName = config.node_name || `${config.action_type}_${Date.now()}`;
|
|
73
|
+
const metadata = getActionMetadata(config.action_type, registry);
|
|
74
|
+
const action = {
|
|
75
|
+
name: nodeName,
|
|
76
|
+
action: {
|
|
77
|
+
name: {
|
|
78
|
+
namespaces: metadata.namespaces,
|
|
79
|
+
name: metadata.apiName,
|
|
80
|
+
},
|
|
81
|
+
version: metadata.version,
|
|
82
|
+
},
|
|
83
|
+
inputs: {},
|
|
84
|
+
displaySettings: {
|
|
85
|
+
displayName: config.display_name,
|
|
86
|
+
description: config.description || "",
|
|
87
|
+
coordinates: { x: 1200, y: 400 },
|
|
88
|
+
},
|
|
89
|
+
typeArguments: {},
|
|
90
|
+
tools: [],
|
|
91
|
+
disableHumanInteraction: false,
|
|
92
|
+
};
|
|
93
|
+
// Build inputs from config
|
|
94
|
+
if (config.inputs) {
|
|
95
|
+
const inputs = {};
|
|
96
|
+
for (const [inputName, binding] of Object.entries(config.inputs)) {
|
|
97
|
+
inputs[inputName] = buildInputBinding(binding);
|
|
98
|
+
}
|
|
99
|
+
action.inputs = inputs;
|
|
100
|
+
}
|
|
101
|
+
// Build runIf condition if provided
|
|
102
|
+
if (config.runIf) {
|
|
103
|
+
const operatorMap = { eq: 1, neq: 2 };
|
|
104
|
+
action.runIf = {
|
|
105
|
+
lhs: {
|
|
106
|
+
actionOutput: {
|
|
107
|
+
actionName: config.runIf.sourceAction,
|
|
108
|
+
output: config.runIf.sourceOutput,
|
|
109
|
+
},
|
|
110
|
+
autoDetectedBinding: false,
|
|
111
|
+
},
|
|
112
|
+
operator: operatorMap[config.runIf.operator] || 1,
|
|
113
|
+
rhs: {
|
|
114
|
+
inline: { enumValue: config.runIf.value },
|
|
115
|
+
autoDetectedBinding: false,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return action;
|
|
120
|
+
}
|
|
121
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
122
|
+
// Operation Executors
|
|
123
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
124
|
+
/**
|
|
125
|
+
* Execute an insert operation.
|
|
126
|
+
*/
|
|
127
|
+
function executeInsert(actions, op, registry, result) {
|
|
128
|
+
const config = op.insert;
|
|
129
|
+
if (!config) {
|
|
130
|
+
result.changesApplied.push("Insert failed: no configuration provided");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!config.action_type || !config.display_name) {
|
|
134
|
+
result.changesApplied.push("Insert failed: action_type and display_name required");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Build the new action
|
|
138
|
+
const newAction = buildWorkflowAction(config, registry);
|
|
139
|
+
const newNodeName = String(newAction.name);
|
|
140
|
+
// Add to actions array
|
|
141
|
+
actions.push(newAction);
|
|
142
|
+
result.nodesAdded.push(newNodeName);
|
|
143
|
+
result.changesApplied.push(`Inserted node "${newNodeName}" (${config.display_name})`);
|
|
144
|
+
// Handle insert_before: modify downstream node
|
|
145
|
+
if (config.insert_before) {
|
|
146
|
+
const targetNode = actions.find(a => String(a.name || "").toLowerCase() === config.insert_before.toLowerCase());
|
|
147
|
+
if (!targetNode) {
|
|
148
|
+
result.changesApplied.push(`Warning: target node "${config.insert_before}" not found for downstream modification`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Option 1: Add runIf to downstream node
|
|
152
|
+
if (config.add_runif_to_downstream && config.new_node_output) {
|
|
153
|
+
const operatorMap = { eq: 1, neq: 2 };
|
|
154
|
+
targetNode.runIf = {
|
|
155
|
+
lhs: {
|
|
156
|
+
actionOutput: {
|
|
157
|
+
actionName: newNodeName,
|
|
158
|
+
output: config.new_node_output,
|
|
159
|
+
},
|
|
160
|
+
autoDetectedBinding: false,
|
|
161
|
+
},
|
|
162
|
+
operator: operatorMap["eq"],
|
|
163
|
+
rhs: {
|
|
164
|
+
inline: { enumValue: "HITL Success" },
|
|
165
|
+
autoDetectedBinding: false,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
result.nodesModified.push(String(targetNode.name));
|
|
169
|
+
result.changesApplied.push(`Added runIf to ${targetNode.name}: waits for ${newNodeName}.${config.new_node_output}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Execute a remove operation.
|
|
175
|
+
*/
|
|
176
|
+
function executeRemove(actions, op, result) {
|
|
177
|
+
const removeConfig = op.remove;
|
|
178
|
+
if (!removeConfig?.nodes?.length) {
|
|
179
|
+
result.changesApplied.push("Remove failed: no nodes specified");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
for (const nodeName of removeConfig.nodes) {
|
|
183
|
+
const index = actions.findIndex(a => String(a.name || "") === nodeName);
|
|
184
|
+
if (index >= 0) {
|
|
185
|
+
actions.splice(index, 1);
|
|
186
|
+
result.nodesRemoved.push(nodeName);
|
|
187
|
+
result.changesApplied.push(`Removed node: ${nodeName}`);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
result.changesApplied.push(`Warning: node "${nodeName}" not found`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Execute a rewire operation.
|
|
196
|
+
*/
|
|
197
|
+
function executeRewire(actions, op, result) {
|
|
198
|
+
const rewireConfig = op.rewire;
|
|
199
|
+
if (!rewireConfig) {
|
|
200
|
+
result.changesApplied.push("Rewire failed: no configuration provided");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const { target_node, target_input, source_node, source_output } = rewireConfig;
|
|
204
|
+
// Find target node
|
|
205
|
+
const targetAction = actions.find(a => String(a.name || "").toLowerCase() === target_node.toLowerCase());
|
|
206
|
+
if (!targetAction) {
|
|
207
|
+
result.changesApplied.push(`Rewire failed: target node "${target_node}" not found`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Update the input binding
|
|
211
|
+
const inputs = (targetAction.inputs || {});
|
|
212
|
+
inputs[target_input] = {
|
|
213
|
+
actionOutput: {
|
|
214
|
+
actionName: source_node,
|
|
215
|
+
output: source_output,
|
|
216
|
+
},
|
|
217
|
+
autoDetectedBinding: false,
|
|
218
|
+
};
|
|
219
|
+
targetAction.inputs = inputs;
|
|
220
|
+
result.nodesModified.push(target_node);
|
|
221
|
+
result.connectionsChanged.push(`${source_node}.${source_output} → ${target_node}.${target_input}`);
|
|
222
|
+
result.changesApplied.push(`Rewired ${target_node}.${target_input} to use ${source_node}.${source_output}`);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Execute a config update operation.
|
|
226
|
+
*/
|
|
227
|
+
function executeConfigUpdate(workflow, op, result) {
|
|
228
|
+
const configUpdates = op.config;
|
|
229
|
+
if (!configUpdates) {
|
|
230
|
+
result.changesApplied.push("Config update failed: no configuration provided");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// Config updates are typically applied to proto_config, not workflow_def
|
|
234
|
+
// Store them for the caller to apply
|
|
235
|
+
result.changesApplied.push(`Config updates prepared: ${Object.keys(configUpdates).join(", ")}`);
|
|
236
|
+
}
|
|
237
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
238
|
+
// Main Operation Applier
|
|
239
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
240
|
+
/**
|
|
241
|
+
* Apply structured modification operations to a workflow.
|
|
242
|
+
*
|
|
243
|
+
* IMPORTANT: This function executes STRUCTURED operations.
|
|
244
|
+
* It does NOT parse natural language - that's the Agent's job.
|
|
245
|
+
*/
|
|
246
|
+
function applyWorkflowModifications(existingWorkflow, operations, registry) {
|
|
247
|
+
// Deep copy workflow
|
|
248
|
+
const workflow = JSON.parse(JSON.stringify(existingWorkflow));
|
|
249
|
+
const actions = (workflow.actions || []);
|
|
250
|
+
const result = {
|
|
251
|
+
workflow,
|
|
252
|
+
changesApplied: [],
|
|
253
|
+
nodesAdded: [],
|
|
254
|
+
nodesRemoved: [],
|
|
255
|
+
nodesModified: [],
|
|
256
|
+
connectionsChanged: [],
|
|
257
|
+
};
|
|
258
|
+
for (const op of operations) {
|
|
259
|
+
switch (op.type) {
|
|
260
|
+
case "insert":
|
|
261
|
+
executeInsert(actions, op, registry, result);
|
|
262
|
+
break;
|
|
263
|
+
case "remove":
|
|
264
|
+
executeRemove(actions, op, result);
|
|
265
|
+
break;
|
|
266
|
+
case "rewire":
|
|
267
|
+
executeRewire(actions, op, result);
|
|
268
|
+
break;
|
|
269
|
+
case "update_config":
|
|
270
|
+
executeConfigUpdate(workflow, op, result);
|
|
271
|
+
break;
|
|
272
|
+
case "consolidate":
|
|
273
|
+
// Consolidation (merging multiple nodes into one) is a multi-step operation
|
|
274
|
+
// that requires the Agent to use multiple operations instead:
|
|
275
|
+
// 1. Add the new consolidated node (insert)
|
|
276
|
+
// 2. Rewire connections to use the new node (rewire)
|
|
277
|
+
// 3. Remove the old nodes (remove)
|
|
278
|
+
// Mark as not executed - Agent should break this down
|
|
279
|
+
result.changesApplied.push("SKIPPED: consolidate not supported as a single operation. " +
|
|
280
|
+
"Use insert + rewire + remove operations instead.");
|
|
281
|
+
break;
|
|
282
|
+
default:
|
|
283
|
+
result.changesApplied.push(`Unknown operation type: ${op.type}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
workflow.actions = actions;
|
|
287
|
+
result.workflow = workflow;
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
291
|
+
// Main Handler
|
|
292
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
293
|
+
/**
|
|
294
|
+
* Handle workflow modify/extend mode.
|
|
295
|
+
*
|
|
296
|
+
* This handler accepts STRUCTURED OPERATIONS from the Agent.
|
|
297
|
+
* It does NOT parse natural language input.
|
|
298
|
+
*
|
|
299
|
+
* @param args.persona_id - The persona to modify
|
|
300
|
+
* @param args.operations - Array of structured ModificationOperation
|
|
301
|
+
* @param args.preview - If true, return preview without deploying
|
|
302
|
+
*/
|
|
303
|
+
export async function handleWorkflowModify(args, client) {
|
|
304
|
+
const personaId = args.persona_id || args.id;
|
|
305
|
+
if (!personaId) {
|
|
306
|
+
return {
|
|
307
|
+
error: "persona_id required",
|
|
308
|
+
hint: "Specify the persona to modify: persona_id='<id>'",
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// Get operations from args
|
|
312
|
+
const operations = args.operations;
|
|
313
|
+
const preview = Boolean(args.preview);
|
|
314
|
+
// If no structured operations provided, return guidance
|
|
315
|
+
if (!operations || operations.length === 0) {
|
|
316
|
+
// Fetch persona for context
|
|
317
|
+
const persona = await client.getPersonaById(personaId);
|
|
318
|
+
if (!persona) {
|
|
319
|
+
return { error: `Persona "${personaId}" not found` };
|
|
320
|
+
}
|
|
321
|
+
const workflow = persona.workflow_def;
|
|
322
|
+
if (!workflow) {
|
|
323
|
+
return { error: "Persona has no workflow_def" };
|
|
324
|
+
}
|
|
325
|
+
const actions = (workflow.actions || []);
|
|
326
|
+
// Run analysis to give Agent context
|
|
327
|
+
const issues = detectWorkflowIssues(workflow);
|
|
328
|
+
const suggestions = suggestWorkflowFixes(issues);
|
|
329
|
+
// Load schema registry for action catalog
|
|
330
|
+
const schemaRegistry = await ensureSchemaRegistry(client);
|
|
331
|
+
const availableActions = schemaRegistry.isLoaded()
|
|
332
|
+
? schemaRegistry.getAllActions().map(a => ({ name: a.name, category: a.category, description: a.description }))
|
|
333
|
+
: [];
|
|
334
|
+
return {
|
|
335
|
+
message: "No operations provided. Returning workflow context for Agent to build operations.",
|
|
336
|
+
persona_id: personaId,
|
|
337
|
+
persona_name: persona.name,
|
|
338
|
+
// Current workflow state
|
|
339
|
+
current_nodes: actions.map(a => ({
|
|
340
|
+
name: a.name,
|
|
341
|
+
display_name: a.displaySettings?.displayName,
|
|
342
|
+
action_type: a.action?.name?.name,
|
|
343
|
+
})),
|
|
344
|
+
// Analysis
|
|
345
|
+
issues: issues.map(i => ({ severity: i.severity, type: i.type, node: i.node, reason: i.reason })),
|
|
346
|
+
suggestions,
|
|
347
|
+
// Available actions from registry
|
|
348
|
+
available_actions: availableActions.slice(0, 20), // Top 20 for context
|
|
349
|
+
// Guidance for Agent
|
|
350
|
+
_tip: "Build ModificationOperation[] and pass as 'operations' parameter",
|
|
351
|
+
_next_step: "Use the current_nodes and available_actions to build structured operations",
|
|
352
|
+
// Example operation structures
|
|
353
|
+
example_operations: {
|
|
354
|
+
insert_hitl: {
|
|
355
|
+
type: "insert",
|
|
356
|
+
insert: {
|
|
357
|
+
action_type: "hitl",
|
|
358
|
+
display_name: "Approval Required",
|
|
359
|
+
insert_before: "target_node_name",
|
|
360
|
+
add_runif_to_downstream: true,
|
|
361
|
+
new_node_output: "hitl_status_HITL Success",
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
remove_nodes: {
|
|
365
|
+
type: "remove",
|
|
366
|
+
remove: { nodes: ["node_to_remove"] },
|
|
367
|
+
},
|
|
368
|
+
rewire: {
|
|
369
|
+
type: "rewire",
|
|
370
|
+
rewire: {
|
|
371
|
+
target_node: "downstream_node",
|
|
372
|
+
target_input: "query",
|
|
373
|
+
source_node: "upstream_node",
|
|
374
|
+
source_output: "response",
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
// Fetch persona
|
|
381
|
+
const persona = await client.getPersonaById(personaId);
|
|
382
|
+
if (!persona) {
|
|
383
|
+
return { error: `Persona "${personaId}" not found` };
|
|
384
|
+
}
|
|
385
|
+
const workflow = persona.workflow_def;
|
|
386
|
+
if (!workflow) {
|
|
387
|
+
return { error: "Persona has no workflow_def to modify" };
|
|
388
|
+
}
|
|
389
|
+
// Load schema registry for action metadata
|
|
390
|
+
const schemaRegistry = await ensureSchemaRegistry(client);
|
|
391
|
+
// Apply the structured operations
|
|
392
|
+
const result = applyWorkflowModifications(workflow, operations, schemaRegistry);
|
|
393
|
+
// Sanitize before deploy
|
|
394
|
+
const sanitized = sanitizeWorkflowForDeploy(result.workflow);
|
|
395
|
+
// Run validation
|
|
396
|
+
const postIssues = detectWorkflowIssues(sanitized);
|
|
397
|
+
const errors = postIssues.filter(i => i.severity === "critical");
|
|
398
|
+
if (preview) {
|
|
399
|
+
return {
|
|
400
|
+
preview: true,
|
|
401
|
+
persona_id: personaId,
|
|
402
|
+
persona_name: persona.name,
|
|
403
|
+
// What would change
|
|
404
|
+
changes_applied: result.changesApplied,
|
|
405
|
+
nodes_added: result.nodesAdded,
|
|
406
|
+
nodes_removed: result.nodesRemoved,
|
|
407
|
+
nodes_modified: result.nodesModified,
|
|
408
|
+
connections_changed: result.connectionsChanged,
|
|
409
|
+
// Validation
|
|
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
|
|
413
|
+
modified_workflow: sanitized,
|
|
414
|
+
_tip: errors.length > 0
|
|
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",
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
// Deploy the workflow
|
|
429
|
+
try {
|
|
430
|
+
await client.updateAiEmployee({
|
|
431
|
+
persona_id: personaId,
|
|
432
|
+
workflow: sanitized,
|
|
433
|
+
});
|
|
434
|
+
return {
|
|
435
|
+
success: true,
|
|
436
|
+
persona_id: personaId,
|
|
437
|
+
persona_name: persona.name,
|
|
438
|
+
changes_applied: result.changesApplied,
|
|
439
|
+
nodes_added: result.nodesAdded,
|
|
440
|
+
nodes_removed: result.nodesRemoved,
|
|
441
|
+
nodes_modified: result.nodesModified,
|
|
442
|
+
connections_changed: result.connectionsChanged,
|
|
443
|
+
_tip: "Workflow updated successfully",
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
return {
|
|
448
|
+
error: `Deploy failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
449
|
+
changes_attempted: result.changesApplied,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
454
|
+
// Exports
|
|
455
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
456
|
+
export { applyWorkflowModifications, buildWorkflowAction, buildInputBinding };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Optimize Handler
|
|
3
|
+
*
|
|
4
|
+
* Detects and fixes workflow issues.
|
|
5
|
+
*/
|
|
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
|
+
/**
|
|
73
|
+
* Handle workflow optimize mode
|
|
74
|
+
*/
|
|
75
|
+
export async function handleWorkflowOptimize(args, client) {
|
|
76
|
+
const personaId = args.persona_id;
|
|
77
|
+
const preview = args.preview !== false;
|
|
78
|
+
if (!personaId) {
|
|
79
|
+
return { error: "persona_id required for optimize mode" };
|
|
80
|
+
}
|
|
81
|
+
const persona = await client.getPersonaById(personaId);
|
|
82
|
+
if (!persona) {
|
|
83
|
+
return { error: `Persona not found: ${personaId}` };
|
|
84
|
+
}
|
|
85
|
+
const existingWorkflow = persona.workflow_def;
|
|
86
|
+
if (!existingWorkflow) {
|
|
87
|
+
return {
|
|
88
|
+
error: `Persona "${persona.name}" has no workflow to optimize`,
|
|
89
|
+
hint: "Use mode='generate' to create a workflow first",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Analyze and detect issues
|
|
93
|
+
const issues = detectWorkflowIssues(existingWorkflow);
|
|
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 = {
|
|
107
|
+
mode: "optimize",
|
|
108
|
+
status: preview ? "preview" : "deployed",
|
|
109
|
+
persona_id: personaId,
|
|
110
|
+
persona_name: persona.name,
|
|
111
|
+
issues_found: issues.length,
|
|
112
|
+
issues: issues.map(i => ({
|
|
113
|
+
type: i.type,
|
|
114
|
+
severity: i.severity,
|
|
115
|
+
reason: i.reason,
|
|
116
|
+
})),
|
|
117
|
+
suggested_fixes: fixes,
|
|
118
|
+
fixed_workflow: fixedWorkflow,
|
|
119
|
+
};
|
|
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
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for workflow handlers
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Sanitize workflow for deployment
|
|
6
|
+
* Removes internal-only fields, validates structure
|
|
7
|
+
*/
|
|
8
|
+
export function sanitizeWorkflowForDeploy(workflow) {
|
|
9
|
+
const sanitized = { ...workflow };
|
|
10
|
+
// Remove internal fields that shouldn't be sent to API
|
|
11
|
+
delete sanitized._internal;
|
|
12
|
+
delete sanitized._metadata;
|
|
13
|
+
delete sanitized._debug;
|
|
14
|
+
// Ensure actions array exists
|
|
15
|
+
if (!sanitized.actions) {
|
|
16
|
+
sanitized.actions = [];
|
|
17
|
+
}
|
|
18
|
+
// Validate and clean actions
|
|
19
|
+
const actions = sanitized.actions;
|
|
20
|
+
sanitized.actions = actions.filter(a => {
|
|
21
|
+
// Must have name
|
|
22
|
+
if (!a.name || typeof a.name !== "string")
|
|
23
|
+
return false;
|
|
24
|
+
// Must have id
|
|
25
|
+
if (!a.id && !a.name)
|
|
26
|
+
return false;
|
|
27
|
+
return true;
|
|
28
|
+
});
|
|
29
|
+
return sanitized;
|
|
30
|
+
}
|