UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

546 lines (545 loc) 20.2 kB
/** * Agent Spawner Module * * High-level API for spawning agents with full type safety, * environment variable handling, provider configuration parsing, * and Redis coordination integration. * * Usage: * const spawner = new AgentSpawner(); * const result = await spawner.spawnAgent({ * agentType: 'backend-developer', * taskId: 'task-123', * iteration: 1, * mode: 'standard' * }); */ import { existsSync, readFileSync } from 'fs'; import { resolve } from 'path'; import { spawn as childSpawn } from 'child_process'; import { v4 as uuidv4 } from 'uuid'; import { getEnvValue, getNetworkName } from '../lib/environment-contract'; /** * AgentSpawner class - Type-safe agent spawning with validation */ export class AgentSpawner { projectRoot; agentProfilesDir; agentConfigFile; constructor(projectRoot){ this.projectRoot = projectRoot || process.cwd(); this.agentProfilesDir = resolve(this.projectRoot, '.claude/agents/cfn-dev-team'); this.agentConfigFile = resolve(this.projectRoot, '.claude/cfn-config/team-providers.json'); } /** * Spawn an agent with the given configuration */ async spawnAgent(config) { const timestamp = new Date().toISOString(); const agentId = this.generateAgentId(config.agentType); try { // Validate configuration this.validateSpawnConfig(config); // Check agent exists const agentExists = await this.validateAgentExists(config.agentType); if (!agentExists) { return { agentId, pid: -1, status: 'failed', timestamp, error: `Agent type not found: ${config.agentType}` }; } // Parse provider configuration if not explicitly provided let provider = config.provider; let model = config.model; if (!provider || !model) { const providerConfig = await this.parseAgentProvider(config.agentType); provider = provider || providerConfig.provider; model = model || providerConfig.model; } // Build environment variables const env = this.buildEnvironment(config, agentId, provider, model); // Spawn the process const pid = await this.spawnProcess(config.agentType, env, config.background); return { agentId, pid, status: 'spawned', timestamp, metadata: { agentType: config.agentType, taskId: config.taskId, iteration: config.iteration, mode: config.mode, provider, model } }; } catch (error) { return { agentId, pid: -1, status: 'failed', timestamp, error: error instanceof Error ? error.message : String(error) }; } } /** * Spawn a worker with team configuration */ async spawnWorker(config) { const timestamp = new Date().toISOString(); const workerId = this.generateWorkerId(config.team); try { // Validate worker configuration this.validateWorkerConfig(config); // Load team provider configuration const teamConfig = this.loadTeamConfig(config.team); // Select model based on complexity const model = this.selectModel(config.team, config.complexity); // Get API key from environment const apiKey = this.getApiKey(config.team, 'workers'); // Build environment for worker const env = this.buildWorkerEnvironment(config, workerId, model, apiKey); // Provider routing this.routeWorkerProvider(config.providerMode, config.team, model, apiKey); return { agentId: workerId, pid: 0, status: 'spawned', timestamp, metadata: { team: config.team, complexity: config.complexity, model, provider: config.providerMode } }; } catch (error) { return { agentId: workerId, pid: -1, status: 'failed', timestamp, error: error instanceof Error ? error.message : String(error) }; } } /** * Validate that an agent type exists in the profiles directory */ async validateAgentExists(agentType) { // Normalize agent type (handle both underscore and hyphen variations) const normalized = agentType.replace(/_/g, '-').toLowerCase(); // Check for agent profile in subdirectories const possiblePaths = [ resolve(this.agentProfilesDir, `${normalized}.md`), // Check in subdirectories ...this.findAgentInSubdirs(normalized) ]; return possiblePaths.some((path)=>existsSync(path)); } /** * Parse provider configuration from agent profile frontmatter */ async parseAgentProvider(agentType) { const agentPath = this.findAgentProfile(agentType); if (!agentPath) { return { provider: 'zai', model: 'glm-4.6' }; } try { const content = readFileSync(agentPath, 'utf-8'); const providerMatch = content.match(/<!-- PROVIDER_PARAMETERS\s*([\s\S]*?)\s*-->/); if (providerMatch) { const params = providerMatch[1]; const providerMatch_ = params.match(/provider:\s*(\w+)/); const modelMatch = params.match(/model:\s*([^\n]+)/); return { provider: providerMatch_?.[1] || 'zai', model: modelMatch?.[1]?.trim() || 'glm-4.6' }; } } catch (error) { // Silent fallback } return { provider: 'zai', model: 'glm-4.6' }; } /** * Generate a unique agent ID */ generateAgentId(agentType) { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 9); return `agent-${agentType}-${timestamp}-${random}`; } /** * Generate a unique worker ID */ generateWorkerId(team) { const uuid = uuidv4().substring(0, 8); return `worker-${team}-${uuid}`; } /** * Validate spawn configuration */ validateSpawnConfig(config) { const errors = []; if (!config.agentType || typeof config.agentType !== 'string') { errors.push('agentType must be a non-empty string'); } if (!config.taskId || typeof config.taskId !== 'string') { errors.push('taskId must be a non-empty string'); } else { const taskIdValidation = this.validateTaskId(config.taskId); if (!taskIdValidation.valid) { errors.push(taskIdValidation.error || 'Invalid taskId format'); } } if (config.iteration === undefined || typeof config.iteration !== 'number') { errors.push('iteration must be a number'); } if (![ 'mvp', 'standard', 'enterprise' ].includes(config.mode)) { errors.push('mode must be mvp, standard, or enterprise'); } if (errors.length > 0) { throw new Error(`Configuration validation failed:\n${errors.join('\n')}`); } } /** * Validate worker configuration */ validateWorkerConfig(config) { const errors = []; if (!config.team || typeof config.team !== 'string') { errors.push('team must be a non-empty string'); } if (![ 'simple', 'complex' ].includes(config.complexity)) { errors.push('complexity must be simple or complex'); } if (![ 'auto', 'zai', 'anthropic' ].includes(config.providerMode)) { errors.push('providerMode must be auto, zai, or anthropic'); } if (errors.length > 0) { throw new Error(`Worker configuration validation failed:\n${errors.join('\n')}`); } } /** * Validate task ID format (CVSS 8.9 - command injection prevention) * Supports both raw IDs and Phase 1 prefixed IDs (cli:*, trigger:*) * Pattern: alphanumeric, underscore, hyphen, dot, and colon (for mode prefix) only, max 128 chars * * Accepted formats: * - Raw: task-123 (16 chars) * - Prefixed: cli:task-123 (20 chars) * - Prefixed: trigger:task-123 (24 chars) */ validateTaskId(taskId) { if (typeof taskId !== 'string' || taskId.length === 0) { return { valid: false, error: 'Task ID must be a non-empty string' }; } // Updated pattern to support namespace prefixes (e.g., cli:, trigger:, task:, orchestrator:) const taskIdPattern = /^([a-z]+:)?[a-zA-Z0-9_.-]{1,64}$/; if (!taskIdPattern.test(taskId)) { return { valid: false, error: 'Invalid task ID format - must contain optional namespace prefix (e.g., "cli:") and alphanumeric characters, dot, underscore, hyphens (max 64 chars)' }; } return { valid: true }; } /** * Build environment variables for agent execution * * Provider routing: Sets both PROVIDER and CLAUDE_API_PROVIDER for compatibility. * - PROVIDER: New convention used by agent-spawner * - CLAUDE_API_PROVIDER: Legacy convention used by anthropic-client.ts getAPIConfig() * * BUG FIX: Previously only set PROVIDER, but anthropic-client.ts checked CLAUDE_API_PROVIDER */ buildEnvironment(config, agentId, provider, model) { const env = { ...process.env, AGENT_ID: agentId, AGENT_TYPE: config.agentType, TASK_ID: config.taskId, ITERATION: String(config.iteration), MODE: config.mode, // Provider routing - set both for compatibility PROVIDER: provider, CLAUDE_API_PROVIDER: provider, MODEL: model, SPAWNED_AT: new Date().toISOString(), PROJECT_ROOT: this.projectRoot, // Redis coordination for CLI mode agents (resolved via environment contract) CFN_REDIS_HOST: getEnvValue('redis_host', 'cli'), CFN_REDIS_PORT: getEnvValue('redis_port', 'cli'), // FIX: Don't use REDIS_PASSWORD from parent env - only explicit CFN_REDIS_PASSWORD // This prevents CLI agents from inheriting the wrong password from shell environment CFN_REDIS_PASSWORD: process.env.CFN_REDIS_PASSWORD || '', CFN_NETWORK_NAME: getNetworkName('cli') }; // Add provider-specific API keys if available // This ensures spawned agents have access to the correct API key for their provider if (provider === 'zai' && process.env.ZAI_API_KEY) { env.ZAI_API_KEY = process.env.ZAI_API_KEY; } if (provider === 'kimi' && process.env.KIMI_API_KEY) { env.KIMI_API_KEY = process.env.KIMI_API_KEY; } if (provider === 'openrouter' && process.env.OPENROUTER_API_KEY) { env.OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; } if (provider === 'anthropic' && process.env.ANTHROPIC_API_KEY) { env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; } // Add optional prompt parameter if provided if (config.prompt) { env.PROMPT = config.prompt; } // Merge user-provided environment variables if (config.env) { Object.assign(env, config.env); } // Debug logging for provider routing console.error(`[agent-spawner] Building env for agent ${agentId}:`); console.error(`[agent-spawner] PROVIDER=${provider}, CLAUDE_API_PROVIDER=${provider}`); console.error(`[agent-spawner] Has ZAI_API_KEY: ${!!env.ZAI_API_KEY}`); console.error(`[agent-spawner] Has KIMI_API_KEY: ${!!env.KIMI_API_KEY}`); console.error(`[agent-spawner] Has ANTHROPIC_API_KEY: ${!!env.ANTHROPIC_API_KEY}`); return env; } /** * Build environment for worker process */ buildWorkerEnvironment(config, workerId, model, apiKey) { const env = { ...process.env, WORKER_ID: workerId, TEAM: config.team, COMPLEXITY: config.complexity, MODEL: model, API_KEY: apiKey, PROJECT_ROOT: this.projectRoot }; if (config.agentType) { env.AGENT_TYPE = config.agentType; } if (config.taskContext) { env.TASK_CONTEXT = config.taskContext; } return env; } /** * Spawn the actual process */ async spawnProcess(agentType, env, background = true) { const args = [ 'src/cli/agent-executor.ts', '--agent-type', agentType ]; const child = childSpawn('tsx', args, { env, stdio: background ? 'ignore' : 'inherit', detached: background }); const pid = child.pid || 0; if (background) { child.unref(); } else { await new Promise((resolve, reject)=>{ child.on('exit', (code)=>{ if (code !== 0) { reject(new Error(`Process exited with code ${code}`)); } else { resolve(undefined); } }); }); } return pid; } /** * Find agent profile in directory structure */ findAgentProfile(agentType) { const normalized = agentType.replace(/_/g, '-').toLowerCase(); // Direct path const directPath = resolve(this.agentProfilesDir, `${normalized}.md`); if (existsSync(directPath)) { return directPath; } // Search in subdirectories const subdirs = [ 'developers', 'testers', 'reviewers', 'architecture', 'dev-ops', 'product-owners', 'coordinators', 'analysts', 'utility' ]; for (const subdir of subdirs){ const path = resolve(this.agentProfilesDir, subdir, `${normalized}.md`); if (existsSync(path)) { return path; } // Check nested subdirectories const nestedDirs = [ 'frontend', 'backend', 'database', 'quality', 'e2e', 'unit', 'validation', 'data' ]; for (const nested of nestedDirs){ const nestedPath = resolve(this.agentProfilesDir, subdir, nested, `${normalized}.md`); if (existsSync(nestedPath)) { return nestedPath; } } } return null; } /** * Find agent in subdirectories */ findAgentInSubdirs(normalized) { const paths = []; const subdirs = [ 'developers', 'testers', 'reviewers', 'architecture', 'dev-ops', 'product-owners', 'coordinators', 'analysts', 'utility' ]; for (const subdir of subdirs){ const path = resolve(this.agentProfilesDir, subdir, `${normalized}.md`); paths.push(path); // Check nested directories const nestedDirs = [ 'frontend', 'backend', 'database', 'quality', 'e2e', 'unit', 'validation', 'data' ]; for (const nested of nestedDirs){ paths.push(resolve(this.agentProfilesDir, subdir, nested, `${normalized}.md`)); } } return paths; } /** * Load team configuration from config file */ loadTeamConfig(team) { if (!existsSync(this.agentConfigFile)) { throw new Error(`Team configuration not found at ${this.agentConfigFile}`); } const content = readFileSync(this.agentConfigFile, 'utf-8'); const config = JSON.parse(content); if (!config.teams || !config.teams[team]) { throw new Error(`Invalid or missing provider configuration for team: ${team}`); } return config.teams[team]; } /** * Select model based on complexity */ selectModel(team, complexity) { const config = this.loadTeamConfig(team); const workers = config.workers; const models = workers?.models; if (models && models[complexity]) { return models[complexity]; } // Fallback to default complexity const defaultComplexity = 'simple'; if (models && models[defaultComplexity]) { return models[defaultComplexity]; } return 'gpt-4'; } /** * Get API key from environment */ getApiKey(team, role) { const config = this.loadTeamConfig(team); const roleConfig = config[role]; if (!roleConfig) { throw new Error(`No configuration found for team=${team}, role=${role}`); } const apiKeyEnvVar = roleConfig.apiKeyEnvVar; if (!apiKeyEnvVar) { throw new Error(`apiKeyEnvVar not configured for team=${team}, role=${role}`); } const apiKey = process.env[apiKeyEnvVar]; if (!apiKey) { throw new Error(`API key not found in environment variable: ${apiKeyEnvVar}`); } return apiKey; } /** * Route worker to appropriate provider */ routeWorkerProvider(providerMode, team, model, apiKey) { const config = this.loadTeamConfig(team); const workers = config.workers; const provider = workers?.provider; const baseUrl = workers?.baseUrl; switch(providerMode){ case 'auto': this.routeAutoProvider(provider, baseUrl, model, apiKey); break; case 'zai': process.env.ZAI_API_KEY = apiKey; process.env.ZAI_BASE_URL = baseUrl; process.env.ZAI_MODEL = model; break; case 'anthropic': process.env.ANTHROPIC_API_KEY = apiKey; process.env.ANTHROPIC_BASE_URL = baseUrl; process.env.ANTHROPIC_MODEL = model; break; default: throw new Error(`Invalid provider mode: ${providerMode}`); } } /** * Route to appropriate provider based on configuration */ routeAutoProvider(provider, baseUrl, model, apiKey) { switch(provider){ case 'zai': process.env.ZAI_API_KEY = apiKey; process.env.ZAI_BASE_URL = baseUrl; process.env.ZAI_MODEL = model; break; case 'anthropic': process.env.ANTHROPIC_API_KEY = apiKey; process.env.ANTHROPIC_BASE_URL = baseUrl; process.env.ANTHROPIC_MODEL = model; break; default: throw new Error(`Unsupported provider: ${provider}`); } } } //# sourceMappingURL=agent-spawner.js.map