c9ai
Version:
Universal AI assistant with vibe-based workflows, hybrid cloud+local AI, and comprehensive tool integration
720 lines (604 loc) • 22 kB
JavaScript
"use strict";
/**
* ExecutionPlanner - Optimizes action execution order and handles dependencies
* Provides intelligent scheduling and variable substitution for C9AI actions
*/
const crypto = require("node:crypto");
class ExecutionPlanner {
constructor() {
this.executionStrategies = {
'sequential': this.planSequentialExecution.bind(this),
'parallel': this.planParallelExecution.bind(this),
'optimal': this.planOptimalExecution.bind(this),
'dependency': this.planDependencyExecution.bind(this)
};
// Performance characteristics of different sigils
this.sigilCharacteristics = {
'calc': { cpu_intensive: false, io_bound: false, network: false, duration: 'short' },
'read': { cpu_intensive: false, io_bound: true, network: false, duration: 'short' },
'write': { cpu_intensive: false, io_bound: true, network: false, duration: 'short' },
'analyze': { cpu_intensive: true, io_bound: true, network: false, duration: 'medium' },
'system': { cpu_intensive: false, io_bound: false, network: false, duration: 'short' },
'count': { cpu_intensive: false, io_bound: true, network: false, duration: 'short' },
'email': { cpu_intensive: false, io_bound: false, network: true, duration: 'medium' },
'search': { cpu_intensive: false, io_bound: false, network: true, duration: 'medium' },
'github-fetch': { cpu_intensive: false, io_bound: false, network: true, duration: 'long' },
'gdrive-fetch': { cpu_intensive: false, io_bound: false, network: true, duration: 'long' },
'shell': { cpu_intensive: true, io_bound: true, network: false, duration: 'variable' },
'compile': { cpu_intensive: true, io_bound: true, network: false, duration: 'long' }
};
}
/**
* Build comprehensive execution plan for action set
*/
async buildExecutionPlan(actions, mode = 'optimal') {
const plan = {
sessionId: this.generateSessionId(),
totalActions: actions.length,
executionMode: mode,
estimatedDuration: 0,
dependencyGraph: null,
executionOrder: [],
parallelGroups: [],
variableSubstitutions: [],
resourceRequirements: {},
riskAssessment: {},
optimizations: [],
warnings: []
};
try {
// Analyze dependencies between actions
plan.dependencyGraph = this.buildDependencyGraph(actions);
// Analyze variable substitutions needed
plan.variableSubstitutions = this.analyzeVariableSubstitutions(actions);
// Calculate resource requirements
plan.resourceRequirements = this.calculateResourceRequirements(actions);
// Choose and execute planning strategy
if (this.executionStrategies[mode]) {
await this.executionStrategies[mode](actions, plan);
} else {
await this.planOptimalExecution(actions, plan);
}
// Calculate estimated duration
plan.estimatedDuration = this.calculateTotalDuration(plan.executionOrder);
// Add optimization suggestions
plan.optimizations = this.suggestOptimizations(actions, plan);
// Validate execution plan
this.validateExecutionPlan(plan);
} catch (error) {
plan.warnings.push(`Planning failed: ${error.message}`);
// Fallback to sequential execution
await this.planSequentialExecution(actions, plan);
}
return plan;
}
/**
* Build dependency graph from actions
*/
buildDependencyGraph(actions) {
const graph = {
nodes: {},
edges: [],
levels: [],
cycles: []
};
// Create nodes
for (const action of actions) {
graph.nodes[action.id] = {
id: action.id,
sigil: action.sigil,
dependencies: [],
dependents: [],
level: 0
};
}
// Find dependencies through variable references
for (const action of actions) {
const deps = this.findVariableDependencies(action, actions);
for (const depId of deps) {
if (graph.nodes[depId]) {
graph.nodes[action.id].dependencies.push(depId);
graph.nodes[depId].dependents.push(action.id);
graph.edges.push({ from: depId, to: action.id });
}
}
}
// Calculate dependency levels (topological sort)
const levels = this.calculateDependencyLevels(graph.nodes);
graph.levels = levels;
// Detect cycles
graph.cycles = this.detectCycles(graph.nodes);
return graph;
}
/**
* Find variable dependencies in action arguments
*/
findVariableDependencies(action, allActions) {
const dependencies = new Set();
const args = action.args || '';
// Look for variable references like ${action-id.result} or ${action-id.output}
const variablePattern = /\$\{([^.}]+)(?:\.[^}]+)?\}/g;
let match;
while ((match = variablePattern.exec(args)) !== null) {
const referencedId = match[1];
// Verify the referenced action exists
if (allActions.find(a => a.id === referencedId)) {
dependencies.add(referencedId);
}
}
return Array.from(dependencies);
}
/**
* Calculate dependency levels for topological ordering
*/
calculateDependencyLevels(nodes) {
const levels = [];
const visited = new Set();
const currentLevel = new Set();
// Find nodes with no dependencies (level 0)
for (const [id, node] of Object.entries(nodes)) {
if (node.dependencies.length === 0) {
node.level = 0;
currentLevel.add(id);
}
}
if (currentLevel.size === 0) {
// No root nodes - possible cycle
return [Object.keys(nodes)]; // Fallback to single level
}
levels.push(Array.from(currentLevel));
visited.add(...currentLevel);
// Calculate subsequent levels
let level = 1;
while (visited.size < Object.keys(nodes).length && level < 100) { // Prevent infinite loops
const nextLevel = new Set();
for (const [id, node] of Object.entries(nodes)) {
if (visited.has(id)) continue;
// Check if all dependencies are satisfied
const allDepsSatisfied = node.dependencies.every(depId => visited.has(depId));
if (allDepsSatisfied) {
node.level = level;
nextLevel.add(id);
}
}
if (nextLevel.size === 0) {
// No progress - remaining nodes might have cycles
const remaining = Object.keys(nodes).filter(id => !visited.has(id));
if (remaining.length > 0) {
levels.push(remaining);
break;
}
} else {
levels.push(Array.from(nextLevel));
visited.add(...nextLevel);
}
level++;
}
return levels;
}
/**
* Detect cycles in dependency graph
*/
detectCycles(nodes) {
const cycles = [];
const visited = new Set();
const recursionStack = new Set();
const dfs = (nodeId, path) => {
if (recursionStack.has(nodeId)) {
// Found cycle
const cycleStart = path.indexOf(nodeId);
cycles.push(path.slice(cycleStart));
return;
}
if (visited.has(nodeId)) return;
visited.add(nodeId);
recursionStack.add(nodeId);
path.push(nodeId);
const node = nodes[nodeId];
if (node) {
for (const depId of node.dependents) {
dfs(depId, [...path]);
}
}
recursionStack.delete(nodeId);
};
for (const nodeId of Object.keys(nodes)) {
if (!visited.has(nodeId)) {
dfs(nodeId, []);
}
}
return cycles;
}
/**
* Analyze variable substitutions needed
*/
analyzeVariableSubstitutions(actions) {
const substitutions = [];
for (const action of actions) {
const args = action.args || '';
const variablePattern = /\$\{([^.}]+)\.([^}]+)\}/g;
let match;
while ((match = variablePattern.exec(args)) !== null) {
substitutions.push({
actionId: action.id,
variableRef: match[0],
sourceActionId: match[1],
property: match[2],
position: match.index
});
}
}
return substitutions;
}
/**
* Calculate resource requirements for action set
*/
calculateResourceRequirements(actions) {
const requirements = {
cpu_intensive_count: 0,
io_bound_count: 0,
network_count: 0,
estimated_memory_mb: 0,
requires_network: false,
requires_file_access: false,
requires_system_access: false
};
for (const action of actions) {
const sigilName = action.sigil.slice(1);
const characteristics = this.sigilCharacteristics[sigilName] || {};
if (characteristics.cpu_intensive) requirements.cpu_intensive_count++;
if (characteristics.io_bound) requirements.io_bound_count++;
if (characteristics.network) {
requirements.network_count++;
requirements.requires_network = true;
}
// Estimate memory usage
switch (sigilName) {
case 'analyze':
requirements.estimated_memory_mb += 50;
requirements.requires_file_access = true;
break;
case 'compile':
requirements.estimated_memory_mb += 100;
requirements.requires_file_access = true;
break;
case 'read':
case 'write':
requirements.estimated_memory_mb += 10;
requirements.requires_file_access = true;
break;
case 'system':
case 'shell':
requirements.requires_system_access = true;
break;
default:
requirements.estimated_memory_mb += 5;
}
}
return requirements;
}
/**
* Plan sequential execution (safe fallback)
*/
async planSequentialExecution(actions, plan) {
plan.executionMode = 'sequential';
plan.executionOrder = actions.map((action, index) => ({
position: index + 1,
actionId: action.id,
sigil: action.sigil,
description: action.description,
waitFor: index > 0 ? [actions[index - 1].id] : []
}));
plan.parallelGroups = []; // No parallel execution
}
/**
* Plan parallel execution where possible
*/
async planParallelExecution(actions, plan) {
plan.executionMode = 'parallel';
if (!plan.dependencyGraph || plan.dependencyGraph.cycles.length > 0) {
// Fall back to sequential if dependencies are complex
return this.planSequentialExecution(actions, plan);
}
// Group actions by dependency level for parallel execution
const parallelGroups = [];
let position = 1;
for (const levelActions of plan.dependencyGraph.levels) {
const group = {
groupId: parallelGroups.length + 1,
actions: levelActions.map(actionId => {
const action = actions.find(a => a.id === actionId);
return {
position: position++,
actionId: actionId,
sigil: action?.sigil,
description: action?.description
};
}),
canRunInParallel: true
};
parallelGroups.push(group);
}
plan.parallelGroups = parallelGroups;
// Flatten to execution order
plan.executionOrder = parallelGroups.flatMap(group => group.actions);
}
/**
* Plan optimal execution considering performance characteristics
*/
async planOptimalExecution(actions, plan) {
plan.executionMode = 'optimal';
// If there are dependencies, use dependency-based planning
if (plan.dependencyGraph && plan.dependencyGraph.edges.length > 0) {
return this.planDependencyExecution(actions, plan);
}
// Group actions by performance characteristics for optimal scheduling
const groups = {
fast_local: [], // calc, system, etc.
io_operations: [], // read, write, analyze
network_operations: [], // email, search, fetch
cpu_intensive: [] // compile, complex analysis
};
for (const action of actions) {
const sigilName = action.sigil.slice(1);
const chars = this.sigilCharacteristics[sigilName] || {};
if (chars.network) {
groups.network_operations.push(action);
} else if (chars.cpu_intensive) {
groups.cpu_intensive.push(action);
} else if (chars.io_bound) {
groups.io_operations.push(action);
} else {
groups.fast_local.push(action);
}
}
// Optimal order: fast operations first, then I/O, then CPU intensive, then network
const optimalOrder = [
...groups.fast_local,
...groups.io_operations,
...groups.cpu_intensive,
...groups.network_operations
];
let position = 1;
plan.executionOrder = optimalOrder.map(action => ({
position: position++,
actionId: action.id,
sigil: action.sigil,
description: action.description,
optimizationReason: this.getOptimizationReason(action)
}));
// Create parallel groups for network operations (can often run concurrently)
if (groups.network_operations.length > 1) {
plan.parallelGroups = [{
groupId: 1,
actions: groups.network_operations.map(action => ({
actionId: action.id,
sigil: action.sigil,
description: action.description
})),
canRunInParallel: true,
reason: 'Network operations can run concurrently'
}];
}
}
/**
* Plan execution considering dependencies
*/
async planDependencyExecution(actions, plan) {
plan.executionMode = 'dependency';
if (plan.dependencyGraph.cycles.length > 0) {
plan.warnings.push('Circular dependencies detected - using sequential execution');
return this.planSequentialExecution(actions, plan);
}
// Use topological ordering from dependency graph
const parallelGroups = [];
let position = 1;
for (const levelActions of plan.dependencyGraph.levels) {
// Within each level, optimize by performance characteristics
const levelActionObjects = levelActions.map(id => actions.find(a => a.id === id));
const optimizedOrder = this.optimizeWithinLevel(levelActionObjects);
const group = {
groupId: parallelGroups.length + 1,
actions: optimizedOrder.map(action => ({
position: position++,
actionId: action.id,
sigil: action.sigil,
description: action.description
})),
canRunInParallel: levelActions.length > 1,
dependencyLevel: parallelGroups.length
};
parallelGroups.push(group);
}
plan.parallelGroups = parallelGroups;
plan.executionOrder = parallelGroups.flatMap(group => group.actions);
}
/**
* Optimize action order within a dependency level
*/
optimizeWithinLevel(actions) {
return actions.sort((a, b) => {
const aChars = this.sigilCharacteristics[a.sigil.slice(1)] || {};
const bChars = this.sigilCharacteristics[b.sigil.slice(1)] || {};
// Fast operations first
if (!aChars.cpu_intensive && !aChars.io_bound && !aChars.network) return -1;
if (!bChars.cpu_intensive && !bChars.io_bound && !bChars.network) return 1;
// Then I/O operations
if (aChars.io_bound && !bChars.io_bound) return -1;
if (bChars.io_bound && !aChars.io_bound) return 1;
// Then CPU intensive
if (aChars.cpu_intensive && !bChars.cpu_intensive) return -1;
if (bChars.cpu_intensive && !aChars.cpu_intensive) return 1;
// Network operations last
if (aChars.network && !bChars.network) return 1;
if (bChars.network && !aChars.network) return -1;
return 0;
});
}
/**
* Get optimization reason for action placement
*/
getOptimizationReason(action) {
const sigilName = action.sigil.slice(1);
const chars = this.sigilCharacteristics[sigilName] || {};
if (chars.network) return 'Network operations scheduled for parallel execution';
if (chars.cpu_intensive) return 'CPU intensive operations scheduled after I/O';
if (chars.io_bound) return 'I/O operations scheduled early';
return 'Fast local operations prioritized';
}
/**
* Calculate total estimated duration
*/
calculateTotalDuration(executionOrder) {
let totalDuration = 0;
for (const orderItem of executionOrder) {
const sigilName = orderItem.sigil?.slice(1);
const estimatedTime = this.getEstimatedDuration(sigilName);
totalDuration += estimatedTime;
}
return totalDuration;
}
/**
* Get estimated duration for sigil
*/
getEstimatedDuration(sigilName) {
const durations = {
'calc': 2,
'read': 3,
'write': 3,
'system': 2,
'count': 5,
'analyze': 15,
'email': 8,
'search': 10,
'github-fetch': 20,
'gdrive-fetch': 25,
'compile': 30,
'shell': 10
};
return durations[sigilName] || 5;
}
/**
* Suggest optimizations for the execution plan
*/
suggestOptimizations(actions, plan) {
const optimizations = [];
// Check for parallelization opportunities
if (plan.executionMode === 'sequential' && actions.length > 3) {
optimizations.push({
type: 'parallelization',
suggestion: 'Consider parallel execution for independent actions',
impact: 'Could reduce total execution time by 30-50%'
});
}
// Check for resource optimization
const networkActions = actions.filter(a =>
['email', 'search', 'github-fetch', 'gdrive-fetch'].includes(a.sigil.slice(1))
);
if (networkActions.length > 2) {
optimizations.push({
type: 'resource_optimization',
suggestion: 'Group network operations to reduce latency overhead',
impact: 'Could reduce network overhead by 20-30%'
});
}
// Check for caching opportunities
const fileOps = actions.filter(a =>
['read', 'analyze'].includes(a.sigil.slice(1))
);
if (fileOps.length > 1) {
optimizations.push({
type: 'caching',
suggestion: 'Cache file reads for multiple operations on same files',
impact: 'Could reduce I/O operations by 40-60%'
});
}
return optimizations;
}
/**
* Validate execution plan for correctness
*/
validateExecutionPlan(plan) {
const warnings = [];
// Check that all actions are included
if (plan.executionOrder.length !== plan.totalActions) {
warnings.push('Execution order missing some actions');
}
// Check for duplicate positions
const positions = plan.executionOrder.map(item => item.position);
const uniquePositions = new Set(positions);
if (positions.length !== uniquePositions.size) {
warnings.push('Duplicate positions found in execution order');
}
// Check dependency satisfaction
if (plan.dependencyGraph && plan.dependencyGraph.edges.length > 0) {
for (const edge of plan.dependencyGraph.edges) {
const fromPos = plan.executionOrder.find(item => item.actionId === edge.from)?.position;
const toPos = plan.executionOrder.find(item => item.actionId === edge.to)?.position;
if (fromPos && toPos && fromPos >= toPos) {
warnings.push(`Dependency violation: ${edge.from} should execute before ${edge.to}`);
}
}
}
plan.warnings.push(...warnings);
}
/**
* Substitute variables in action arguments
*/
async substituteVariables(action, previousResults) {
let args = action.args || '';
const variablePattern = /\$\{([^.}]+)\.([^}]+)\}/g;
let match;
const substitutions = [];
while ((match = variablePattern.exec(args)) !== null) {
const sourceActionId = match[1];
const property = match[2];
const fullMatch = match[0];
// Find result from previous execution
const sourceResult = previousResults[sourceActionId];
if (sourceResult) {
let substitutionValue = '';
// Extract the requested property
switch (property) {
case 'result':
substitutionValue = sourceResult.result || sourceResult.output || '';
break;
case 'output':
substitutionValue = sourceResult.output || sourceResult.result || '';
break;
case 'success':
substitutionValue = sourceResult.success ? 'true' : 'false';
break;
case 'error':
substitutionValue = sourceResult.error || '';
break;
default:
// Try to get property directly
substitutionValue = sourceResult[property] || '';
}
substitutions.push({
variable: fullMatch,
value: String(substitutionValue),
sourceActionId,
property
});
}
}
// Perform substitutions
for (const sub of substitutions) {
args = args.replace(sub.variable, sub.value);
}
return {
originalArgs: action.args,
substitutedArgs: args,
substitutions: substitutions
};
}
/**
* Generate unique session ID
*/
generateSessionId() {
return `plan_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
}
}
module.exports = ExecutionPlanner;