@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,163 @@
|
|
|
1
|
+
|
|
2
|
+
const registry = new Map();
|
|
3
|
+
|
|
4
|
+
export function registerNode(type, impl) {
|
|
5
|
+
registry.set(type, impl);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getNodeImpl(type) {
|
|
9
|
+
return registry.get(type);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function hasNode(type) {
|
|
13
|
+
return registry.has(type);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function listNodeTypes() {
|
|
17
|
+
return Array.from(registry.keys());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getNodeTemplate(type) {
|
|
21
|
+
const impl = registry.get(type);
|
|
22
|
+
if (!impl) return null;
|
|
23
|
+
|
|
24
|
+
if (impl.factory && typeof impl.create === 'function') {
|
|
25
|
+
return impl.create.toString();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof impl.execute === 'function') {
|
|
29
|
+
return impl.execute.toString();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof impl === 'function') {
|
|
33
|
+
return impl.toString();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ------------------------------------------------------------------
|
|
40
|
+
// Dynamic / factory nodes (framework-provided)
|
|
41
|
+
// ------------------------------------------------------------------
|
|
42
|
+
registerNode('ai_agent', {
|
|
43
|
+
name: 'ai_agent',
|
|
44
|
+
factory: true,
|
|
45
|
+
create: (nodeId, nodeConfig = {}) => ({
|
|
46
|
+
name: nodeId,
|
|
47
|
+
execute: async (state) => {
|
|
48
|
+
const { invokeAgent } = await import('./agents/index.js');
|
|
49
|
+
|
|
50
|
+
const prompt = nodeConfig.extraPromptInstructions || 'Execute the task based on the current state.';
|
|
51
|
+
const fullPrompt = buildAIAgentPrompt(prompt, state);
|
|
52
|
+
|
|
53
|
+
const result = await invokeAgent(fullPrompt, {
|
|
54
|
+
cwd: state.workspace || process.cwd(),
|
|
55
|
+
model: state.model,
|
|
56
|
+
tools: nodeConfig.resolvedTools || null
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
output: { raw: result, nodeId },
|
|
62
|
+
raw: typeof result === 'string' ? result : result.raw
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse @variable references from prompt text
|
|
70
|
+
* @param {string} prompt - The prompt containing @references
|
|
71
|
+
* @returns {string[]} Array of unique variable paths (e.g., ["context.summary", "previous_node_output"])
|
|
72
|
+
*/
|
|
73
|
+
function parseVariableReferences(prompt) {
|
|
74
|
+
const refRegex = /@([\w.]+)/g;
|
|
75
|
+
const refs = new Set();
|
|
76
|
+
let match;
|
|
77
|
+
while ((match = refRegex.exec(prompt)) !== null) {
|
|
78
|
+
refs.add(match[1]);
|
|
79
|
+
}
|
|
80
|
+
return Array.from(refs);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get a value from state using dot notation path
|
|
85
|
+
* @param {Object} state - The state object
|
|
86
|
+
* @param {string} path - Dot-notation path (e.g., "context.summary")
|
|
87
|
+
* @returns {*} The value at the path, or undefined if not found
|
|
88
|
+
*/
|
|
89
|
+
function getStateValue(state, path) {
|
|
90
|
+
const parts = path.split('.');
|
|
91
|
+
let value = state;
|
|
92
|
+
for (const part of parts) {
|
|
93
|
+
if (value == null) return undefined;
|
|
94
|
+
value = value[part];
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Format a value for inclusion in the prompt
|
|
101
|
+
* @param {*} value - The value to format
|
|
102
|
+
* @returns {string} Formatted string representation
|
|
103
|
+
*/
|
|
104
|
+
function formatValueForPrompt(value) {
|
|
105
|
+
if (value === undefined || value === null) {
|
|
106
|
+
return '[not available]';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (typeof value === 'string') {
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (typeof value === 'object') {
|
|
114
|
+
if (value.raw && typeof value.raw === 'string') {
|
|
115
|
+
return value.raw;
|
|
116
|
+
}
|
|
117
|
+
return JSON.stringify(value, null, 2);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return String(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build AI agent prompt with explicit variable references
|
|
125
|
+
* Only includes context that is explicitly referenced with @variable syntax
|
|
126
|
+
* @param {string} basePrompt - The user's prompt with @references
|
|
127
|
+
* @param {Object} state - The workflow state
|
|
128
|
+
* @returns {string} The full prompt with referenced context appended
|
|
129
|
+
*/
|
|
130
|
+
function buildAIAgentPrompt(basePrompt, state) {
|
|
131
|
+
const refs = parseVariableReferences(basePrompt);
|
|
132
|
+
|
|
133
|
+
if (refs.length === 0) {
|
|
134
|
+
return basePrompt;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const contextParts = [];
|
|
138
|
+
const processedRefs = new Set();
|
|
139
|
+
|
|
140
|
+
for (const ref of refs) {
|
|
141
|
+
const rootPath = ref.split('.')[0];
|
|
142
|
+
if (processedRefs.has(rootPath)) continue;
|
|
143
|
+
|
|
144
|
+
const value = getStateValue(state, ref);
|
|
145
|
+
if (value !== undefined) {
|
|
146
|
+
const formatted = formatValueForPrompt(value);
|
|
147
|
+
|
|
148
|
+
const label = ref.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
149
|
+
|
|
150
|
+
contextParts.push(`## ${label}\n${formatted}`);
|
|
151
|
+
|
|
152
|
+
if (!ref.includes('.')) {
|
|
153
|
+
processedRefs.add(rootPath);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (contextParts.length === 0) {
|
|
159
|
+
return basePrompt;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return `${basePrompt}\n\n---\n# Referenced Context\n\n${contextParts.join('\n\n')}`;
|
|
163
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Node class for workflow graph
|
|
3
|
+
* Each node = one agent execution with a specific prompt
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { OutputParser } from './output-parser.js';
|
|
7
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join, dirname } from 'path';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { DEFAULT_MODELS } from '../constants.js';
|
|
11
|
+
import { invokeAgent } from './agents/index.js';
|
|
12
|
+
import { logger } from '../utils/logger.js';
|
|
13
|
+
import { timeline } from '../utils/timeline.js';
|
|
14
|
+
import { SESSION_INFO_FILE } from './constants.js';
|
|
15
|
+
|
|
16
|
+
export class Node {
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.config = config; // Store full config for timeout access
|
|
19
|
+
this.name = config.name;
|
|
20
|
+
this.prompt = config.prompt;
|
|
21
|
+
this.outputSchema = config.outputSchema;
|
|
22
|
+
|
|
23
|
+
// Enforce outputSchema requirement (allow both Zod schemas and JSON Schema from runtime compilation)
|
|
24
|
+
if (!this.outputSchema) {
|
|
25
|
+
// Only enforce for non-custom code (registry nodes must have schema)
|
|
26
|
+
if (!config._isCustomCode) {
|
|
27
|
+
throw new Error(`Node '${this.name}' must define outputSchema (Zod schema). This defines the contract for what the node returns to state.`);
|
|
28
|
+
}
|
|
29
|
+
// Custom code from graph-compiler gets a pass (already validated in UI)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.isZodSchema = this.outputSchema && typeof this.outputSchema._def !== 'undefined';
|
|
33
|
+
this.parser = config.outputSchema && !this.isZodSchema ? new OutputParser(config.outputSchema) : null;
|
|
34
|
+
this.retries = config.retries || 0;
|
|
35
|
+
this.onComplete = config.onComplete;
|
|
36
|
+
this.customExecute = config.execute;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async execute(context, state) {
|
|
40
|
+
const _agent = context?.agent || context;
|
|
41
|
+
|
|
42
|
+
// Helpers to get state values - works with both old and new call patterns
|
|
43
|
+
// context may be: 1) nodeContext object from unified graph.run, or 2) agent from old calls
|
|
44
|
+
// state may be: 1) WorkflowState instance, or 2) undefined (for inline test nodes)
|
|
45
|
+
const getAllState = () => {
|
|
46
|
+
if (state && typeof state.getAll === 'function') {
|
|
47
|
+
return state.getAll();
|
|
48
|
+
}
|
|
49
|
+
return context;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const _getState = (key) => {
|
|
53
|
+
if (state && typeof state.get === 'function') {
|
|
54
|
+
return state.get(key);
|
|
55
|
+
}
|
|
56
|
+
return context?.[key];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (typeof this.customExecute === 'function') {
|
|
60
|
+
logger.info(`⚡ Using custom execute method (skipping LLM)`);
|
|
61
|
+
try {
|
|
62
|
+
const result = await this.customExecute(context);
|
|
63
|
+
|
|
64
|
+
// Propagate inner failure (e.g. from wrapCustomCode's catch)
|
|
65
|
+
if (typeof result === 'object' && result !== null && result.success === false) {
|
|
66
|
+
return { success: false, error: result.error || 'Node execution failed', raw: result.raw || null };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (this.isZodSchema) {
|
|
70
|
+
logger.debug(`Validating return value against outputSchema...`);
|
|
71
|
+
const validated = this.outputSchema.parse(result);
|
|
72
|
+
return { success: true, output: validated, raw: null };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { success: true, output: result, raw: null };
|
|
76
|
+
} catch (error) {
|
|
77
|
+
logger.error(`❌ Node '${this.name}' execution failed: ${error.message}`);
|
|
78
|
+
if (error.name === 'ZodError') {
|
|
79
|
+
logger.error(`Schema validation errors: ${JSON.stringify(error.errors, null, 2)}`);
|
|
80
|
+
}
|
|
81
|
+
return { success: false, error: error.message, raw: null };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let prompt = typeof this.prompt === 'function'
|
|
86
|
+
? this.prompt(getAllState())
|
|
87
|
+
: this.prompt;
|
|
88
|
+
|
|
89
|
+
const skillHints = _getState('_skillHints');
|
|
90
|
+
if (skillHints) {
|
|
91
|
+
prompt = `${skillHints}\n\n${prompt}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const allState = getAllState();
|
|
95
|
+
const cwd = allState.cwd || process.cwd();
|
|
96
|
+
const sessionPath = allState.sessionPath;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
if (sessionPath) {
|
|
100
|
+
const sessionInfoPath = join(sessionPath, '..', SESSION_INFO_FILE);
|
|
101
|
+
if (existsSync(sessionInfoPath)) {
|
|
102
|
+
const sessionInfo = JSON.parse(readFileSync(sessionInfoPath, 'utf-8'));
|
|
103
|
+
sessionInfo.currentNode = this.name;
|
|
104
|
+
writeFileSync(sessionInfoPath, JSON.stringify(sessionInfo, null, 2), 'utf-8');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
logger.debug(`Could not update session info: ${err.message}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(`\n${chalk.bold('Prompt sent to LLM:')}`);
|
|
112
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
113
|
+
console.log(chalk.dim(prompt));
|
|
114
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
115
|
+
|
|
116
|
+
let lastError = null;
|
|
117
|
+
for (let attempt = 0; attempt <= this.retries; attempt++) {
|
|
118
|
+
try {
|
|
119
|
+
logger.debug(`Node.execute attempt ${attempt} for '${this.name}'`);
|
|
120
|
+
|
|
121
|
+
const _timeout = this.config?.timeout || 300000;
|
|
122
|
+
|
|
123
|
+
// Model resolution priority:
|
|
124
|
+
// 1. Node-specific override: config.models[nodeName]
|
|
125
|
+
// 2. Global default: config.models.default
|
|
126
|
+
// 3. Agent-specific: config.agent.claude.model or config.agent.cursor.model
|
|
127
|
+
// 4. Fallback: DEFAULT_MODELS.CLAUDE or 'auto'
|
|
128
|
+
const currentAllState = getAllState();
|
|
129
|
+
const zibbyConfig = currentAllState.config || {};
|
|
130
|
+
const modelsConfig = zibbyConfig.models || {};
|
|
131
|
+
|
|
132
|
+
// Infer provider from agent config (claude/cursor key presence)
|
|
133
|
+
const inferProvider = (agentConfig) => {
|
|
134
|
+
if (agentConfig?.provider) return agentConfig.provider;
|
|
135
|
+
if (agentConfig?.claude) return 'claude';
|
|
136
|
+
if (agentConfig?.cursor) return 'cursor';
|
|
137
|
+
return 'unknown';
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const agentType = inferProvider(zibbyConfig?.agent) || currentAllState.agentType || 'unknown';
|
|
141
|
+
const agentModel = agentType && zibbyConfig.agent?.[agentType]?.model;
|
|
142
|
+
|
|
143
|
+
let model = modelsConfig[this.name] || modelsConfig.default || null;
|
|
144
|
+
|
|
145
|
+
if (!model || model === 'auto') {
|
|
146
|
+
model = agentModel || null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!model) {
|
|
150
|
+
model = agentType === 'claude' ? DEFAULT_MODELS.CLAUDE : 'auto';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const agentDisplay = agentType === 'claude' ? 'Claude (Anthropic API)' : agentType === 'cursor' ? 'Cursor (CLI)' : agentType;
|
|
154
|
+
|
|
155
|
+
{
|
|
156
|
+
const displayModel = (model && model !== 'auto') ? model : (agentType === 'claude' ? DEFAULT_MODELS.CLAUDE : 'agent default');
|
|
157
|
+
const skills = this.config.skills || [];
|
|
158
|
+
const skillsSuffix = skills.length > 0 ? ` → skills: [${skills.join(', ')}]` : '';
|
|
159
|
+
timeline.step(`${agentDisplay} (${displayModel})${skillsSuffix}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const agentContext = {
|
|
163
|
+
state: getAllState()
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const agentOptions = {
|
|
167
|
+
model,
|
|
168
|
+
workspace: cwd,
|
|
169
|
+
schema: this.isZodSchema ? this.outputSchema : null,
|
|
170
|
+
skills: this.config.skills || null,
|
|
171
|
+
sessionPath,
|
|
172
|
+
config: zibbyConfig
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const result = await invokeAgent(prompt, agentContext, agentOptions);
|
|
176
|
+
|
|
177
|
+
let rawOutput, extractedJson;
|
|
178
|
+
|
|
179
|
+
if (typeof result === 'string') {
|
|
180
|
+
rawOutput = result;
|
|
181
|
+
extractedJson = null;
|
|
182
|
+
} else if (result.structured) {
|
|
183
|
+
rawOutput = result.raw || JSON.stringify(result.structured, null, 2);
|
|
184
|
+
extractedJson = result.structured;
|
|
185
|
+
} else {
|
|
186
|
+
rawOutput = result.raw || JSON.stringify(result, null, 2);
|
|
187
|
+
extractedJson = result.extracted || null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (sessionPath) {
|
|
191
|
+
try {
|
|
192
|
+
const debugPath = join(sessionPath, this.name, 'raw_stream_output.txt');
|
|
193
|
+
mkdirSync(dirname(debugPath), { recursive: true });
|
|
194
|
+
writeFileSync(debugPath, typeof rawOutput === 'string' ? rawOutput : JSON.stringify(rawOutput), 'utf-8');
|
|
195
|
+
} catch (err) {
|
|
196
|
+
logger.debug(`Could not save raw output: ${err.message}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (this.isZodSchema && extractedJson) {
|
|
201
|
+
console.log(`\n🔍 ${chalk.cyan('Validated output:')} ${chalk.white(JSON.stringify(extractedJson, null, 2))}`);
|
|
202
|
+
|
|
203
|
+
let finalOutput = extractedJson;
|
|
204
|
+
if (typeof this.onComplete === 'function') {
|
|
205
|
+
try {
|
|
206
|
+
finalOutput = await this.onComplete(getAllState(), extractedJson);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
logger.warn(`onComplete hook failed: ${err.message}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { success: true, output: finalOutput, raw: rawOutput };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (typeof this.onComplete === 'function') {
|
|
216
|
+
try {
|
|
217
|
+
const onCompleteResult = await this.onComplete(getAllState(), { raw: rawOutput });
|
|
218
|
+
return { success: true, output: onCompleteResult, raw: rawOutput };
|
|
219
|
+
} catch (err) {
|
|
220
|
+
throw new Error(`onComplete failed: ${err.message}`, { cause: err });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (this.parser) {
|
|
225
|
+
const parsed = this.parser.parse(rawOutput);
|
|
226
|
+
console.log(`\n🔍 ${chalk.cyan('Parsed output:')} ${chalk.white(JSON.stringify(parsed, null, 2))}`);
|
|
227
|
+
timeline.step('Output parsed');
|
|
228
|
+
return { success: true, output: parsed, raw: rawOutput };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { success: true, output: rawOutput, raw: rawOutput };
|
|
232
|
+
} catch (error) {
|
|
233
|
+
lastError = error;
|
|
234
|
+
if (attempt < this.retries) {
|
|
235
|
+
logger.info(`Node '${this.name}' failed, retrying (${attempt + 1}/${this.retries})...`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { success: false, error: lastError.message, raw: null };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export class ConditionalNode extends Node {
|
|
246
|
+
constructor(config) {
|
|
247
|
+
super({ ...config, _isCustomCode: true });
|
|
248
|
+
this.condition = config.condition;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async execute(context, state) {
|
|
252
|
+
const stateValues = (state && typeof state.getAll === 'function')
|
|
253
|
+
? state.getAll()
|
|
254
|
+
: context;
|
|
255
|
+
const nextNode = this.condition(stateValues);
|
|
256
|
+
return { success: true, output: { nextNode }, raw: null };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured output parsing with validation
|
|
3
|
+
* Similar to LangChain's StructuredOutputParser
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class OutputParser {
|
|
7
|
+
constructor(schema) {
|
|
8
|
+
this.schema = schema;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
parse(text) {
|
|
12
|
+
// Try to extract JSON from markdown code blocks
|
|
13
|
+
const jsonMatch = text.match(/```json\s*([\s\S]*?)\s*```/);
|
|
14
|
+
if (jsonMatch) {
|
|
15
|
+
return this.validate(JSON.parse(jsonMatch[1]));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Try to find JSON object in text
|
|
19
|
+
const objectMatch = text.match(/\{[\s\S]*\}/);
|
|
20
|
+
if (objectMatch) {
|
|
21
|
+
return this.validate(JSON.parse(objectMatch[0]));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Fallback: return raw text if no JSON found
|
|
25
|
+
return this.validate({ result: text.trim() });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
validate(data) {
|
|
29
|
+
const errors = [];
|
|
30
|
+
|
|
31
|
+
for (const [key, validator] of Object.entries(this.schema)) {
|
|
32
|
+
if (validator.required && !(key in data)) {
|
|
33
|
+
errors.push(`Missing required field: ${key}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (key in data && validator.type) {
|
|
37
|
+
const actualType = typeof data[key];
|
|
38
|
+
if (actualType !== validator.type) {
|
|
39
|
+
errors.push(`Field '${key}' expected ${validator.type}, got ${actualType}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (validator.validate && key in data) {
|
|
44
|
+
const validationError = validator.validate(data[key]);
|
|
45
|
+
if (validationError) {
|
|
46
|
+
errors.push(`Field '${key}': ${validationError}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (errors.length > 0) {
|
|
52
|
+
throw new Error(`Output validation failed:\n${errors.join('\n')}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Helper to create common schemas
|
|
60
|
+
export const SchemaTypes = {
|
|
61
|
+
string: (required = true) => ({ type: 'string', required }),
|
|
62
|
+
number: (required = true) => ({ type: 'number', required }),
|
|
63
|
+
boolean: (required = true) => ({ type: 'boolean', required }),
|
|
64
|
+
array: (required = true) => ({ type: 'object', required, validate: (v) => Array.isArray(v) ? null : 'must be an array' }),
|
|
65
|
+
enum: (values, required = true) => ({
|
|
66
|
+
type: 'string',
|
|
67
|
+
required,
|
|
68
|
+
validate: (v) => values.includes(v) ? null : `must be one of: ${values.join(', ')}`
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Registry
|
|
3
|
+
*
|
|
4
|
+
* Central registry for skill definitions. A skill describes an MCP server
|
|
5
|
+
* and the tool schemas it exposes. The framework resolves skills generically
|
|
6
|
+
* at runtime -- no skill is referenced by name in agent strategies.
|
|
7
|
+
*
|
|
8
|
+
* Built-in skills are registered by importing @zibby/skills (side-effect).
|
|
9
|
+
* Community / user skills call registerSkill() directly.
|
|
10
|
+
*
|
|
11
|
+
* See @zibby/skills/README.md for the full skill authoring guide.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const _registry = new Map();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register a skill definition.
|
|
18
|
+
* @param {Object} skill
|
|
19
|
+
* @param {string} skill.id - Unique identifier (used in node `skills` arrays)
|
|
20
|
+
* @param {'mcp'|'function'} [skill.type] - Skill type: 'mcp' (default) or 'function' (auto-bridged)
|
|
21
|
+
* @param {string} skill.serverName - MCP server name (key in mcpServers config)
|
|
22
|
+
* @param {string[]} skill.allowedTools - Tool patterns for Claude SDK (e.g. ['mcp__playwright__*'])
|
|
23
|
+
* @param {Function} [skill.resolve] - (options?) => { command, args, env? } | null
|
|
24
|
+
* @param {string[]} [skill.envKeys] - Required environment variable names
|
|
25
|
+
* @param {string} [skill.description] - Human-readable description
|
|
26
|
+
* @param {Object[]} [skill.tools] - Tool schemas for compile-time validation
|
|
27
|
+
* @param {string} [skill.cursorKey] - Override key for ~/.cursor/mcp.json
|
|
28
|
+
* @param {string} [skill.sessionEnvKey] - Env var injected with session info path (Cursor only)
|
|
29
|
+
*/
|
|
30
|
+
export function registerSkill(skill) {
|
|
31
|
+
if (!skill || typeof skill.id !== 'string') {
|
|
32
|
+
throw new Error('Skill definition must include a string id');
|
|
33
|
+
}
|
|
34
|
+
_registry.set(skill.id, Object.freeze({ ...skill }));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @returns {object|null} */
|
|
38
|
+
export function getSkill(id) {
|
|
39
|
+
return _registry.get(id) || null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @returns {boolean} */
|
|
43
|
+
export function hasSkill(id) {
|
|
44
|
+
return _registry.has(id);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @returns {Map<string, object>} shallow copy */
|
|
48
|
+
export function getAllSkills() {
|
|
49
|
+
return new Map(_registry);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @returns {string[]} */
|
|
53
|
+
export function listSkillIds() {
|
|
54
|
+
return Array.from(_registry.keys());
|
|
55
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Schema Utility Functions
|
|
3
|
+
* Helper functions for working with Zod state schemas
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get JSON Schema for frontend/API docs
|
|
10
|
+
* @param {import('zod').ZodSchema} zodSchema - Zod schema to convert
|
|
11
|
+
* @returns {Object} JSON Schema in OpenAPI 3 format
|
|
12
|
+
*/
|
|
13
|
+
export function getJsonSchema(zodSchema) {
|
|
14
|
+
if (!zodSchema) return null;
|
|
15
|
+
return zodToJsonSchema(zodSchema, { target: 'openApi3' });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Convert Zod schema to Variable Inspector format
|
|
20
|
+
* @param {import('zod').ZodSchema} zodSchema - Zod schema to convert
|
|
21
|
+
* @param {string} prefix - Optional prefix for nested paths
|
|
22
|
+
* @returns {Array} Flattened variable definitions
|
|
23
|
+
*/
|
|
24
|
+
export function schemaToVariables(zodSchema, prefix = '') {
|
|
25
|
+
const jsonSchema = zodToJsonSchema(zodSchema);
|
|
26
|
+
return flattenJsonSchema(jsonSchema, prefix);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function flattenJsonSchema(schema, prefix = '') {
|
|
30
|
+
const variables = [];
|
|
31
|
+
|
|
32
|
+
if (schema.type === 'object' && schema.properties) {
|
|
33
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
34
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
35
|
+
const isRequired = schema.required?.includes(key) ?? false;
|
|
36
|
+
|
|
37
|
+
variables.push({
|
|
38
|
+
name: key,
|
|
39
|
+
path,
|
|
40
|
+
type: propSchema.type || 'unknown',
|
|
41
|
+
description: propSchema.description,
|
|
42
|
+
optional: !isRequired,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (propSchema.type === 'object' && propSchema.properties) {
|
|
46
|
+
variables.push(...flattenJsonSchema(propSchema, path));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return variables;
|
|
52
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State management for agent workflow graphs
|
|
3
|
+
* Similar to LangGraph's state annotation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
7
|
+
|
|
8
|
+
function assertSafeKey(key) {
|
|
9
|
+
if (UNSAFE_KEYS.has(key)) {
|
|
10
|
+
throw new Error(`Invalid state key: "${key}"`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class WorkflowState {
|
|
15
|
+
constructor(initialState = {}) {
|
|
16
|
+
this._state = Object.create(null);
|
|
17
|
+
Object.assign(this._state, {
|
|
18
|
+
messages: [],
|
|
19
|
+
errors: [],
|
|
20
|
+
artifacts: {},
|
|
21
|
+
metadata: {},
|
|
22
|
+
...initialState
|
|
23
|
+
});
|
|
24
|
+
this._history = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get(key) {
|
|
28
|
+
return this._state[key];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
set(key, value) {
|
|
32
|
+
assertSafeKey(key);
|
|
33
|
+
this._history.push({ ...this._state });
|
|
34
|
+
this._state[key] = value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
update(updates) {
|
|
38
|
+
const keys = Object.getOwnPropertyNames(updates);
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
assertSafeKey(key);
|
|
41
|
+
}
|
|
42
|
+
this._history.push({ ...this._state });
|
|
43
|
+
for (const key of keys) {
|
|
44
|
+
this._state[key] = updates[key];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
append(key, value) {
|
|
49
|
+
assertSafeKey(key);
|
|
50
|
+
this._history.push({ ...this._state });
|
|
51
|
+
if (!Array.isArray(this._state[key])) {
|
|
52
|
+
this._state[key] = [];
|
|
53
|
+
}
|
|
54
|
+
this._state[key].push(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getAll() {
|
|
58
|
+
return { ...this._state };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
rollback() {
|
|
62
|
+
if (this._history.length > 0) {
|
|
63
|
+
this._state = this._history.pop();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|