@ema.co/mcp-toolkit 2026.2.23 → 2026.2.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/demo-generator.js +177 -0
- package/dist/mcp/domain/validation-rules.js +13 -0
- package/dist/mcp/domain/workflow-graph-optimizer.js +96 -14
- package/dist/mcp/domain/workflow-graph-transforms.js +50 -54
- package/dist/mcp/domain/workflow-graph.js +39 -17
- package/dist/mcp/handlers/data/index.js +74 -15
- package/dist/mcp/handlers/persona/update.js +34 -8
- package/dist/mcp/handlers/reference/index.js +34 -11
- package/dist/mcp/handlers/workflow/adapter.js +7 -4
- package/dist/mcp/handlers/workflow/deploy.js +3 -0
- package/dist/mcp/handlers/workflow/optimize.js +9 -5
- package/dist/mcp/handlers/workflow/validation.js +20 -4
- package/dist/mcp/knowledge.js +174 -13
- package/dist/mcp/resources-dynamic.js +5 -0
- package/dist/mcp/tools.js +10 -4
- package/dist/sdk/client-adapter.js +2 -0
- package/dist/sdk/ema-client.js +2 -0
- package/dist/sdk/generated/api-client/client/client.gen.js +3 -3
- package/dist/sdk/generated/api-client/client/index.js +5 -5
- package/dist/sdk/generated/api-client/client/utils.gen.js +4 -4
- package/dist/sdk/generated/api-client/client.gen.js +1 -1
- package/dist/sdk/generated/api-client/core/utils.gen.js +1 -1
- package/dist/sdk/generated/api-client/index.js +1 -1
- package/dist/sdk/generated/api-client/sdk.gen.js +2 -2
- package/package.json +2 -2
- package/dist/mcp/domain/workflow-optimizer.js +0 -671
- package/dist/mcp/handlers/workflow/fix.js +0 -587
|
@@ -1,587 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workflow Fix/Apply Helpers (MCP-Specific)
|
|
3
|
-
*
|
|
4
|
-
* NOTE: This is DIFFERENT from sdk/workflow-fixer.ts:
|
|
5
|
-
*
|
|
6
|
-
* - applyWorkflowFixes() (here): MCP handler helper that takes PRE-DETECTED issues
|
|
7
|
-
* and persona context. Handles Voice AI results mapping, HITL paths, etc.
|
|
8
|
-
*
|
|
9
|
-
* - autoFixWorkflow() (SDK): Self-contained auto-detection + fixing without
|
|
10
|
-
* persona context. Better for SDK consumers who want one-call fixing.
|
|
11
|
-
*
|
|
12
|
-
* These are COMPLEMENTARY, not duplicates.
|
|
13
|
-
*/
|
|
14
|
-
/**
|
|
15
|
-
* Generate a human-readable summary of a workflow for Auto Builder prompts.
|
|
16
|
-
* This creates a description the Auto Builder can understand to recreate the workflow.
|
|
17
|
-
*/
|
|
18
|
-
export function summarizeWorkflow(workflowDef) {
|
|
19
|
-
const actions = workflowDef.actions;
|
|
20
|
-
if (!actions || actions.length === 0) {
|
|
21
|
-
return "Empty workflow with no actions";
|
|
22
|
-
}
|
|
23
|
-
const lines = [];
|
|
24
|
-
// Count action types
|
|
25
|
-
const actionTypes = new Map();
|
|
26
|
-
const nodeNames = [];
|
|
27
|
-
for (const action of actions) {
|
|
28
|
-
const actionType = action.actionType;
|
|
29
|
-
const typeName = actionType?.name?.name ?? "unknown";
|
|
30
|
-
actionTypes.set(typeName, (actionTypes.get(typeName) ?? 0) + 1);
|
|
31
|
-
const nodeName = String(action.name ?? "");
|
|
32
|
-
if (nodeName)
|
|
33
|
-
nodeNames.push(nodeName);
|
|
34
|
-
}
|
|
35
|
-
// Summary header
|
|
36
|
-
lines.push(`Workflow with ${actions.length} nodes:`);
|
|
37
|
-
// List action types and counts
|
|
38
|
-
for (const [type, count] of actionTypes.entries()) {
|
|
39
|
-
lines.push(`- ${type}: ${count}`);
|
|
40
|
-
}
|
|
41
|
-
// List node names
|
|
42
|
-
if (nodeNames.length > 0) {
|
|
43
|
-
lines.push(`\nNodes: ${nodeNames.slice(0, 20).join(", ")}${nodeNames.length > 20 ? "..." : ""}`);
|
|
44
|
-
}
|
|
45
|
-
const FEATURE_INDICATORS = {
|
|
46
|
-
categorizer: "This workflow uses intent-based routing with a categorizer.",
|
|
47
|
-
hitl: "This workflow includes human-in-the-loop approval steps.",
|
|
48
|
-
general_hitl: "This workflow includes human-in-the-loop approval steps.",
|
|
49
|
-
entity_extraction: "This workflow extracts structured data from documents.",
|
|
50
|
-
search: "This workflow searches a knowledge base.",
|
|
51
|
-
};
|
|
52
|
-
for (const [actionType, description] of Object.entries(FEATURE_INDICATORS)) {
|
|
53
|
-
if (actionTypes.has(actionType)) {
|
|
54
|
-
lines.push(`\n${description}`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
// Include raw workflow JSON for Auto Builder to parse
|
|
58
|
-
lines.push("\n--- Full workflow_def JSON ---");
|
|
59
|
-
lines.push(JSON.stringify(workflowDef, null, 2));
|
|
60
|
-
return lines.join("\n");
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Apply automatic fixes to a workflow based on detected issues.
|
|
64
|
-
* This function attempts to fix common issues like missing WORKFLOW_OUTPUT,
|
|
65
|
-
* wrong input sources, and other structural problems.
|
|
66
|
-
*
|
|
67
|
-
* @deprecated This function violates the LLM-driven architecture principle.
|
|
68
|
-
* MCP should not auto-fix workflows - it should return suggestions for the Agent to apply.
|
|
69
|
-
* Per CLAUDE.md: "Auto-fix issues | ❌ FORBIDDEN | ✅ Proposes fixes"
|
|
70
|
-
*
|
|
71
|
-
* This function will be removed in a future version. Instead:
|
|
72
|
-
* 1. Use workflow(mode="get") to get current workflow_def
|
|
73
|
-
* 2. Return issues as suggestions to the Agent
|
|
74
|
-
* 3. Agent modifies workflow_def and deploys via workflow(mode="deploy")
|
|
75
|
-
*/
|
|
76
|
-
export function applyWorkflowFixes(workflowDef, issues, persona) {
|
|
77
|
-
// Log deprecation warning
|
|
78
|
-
console.warn("[DEPRECATED] applyWorkflowFixes() called - this function violates LLM-driven architecture and will be removed");
|
|
79
|
-
// Deep clone the workflow to avoid mutating the original
|
|
80
|
-
const fixedWorkflow = JSON.parse(JSON.stringify(workflowDef));
|
|
81
|
-
const appliedFixes = [];
|
|
82
|
-
const actions = fixedWorkflow.actions;
|
|
83
|
-
if (!actions) {
|
|
84
|
-
return { fixedWorkflow, appliedFixes: [{ issue_type: "unknown", description: "No actions array found in workflow", applied: false }] };
|
|
85
|
-
}
|
|
86
|
-
for (const issue of issues) {
|
|
87
|
-
let fixed = false;
|
|
88
|
-
let fixDescription = "";
|
|
89
|
-
let failReason = "";
|
|
90
|
-
switch (issue.type) {
|
|
91
|
-
case "missing_workflow_output": {
|
|
92
|
-
// For Voice AI, WORKFLOW_OUTPUT is handled via results mapping, not a node
|
|
93
|
-
// Check if results mapping exists
|
|
94
|
-
const results = fixedWorkflow.results;
|
|
95
|
-
if (results && Object.keys(results).length > 0) {
|
|
96
|
-
// Results mapping exists, this is valid for Voice AI
|
|
97
|
-
fixed = true;
|
|
98
|
-
fixDescription = "Workflow uses results mapping (valid for Voice AI) - no WORKFLOW_OUTPUT node needed";
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
// Try to add results mapping for all response nodes
|
|
102
|
-
const responseNodes = actions.filter(a => {
|
|
103
|
-
const name = String(a.name ?? "");
|
|
104
|
-
return name.includes("respond") || name.includes("response");
|
|
105
|
-
});
|
|
106
|
-
if (responseNodes.length > 0) {
|
|
107
|
-
const newResults = {};
|
|
108
|
-
for (const respNode of responseNodes) {
|
|
109
|
-
const nodeName = String(respNode.name);
|
|
110
|
-
newResults[`${nodeName}.response_with_sources`] = {
|
|
111
|
-
actionName: nodeName,
|
|
112
|
-
outputName: "response_with_sources",
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
fixedWorkflow.results = newResults;
|
|
116
|
-
fixed = true;
|
|
117
|
-
fixDescription = `Added results mapping for ${responseNodes.length} response node(s)`;
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
failReason = "No response nodes found to map to results";
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
case "wrong_input_source": {
|
|
126
|
-
const targetNode = actions.find(a => String(a.name) === issue.node);
|
|
127
|
-
if (!targetNode || !targetNode.inputs) {
|
|
128
|
-
failReason = `Could not find node "${issue.node}" or its inputs`;
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
const inputs = targetNode.inputs;
|
|
132
|
-
const nodeType = String(targetNode.actionType || targetNode.type || "");
|
|
133
|
-
// CASE 1: Email field needs entity extraction
|
|
134
|
-
// This requires adding a new node, not just rebinding
|
|
135
|
-
const isEmailField = issue.reason?.toLowerCase().includes("email") ||
|
|
136
|
-
issue.current?.toLowerCase().includes("email_to") ||
|
|
137
|
-
(nodeType.includes("send_email") && inputs.email_to);
|
|
138
|
-
if (isEmailField && inputs.email_to) {
|
|
139
|
-
// Check if entity_extraction already exists
|
|
140
|
-
let extractorNode = actions.find(a => String(a.actionType || a.type || "").includes("entity_extraction") ||
|
|
141
|
-
String(a.name).includes("entity_extract"));
|
|
142
|
-
if (!extractorNode) {
|
|
143
|
-
// Create entity_extraction node
|
|
144
|
-
const extractorName = "entity_extractor_email";
|
|
145
|
-
extractorNode = {
|
|
146
|
-
name: extractorName,
|
|
147
|
-
actionType: "entity_extraction",
|
|
148
|
-
inputs: {
|
|
149
|
-
conversation: {
|
|
150
|
-
actionOutput: {
|
|
151
|
-
actionName: "trigger",
|
|
152
|
-
output: "chat_conversation",
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
entities_to_extract: {
|
|
156
|
-
literalValue: {
|
|
157
|
-
value: "email_address",
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
outputs: ["email_address", "extracted_entities"],
|
|
162
|
-
};
|
|
163
|
-
actions.push(extractorNode);
|
|
164
|
-
// Wire email_to to use extracted email
|
|
165
|
-
inputs.email_to = {
|
|
166
|
-
actionOutput: {
|
|
167
|
-
actionName: extractorName,
|
|
168
|
-
output: "email_address",
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
fixed = true;
|
|
172
|
-
fixDescription = `Added entity_extraction node "${extractorName}" and wired ${issue.node}.email_to to use extracted email_address`;
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
// Extractor exists, just rewire
|
|
176
|
-
inputs.email_to = {
|
|
177
|
-
actionOutput: {
|
|
178
|
-
actionName: String(extractorNode.name),
|
|
179
|
-
output: "email_address",
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
fixed = true;
|
|
183
|
-
fixDescription = `Wired ${issue.node}.email_to to use ${extractorNode.name}.email_address`;
|
|
184
|
-
}
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
// CASE 2: Simple rebinding (conversation/query sources)
|
|
188
|
-
let inputField;
|
|
189
|
-
const possibleFields = ["conversation", "query", "input", "text"];
|
|
190
|
-
for (const field of possibleFields) {
|
|
191
|
-
if (inputs[field]) {
|
|
192
|
-
inputField = field;
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (inputField && inputs[inputField]) {
|
|
197
|
-
const inputBinding = inputs[inputField];
|
|
198
|
-
if (inputBinding.actionOutput) {
|
|
199
|
-
const actionOutput = inputBinding.actionOutput;
|
|
200
|
-
// Update to use recommended source
|
|
201
|
-
if (issue.recommended === "chat_conversation") {
|
|
202
|
-
actionOutput.actionName = "trigger";
|
|
203
|
-
actionOutput.output = "chat_conversation";
|
|
204
|
-
fixed = true;
|
|
205
|
-
}
|
|
206
|
-
else if (issue.recommended === "user_query") {
|
|
207
|
-
actionOutput.actionName = "trigger";
|
|
208
|
-
actionOutput.output = "user_query";
|
|
209
|
-
fixed = true;
|
|
210
|
-
}
|
|
211
|
-
else if (issue.recommended === "summarized_conversation") {
|
|
212
|
-
// Check if summarizer exists
|
|
213
|
-
const summarizer = actions.find(a => String(a.name).includes("summarizer") ||
|
|
214
|
-
String(a.name).includes("conversation_to_search"));
|
|
215
|
-
if (summarizer) {
|
|
216
|
-
actionOutput.actionName = String(summarizer.name);
|
|
217
|
-
actionOutput.output = "summarized_conversation";
|
|
218
|
-
fixed = true;
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
failReason = "Recommended summarized_conversation but no summarizer node exists";
|
|
222
|
-
break;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (fixed) {
|
|
226
|
-
fixDescription = `Changed ${issue.node}.${inputField} from ${issue.current} to ${issue.recommended}`;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
else if (!inputField) {
|
|
231
|
-
failReason = `Could not find input field (tried: conversation, query, input, text) in node "${issue.node}"`;
|
|
232
|
-
}
|
|
233
|
-
if (!fixed && !failReason) {
|
|
234
|
-
failReason = `Could not determine how to fix input source for "${issue.node}"`;
|
|
235
|
-
}
|
|
236
|
-
break;
|
|
237
|
-
}
|
|
238
|
-
case "incomplete_hitl": {
|
|
239
|
-
// This requires adding new nodes which is complex
|
|
240
|
-
// Mark as not auto-fixable
|
|
241
|
-
failReason = "HITL paths require manual configuration of success/failure response nodes";
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
244
|
-
case "missing_category_edge": {
|
|
245
|
-
// Check if using runIf pattern (valid alternative to edges)
|
|
246
|
-
const nodesWithRunIf = actions.filter(a => a.runIf !== undefined);
|
|
247
|
-
const categorizerName = issue.node;
|
|
248
|
-
const nodesUsingCategorizer = nodesWithRunIf.filter(a => {
|
|
249
|
-
const runIf = a.runIf;
|
|
250
|
-
if (!runIf?.lhs)
|
|
251
|
-
return false;
|
|
252
|
-
const lhs = runIf.lhs;
|
|
253
|
-
const actionOutput = lhs.actionOutput;
|
|
254
|
-
return actionOutput?.actionName === categorizerName;
|
|
255
|
-
});
|
|
256
|
-
if (nodesUsingCategorizer.length > 0) {
|
|
257
|
-
fixed = true;
|
|
258
|
-
fixDescription = `Categorizer "${categorizerName}" uses runIf pattern (${nodesUsingCategorizer.length} conditional nodes) - valid alternative to explicit edges`;
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
failReason = "Categorizer needs manual configuration of category routing edges";
|
|
262
|
-
}
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
case "malformed_runif": {
|
|
266
|
-
// Fix malformed runIf: "category_<Name>" vs "true" -> "category" vs "<Name>"
|
|
267
|
-
const targetNode = actions.find(a => String(a.name) === issue.node);
|
|
268
|
-
if (targetNode?.runIf) {
|
|
269
|
-
const runIf = targetNode.runIf;
|
|
270
|
-
const lhs = runIf.lhs;
|
|
271
|
-
const rhs = runIf.rhs;
|
|
272
|
-
if (lhs?.actionOutput && rhs?.inline) {
|
|
273
|
-
const actionOutput = lhs.actionOutput;
|
|
274
|
-
const inlineRhs = rhs.inline;
|
|
275
|
-
const output = String(actionOutput.output ?? "");
|
|
276
|
-
if (output.startsWith("category_")) {
|
|
277
|
-
// Extract category name and fix the format
|
|
278
|
-
const categoryName = output.replace(/^category_/, "");
|
|
279
|
-
actionOutput.output = "category";
|
|
280
|
-
inlineRhs.enumValue = categoryName;
|
|
281
|
-
fixed = true;
|
|
282
|
-
fixDescription = `Fixed runIf for "${issue.node}": output="category", enumValue="${categoryName}"`;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
if (!fixed) {
|
|
287
|
-
failReason = `Could not find node "${issue.node}" or its runIf condition`;
|
|
288
|
-
}
|
|
289
|
-
break;
|
|
290
|
-
}
|
|
291
|
-
case "malformed_hitl_runif": {
|
|
292
|
-
// Fix malformed HITL runIf: "hitl_status_HITL Success" -> output="hitl_status", enumValue="HITL Success"
|
|
293
|
-
const targetNodeHitl = actions.find(a => String(a.name) === issue.node);
|
|
294
|
-
if (targetNodeHitl?.runIf) {
|
|
295
|
-
const runIfHitl = targetNodeHitl.runIf;
|
|
296
|
-
const lhsHitl = runIfHitl.lhs;
|
|
297
|
-
const rhsHitl = runIfHitl.rhs;
|
|
298
|
-
if (lhsHitl?.actionOutput && rhsHitl?.inline) {
|
|
299
|
-
const actionOutputHitl = lhsHitl.actionOutput;
|
|
300
|
-
const inlineRhsHitl = rhsHitl.inline;
|
|
301
|
-
const outputHitl = String(actionOutputHitl.output ?? "");
|
|
302
|
-
// Malformed patterns: "hitl_status_HITL Success" or "hitl_status_HITL Failure" or "hitl_status HITL Success"
|
|
303
|
-
const hitlMatch = outputHitl.match(/^hitl_status[_\s]?(HITL[ _]?(?:Success|Failure))$/i);
|
|
304
|
-
if (hitlMatch) {
|
|
305
|
-
// Extract the enum value (e.g., "HITL Success")
|
|
306
|
-
let enumVal = hitlMatch[1].replace(/_/g, " ");
|
|
307
|
-
// Normalize to "HITL Success" or "HITL Failure"
|
|
308
|
-
if (enumVal.toLowerCase().includes("success")) {
|
|
309
|
-
enumVal = "HITL Success";
|
|
310
|
-
}
|
|
311
|
-
else if (enumVal.toLowerCase().includes("failure")) {
|
|
312
|
-
enumVal = "HITL Failure";
|
|
313
|
-
}
|
|
314
|
-
actionOutputHitl.output = "hitl_status";
|
|
315
|
-
inlineRhsHitl.enumValue = enumVal;
|
|
316
|
-
fixed = true;
|
|
317
|
-
fixDescription = `Fixed HITL runIf for "${issue.node}": output="hitl_status", enumValue="${enumVal}"`;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
if (!fixed) {
|
|
322
|
-
failReason = `Could not find node "${issue.node}" or its runIf condition`;
|
|
323
|
-
}
|
|
324
|
-
break;
|
|
325
|
-
}
|
|
326
|
-
case "orphan": {
|
|
327
|
-
// Remove orphan nodes
|
|
328
|
-
const orphanIndex = actions.findIndex(a => String(a.name) === issue.node);
|
|
329
|
-
if (orphanIndex >= 0) {
|
|
330
|
-
actions.splice(orphanIndex, 1);
|
|
331
|
-
fixed = true;
|
|
332
|
-
fixDescription = `Removed orphan node "${issue.node}" (not connected to workflow)`;
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
failReason = `Could not find orphan node "${issue.node}" to remove`;
|
|
336
|
-
}
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
case "redundant_search": {
|
|
340
|
-
// This is an optimization, not a critical fix - skip auto-fix
|
|
341
|
-
failReason = "Redundant search is an optimization suggestion - consider consolidating manually";
|
|
342
|
-
break;
|
|
343
|
-
}
|
|
344
|
-
case "type_mismatch": {
|
|
345
|
-
// Type mismatch fix strategy:
|
|
346
|
-
// 1. Handle multiBinding inputs - find and fix bindings inside multiBinding
|
|
347
|
-
// 2. Handle nested named_inputs targets - find binding inside named_inputs.multiBinding
|
|
348
|
-
// 3. For CHAT_CONVERSATION expected: use trigger.chat_conversation, move old to named_inputs
|
|
349
|
-
// 4. For SEARCH_RESULT expected: find actual search node output
|
|
350
|
-
// 5. For DOCUMENT/TEXT_WITH_SOURCES mismatches: move to named_inputs (accepts ANY type)
|
|
351
|
-
const sourceNode = issue.source?.split(".")[0];
|
|
352
|
-
const sourceOutput = issue.source?.split(".")[1];
|
|
353
|
-
const targetNode = issue.target?.split(".")[0];
|
|
354
|
-
const targetInput = issue.target?.split(".")[1];
|
|
355
|
-
const expectedType = issue.expected;
|
|
356
|
-
const gotType = issue.got;
|
|
357
|
-
if (!sourceNode || !targetNode || !targetInput) {
|
|
358
|
-
failReason = "Missing source/target node information";
|
|
359
|
-
break;
|
|
360
|
-
}
|
|
361
|
-
const targetAction = actions.find(a => String(a.name) === targetNode);
|
|
362
|
-
if (!targetAction || !targetAction.inputs) {
|
|
363
|
-
failReason = `Could not find target node "${targetNode}"`;
|
|
364
|
-
break;
|
|
365
|
-
}
|
|
366
|
-
const inputs = targetAction.inputs;
|
|
367
|
-
let inputBinding = inputs[targetInput];
|
|
368
|
-
let actionOutput;
|
|
369
|
-
let isNestedInNamedInputs = false;
|
|
370
|
-
let nestedBindingIndex = -1;
|
|
371
|
-
// Check for autoDetectedBinding - some bindings use this structure alongside or instead of others
|
|
372
|
-
if (inputBinding && !inputBinding.actionOutput && inputBinding.autoDetectedBinding) {
|
|
373
|
-
const autoBinding = inputBinding.autoDetectedBinding;
|
|
374
|
-
if (autoBinding.actionOutput) {
|
|
375
|
-
actionOutput = autoBinding.actionOutput;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
// Check if targetInput is a named_inputs reference (e.g., "named_inputs_conversation")
|
|
379
|
-
if (!inputBinding && targetInput.startsWith("named_inputs_")) {
|
|
380
|
-
const namedInputKey = targetInput.replace("named_inputs_", "");
|
|
381
|
-
const namedInputs = inputs.named_inputs;
|
|
382
|
-
if (namedInputs?.multiBinding) {
|
|
383
|
-
const mb = namedInputs.multiBinding;
|
|
384
|
-
if (mb.elements) {
|
|
385
|
-
nestedBindingIndex = mb.elements.findIndex(el => el.namedBinding?.name?.toLowerCase() === namedInputKey.toLowerCase());
|
|
386
|
-
if (nestedBindingIndex >= 0) {
|
|
387
|
-
const namedBinding = mb.elements[nestedBindingIndex].namedBinding;
|
|
388
|
-
if (namedBinding?.value?.actionOutput) {
|
|
389
|
-
actionOutput = namedBinding.value.actionOutput;
|
|
390
|
-
isNestedInNamedInputs = true;
|
|
391
|
-
inputBinding = namedBinding.value;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
// If we couldn't find the binding in multiBinding, the type mismatch for named_inputs
|
|
397
|
-
// is actually acceptable since named_inputs accepts ANY type
|
|
398
|
-
if (!actionOutput && namedInputs) {
|
|
399
|
-
fixed = true;
|
|
400
|
-
fixDescription = `${targetNode}.${targetInput} is in named_inputs (accepts any type) - type mismatch is acceptable`;
|
|
401
|
-
break;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
// If direct binding exists, check for actionOutput or multiBinding
|
|
405
|
-
if (!actionOutput && inputBinding) {
|
|
406
|
-
if (inputBinding.actionOutput) {
|
|
407
|
-
actionOutput = inputBinding.actionOutput;
|
|
408
|
-
}
|
|
409
|
-
else if (inputBinding.multiBinding) {
|
|
410
|
-
// Handle multiBinding - find the element that references the source node
|
|
411
|
-
const mb = inputBinding.multiBinding;
|
|
412
|
-
if (mb.elements) {
|
|
413
|
-
for (let i = 0; i < mb.elements.length; i++) {
|
|
414
|
-
const el = mb.elements[i];
|
|
415
|
-
const elActionOutput = el.namedBinding?.value?.actionOutput;
|
|
416
|
-
if (elActionOutput && String(elActionOutput.actionName) === sourceNode) {
|
|
417
|
-
actionOutput = elActionOutput;
|
|
418
|
-
nestedBindingIndex = i;
|
|
419
|
-
break;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
// Also check if elements have direct actionOutput (not inside namedBinding)
|
|
424
|
-
if (!actionOutput && mb.elements) {
|
|
425
|
-
for (let i = 0; i < mb.elements.length; i++) {
|
|
426
|
-
const el = mb.elements[i];
|
|
427
|
-
// Check for actionOutput directly on element
|
|
428
|
-
if (el.actionOutput) {
|
|
429
|
-
actionOutput = el.actionOutput;
|
|
430
|
-
nestedBindingIndex = i;
|
|
431
|
-
break;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
// If source not found in multiBinding, DON'T mark as fixed - the type mismatch is real
|
|
436
|
-
// We need to find and remove/fix the mismatched binding
|
|
437
|
-
if (!actionOutput && mb.elements) {
|
|
438
|
-
// Find ANY binding that has type mismatch and try to fix it
|
|
439
|
-
for (let i = 0; i < mb.elements.length; i++) {
|
|
440
|
-
const el = mb.elements[i];
|
|
441
|
-
const elActionOutput = el.namedBinding?.value?.actionOutput;
|
|
442
|
-
if (elActionOutput) {
|
|
443
|
-
// Use the first actionOutput we find - it's likely the mismatched one
|
|
444
|
-
actionOutput = elActionOutput;
|
|
445
|
-
nestedBindingIndex = i;
|
|
446
|
-
break;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
if (!actionOutput) {
|
|
453
|
-
failReason = `No actionOutput binding found for ${targetNode}.${targetInput}`;
|
|
454
|
-
break;
|
|
455
|
-
}
|
|
456
|
-
// === FIX STRATEGY 1: CHAT_CONVERSATION expected ===
|
|
457
|
-
// If binding is nested in named_inputs, redirect to trigger.chat_conversation
|
|
458
|
-
if (expectedType?.includes("CHAT_CONVERSATION")) {
|
|
459
|
-
const oldActionName = String(actionOutput.actionName ?? "");
|
|
460
|
-
const oldOutput = String(actionOutput.output ?? "");
|
|
461
|
-
if (isNestedInNamedInputs) {
|
|
462
|
-
// Binding is inside named_inputs - change it to use trigger.chat_conversation
|
|
463
|
-
actionOutput.actionName = "trigger";
|
|
464
|
-
actionOutput.output = "chat_conversation";
|
|
465
|
-
// Rename the named binding to indicate it's now conversation
|
|
466
|
-
const namedInputs = inputs.named_inputs;
|
|
467
|
-
const mb = namedInputs.multiBinding;
|
|
468
|
-
if (mb.elements && nestedBindingIndex >= 0 && mb.elements[nestedBindingIndex]?.namedBinding) {
|
|
469
|
-
// Keep the old binding as additional context
|
|
470
|
-
mb.elements.push({
|
|
471
|
-
namedBinding: {
|
|
472
|
-
name: `${targetInput.replace("named_inputs_", "")}_context`,
|
|
473
|
-
value: { actionOutput: { actionName: oldActionName, output: oldOutput } }
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
fixed = true;
|
|
478
|
-
fixDescription = `Fixed ${targetNode}.${targetInput}: now uses trigger.chat_conversation`;
|
|
479
|
-
break;
|
|
480
|
-
}
|
|
481
|
-
// Standard case: direct binding - change to trigger.chat_conversation
|
|
482
|
-
actionOutput.actionName = "trigger";
|
|
483
|
-
actionOutput.output = "chat_conversation";
|
|
484
|
-
// Add old binding to named_inputs so it's still available
|
|
485
|
-
const namedInputs = inputs.named_inputs;
|
|
486
|
-
const contextName = targetInput === "conversation" ? "context" : `${targetInput}_context`;
|
|
487
|
-
if (namedInputs?.multiBinding) {
|
|
488
|
-
// Append to existing multiBinding
|
|
489
|
-
const mb = namedInputs.multiBinding;
|
|
490
|
-
if (!mb.elements)
|
|
491
|
-
mb.elements = [];
|
|
492
|
-
mb.elements.push({
|
|
493
|
-
namedBinding: {
|
|
494
|
-
name: contextName,
|
|
495
|
-
value: { actionOutput: { actionName: oldActionName, output: oldOutput } }
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
else {
|
|
500
|
-
// Create new named_inputs with multiBinding
|
|
501
|
-
inputs.named_inputs = {
|
|
502
|
-
multiBinding: {
|
|
503
|
-
elements: [{
|
|
504
|
-
namedBinding: {
|
|
505
|
-
name: contextName,
|
|
506
|
-
value: { actionOutput: { actionName: oldActionName, output: oldOutput } }
|
|
507
|
-
}
|
|
508
|
-
}]
|
|
509
|
-
}
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
fixed = true;
|
|
513
|
-
fixDescription = `Fixed ${targetNode}.${targetInput}: now uses trigger.chat_conversation, old binding moved to named_inputs.${contextName}`;
|
|
514
|
-
break;
|
|
515
|
-
}
|
|
516
|
-
// === FIX STRATEGY 2: SEARCH_RESULT expected ===
|
|
517
|
-
// Find an actual search node and use its search_results output
|
|
518
|
-
if (expectedType?.includes("SEARCH_RESULT") && gotType?.includes("TEXT_WITH_SOURCES")) {
|
|
519
|
-
// Find a search node (excluding web_search which has different output)
|
|
520
|
-
const searchNode = actions.find(a => {
|
|
521
|
-
const name = String(a.name ?? "");
|
|
522
|
-
const actionType = a.actionType?.name?.name ?? "";
|
|
523
|
-
return (name.includes("search") || actionType.includes("search_datastore"))
|
|
524
|
-
&& !name.includes("web_search")
|
|
525
|
-
&& !name.includes("combine");
|
|
526
|
-
});
|
|
527
|
-
if (searchNode) {
|
|
528
|
-
// Change to use search node's output directly
|
|
529
|
-
// Don't add old binding to named_inputs - it causes type mismatches
|
|
530
|
-
actionOutput.actionName = String(searchNode.name);
|
|
531
|
-
actionOutput.output = "search_results";
|
|
532
|
-
fixed = true;
|
|
533
|
-
fixDescription = `Fixed ${targetNode}.${targetInput}: now uses ${searchNode.name}.search_results`;
|
|
534
|
-
}
|
|
535
|
-
else {
|
|
536
|
-
failReason = `No search_datastore node found to provide SEARCH_RESULT for ${targetNode}`;
|
|
537
|
-
}
|
|
538
|
-
break;
|
|
539
|
-
}
|
|
540
|
-
// === FIX STRATEGY 3: DOCUMENT or other type → remove the mismatched binding ===
|
|
541
|
-
// Simply remove the bad binding - don't move to named_inputs (causes type mismatches)
|
|
542
|
-
if (gotType?.includes("DOCUMENT") ||
|
|
543
|
-
(expectedType?.includes("TEXT_WITH_SOURCES") && !gotType?.includes("TEXT_WITH_SOURCES"))) {
|
|
544
|
-
// If binding is inside multiBinding of the target input, remove the bad element
|
|
545
|
-
if (inputBinding?.multiBinding && nestedBindingIndex >= 0) {
|
|
546
|
-
const mb = inputBinding.multiBinding;
|
|
547
|
-
if (mb.elements && mb.elements.length > 1) {
|
|
548
|
-
// Remove the mismatched element, keep others
|
|
549
|
-
mb.elements.splice(nestedBindingIndex, 1);
|
|
550
|
-
fixed = true;
|
|
551
|
-
fixDescription = `Fixed ${targetNode}.${targetInput}: removed mismatched binding from multiBinding`;
|
|
552
|
-
break;
|
|
553
|
-
}
|
|
554
|
-
else if (mb.elements && mb.elements.length === 1) {
|
|
555
|
-
// Only one element - remove entire binding
|
|
556
|
-
delete inputBinding.multiBinding;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
// Remove the mismatched binding from the typed input entirely
|
|
560
|
-
delete inputs[targetInput];
|
|
561
|
-
fixed = true;
|
|
562
|
-
fixDescription = `Fixed ${targetNode}.${targetInput}: removed mismatched binding (input now unbound)`;
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
565
|
-
// === FALLBACK: Remove mismatched binding ===
|
|
566
|
-
// Don't move to named_inputs - it causes type mismatches
|
|
567
|
-
{
|
|
568
|
-
// Remove the mismatched binding entirely
|
|
569
|
-
delete inputs[targetInput];
|
|
570
|
-
fixed = true;
|
|
571
|
-
fixDescription = `Fixed ${targetNode}.${targetInput}: removed mismatched binding (${gotType} → expected ${expectedType})`;
|
|
572
|
-
}
|
|
573
|
-
break;
|
|
574
|
-
}
|
|
575
|
-
default: {
|
|
576
|
-
failReason = `Auto-fix not implemented for issue type: ${issue.type}`;
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
appliedFixes.push({
|
|
580
|
-
issue_type: issue.type,
|
|
581
|
-
description: fixed ? fixDescription : `Could not fix: ${issue.reason}`,
|
|
582
|
-
applied: fixed,
|
|
583
|
-
reason: fixed ? undefined : failReason,
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
return { fixedWorkflow, appliedFixes };
|
|
587
|
-
}
|