@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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/package.json +94 -0
  4. package/src/agents/base.js +361 -0
  5. package/src/constants.js +47 -0
  6. package/src/enrichment/base.js +49 -0
  7. package/src/enrichment/enrichers/accessibility-enricher.js +197 -0
  8. package/src/enrichment/enrichers/dom-enricher.js +171 -0
  9. package/src/enrichment/enrichers/page-state-enricher.js +129 -0
  10. package/src/enrichment/enrichers/position-enricher.js +67 -0
  11. package/src/enrichment/index.js +96 -0
  12. package/src/enrichment/mcp-integration.js +149 -0
  13. package/src/enrichment/mcp-ref-enricher.js +78 -0
  14. package/src/enrichment/pipeline.js +192 -0
  15. package/src/enrichment/trace-text-enricher.js +115 -0
  16. package/src/framework/AGENTS.md +98 -0
  17. package/src/framework/agents/base.js +72 -0
  18. package/src/framework/agents/claude-strategy.js +278 -0
  19. package/src/framework/agents/cursor-strategy.js +459 -0
  20. package/src/framework/agents/index.js +105 -0
  21. package/src/framework/agents/utils/cursor-output-formatter.js +67 -0
  22. package/src/framework/agents/utils/openai-proxy-formatter.js +249 -0
  23. package/src/framework/code-generator.js +301 -0
  24. package/src/framework/constants.js +33 -0
  25. package/src/framework/context-loader.js +101 -0
  26. package/src/framework/function-bridge.js +78 -0
  27. package/src/framework/function-skill-registry.js +20 -0
  28. package/src/framework/graph-compiler.js +342 -0
  29. package/src/framework/graph.js +610 -0
  30. package/src/framework/index.js +28 -0
  31. package/src/framework/node-registry.js +163 -0
  32. package/src/framework/node.js +259 -0
  33. package/src/framework/output-parser.js +71 -0
  34. package/src/framework/skill-registry.js +55 -0
  35. package/src/framework/state-utils.js +52 -0
  36. package/src/framework/state.js +67 -0
  37. package/src/framework/tool-resolver.js +65 -0
  38. package/src/index.js +342 -0
  39. package/src/runtime/generation/base.js +46 -0
  40. package/src/runtime/generation/index.js +70 -0
  41. package/src/runtime/generation/mcp-ref-strategy.js +197 -0
  42. package/src/runtime/generation/stable-id-strategy.js +170 -0
  43. package/src/runtime/stable-id-runtime.js +248 -0
  44. package/src/runtime/verification/base.js +44 -0
  45. package/src/runtime/verification/index.js +67 -0
  46. package/src/runtime/verification/playwright-json-strategy.js +119 -0
  47. package/src/runtime/zibby-runtime.js +299 -0
  48. package/src/sync/index.js +2 -0
  49. package/src/sync/uploader.js +29 -0
  50. package/src/tools/run-playwright-test.js +158 -0
  51. package/src/utils/adf-converter.js +68 -0
  52. package/src/utils/ast-utils.js +37 -0
  53. package/src/utils/ci-setup.js +124 -0
  54. package/src/utils/cursor-utils.js +71 -0
  55. package/src/utils/logger.js +144 -0
  56. package/src/utils/mcp-config-writer.js +115 -0
  57. package/src/utils/node-schema-parser.js +522 -0
  58. package/src/utils/post-process-events.js +55 -0
  59. package/src/utils/result-handler.js +102 -0
  60. package/src/utils/ripple-effect.js +84 -0
  61. package/src/utils/selector-generator.js +239 -0
  62. package/src/utils/streaming-parser.js +387 -0
  63. package/src/utils/test-post-processor.js +211 -0
  64. package/src/utils/timeline.js +217 -0
  65. package/src/utils/trace-parser.js +325 -0
  66. package/src/utils/video-organizer.js +91 -0
  67. package/templates/browser-test-automation/README.md +114 -0
  68. package/templates/browser-test-automation/graph.js +54 -0
  69. package/templates/browser-test-automation/nodes/execute-live.js +250 -0
  70. package/templates/browser-test-automation/nodes/generate-script.js +77 -0
  71. package/templates/browser-test-automation/nodes/index.js +3 -0
  72. package/templates/browser-test-automation/nodes/preflight.js +59 -0
  73. package/templates/browser-test-automation/nodes/utils.js +154 -0
  74. package/templates/browser-test-automation/result-handler.js +286 -0
  75. package/templates/code-analysis/graph.js +72 -0
  76. package/templates/code-analysis/index.js +18 -0
  77. package/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
  78. package/templates/code-analysis/nodes/create-pr-node.js +175 -0
  79. package/templates/code-analysis/nodes/finalize-node.js +118 -0
  80. package/templates/code-analysis/nodes/generate-code-node.js +425 -0
  81. package/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
  82. package/templates/code-analysis/nodes/services/prMetaService.js +86 -0
  83. package/templates/code-analysis/nodes/setup-node.js +142 -0
  84. package/templates/code-analysis/prompts/analyze-ticket.md +181 -0
  85. package/templates/code-analysis/prompts/generate-code.md +33 -0
  86. package/templates/code-analysis/prompts/generate-test-cases.md +110 -0
  87. package/templates/code-analysis/state.js +40 -0
  88. package/templates/code-implementation/graph.js +35 -0
  89. package/templates/code-implementation/index.js +7 -0
  90. package/templates/code-implementation/state.js +14 -0
  91. package/templates/global-setup.js +56 -0
  92. package/templates/index.js +94 -0
  93. package/templates/register-nodes.js +24 -0
