aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
638 lines (556 loc) • 18.1 kB
JavaScript
/**
* Dependency Analyzer for Synkra AIOS
* Analyzes and resolves dependencies between components
* @module dependency-analyzer
*/
const fs = require('fs-extra');
const path = require('path');
const yaml = require('js-yaml');
const chalk = require('chalk');
class DependencyAnalyzer {
constructor(options = {}) {
this.rootPath = options.rootPath || process.cwd();
this.manifestPath = path.join(this.rootPath, 'aios-core', 'team-manifest.yaml');
// Component paths
this.paths = {
agents: path.join(this.rootPath, 'aios-core', 'agents'),
tasks: path.join(this.rootPath, 'aios-core', 'tasks'),
workflows: path.join(this.rootPath, 'aios-core', 'workflows'),
};
// Dependency cache
this.dependencyCache = new Map();
}
/**
* Analyze dependencies for a component
* @param {string} componentType - Type of component (agent/task/workflow)
* @param {Object} componentData - Component configuration data
* @returns {Promise<Object>} Dependency analysis result
*/
async analyzeDependencies(componentType, componentData) {
const dependencies = {
required: [],
optional: [],
missing: [],
circular: false,
graph: new Map(),
};
switch (componentType) {
case 'agent':
await this.analyzeAgentDependencies(componentData, dependencies);
break;
case 'task':
await this.analyzeTaskDependencies(componentData, dependencies);
break;
case 'workflow':
await this.analyzeWorkflowDependencies(componentData, dependencies);
break;
}
// Check for circular dependencies
dependencies.circular = this.detectCircularDependencies(dependencies.graph);
return dependencies;
}
/**
* Analyze agent dependencies
* @private
*/
async analyzeAgentDependencies(agentData, dependencies) {
// Check for task dependencies from commands
if (agentData.commands && Array.isArray(agentData.commands)) {
for (const command of agentData.commands) {
const taskId = this.commandToTaskId(command);
const taskPath = path.join(this.paths.tasks, `${taskId}.md`);
if (await fs.pathExists(taskPath)) {
dependencies.required.push({
type: 'task',
id: taskId,
path: taskPath,
reason: `Command '${command}' requires task`,
});
} else {
dependencies.missing.push({
type: 'task',
id: taskId,
reason: `Command '${command}' requires task file`,
});
}
}
}
// Check for workflow dependencies
if (agentData.workflows && Array.isArray(agentData.workflows)) {
for (const workflowId of agentData.workflows) {
const workflowPath = path.join(this.paths.workflows, `${workflowId}.yaml`);
if (await fs.pathExists(workflowPath)) {
dependencies.optional.push({
type: 'workflow',
id: workflowId,
path: workflowPath,
reason: 'Agent workflow reference',
});
}
}
}
// Check for agent dependencies
if (agentData.dependencies?.agents) {
for (const agentId of agentData.dependencies.agents) {
const agentPath = path.join(this.paths.agents, `${agentId}.md`);
if (await fs.pathExists(agentPath)) {
dependencies.required.push({
type: 'agent',
id: agentId,
path: agentPath,
reason: 'Explicit agent dependency',
});
} else {
dependencies.missing.push({
type: 'agent',
id: agentId,
reason: 'Required agent not found',
});
}
}
}
}
/**
* Analyze task dependencies
* @private
*/
async analyzeTaskDependencies(taskData, dependencies) {
// Check for agent dependency
if (taskData.agentName) {
const agentPath = path.join(this.paths.agents, `${taskData.agentName}.md`);
if (await fs.pathExists(agentPath)) {
dependencies.required.push({
type: 'agent',
id: taskData.agentName,
path: agentPath,
reason: 'Task belongs to agent',
});
} else {
dependencies.missing.push({
type: 'agent',
id: taskData.agentName,
reason: 'Agent not found for task',
});
}
}
// Check for other task dependencies
if (taskData.dependencies?.tasks) {
for (const taskId of taskData.dependencies.tasks) {
const taskPath = path.join(this.paths.tasks, `${taskId}.md`);
if (await fs.pathExists(taskPath)) {
dependencies.required.push({
type: 'task',
id: taskId,
path: taskPath,
reason: 'Task dependency',
});
} else {
dependencies.missing.push({
type: 'task',
id: taskId,
reason: 'Required task not found',
});
}
}
}
}
/**
* Analyze workflow dependencies
* @private
*/
async analyzeWorkflowDependencies(workflowData, dependencies) {
// Extract task references from workflow steps
const taskIds = new Set();
if (workflowData.steps && Array.isArray(workflowData.steps)) {
for (const step of workflowData.steps) {
if (step.type === 'task' && step.taskId) {
taskIds.add(step.taskId);
} else if (step.action?.includes('task:')) {
const taskMatch = step.action.match(/task:([a-z0-9-]+)/);
if (taskMatch) {
taskIds.add(taskMatch[1]);
}
}
}
}
// Check each task dependency
for (const taskId of taskIds) {
const taskPath = path.join(this.paths.tasks, `${taskId}.md`);
if (await fs.pathExists(taskPath)) {
dependencies.required.push({
type: 'task',
id: taskId,
path: taskPath,
reason: 'Workflow step requires task',
});
// Also check the task's agent dependency
const taskContent = await fs.readFile(taskPath, 'utf8');
const agentMatch = taskContent.match(/\*\*Agent:\*\*\s*([a-z0-9-]+)/);
if (agentMatch) {
const agentId = agentMatch[1];
const agentPath = path.join(this.paths.agents, `${agentId}.md`);
if (await fs.pathExists(agentPath)) {
dependencies.required.push({
type: 'agent',
id: agentId,
path: agentPath,
reason: 'Task requires agent',
});
}
}
} else {
dependencies.missing.push({
type: 'task',
id: taskId,
reason: 'Workflow step requires task',
});
}
}
// Check for sub-workflow dependencies
if (workflowData.dependencies?.workflows) {
for (const workflowId of workflowData.dependencies.workflows) {
const workflowPath = path.join(this.paths.workflows, `${workflowId}.yaml`);
if (await fs.pathExists(workflowPath)) {
dependencies.required.push({
type: 'workflow',
id: workflowId,
path: workflowPath,
reason: 'Sub-workflow dependency',
});
} else {
dependencies.missing.push({
type: 'workflow',
id: workflowId,
reason: 'Required workflow not found',
});
}
}
}
}
/**
* Convert command name to task ID
* @private
*/
commandToTaskId(command) {
// Remove asterisk if present
const cleanCommand = command.replace(/^\*/, '');
// Handle common patterns
if (cleanCommand.startsWith('create-')) {
return cleanCommand;
}
// Convert to task ID format
return cleanCommand.replace(/([A-Z])/g, '-$1').toLowerCase();
}
/**
* Detect circular dependencies
* @private
*/
detectCircularDependencies(graph) {
const visited = new Set();
const recursionStack = new Set();
const hasCycle = (node, path = []) => {
if (recursionStack.has(node)) {
console.log(chalk.red(`\n⚠️ Circular dependency detected: ${[...path, node].join(' → ')}`));
return true;
}
if (visited.has(node)) {
return false;
}
visited.add(node);
recursionStack.add(node);
const neighbors = graph.get(node) || [];
for (const neighbor of neighbors) {
if (hasCycle(neighbor, [...path, node])) {
return true;
}
}
recursionStack.delete(node);
return false;
};
for (const node of graph.keys()) {
if (hasCycle(node)) {
return true;
}
}
return false;
}
/**
* Validate all dependencies exist
* @param {Array} components - Components to validate
* @returns {Promise<Object>} Validation result
*/
async validateDependencies(components) {
const results = {
valid: true,
issues: [],
resolutions: [],
};
for (const component of components) {
const deps = await this.analyzeDependencies(component.type, component.config);
if (deps.missing.length > 0) {
results.valid = false;
results.issues.push({
component: component.config.name || component.config.id,
missing: deps.missing,
});
// Suggest resolutions
for (const missing of deps.missing) {
results.resolutions.push({
action: 'create',
type: missing.type,
id: missing.id,
reason: missing.reason,
});
}
}
if (deps.circular) {
results.valid = false;
results.issues.push({
component: component.config.name || component.config.id,
issue: 'Circular dependency detected',
});
}
}
return results;
}
/**
* Get creation order for components based on dependencies
* @param {Array} components - Components to order
* @returns {Promise<Array>} Ordered components
*/
async getCreationOrder(components) {
const graph = new Map();
const inDegree = new Map();
// Initialize graph
for (const component of components) {
const id = this.getComponentId(component);
graph.set(id, []);
inDegree.set(id, 0);
}
// Build dependency graph
for (const component of components) {
const id = this.getComponentId(component);
const deps = await this.analyzeDependencies(component.type, component.config);
for (const dep of deps.required) {
const depId = `${dep.type}:${dep.id}`;
// Only add edge if dependency is in our component list
if (graph.has(depId)) {
graph.get(depId).push(id);
inDegree.set(id, inDegree.get(id) + 1);
}
}
}
// Topological sort using Kahn's algorithm
const queue = [];
const ordered = [];
// Find nodes with no dependencies
for (const [id, degree] of inDegree.entries()) {
if (degree === 0) {
queue.push(id);
}
}
while (queue.length > 0) {
const current = queue.shift();
ordered.push(current);
// Process neighbors
for (const neighbor of graph.get(current) || []) {
inDegree.set(neighbor, inDegree.get(neighbor) - 1);
if (inDegree.get(neighbor) === 0) {
queue.push(neighbor);
}
}
}
// Check for cycles
if (ordered.length !== components.length) {
throw new Error('Circular dependency detected - cannot determine creation order');
}
// Map back to components
const componentMap = new Map();
for (const component of components) {
const id = this.getComponentId(component);
componentMap.set(id, component);
}
return ordered.map(id => componentMap.get(id));
}
/**
* Get component ID for graph
* @private
*/
getComponentId(component) {
const name = component.config.agentName ||
component.config.taskId ||
component.config.workflowId ||
component.config.name ||
component.config.id;
return `${component.type}:${name}`;
}
/**
* Create missing dependencies interactively
* @param {Array} missing - Missing dependencies
* @returns {Promise<Array>} Components to create
*/
async promptForMissingDependencies(missing) {
const inquirer = require('inquirer');
const componentsToCreate = [];
console.log(chalk.yellow('\n⚠️ Missing dependencies detected:'));
for (const dep of missing) {
console.log(chalk.gray(` - ${dep.type}: ${dep.id} (${dep.reason})`));
}
const { action } = await inquirer.prompt([{
type: 'list',
name: 'action',
message: 'How would you like to handle missing dependencies?',
choices: [
{ name: 'Create all missing dependencies', value: 'create-all' },
{ name: 'Select which to create', value: 'select' },
{ name: 'Skip dependency creation', value: 'skip' },
],
}]);
if (action === 'skip') {
return [];
}
if (action === 'create-all') {
for (const dep of missing) {
componentsToCreate.push({
type: dep.type,
config: await this.getMinimalConfig(dep.type, dep.id),
});
}
} else {
// Select which to create
const { selected } = await inquirer.prompt([{
type: 'checkbox',
name: 'selected',
message: 'Select dependencies to create:',
choices: missing.map(dep => ({
name: `${dep.type}: ${dep.id}`,
value: dep,
checked: true,
})),
}]);
for (const dep of selected) {
componentsToCreate.push({
type: dep.type,
config: await this.getMinimalConfig(dep.type, dep.id),
});
}
}
return componentsToCreate;
}
/**
* Validate workflow dependencies
* @param {Object} workflowData - Workflow configuration
* @returns {Promise<Object>} Validation result
*/
async validateWorkflowDependencies(workflowData) {
const result = {
valid: true,
issues: [],
taskDependencies: [],
missingTasks: [],
};
// Extract all task references
const taskRefs = new Set();
if (workflowData.steps && Array.isArray(workflowData.steps)) {
for (const step of workflowData.steps) {
if (step.type === 'task' && step.taskId) {
taskRefs.add(step.taskId);
} else if (step.action && typeof step.action === 'string') {
// Extract task references from action strings
const taskMatches = step.action.match(/task:([a-z0-9-]+)/g);
if (taskMatches) {
taskMatches.forEach(match => {
const taskId = match.replace('task:', '');
taskRefs.add(taskId);
});
}
}
// Check step dependencies
if (step.dependencies && Array.isArray(step.dependencies)) {
for (const depId of step.dependencies) {
if (!workflowData.steps.find(s => s.id === depId)) {
result.valid = false;
result.issues.push({
step: step.id || step.name,
issue: `References non-existent step: ${depId}`,
});
}
}
}
}
}
// Validate each task reference
for (const taskId of taskRefs) {
const taskPath = path.join(this.paths.tasks, `${taskId}.md`);
if (await fs.pathExists(taskPath)) {
result.taskDependencies.push({
taskId,
path: taskPath,
exists: true,
});
} else {
result.valid = false;
result.missingTasks.push(taskId);
}
}
// Check for circular step dependencies
if (workflowData.steps) {
const stepGraph = new Map();
for (const step of workflowData.steps) {
const stepId = step.id || step.name;
const deps = step.dependencies || [];
stepGraph.set(stepId, deps);
}
if (this.detectCircularDependencies(stepGraph)) {
result.valid = false;
result.issues.push({
issue: 'Circular dependency detected in workflow steps',
});
}
}
return result;
}
/**
* Get minimal config for dependency creation
* @private
*/
async getMinimalConfig(type, id) {
const inquirer = require('inquirer');
switch (type) {
case 'agent':
const { agentTitle } = await inquirer.prompt([{
type: 'input',
name: 'agentTitle',
message: `Title for agent '${id}':`,
default: id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
}]);
return {
agentName: id,
agentTitle,
whenToUse: `Dependency for ${id}`,
commands: [],
};
case 'task':
const { taskTitle } = await inquirer.prompt([{
type: 'input',
name: 'taskTitle',
message: `Title for task '${id}':`,
default: id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
}]);
return {
taskId: id,
taskTitle,
taskDescription: `Dependency task for ${id}`,
agentName: 'aios-developer',
};
case 'workflow':
return {
workflowId: id,
workflowName: id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
workflowType: 'standard',
steps: [],
};
}
}
}
module.exports = DependencyAnalyzer;