@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,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';
|