@@ -0,0 +1,610 @@
1
+ /**
2
+ * Graph execution engine
3
+ * Similar to LangGraph's StateGraph
4
+ */
5
+
6
+ import { WorkflowState } from './state.js';
7
+ import { Node, ConditionalNode } from './node.js';
8
+ import { ContextLoader } from './context-loader.js';
9
+ import { mkdirSync, existsSync, writeFileSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { config as loadDotenv } from 'dotenv';
12
+ import { zodToJsonSchema } from 'zod-to-json-schema';
13
+ import Handlebars from 'handlebars';
14
+ import { invokeAgent as coreInvokeAgent } from './agents/index.js';
15
+ import { DEFAULT_OUTPUT_BASE, SESSIONS_DIR, SESSION_INFO_FILE, CI_ENV_VARS } from './constants.js';
16
+ import { timeline } from '../utils/timeline.js';
17
+
18
+ export class WorkflowGraph {
19
+ constructor(options = {}) {
20
+ this.nodes = new Map();
21
+ this.edges = new Map();
22
+ this.entryPoint = null;
23
+ this.middleware = Array.isArray(options.middleware) ? [...options.middleware] : [];
24
+ if (options.nodeMiddleware) this.middleware.push(options.nodeMiddleware);
25
+ this.nodeTypeMap = new Map();
26
+ this.conditionalCodeMap = new Map();
27
+ this.stateSchema = options.stateSchema || null;
28
+ this.nodePrompts = new Map();
29
+ this.nodeOptions = new Map();
30
+ }
31
+
32
+ /**
33
+ * Set the state schema for this graph
34
+ * @param {Object} schema - Schema definition
35
+ */
36
+ setStateSchema(schema) {
37
+ this.stateSchema = schema;
38
+ return this;
39
+ }
40
+
41
+ /**
42
+ * Get the state schema
43
+ */
44
+ getStateSchema() {
45
+ return this.stateSchema;
46
+ }
47
+
48
+ addNode(name, nodeOrConfig, options = {}) {
49
+ const node = nodeOrConfig instanceof Node ? nodeOrConfig : new Node(nodeOrConfig);
50
+ node.name = name; // Ensure name is set
51
+ this.nodes.set(name, node);
52
+
53
+ // Store prompt if provided
54
+ if (options.prompt) {
55
+ this.nodePrompts.set(name, options.prompt);
56
+ }
57
+
58
+ // Store other options
59
+ if (Object.keys(options).length > 0) {
60
+ this.nodeOptions.set(name, options);
61
+ }
62
+
63
+ return this;
64
+ }
65
+
66
+ addConditionalNode(name, config) {
67
+ const node = new ConditionalNode({ ...config, name });
68
+ this.nodes.set(name, node);
69
+ return this;
70
+ }
71
+
72
+ addEdge(from, to) {
73
+ this.edges.set(from, to);
74
+ return this;
75
+ }
76
+
77
+ setNodeType(name, nodeType) {
78
+ this.nodeTypeMap.set(name, nodeType);
79
+ return this;
80
+ }
81
+
82
+ addConditionalEdges(from, routes, { labels } = {}) {
83
+ this.edges.set(from, { conditional: true, routes, labels });
84
+ if (typeof routes === 'function') {
85
+ this.conditionalCodeMap.set(from, routes.toString());
86
+ }
87
+ return this;
88
+ }
89
+
90
+ setEntryPoint(nodeName) {
91
+ this.entryPoint = nodeName;
92
+ return this;
93
+ }
94
+
95
+ use(middlewareFn) {
96
+ if (typeof middlewareFn === 'function') {
97
+ this.middleware.push(middlewareFn);
98
+ }
99
+ return this;
100
+ }
101
+
102
+ _composeMiddleware(nodeName, coreFn, stateValues, state) {
103
+ let fn = coreFn;
104
+ for (let i = this.middleware.length - 1; i >= 0; i--) {
105
+ const mw = this.middleware[i];
106
+ const next = fn;
107
+ fn = () => mw(nodeName, next, stateValues, state);
108
+ }
109
+ return fn();
110
+ }
111
+
112
+ serialize() {
113
+ const nodes = [];
114
+ const nodeConfigs = {};
115
+
116
+ for (const [nodeId, node] of this.nodes) {
117
+ const nodeType = this.nodeTypeMap.get(nodeId) || nodeId;
118
+ nodes.push({ id: nodeId, type: nodeType, data: { nodeType, label: nodeId } });
119
+
120
+ const isCustom = node._isCustomCode || false;
121
+ const config = {};
122
+
123
+ if (isCustom && typeof node.execute === 'function') {
124
+ config.customCode = node.execute.toString();
125
+ }
126
+
127
+ // Include prompt in config
128
+ const prompt = this.nodePrompts.get(nodeId);
129
+ if (prompt) {
130
+ config.prompt = prompt;
131
+ }
132
+
133
+ // Include execute code for all nodes (for UI display)
134
+ // Use customExecute (the node's actual implementation) not the base class execute
135
+ if (typeof node.customExecute === 'function') {
136
+ config.executeCode = node.customExecute.toString();
137
+ }
138
+
139
+ // Include output schema for UI (Zod → JSON Schema)
140
+ if (node.outputSchema) {
141
+ try {
142
+ // Check if it's a Zod schema
143
+ const isZodSchema = typeof node.outputSchema._def !== 'undefined';
144
+
145
+ if (isZodSchema) {
146
+ // Convert Zod to JSON Schema for frontend
147
+ const jsonSchema = zodToJsonSchema(node.outputSchema, { target: 'openApi3' });
148
+
149
+ // Also generate flattened variable paths for Variable Inspector
150
+ const variables = this._flattenJsonSchemaToVariables(jsonSchema);
151
+
152
+ config.outputSchema = {
153
+ jsonSchema,
154
+ variables
155
+ };
156
+ } else {
157
+ // Legacy non-Zod schema (will be deprecated)
158
+ config.outputSchema = { schema: node.outputSchema };
159
+ }
160
+ } catch (error) {
161
+ console.warn(`Failed to convert schema for ${nodeId}:`, error.message);
162
+ }
163
+ }
164
+
165
+ const toolDefs = (this.resolvedToolsMap || {})[nodeId];
166
+ if (toolDefs?.toolIds) {
167
+ config.tools = toolDefs.toolIds;
168
+ }
169
+
170
+ if (Object.keys(config).length > 0) {
171
+ nodeConfigs[nodeId] = config;
172
+ }
173
+ }
174
+
175
+ const edges = [];
176
+ for (const [from, target] of this.edges) {
177
+ if (typeof target === 'string') {
178
+ edges.push({ source: from, target });
179
+ } else if (target.conditional) {
180
+ const codeStr = this.conditionalCodeMap.get(from) || target.routes.toString();
181
+ const possibleTargets = this._inferConditionalTargets(target.routes);
182
+ const labels = target.labels || {};
183
+ for (const t of possibleTargets) {
184
+ const edge = {
185
+ source: from,
186
+ target: t,
187
+ data: { conditionalCode: codeStr }
188
+ };
189
+ if (labels[t]) {
190
+ edge.label = labels[t];
191
+ }
192
+ edges.push(edge);
193
+ }
194
+ }
195
+ }
196
+
197
+ // Convert Zod schema to JSON Schema for serialization
198
+ let jsonSchema = null;
199
+ if (this.stateSchema) {
200
+ try {
201
+ jsonSchema = zodToJsonSchema(this.stateSchema, { target: 'openApi3' });
202
+ } catch {
203
+ jsonSchema = this.stateSchema;
204
+ }
205
+ }
206
+
207
+ return {
208
+ nodes,
209
+ edges,
210
+ nodeConfigs,
211
+ stateSchema: jsonSchema
212
+ };
213
+ }
214
+
215
+ _inferConditionalTargets(routeFn) {
216
+ const fnStr = routeFn.toString();
217
+ const targets = new Set();
218
+ const returnPattern = /return\s+['"]([^'"]+)['"]/g;
219
+ let match;
220
+ while ((match = returnPattern.exec(fnStr)) !== null) {
221
+ targets.add(match[1]);
222
+ }
223
+ return [...targets];
224
+ }
225
+
226
+ /**
227
+ * Flatten JSON Schema to variable paths for Variable Inspector
228
+ * @param {Object} jsonSchema - JSON Schema object (may have $ref/definitions from zodToJsonSchema)
229
+ * @param {string} prefix - Path prefix for nested properties
230
+ * @returns {Array} Array of { path, type, label, optional } objects
231
+ */
232
+ _flattenJsonSchemaToVariables(jsonSchema, prefix = '') {
233
+ // Handle zodToJsonSchema format with $ref and definitions
234
+ // Example: { $ref: "#/definitions/node_name", definitions: { node_name: { type: 'object', properties: {...} } } }
235
+ let rootSchema = jsonSchema;
236
+
237
+ if (jsonSchema.$ref && jsonSchema.definitions) {
238
+ // Extract the referenced definition
239
+ const refName = jsonSchema.$ref.replace('#/definitions/', '');
240
+ rootSchema = jsonSchema.definitions[refName] || jsonSchema;
241
+ }
242
+
243
+ return this._flattenSchema(rootSchema, prefix);
244
+ }
245
+
246
+ /**
247
+ * Internal recursive schema flattener
248
+ */
249
+ _flattenSchema(schema, prefix = '') {
250
+ if (!schema || typeof schema !== 'object') {
251
+ return [];
252
+ }
253
+
254
+ const variables = [];
255
+ const properties = schema.properties || {};
256
+ const required = schema.required || [];
257
+
258
+ for (const [key, propSchema] of Object.entries(properties)) {
259
+ const path = prefix ? `${prefix}.${key}` : key;
260
+ const isOptional = !required.includes(key);
261
+
262
+ // Add this property
263
+ variables.push({
264
+ path,
265
+ type: propSchema.type || 'unknown',
266
+ label: propSchema.description || this._formatLabel(key),
267
+ optional: isOptional
268
+ });
269
+
270
+ // Recurse into nested objects
271
+ if (propSchema.type === 'object' && propSchema.properties) {
272
+ const nested = this._flattenSchema(propSchema, path);
273
+ variables.push(...nested);
274
+ }
275
+
276
+ // Recurse into arrays with object items
277
+ if (propSchema.type === 'array' && propSchema.items?.type === 'object' && propSchema.items.properties) {
278
+ const nested = this._flattenSchema(propSchema.items, `${path}[]`);
279
+ variables.push(...nested);
280
+ }
281
+ }
282
+
283
+ return variables;
284
+ }
285
+
286
+ /**
287
+ * Format camelCase to Title Case
288
+ */
289
+ _formatLabel(str) {
290
+ return str
291
+ .replace(/([A-Z])/g, ' $1')
292
+ .replace(/^./, s => s.toUpperCase())
293
+ .trim();
294
+ }
295
+
296
+ /**
297
+ * Generate session ID with CI environment awareness
298
+ * Priority: CI_JOB_ID > GITHUB_RUN_ID > CIRCLE_WORKFLOW_ID > BUILD_ID > Date.now()
299
+ */
300
+ _getSessionId(config = {}) {
301
+ // Detect CI environment by checking all known CI env vars
302
+ const ciSessionId = CI_ENV_VARS
303
+ .map(envVar => process.env[envVar])
304
+ .find(val => val);
305
+
306
+ const baseId = ciSessionId || Date.now().toString();
307
+
308
+ // Apply prefix if configured
309
+ const prefix = config.paths?.sessionPrefix;
310
+ return prefix ? `${prefix}_${baseId}` : baseId;
311
+ }
312
+
313
+ _summarizeNodeOutput(nodeName, output) {
314
+ if (!output || typeof output !== 'object') return [];
315
+ const details = [];
316
+
317
+ if (output.success !== undefined) {
318
+ details.push(`Result: ${output.success ? 'passed' : 'failed'}`);
319
+ }
320
+
321
+ for (const [key, value] of Object.entries(output)) {
322
+ if (key === 'success' || key === 'raw' || key === 'nextNode') continue;
323
+ if (typeof value === 'string' && value.length <= 80) {
324
+ details.push(`${key}: ${value}`);
325
+ } else if (Array.isArray(value)) {
326
+ const total = value.length;
327
+ const passed = value.filter(v => v?.passed === true).length;
328
+ const hasPassed = value.some(v => v?.passed !== undefined);
329
+ if (hasPassed) {
330
+ const failed = total - passed;
331
+ details.push(`${key}: ${passed}/${total} passed${failed ? `, ${failed} failed` : ''}`);
332
+ } else {
333
+ details.push(`${key}: ${total} items`);
334
+ }
335
+ }
336
+ if (details.length >= 4) break;
337
+ }
338
+ return details;
339
+ }
340
+
341
+ async run(agent, initialState = {}) {
342
+ if (!this.entryPoint) {
343
+ throw new Error('No entry point set for graph');
344
+ }
345
+
346
+ const cwd = initialState.cwd || process.cwd();
347
+
348
+ loadDotenv({ path: join(cwd, '.env') });
349
+
350
+ // Auto-resolve config if not provided
351
+ let config = initialState.config || {};
352
+ if (!config || Object.keys(config).length === 0) {
353
+ try {
354
+ const configPath = join(cwd, '.zibby.config.js');
355
+ if (existsSync(configPath)) {
356
+ const configModule = await import(configPath);
357
+ config = configModule.default || {};
358
+ }
359
+ } catch { /* no config file */ }
360
+ }
361
+
362
+ // ECS/CI: enable strictMode by default for reliable structured output
363
+ if (process.env.EXECUTION_ID && !config.agent?.strictMode) {
364
+ config.agent = { ...config.agent, strictMode: true };
365
+ }
366
+
367
+ // Auto-infer agent type: initialState > config > AGENT_TYPE env > default
368
+ let agentType = initialState.agentType;
369
+ if (!agentType) {
370
+ const ac = config?.agent;
371
+ if (ac?.provider) agentType = ac.provider;
372
+ else if (ac?.claude) agentType = 'claude';
373
+ else if (ac?.cursor) agentType = 'cursor';
374
+ else agentType = process.env.AGENT_TYPE || 'cursor';
375
+ }
376
+
377
+ // Auto-resolve context config from agent constructor config
378
+ const contextConfig = initialState.contextConfig || agent?.config?.contextConfig || agent?.config?.context || config?.context || {};
379
+
380
+ // Validate initial state against Zod schema if defined
381
+ if (this.stateSchema) {
382
+ const result = this.stateSchema.safeParse(initialState);
383
+ if (!result.success) {
384
+ const errors = result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`);
385
+ console.error('❌ Initial state validation failed:');
386
+ errors.forEach(e => console.error(` - ${e}`));
387
+ throw new Error(`State validation failed: ${errors.join(', ')}`);
388
+ }
389
+ timeline.step('State validated against schema');
390
+ }
391
+
392
+ let sessionPath = initialState.sessionPath;
393
+ let sessionTimestamp = initialState.sessionTimestamp;
394
+ let sessionId;
395
+
396
+ if (!sessionPath) {
397
+ sessionId = this._getSessionId(config);
398
+ sessionTimestamp = sessionTimestamp || Date.now();
399
+
400
+ const outputBase = config.paths?.output || DEFAULT_OUTPUT_BASE;
401
+ sessionPath = join(cwd, outputBase, SESSIONS_DIR, sessionId);
402
+
403
+ if (!existsSync(sessionPath)) {
404
+ mkdirSync(sessionPath, { recursive: true });
405
+ }
406
+
407
+ timeline.step(`Session ${sessionId}`);
408
+ } else {
409
+ sessionId = sessionPath.split('/').pop();
410
+ timeline.step(`Session ${sessionId}`);
411
+ }
412
+
413
+ const context = await ContextLoader.loadContext(
414
+ initialState.specPath || '',
415
+ cwd,
416
+ contextConfig
417
+ );
418
+
419
+ if (Object.keys(context).length > 0) {
420
+ timeline.step(`Context loaded: ${Object.keys(context).join(', ')}`);
421
+ }
422
+
423
+ let outputPath = initialState.outputPath;
424
+ if (!outputPath && initialState.specPath) {
425
+ if (agent?.calculateOutputPath) {
426
+ outputPath = agent.calculateOutputPath(initialState.specPath);
427
+ } else {
428
+ console.warn(`⚠️ outputPath not resolved (specPath=${initialState.specPath})`);
429
+ }
430
+ }
431
+
432
+ const state = new WorkflowState({
433
+ ...initialState,
434
+ config,
435
+ agentType,
436
+ outputPath,
437
+ sessionPath,
438
+ sessionTimestamp,
439
+ context,
440
+ resolvedTools: this.resolvedToolsMap || {}
441
+ });
442
+ let currentNode = this.entryPoint;
443
+ const executionLog = [];
444
+
445
+ while (currentNode && currentNode !== 'END') {
446
+ const node = this.nodes.get(currentNode);
447
+ if (!node) {
448
+ throw new Error(`Node '${currentNode}' not found in graph`);
449
+ }
450
+
451
+ // Update session info with current node for MCP recorder
452
+ const outputBase = (state.get('config')?.paths?.output || DEFAULT_OUTPUT_BASE);
453
+ const sessionInfoPath = join(cwd, outputBase, SESSION_INFO_FILE);
454
+ mkdirSync(join(cwd, outputBase), { recursive: true });
455
+ writeFileSync(sessionInfoPath, JSON.stringify({
456
+ sessionPath,
457
+ sessionTimestamp,
458
+ currentNode,
459
+ createdAt: new Date().toISOString(),
460
+ config: state.get('config') // Pass full config to MCP recorder (for playwrightArtifacts, etc)
461
+ }), 'utf-8');
462
+
463
+ const nodeTools = (this.resolvedToolsMap || {})[currentNode] || null;
464
+ state.set('_currentNodeTools', nodeTools);
465
+
466
+ const allNodeConfigs = state.get('nodeConfigs') || {};
467
+ state.set('_currentNodeConfig', allNodeConfigs[currentNode] || {});
468
+
469
+ timeline.nodeStart(currentNode);
470
+ const startTime = Date.now();
471
+
472
+ // Get prompt template for this node
473
+ const promptTemplate = this.nodePrompts.get(currentNode);
474
+
475
+ // Create invokeAgent wrapper with template rendering
476
+ const invokeAgent = async (promptValues = {}, options = {}) => {
477
+ let finalPrompt = options.prompt || '';
478
+
479
+ // If node has a template, render it with provided values
480
+ if (promptTemplate) {
481
+ try {
482
+ const template = Handlebars.compile(promptTemplate, { noEscape: true });
483
+ finalPrompt = template(promptValues);
484
+ } catch (err) {
485
+ console.error(`❌ Template rendering failed for node '${currentNode}':`, err.message);
486
+ throw new Error(`Template rendering failed: ${err.message}`, { cause: err });
487
+ }
488
+ } else if (!finalPrompt) {
489
+ throw new Error(`No prompt template configured for node '${currentNode}' and no prompt provided in options`);
490
+ }
491
+
492
+ const agentContext = {
493
+ state: state.getAll(),
494
+ images: options.images || [],
495
+ };
496
+
497
+ const agentOptions = {
498
+ model: options.model || state.get('model'),
499
+ workspace: state.get('workspace'),
500
+ schema: options.schema,
501
+ ...options
502
+ };
503
+
504
+ return coreInvokeAgent(finalPrompt, agentContext, agentOptions);
505
+ };
506
+
507
+ // Create context object for node execution
508
+ // New unified signature: execute(context) where context has { state, invokeAgent, agent, ... }
509
+ // plus backward-compatible spread of state values
510
+ const nodeContext = {
511
+ // New unified API
512
+ state,
513
+ invokeAgent,
514
+ agent, // Add agent for base Node class LLM execution path
515
+ nodeId: currentNode,
516
+ promptTemplate,
517
+ getPromptTemplate: () => promptTemplate,
518
+ // Spread state values so nodes can destructure: const { workspace, ...rest } = context;
519
+ ...state.getAll()
520
+ };
521
+
522
+ try {
523
+ // Execute node with context
524
+ // Node.execute() still expects (agent, state) signature for internal processing
525
+ // For nodes with customExecute, it will call customExecute(state.getAll())
526
+ // We pass nodeContext as "agent" for nodes that define their own execute method
527
+ let result;
528
+ if (this.middleware.length > 0) {
529
+ result = await this._composeMiddleware(currentNode, async () => {
530
+ return node.execute(nodeContext, state);
531
+ }, state.getAll(), state);
532
+ } else {
533
+ result = await node.execute(nodeContext, state);
534
+ }
535
+
536
+ const duration = Date.now() - startTime;
537
+
538
+ executionLog.push({
539
+ node: currentNode,
540
+ success: result.success,
541
+ duration,
542
+ timestamp: new Date().toISOString()
543
+ });
544
+
545
+ if (!result.success) {
546
+ state.append('errors', { node: currentNode, error: result.error });
547
+
548
+ const maxRetries = node.config?.retries || 0;
549
+ const retryKey = `${currentNode}_retries`;
550
+ const currentRetries = state.getAll()[retryKey] || 0;
551
+
552
+ if (currentRetries < maxRetries) {
553
+ timeline.stepInfo(`Retrying (attempt ${currentRetries + 1}/${maxRetries})`);
554
+ state.update({
555
+ [retryKey]: currentRetries + 1,
556
+ [`${currentNode}_raw`]: result.raw
557
+ });
558
+ continue;
559
+ }
560
+
561
+ timeline.nodeFailed(currentNode, result.error, { duration });
562
+ throw new Error(`Node '${currentNode}' failed after ${currentRetries} attempts: ${result.error}`);
563
+ }
564
+
565
+ state.update({
566
+ [currentNode]: result.output
567
+ });
568
+
569
+ const details = this._summarizeNodeOutput(currentNode, result.output);
570
+ timeline.nodeComplete(currentNode, { duration, details });
571
+
572
+ const edge = this.edges.get(currentNode);
573
+ if (!edge) {
574
+ currentNode = 'END';
575
+ } else if (edge.conditional) {
576
+ const currentState = state.getAll();
577
+ const nextNode = edge.routes(currentState);
578
+
579
+ timeline.route(currentNode, nextNode);
580
+ currentNode = nextNode;
581
+ } else {
582
+ // Direct edge
583
+ currentNode = edge;
584
+ }
585
+ } catch (error) {
586
+ if (timeline.isInsideNode) {
587
+ timeline.nodeFailed(currentNode, error.message, { duration: Date.now() - startTime });
588
+ }
589
+
590
+ state.set('failed', true);
591
+ state.set('failedAt', currentNode);
592
+ throw error;
593
+ }
594
+ }
595
+
596
+ timeline.graphComplete();
597
+ const result = {
598
+ success: true,
599
+ state: state.getAll(),
600
+ executionLog
601
+ };
602
+
603
+ if (agent && typeof agent.onComplete === 'function') {
604
+ await agent.onComplete(result);
605
+ }
606
+
607
+ return result;
608
+ }
609
+ }
610
+
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Zibby Workflow Framework
3
+ * A graph-based orchestration system for AI agents
4
+ * Inspired by LangGraph, but agent-agnostic
5
+ */
6
+
7
+ export { WorkflowGraph } from './graph.js';
8
+ export { Node, ConditionalNode } from './node.js';
9
+ export { WorkflowState } from './state.js';
10
+ export { OutputParser, SchemaTypes } from './output-parser.js';
11
+
12
+ // Graph compiler (serialized config -> executable WorkflowGraph)
13
+ export { compileGraph, validateGraphConfig, extractSteps, CompilationError } from './graph-compiler.js';
14
+
15
+ export { registerNode, getNodeImpl, hasNode, listNodeTypes, getNodeTemplate } from './node-registry.js';
16
+ export { resolveNodeTools, getResolvedToolDefinitions, NODE_DEFAULT_TOOLS } from './tool-resolver.js';
17
+ export { registerSkill, getSkill, hasSkill, getAllSkills, listSkillIds } from './skill-registry.js';
18
+ export { generateWorkflowCode, generateNodeConfigsJson } from './code-generator.js';
19
+ export { hasAgentCall } from '../utils/ast-utils.js';
20
+
21
+ // Agent strategies (core framework capability)
22
+ export {
23
+ invokeAgent,
24
+ getAgentStrategy,
25
+ CursorAgentStrategy,
26
+ ClaudeAgentStrategy,
27
+ AgentStrategy
28
+ } from './agents/index.js';