@zibby/core 0.1.20 → 0.1.22

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.
Files changed (167) hide show
  1. package/dist/agents/base.js +17 -0
  2. package/dist/backend-client.js +1 -0
  3. package/dist/constants/tool-names.js +1 -0
  4. package/dist/constants/zibby-scratch.js +1 -0
  5. package/dist/constants.js +1 -0
  6. package/dist/enrichment/base.js +1 -0
  7. package/dist/enrichment/enrichers/accessibility-enricher.js +1 -0
  8. package/dist/enrichment/enrichers/dom-enricher.js +1 -0
  9. package/dist/enrichment/enrichers/page-state-enricher.js +1 -0
  10. package/dist/enrichment/enrichers/position-enricher.js +1 -0
  11. package/dist/enrichment/index.js +1 -0
  12. package/dist/enrichment/mcp-integration.js +1 -0
  13. package/dist/enrichment/mcp-ref-enricher.js +1 -0
  14. package/dist/enrichment/pipeline.js +3 -0
  15. package/dist/enrichment/trace-text-enricher.js +1 -0
  16. package/dist/framework/agents/assistant-strategy.js +5 -0
  17. package/dist/framework/agents/base.js +1 -0
  18. package/dist/framework/agents/claude-strategy.js +4 -0
  19. package/dist/framework/agents/codex-strategy.js +4 -0
  20. package/dist/framework/agents/cursor-strategy.js +32 -0
  21. package/dist/framework/agents/gemini-strategy.js +11 -0
  22. package/dist/framework/agents/index.js +13 -0
  23. package/dist/framework/agents/middleware/assistant-round-pipeline.js +3 -0
  24. package/dist/framework/agents/providers/base.js +1 -0
  25. package/dist/framework/agents/providers/index.js +1 -0
  26. package/dist/framework/agents/providers/openai-transport.js +2 -0
  27. package/dist/framework/agents/providers/openai.js +1 -0
  28. package/dist/framework/agents/providers/transport-base.js +1 -0
  29. package/dist/framework/agents/utils/auth-resolver.js +1 -0
  30. package/dist/framework/agents/utils/cursor-output-formatter.js +1 -0
  31. package/dist/framework/agents/utils/openai-proxy-formatter.js +9 -0
  32. package/dist/framework/agents/utils/payload-budget.js +3 -0
  33. package/dist/framework/agents/utils/structured-output-formatter.js +21 -0
  34. package/dist/framework/code-generator.js +10 -0
  35. package/dist/framework/constants.js +1 -0
  36. package/dist/framework/context-loader.js +5 -0
  37. package/dist/framework/function-bridge.js +2 -0
  38. package/dist/framework/function-skill-registry.js +1 -0
  39. package/dist/framework/graph-compiler.js +1 -0
  40. package/dist/framework/graph.js +5 -0
  41. package/dist/framework/index.js +1 -0
  42. package/dist/framework/mcp-client.js +2 -0
  43. package/dist/framework/node-registry.js +9 -0
  44. package/dist/framework/node.js +5 -0
  45. package/dist/framework/output-parser.js +3 -0
  46. package/dist/framework/skill-registry.js +1 -0
  47. package/dist/framework/state-utils.js +1 -0
  48. package/dist/framework/state.js +1 -0
  49. package/dist/framework/tool-resolver.js +1 -0
  50. package/dist/index.js +8 -0
  51. package/dist/runtime/generation/base.js +1 -0
  52. package/dist/runtime/generation/index.js +3 -0
  53. package/dist/runtime/generation/mcp-ref-strategy.js +41 -0
  54. package/dist/runtime/generation/stable-id-strategy.js +16 -0
  55. package/dist/runtime/stable-id-runtime.js +1 -0
  56. package/dist/runtime/verification/base.js +1 -0
  57. package/dist/runtime/verification/index.js +3 -0
  58. package/dist/runtime/verification/playwright-json-strategy.js +1 -0
  59. package/dist/runtime/zibby-runtime.js +1 -0
  60. package/dist/sync/index.js +1 -0
  61. package/dist/sync/uploader.js +1 -0
  62. package/dist/tools/run-playwright-test.js +5 -0
  63. package/dist/utils/adf-converter.js +7 -0
  64. package/dist/utils/ast-utils.js +1 -0
  65. package/dist/utils/ci-setup.js +5 -0
  66. package/dist/utils/cursor-mcp-isolated-home.js +1 -0
  67. package/dist/utils/cursor-utils.js +18 -0
  68. package/dist/utils/live-frame-discovery.js +1 -0
  69. package/dist/utils/logger.js +1 -0
  70. package/dist/utils/mcp-config-writer.js +10 -0
  71. package/dist/utils/mission-control-from-run-states.js +1 -0
  72. package/dist/utils/node-schema-parser.js +1 -0
  73. package/dist/utils/parallel-config.js +1 -0
  74. package/dist/utils/post-process-events.js +1 -0
  75. package/dist/utils/result-handler.js +1 -0
  76. package/{src → dist}/utils/ripple-effect.js +3 -12
  77. package/dist/utils/run-capacity-coordinator.js +1 -0
  78. package/dist/utils/run-capacity-queue.js +2 -0
  79. package/dist/utils/run-index-merge.js +1 -0
  80. package/dist/utils/run-index-post-cli.js +1 -0
  81. package/dist/utils/run-registry.js +3 -0
  82. package/dist/utils/run-state-session.js +2 -0
  83. package/dist/utils/selector-generator.js +4 -0
  84. package/dist/utils/session-state-constants.js +1 -0
  85. package/dist/utils/session-state-live-runs.js +1 -0
  86. package/dist/utils/streaming-parser.js +4 -0
  87. package/dist/utils/test-post-processor.js +18 -0
  88. package/dist/utils/timeline.js +14 -0
  89. package/dist/utils/trace-parser.js +2 -0
  90. package/dist/utils/video-organizer.js +3 -0
  91. package/package.json +49 -35
  92. package/templates/browser-test-automation/README.md +29 -7
  93. package/templates/browser-test-automation/chat.mjs +36 -0
  94. package/templates/browser-test-automation/graph.mjs +5 -9
  95. package/templates/browser-test-automation/nodes/execute-live.mjs +30 -58
  96. package/templates/browser-test-automation/nodes/generate-script.mjs +32 -12
  97. package/templates/browser-test-automation/nodes/utils.mjs +153 -10
  98. package/templates/browser-test-automation/pipeline-ids.js +12 -0
  99. package/templates/browser-test-automation/result-handler.mjs +78 -2
  100. package/templates/browser-test-automation/run-index.mjs +418 -0
  101. package/scripts/export-default-workflows.js +0 -51
  102. package/scripts/patch-cursor-mcp.js +0 -174
  103. package/scripts/setup-ci.sh +0 -115
  104. package/scripts/setup-official-playwright-mcp.sh +0 -226
  105. package/scripts/test-with-video.sh +0 -49
  106. package/src/agents/base.js +0 -361
  107. package/src/constants.js +0 -47
  108. package/src/enrichment/base.js +0 -49
  109. package/src/enrichment/enrichers/accessibility-enricher.js +0 -197
  110. package/src/enrichment/enrichers/dom-enricher.js +0 -171
  111. package/src/enrichment/enrichers/page-state-enricher.js +0 -129
  112. package/src/enrichment/enrichers/position-enricher.js +0 -67
  113. package/src/enrichment/index.js +0 -96
  114. package/src/enrichment/mcp-integration.js +0 -149
  115. package/src/enrichment/mcp-ref-enricher.js +0 -78
  116. package/src/enrichment/pipeline.js +0 -192
  117. package/src/enrichment/trace-text-enricher.js +0 -115
  118. package/src/framework/AGENTS.md +0 -98
  119. package/src/framework/agents/base.js +0 -72
  120. package/src/framework/agents/claude-strategy.js +0 -278
  121. package/src/framework/agents/cursor-strategy.js +0 -540
  122. package/src/framework/agents/index.js +0 -105
  123. package/src/framework/agents/utils/cursor-output-formatter.js +0 -67
  124. package/src/framework/agents/utils/openai-proxy-formatter.js +0 -249
  125. package/src/framework/code-generator.js +0 -301
  126. package/src/framework/constants.js +0 -33
  127. package/src/framework/context-loader.js +0 -101
  128. package/src/framework/function-bridge.js +0 -78
  129. package/src/framework/function-skill-registry.js +0 -20
  130. package/src/framework/graph-compiler.js +0 -342
  131. package/src/framework/graph.js +0 -610
  132. package/src/framework/index.js +0 -28
  133. package/src/framework/node-registry.js +0 -163
  134. package/src/framework/node.js +0 -259
  135. package/src/framework/output-parser.js +0 -71
  136. package/src/framework/skill-registry.js +0 -55
  137. package/src/framework/state-utils.js +0 -52
  138. package/src/framework/state.js +0 -67
  139. package/src/framework/tool-resolver.js +0 -65
  140. package/src/index.js +0 -345
  141. package/src/runtime/generation/base.js +0 -46
  142. package/src/runtime/generation/index.js +0 -70
  143. package/src/runtime/generation/mcp-ref-strategy.js +0 -197
  144. package/src/runtime/generation/stable-id-strategy.js +0 -170
  145. package/src/runtime/stable-id-runtime.js +0 -248
  146. package/src/runtime/verification/base.js +0 -44
  147. package/src/runtime/verification/index.js +0 -67
  148. package/src/runtime/verification/playwright-json-strategy.js +0 -119
  149. package/src/runtime/zibby-runtime.js +0 -299
  150. package/src/sync/index.js +0 -2
  151. package/src/sync/uploader.js +0 -29
  152. package/src/tools/run-playwright-test.js +0 -158
  153. package/src/utils/adf-converter.js +0 -68
  154. package/src/utils/ast-utils.js +0 -37
  155. package/src/utils/ci-setup.js +0 -124
  156. package/src/utils/cursor-utils.js +0 -71
  157. package/src/utils/logger.js +0 -144
  158. package/src/utils/mcp-config-writer.js +0 -115
  159. package/src/utils/node-schema-parser.js +0 -522
  160. package/src/utils/post-process-events.js +0 -55
  161. package/src/utils/result-handler.js +0 -102
  162. package/src/utils/selector-generator.js +0 -239
  163. package/src/utils/streaming-parser.js +0 -387
  164. package/src/utils/test-post-processor.js +0 -211
  165. package/src/utils/timeline.js +0 -217
  166. package/src/utils/trace-parser.js +0 -325
  167. package/src/utils/video-organizer.js +0 -91
