@zibby/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/package.json +94 -0
- package/src/agents/base.js +361 -0
- package/src/constants.js +47 -0
- package/src/enrichment/base.js +49 -0
- package/src/enrichment/enrichers/accessibility-enricher.js +197 -0
- package/src/enrichment/enrichers/dom-enricher.js +171 -0
- package/src/enrichment/enrichers/page-state-enricher.js +129 -0
- package/src/enrichment/enrichers/position-enricher.js +67 -0
- package/src/enrichment/index.js +96 -0
- package/src/enrichment/mcp-integration.js +149 -0
- package/src/enrichment/mcp-ref-enricher.js +78 -0
- package/src/enrichment/pipeline.js +192 -0
- package/src/enrichment/trace-text-enricher.js +115 -0
- package/src/framework/AGENTS.md +98 -0
- package/src/framework/agents/base.js +72 -0
- package/src/framework/agents/claude-strategy.js +278 -0
- package/src/framework/agents/cursor-strategy.js +459 -0
- package/src/framework/agents/index.js +105 -0
- package/src/framework/agents/utils/cursor-output-formatter.js +67 -0
- package/src/framework/agents/utils/openai-proxy-formatter.js +249 -0
- package/src/framework/code-generator.js +301 -0
- package/src/framework/constants.js +33 -0
- package/src/framework/context-loader.js +101 -0
- package/src/framework/function-bridge.js +78 -0
- package/src/framework/function-skill-registry.js +20 -0
- package/src/framework/graph-compiler.js +342 -0
- package/src/framework/graph.js +610 -0
- package/src/framework/index.js +28 -0
- package/src/framework/node-registry.js +163 -0
- package/src/framework/node.js +259 -0
- package/src/framework/output-parser.js +71 -0
- package/src/framework/skill-registry.js +55 -0
- package/src/framework/state-utils.js +52 -0
- package/src/framework/state.js +67 -0
- package/src/framework/tool-resolver.js +65 -0
- package/src/index.js +342 -0
- package/src/runtime/generation/base.js +46 -0
- package/src/runtime/generation/index.js +70 -0
- package/src/runtime/generation/mcp-ref-strategy.js +197 -0
- package/src/runtime/generation/stable-id-strategy.js +170 -0
- package/src/runtime/stable-id-runtime.js +248 -0
- package/src/runtime/verification/base.js +44 -0
- package/src/runtime/verification/index.js +67 -0
- package/src/runtime/verification/playwright-json-strategy.js +119 -0
- package/src/runtime/zibby-runtime.js +299 -0
- package/src/sync/index.js +2 -0
- package/src/sync/uploader.js +29 -0
- package/src/tools/run-playwright-test.js +158 -0
- package/src/utils/adf-converter.js +68 -0
- package/src/utils/ast-utils.js +37 -0
- package/src/utils/ci-setup.js +124 -0
- package/src/utils/cursor-utils.js +71 -0
- package/src/utils/logger.js +144 -0
- package/src/utils/mcp-config-writer.js +115 -0
- package/src/utils/node-schema-parser.js +522 -0
- package/src/utils/post-process-events.js +55 -0
- package/src/utils/result-handler.js +102 -0
- package/src/utils/ripple-effect.js +84 -0
- package/src/utils/selector-generator.js +239 -0
- package/src/utils/streaming-parser.js +387 -0
- package/src/utils/test-post-processor.js +211 -0
- package/src/utils/timeline.js +217 -0
- package/src/utils/trace-parser.js +325 -0
- package/src/utils/video-organizer.js +91 -0
- package/templates/browser-test-automation/README.md +114 -0
- package/templates/browser-test-automation/graph.js +54 -0
- package/templates/browser-test-automation/nodes/execute-live.js +250 -0
- package/templates/browser-test-automation/nodes/generate-script.js +77 -0
- package/templates/browser-test-automation/nodes/index.js +3 -0
- package/templates/browser-test-automation/nodes/preflight.js +59 -0
- package/templates/browser-test-automation/nodes/utils.js +154 -0
- package/templates/browser-test-automation/result-handler.js +286 -0
- package/templates/code-analysis/graph.js +72 -0
- package/templates/code-analysis/index.js +18 -0
- package/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
- package/templates/code-analysis/nodes/create-pr-node.js +175 -0
- package/templates/code-analysis/nodes/finalize-node.js +118 -0
- package/templates/code-analysis/nodes/generate-code-node.js +425 -0
- package/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
- package/templates/code-analysis/nodes/services/prMetaService.js +86 -0
- package/templates/code-analysis/nodes/setup-node.js +142 -0
- package/templates/code-analysis/prompts/analyze-ticket.md +181 -0
- package/templates/code-analysis/prompts/generate-code.md +33 -0
- package/templates/code-analysis/prompts/generate-test-cases.md +110 -0
- package/templates/code-analysis/state.js +40 -0
- package/templates/code-implementation/graph.js +35 -0
- package/templates/code-implementation/index.js +7 -0
- package/templates/code-implementation/state.js +14 -0
- package/templates/global-setup.js +56 -0
- package/templates/index.js +94 -0
- package/templates/register-nodes.js +24 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Function Bridge — wraps plain handler functions in an MCP server.
|
|
4
|
+
*
|
|
5
|
+
* Usage: node function-bridge.js <modulePath> <skillId>
|
|
6
|
+
*
|
|
7
|
+
* 1. Dynamically imports the user's module (which calls functionSkill(),
|
|
8
|
+
* populating the handler registry).
|
|
9
|
+
* 2. Reads the registered handlers and tool schemas.
|
|
10
|
+
* 3. Starts an MCP server over stdio so agent strategies can talk to it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
14
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
|
|
17
|
+
const [modulePath, skillId] = process.argv.slice(2);
|
|
18
|
+
|
|
19
|
+
if (!modulePath || !skillId) {
|
|
20
|
+
console.error('Usage: node function-bridge.js <modulePath> <skillId>');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await import(modulePath);
|
|
25
|
+
|
|
26
|
+
const { getHandlers } = await import('./function-skill-registry.js');
|
|
27
|
+
const registration = getHandlers(skillId);
|
|
28
|
+
|
|
29
|
+
if (!registration) {
|
|
30
|
+
console.error(`No handlers registered for skill "${skillId}". Did the module call functionSkill()?`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { handlers, tools } = registration;
|
|
35
|
+
|
|
36
|
+
function jsonSchemaToZod(schema) {
|
|
37
|
+
if (!schema || schema.type !== 'object') return z.object({});
|
|
38
|
+
|
|
39
|
+
const shape = {};
|
|
40
|
+
for (const [key, prop] of Object.entries(schema.properties || {})) {
|
|
41
|
+
let field;
|
|
42
|
+
switch (prop.type) {
|
|
43
|
+
case 'number': case 'integer': field = z.number(); break;
|
|
44
|
+
case 'boolean': field = z.boolean(); break;
|
|
45
|
+
case 'array': field = z.array(z.any()); break;
|
|
46
|
+
default: field = z.string(); break;
|
|
47
|
+
}
|
|
48
|
+
if (prop.description) field = field.describe(prop.description);
|
|
49
|
+
if (!schema.required?.includes(key)) field = field.optional();
|
|
50
|
+
shape[key] = field;
|
|
51
|
+
}
|
|
52
|
+
return z.object(shape);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const server = new McpServer(
|
|
56
|
+
{ name: `zibby-fn-${skillId}`, version: '1.0.0' },
|
|
57
|
+
{ capabilities: { tools: {} } }
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
for (const tool of tools) {
|
|
61
|
+
const zodSchema = jsonSchemaToZod(tool.input_schema);
|
|
62
|
+
server.registerTool(
|
|
63
|
+
tool.name,
|
|
64
|
+
{ title: tool.name, description: tool.description || '', inputSchema: zodSchema },
|
|
65
|
+
async (args) => {
|
|
66
|
+
try {
|
|
67
|
+
const result = await handlers[tool.name](args);
|
|
68
|
+
const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
69
|
+
return { content: [{ type: 'text', text }] };
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const transport = new StdioServerTransport();
|
|
78
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function Skill Handler Registry
|
|
3
|
+
*
|
|
4
|
+
* Shared in-memory store for function skill handlers. When a module calls
|
|
5
|
+
* functionSkill(), handlers are stored here. When the bridge imports that
|
|
6
|
+
* module, it reads the handlers from this registry.
|
|
7
|
+
*
|
|
8
|
+
* This works because Node.js module caching ensures both the user's module
|
|
9
|
+
* and the bridge resolve to the same instance of this registry.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const _registry = new Map();
|
|
13
|
+
|
|
14
|
+
export function registerHandlers(skillId, handlers, tools) {
|
|
15
|
+
_registry.set(skillId, { handlers, tools });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getHandlers(skillId) {
|
|
19
|
+
return _registry.get(skillId) || null;
|
|
20
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/* global require */
|
|
2
|
+
import { WorkflowGraph } from './graph.js';
|
|
3
|
+
import { getNodeImpl, hasNode } from './node-registry.js';
|
|
4
|
+
import { resolveNodeTools } from './tool-resolver.js';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
export function compileGraph(config, options = {}) {
|
|
8
|
+
const { nodes, edges, nodeConfigs = {} } = config;
|
|
9
|
+
|
|
10
|
+
if (!Array.isArray(nodes) || nodes.length === 0) {
|
|
11
|
+
throw new CompilationError('Graph must have at least one node');
|
|
12
|
+
}
|
|
13
|
+
if (!Array.isArray(edges)) {
|
|
14
|
+
throw new CompilationError('Graph edges must be an array');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const graph = new WorkflowGraph(options);
|
|
18
|
+
|
|
19
|
+
if (options.stateSchema) {
|
|
20
|
+
graph.setStateSchema(options.stateSchema);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const decisionNodeIds = new Set();
|
|
24
|
+
const nodeMap = new Map();
|
|
25
|
+
const resolvedToolsMap = {};
|
|
26
|
+
|
|
27
|
+
// Pass 1: Identify decision nodes (visual-only, not executable)
|
|
28
|
+
for (const node of nodes) {
|
|
29
|
+
const nodeType = resolveNodeType(node);
|
|
30
|
+
nodeMap.set(node.id, { ...node, resolvedType: nodeType });
|
|
31
|
+
if (nodeType === 'decision') {
|
|
32
|
+
decisionNodeIds.add(node.id);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Pass 2: Add executable nodes and resolve tools
|
|
37
|
+
for (const [nodeId, node] of nodeMap) {
|
|
38
|
+
if (decisionNodeIds.has(nodeId)) continue;
|
|
39
|
+
|
|
40
|
+
const nodeType = node.resolvedType;
|
|
41
|
+
const nodeConfig = nodeConfigs[nodeId] || {};
|
|
42
|
+
const userToolIds = nodeConfig.tools;
|
|
43
|
+
const resolved = resolveNodeTools(nodeType, userToolIds);
|
|
44
|
+
if (resolved) {
|
|
45
|
+
resolvedToolsMap[nodeId] = resolved;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const nodeOptions = {};
|
|
49
|
+
if (nodeConfig.prompt) {
|
|
50
|
+
nodeOptions.prompt = nodeConfig.prompt;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const isRegistered = hasNode(nodeType);
|
|
54
|
+
logger.debug(`[compiler] Node "${nodeId}" type="${nodeType}" registered=${isRegistered} hasCustomCode=${!!nodeConfig.customCode} hasExecuteCode=${!!nodeConfig.executeCode}`);
|
|
55
|
+
|
|
56
|
+
if (nodeConfig.customCode && !isRegistered) {
|
|
57
|
+
logger.debug(`[compiler] → using customCode (unregistered node)`);
|
|
58
|
+
graph.addNode(nodeId, wrapCustomCode(nodeId, nodeConfig.customCode, nodeConfig), nodeOptions);
|
|
59
|
+
graph.setNodeType(nodeId, nodeType);
|
|
60
|
+
} else if (isRegistered) {
|
|
61
|
+
logger.debug(`[compiler] → using registered implementation`);
|
|
62
|
+
const impl = getNodeImpl(nodeType);
|
|
63
|
+
if (impl.factory) {
|
|
64
|
+
graph.addNode(nodeId, impl.create(nodeId, {
|
|
65
|
+
...nodeConfig,
|
|
66
|
+
resolvedTools: resolved
|
|
67
|
+
}), nodeOptions);
|
|
68
|
+
} else {
|
|
69
|
+
graph.addNode(nodeId, impl, nodeOptions);
|
|
70
|
+
}
|
|
71
|
+
graph.setNodeType(nodeId, nodeType);
|
|
72
|
+
} else if (nodeConfig.executeCode) {
|
|
73
|
+
logger.debug(`[compiler] → using executeCode (fallback)`);
|
|
74
|
+
graph.addNode(nodeId, wrapCustomCode(nodeId, nodeConfig.executeCode, nodeConfig), nodeOptions);
|
|
75
|
+
graph.setNodeType(nodeId, nodeType);
|
|
76
|
+
} else {
|
|
77
|
+
throw new CompilationError(
|
|
78
|
+
`Unknown node type "${nodeType}" for node "${nodeId}". ` +
|
|
79
|
+
`Did you forget to register it?`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
graph.resolvedToolsMap = resolvedToolsMap;
|
|
85
|
+
|
|
86
|
+
// Pass 3: Determine entry point (node with no incoming non-decision edges)
|
|
87
|
+
const incomingTargets = new Set();
|
|
88
|
+
for (const edge of edges) {
|
|
89
|
+
if (!decisionNodeIds.has(edge.target)) {
|
|
90
|
+
incomingTargets.add(edge.target);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const entryNode = nodes.find(
|
|
94
|
+
n => !decisionNodeIds.has(n.id) && !incomingTargets.has(n.id)
|
|
95
|
+
);
|
|
96
|
+
if (!entryNode) {
|
|
97
|
+
throw new CompilationError('Could not determine entry point: no node without incoming edges found');
|
|
98
|
+
}
|
|
99
|
+
graph.setEntryPoint(entryNode.id);
|
|
100
|
+
|
|
101
|
+
// Pass 4: Wire edges, collapsing decision nodes into addConditionalEdges
|
|
102
|
+
const edgesBySource = groupBy(edges, 'source');
|
|
103
|
+
|
|
104
|
+
for (const edge of edges) {
|
|
105
|
+
const sourceIsDecision = decisionNodeIds.has(edge.source);
|
|
106
|
+
const targetIsDecision = decisionNodeIds.has(edge.target);
|
|
107
|
+
|
|
108
|
+
if (sourceIsDecision) {
|
|
109
|
+
// Edges FROM decision nodes are handled when we process the edge INTO the decision
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (targetIsDecision) {
|
|
114
|
+
// Edge goes into a decision node: find all outgoing edges from the decision
|
|
115
|
+
// and compile them into a single addConditionalEdges call
|
|
116
|
+
const decisionId = edge.target;
|
|
117
|
+
const outgoingEdges = edgesBySource.get(decisionId) || [];
|
|
118
|
+
|
|
119
|
+
if (outgoingEdges.length === 0) {
|
|
120
|
+
throw new CompilationError(
|
|
121
|
+
`Decision node "${decisionId}" has no outgoing edges`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const routeFn = compileConditionalRoutes(decisionId, outgoingEdges, decisionNodeIds);
|
|
126
|
+
graph.addConditionalEdges(edge.source, routeFn);
|
|
127
|
+
} else {
|
|
128
|
+
// Regular edge between two executable nodes
|
|
129
|
+
graph.addEdge(edge.source, edge.target);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return graph;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function validateGraphConfig(config) {
|
|
137
|
+
const errors = [];
|
|
138
|
+
|
|
139
|
+
if (!config || typeof config !== 'object') {
|
|
140
|
+
return { valid: false, errors: ['Config must be a non-null object'] };
|
|
141
|
+
}
|
|
142
|
+
if (!Array.isArray(config.nodes) || config.nodes.length === 0) {
|
|
143
|
+
errors.push('Graph must have at least one node');
|
|
144
|
+
}
|
|
145
|
+
if (!Array.isArray(config.edges)) {
|
|
146
|
+
errors.push('Graph edges must be an array');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (errors.length > 0) return { valid: false, errors };
|
|
150
|
+
|
|
151
|
+
const nodeConfigs = config.nodeConfigs || {};
|
|
152
|
+
|
|
153
|
+
for (const node of config.nodes) {
|
|
154
|
+
const nodeType = resolveNodeType(node);
|
|
155
|
+
if (nodeType === 'decision') continue;
|
|
156
|
+
if (hasNode(nodeType)) continue;
|
|
157
|
+
|
|
158
|
+
const nc = nodeConfigs[node.id] || {};
|
|
159
|
+
if (nc.customCode || nc.executeCode) continue;
|
|
160
|
+
|
|
161
|
+
errors.push(`Unknown node type "${nodeType}" for node "${node.id}". Register it or provide customCode/executeCode.`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check edge references are valid
|
|
165
|
+
const nodeIds = new Set(config.nodes.map(n => n.id));
|
|
166
|
+
for (const edge of config.edges) {
|
|
167
|
+
if (!nodeIds.has(edge.source)) {
|
|
168
|
+
errors.push(`Edge references unknown source node "${edge.source}"`);
|
|
169
|
+
}
|
|
170
|
+
if (!nodeIds.has(edge.target)) {
|
|
171
|
+
errors.push(`Edge references unknown target node "${edge.target}"`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check for exactly one entry point
|
|
176
|
+
const decisionIds = new Set(
|
|
177
|
+
config.nodes.filter(n => resolveNodeType(n) === 'decision').map(n => n.id)
|
|
178
|
+
);
|
|
179
|
+
const incomingTargets = new Set();
|
|
180
|
+
for (const edge of config.edges) {
|
|
181
|
+
if (!decisionIds.has(edge.target)) {
|
|
182
|
+
incomingTargets.add(edge.target);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const entryNodes = config.nodes.filter(
|
|
186
|
+
n => !decisionIds.has(n.id) && !incomingTargets.has(n.id)
|
|
187
|
+
);
|
|
188
|
+
if (entryNodes.length === 0) {
|
|
189
|
+
errors.push('No entry point found (every node has incoming edges)');
|
|
190
|
+
} else if (entryNodes.length > 1) {
|
|
191
|
+
errors.push(
|
|
192
|
+
`Multiple entry points found: ${entryNodes.map(n => n.id).join(', ')}. Graph must have exactly one.`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check decision nodes have outgoing edges with conditionalCode
|
|
197
|
+
for (const decisionId of decisionIds) {
|
|
198
|
+
const outgoing = config.edges.filter(e => e.source === decisionId);
|
|
199
|
+
if (outgoing.length === 0) {
|
|
200
|
+
errors.push(`Decision node "${decisionId}" has no outgoing edges`);
|
|
201
|
+
}
|
|
202
|
+
const hasCode = outgoing.some(
|
|
203
|
+
e => e.data?.conditionalCode || e.conditionalCode
|
|
204
|
+
);
|
|
205
|
+
if (!hasCode) {
|
|
206
|
+
errors.push(`Decision node "${decisionId}" outgoing edges have no conditionalCode`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { valid: errors.length === 0, errors };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function extractSteps(config) {
|
|
214
|
+
if (!config || !Array.isArray(config.nodes)) return [];
|
|
215
|
+
return config.nodes
|
|
216
|
+
.filter(n => resolveNodeType(n) !== 'decision')
|
|
217
|
+
.map(n => n.id);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---- Internal helpers ----
|
|
221
|
+
|
|
222
|
+
function resolveNodeType(node) {
|
|
223
|
+
const raw = node.data?.nodeType || node.data?.type || node.type;
|
|
224
|
+
// React Flow stores UI component type (e.g. "workflowNode", "custom") — fall back to node id
|
|
225
|
+
if (raw === 'workflowNode' || raw === 'custom' || raw === 'default') {
|
|
226
|
+
return node.id;
|
|
227
|
+
}
|
|
228
|
+
return raw;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function groupBy(arr, key) {
|
|
232
|
+
const map = new Map();
|
|
233
|
+
for (const item of arr) {
|
|
234
|
+
const k = item[key];
|
|
235
|
+
if (!map.has(k)) map.set(k, []);
|
|
236
|
+
map.get(k).push(item);
|
|
237
|
+
}
|
|
238
|
+
return map;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function compileConditionalRoutes(decisionId, outgoingEdges, decisionNodeIds) {
|
|
242
|
+
// All outgoing edges from a decision share the same conditionalCode
|
|
243
|
+
// (the function returns a node name string to route to)
|
|
244
|
+
const edgeWithCode = outgoingEdges.find(
|
|
245
|
+
e => e.data?.conditionalCode || e.conditionalCode
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (!edgeWithCode) {
|
|
249
|
+
throw new CompilationError(
|
|
250
|
+
`Decision node "${decisionId}" has no conditionalCode on its outgoing edges`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const code = edgeWithCode.data?.conditionalCode || edgeWithCode.conditionalCode;
|
|
255
|
+
const validTargets = new Set(
|
|
256
|
+
outgoingEdges.map(e => e.target).filter(t => !decisionNodeIds.has(t))
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Compile the route function string into an actual function
|
|
260
|
+
// The code is expected to be: "function route(state) { ... return 'node_name'; }"
|
|
261
|
+
let routeFn;
|
|
262
|
+
try {
|
|
263
|
+
|
|
264
|
+
const factory = new Function(`return (${code})`);
|
|
265
|
+
const compiled = factory();
|
|
266
|
+
|
|
267
|
+
routeFn = (state) => {
|
|
268
|
+
const result = compiled(state);
|
|
269
|
+
if (!validTargets.has(result)) {
|
|
270
|
+
logger.warn(
|
|
271
|
+
`Conditional route from "${decisionId}" returned "${result}" ` +
|
|
272
|
+
`which is not a valid target. Valid: ${[...validTargets].join(', ')}`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
};
|
|
277
|
+
} catch (err) {
|
|
278
|
+
throw new CompilationError(
|
|
279
|
+
`Failed to compile conditionalCode for decision "${decisionId}": ${err.message}`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return routeFn;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function wrapCustomCode(nodeId, codeString, nodeConfig = {}) {
|
|
287
|
+
let executeFn;
|
|
288
|
+
try {
|
|
289
|
+
|
|
290
|
+
executeFn = new Function(
|
|
291
|
+
'invokeAgent', 'require', 'console',
|
|
292
|
+
`return (${codeString})`
|
|
293
|
+
);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
throw new CompilationError(
|
|
296
|
+
`Failed to compile customCode for node "${nodeId}": ${err.message}`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const boundExecute = executeFn(
|
|
301
|
+
async (...args) => {
|
|
302
|
+
const { invokeAgent } = await import('./agents/index.js');
|
|
303
|
+
return invokeAgent(...args);
|
|
304
|
+
},
|
|
305
|
+
typeof require !== 'undefined' ? require : undefined,
|
|
306
|
+
console
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Reconstruct outputSchema from nodeConfig if available
|
|
310
|
+
let outputSchema = null;
|
|
311
|
+
if (nodeConfig.outputSchema) {
|
|
312
|
+
// Schema is already in JSON format from the enrichment
|
|
313
|
+
// For runtime, we need it as a validation object
|
|
314
|
+
// For now, we'll store the JSON schema for later use
|
|
315
|
+
outputSchema = nodeConfig.outputSchema.jsonSchema || nodeConfig.outputSchema;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
name: nodeId,
|
|
320
|
+
_isCustomCode: true,
|
|
321
|
+
outputSchema, // Include schema so Node constructor doesn't throw
|
|
322
|
+
execute: async (state) => {
|
|
323
|
+
try {
|
|
324
|
+
const result = await boundExecute(state);
|
|
325
|
+
return typeof result === 'object' && 'success' in result
|
|
326
|
+
? result
|
|
327
|
+
: { success: true, output: result, raw: null };
|
|
328
|
+
} catch (err) {
|
|
329
|
+
return { success: false, error: err.message, raw: null };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
class CompilationError extends Error {
|
|
336
|
+
constructor(message) {
|
|
337
|
+
super(message);
|
|
338
|
+
this.name = 'CompilationError';
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export { CompilationError };
|