@@ -1,101 +0,0 @@
1
- import { existsSync, readFileSync } from 'fs';
2
- import { join, dirname } from 'path';
3
-
4
- export class ContextLoader {
5
- static async loadContext(specPath, cwd, config = {}) {
6
- const context = {};
7
-
8
- // Get list of context filenames to search for (default: CONTEXT.md, AGENTS.md)
9
- const filenames = config.filenames || ['CONTEXT.md', 'AGENTS.md'];
10
-
11
- // Auto-discover cascade: search from spec directory up to root
12
- if (specPath) {
13
- const specDir = dirname(join(cwd, specPath));
14
-
15
- for (const filename of filenames) {
16
- const content = await this.findAndMergeContextFiles(filename, specDir, cwd);
17
- if (content) {
18
- // Use filename without extension as key (e.g., CONTEXT.md -> context)
19
- const key = filename.replace(/\.[^.]+$/, '').toLowerCase();
20
- context[key] = content;
21
- }
22
- }
23
- }
24
-
25
- // Also load any explicitly configured discovery files
26
- const discovery = config.discovery || {};
27
- for (const [key, pathTemplate] of Object.entries(discovery)) {
28
- try {
29
- const resolvedPath = join(cwd, pathTemplate);
30
- if (existsSync(resolvedPath)) {
31
- const content = await this.loadFile(resolvedPath);
32
- context[key] = content;
33
- }
34
- } catch (err) {
35
- console.warn(`⚠️ Could not load context '${key}' from '${pathTemplate}': ${err.message}`);
36
- }
37
- }
38
-
39
- return context;
40
- }
41
-
42
- // Auto-discover: search up the directory tree for context files
43
- static async findAndMergeContextFiles(filename, startDir, rootDir) {
44
- const contents = [];
45
- let currentDir = startDir;
46
-
47
- // Walk up the tree from spec directory to root
48
- while (currentDir.startsWith(rootDir)) {
49
- const contextPath = join(currentDir, filename);
50
-
51
- if (existsSync(contextPath)) {
52
- try {
53
- const content = await this.loadFile(contextPath);
54
- contents.unshift(content); // Add to beginning (root context first)
55
- } catch (err) {
56
- console.warn(`⚠️ Could not load ${filename} from ${contextPath}: ${err.message}`);
57
- }
58
- }
59
-
60
- // Move up one directory
61
- const parentDir = dirname(currentDir);
62
- if (parentDir === currentDir) break; // Reached filesystem root
63
- currentDir = parentDir;
64
- }
65
-
66
- // Merge all context files (root → specific)
67
- if (contents.length === 0) return null;
68
-
69
- // If all files are strings (markdown), concatenate with separator
70
- if (contents.every(c => typeof c === 'string')) {
71
- return contents.join('\n\n---\n\n');
72
- }
73
-
74
- // If objects (JSON/JS), merge them
75
- if (contents.every(c => typeof c === 'object')) {
76
- return Object.assign({}, ...contents);
77
- }
78
-
79
- // Mixed types: return the most specific (last one)
80
- return contents[contents.length - 1];
81
- }
82
-
83
- static async loadFile(filePath) {
84
- const content = readFileSync(filePath, 'utf-8');
85
-
86
- // Try to parse as JSON
87
- if (filePath.endsWith('.json')) {
88
- return JSON.parse(content);
89
- }
90
-
91
- // Try to import as JS module
92
- if (filePath.endsWith('.js') || filePath.endsWith('.mjs')) {
93
- const { pathToFileURL } = await import('url');
94
- const module = await import(pathToFileURL(filePath).href);
95
- return module.default || module;
96
- }
97
-
98
- // Return raw content (markdown, yaml, txt, etc.)
99
- return content;
100
- }
101
- }
@@ -1,78 +0,0 @@
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);
@@ -1,20 +0,0 @@
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
- }
@@ -1,342 +0,0 @@
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 };