claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes CodeSearch (hybrid SQLite + pgvector), mem0/memgraph specialists, and all CFN skills.
448 lines (447 loc) • 16 kB
JavaScript
/**
* Agent Spawner - TypeScript implementation of spawn-agents.sh
*
* Spawns Loop 3 agents with enriched historical context from Redis.
* Supports wave-based memory allocation and parallel agent spawning.
*
* @module agent-spawner
*/ import { execFile, spawn } from 'child_process';
import { promises as fs } from 'fs';
import { promisify } from 'util';
const execFileAsync = promisify(execFile);
/**
* Default logger implementation
*/ let ConsoleLogger = class ConsoleLogger {
info(message, data) {
console.log(`[INFO] ${message}`, data);
}
warn(message, data) {
console.warn(`[WARN] ${message}`, data);
}
error(message, data) {
console.error(`[ERROR] ${message}`, data);
}
debug(message, data) {
if (process.env.DEBUG) {
console.debug(`[DEBUG] ${message}`, data);
}
}
};
/**
* Default context enricher implementation
*/ let DefaultContextEnricher = class DefaultContextEnricher {
logger;
constructor(logger){
this.logger = logger;
}
async enrich(taskId, agentType, originalContext) {
const startTime = Date.now();
try {
// Validate inputs
this.validateInputs(taskId, agentType);
// In a real implementation, this would call context-injection.sh
// For now, return original context with metadata
const injectionTime = Date.now() - startTime;
if (injectionTime > 200) {
this.logger.warn(`Context injection exceeded 200ms threshold: ${injectionTime}ms`);
}
return {
originalContext,
injectionTime,
success: true
};
} catch (error) {
const injectionTime = Date.now() - startTime;
this.logger.warn(`Context injection failed for ${agentType}, using original context`, error);
return {
originalContext,
injectionTime,
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
validateInputs(taskId, agentType) {
if (!taskId || typeof taskId !== 'string') {
throw new Error('Invalid task ID');
}
if (!agentType || typeof agentType !== 'string') {
throw new Error('Invalid agent type');
}
}
};
/**
* Memory tier analyzer
*/ let MemoryTierAnalyzer = class MemoryTierAnalyzer {
tierMapping = new Map([
[
'512mb',
512
],
[
'1gb',
1024
],
[
'2gb',
2048
],
[
'4gb',
4096
]
]);
analyzeTier(agentType) {
// Analyze agent type to determine memory requirements
// This would typically analyze file clusters or agent complexity
if (agentType.includes('orchestrator')) return '4gb';
if (agentType.includes('validator') || agentType.includes('reviewer')) return '2gb';
if (agentType.includes('specialist')) return '1gb';
return '512mb';
}
getTierMemory(tier) {
return this.tierMapping.get(tier) || 512;
}
getAllTiers() {
return Array.from(this.tierMapping.keys());
}
};
/**
* Wave manager for memory budget allocation
*/ let WaveManager = class WaveManager {
memoryBudget = 40 * 1024;
usedMemory = 0;
tierAnalyzer;
logger;
constructor(logger){
this.logger = logger;
this.tierAnalyzer = new MemoryTierAnalyzer();
}
/**
* Allocate agents into waves based on memory constraints
*/ allocateWaves(agentTypes) {
const waves = [];
let currentWave = [];
let waveMemory = 0;
for (const agentType of agentTypes){
const tier = this.tierAnalyzer.analyzeTier(agentType);
const memory = this.tierAnalyzer.getTierMemory(tier);
// Check if adding this agent exceeds budget
if (waveMemory + memory > this.memoryBudget) {
// Start new wave
if (currentWave.length > 0) {
waves.push(currentWave);
}
currentWave = [
agentType
];
waveMemory = memory;
} else {
currentWave.push(agentType);
waveMemory += memory;
}
}
// Add final wave
if (currentWave.length > 0) {
waves.push(currentWave);
}
this.logger.info(`Allocated ${agentTypes.length} agents into ${waves.length} waves`);
return waves;
}
/**
* Get memory tier for agent type
*/ getTier(agentType) {
return this.tierAnalyzer.analyzeTier(agentType);
}
/**
* Reset budget (for testing)
*/ reset() {
this.usedMemory = 0;
}
/**
* Get remaining memory budget
*/ getRemaining() {
return Math.max(0, this.memoryBudget - this.usedMemory);
}
};
/**
* Input sanitizer for security
*/ let InputSanitizer = class InputSanitizer {
/**
* Sanitize input by removing dangerous characters
* Only allows alphanumeric, dash, underscore, dot, comma, colon
*/ sanitize(input) {
if (typeof input !== 'string') {
throw new Error('Input must be a string');
}
return input.replace(/[^a-zA-Z0-9._:,\-]/g, '');
}
/**
* Validate task ID format
*/ validateTaskId(taskId) {
const sanitized = this.sanitize(taskId);
return sanitized === taskId && sanitized.length > 0;
}
/**
* Validate agent type
*/ validateAgentType(agentType) {
const sanitized = this.sanitize(agentType);
return sanitized === agentType && sanitized.length > 0;
}
};
/**
* Main Agent Spawner class
*/ export class AgentSpawner {
config;
logger;
redisClient = null;
contextEnricher;
waveManager;
sanitizer;
spawnResults = [];
constructor(config, logger, contextEnricher, redisClient){
this.config = this.validateConfig(config);
this.logger = logger || new ConsoleLogger();
this.contextEnricher = contextEnricher || new DefaultContextEnricher(this.logger);
this.redisClient = redisClient || null;
this.waveManager = new WaveManager(this.logger);
this.sanitizer = new InputSanitizer();
this.spawnResults = [];
}
/**
* Validate spawn configuration
*/ validateConfig(config) {
if (!config.taskId || typeof config.taskId !== 'string') {
throw new Error('Invalid or missing taskId');
}
if (config.iteration === undefined || config.iteration < 0) {
throw new Error('Invalid or missing iteration');
}
if (!Array.isArray(config.agents) || config.agents.length === 0) {
throw new Error('Invalid or missing agents');
}
if (!config.originalContext || typeof config.originalContext !== 'string') {
throw new Error('Invalid or missing originalContext');
}
return {
...config,
logDir: config.logDir || '.artifacts/logs',
redisHost: config.redisHost || 'localhost',
redisPort: config.redisPort || 6379,
projectRoot: config.projectRoot || process.cwd()
};
}
/**
* Spawn all agents and return summary
*/ async spawn() {
const startTime = Date.now();
try {
this.logger.info('Starting agent spawning with context injection');
this.logger.info(`Task ID: ${this.config.taskId}, Iteration: ${this.config.iteration}`);
// Create log directory
await this.ensureLogDir();
// Validate inputs
this.validateInputs();
// Allocate agents into waves
const waves = this.waveManager.allocateWaves(this.config.agents);
// Spawn each wave sequentially to respect memory budget
for(let waveIdx = 0; waveIdx < waves.length; waveIdx++){
const wave = waves[waveIdx];
this.logger.info(`Starting wave ${waveIdx + 1} with ${wave.length} agents`);
// Spawn agents in wave in parallel
await Promise.all(wave.map((agentType)=>this.spawnSingleAgent(agentType)));
this.logger.info(`Wave ${waveIdx + 1} complete`);
}
// Validate that at least one agent was spawned
if (this.spawnResults.length === 0) {
throw new Error('No agents were spawned');
}
const injectionSuccessCount = this.spawnResults.filter((r)=>r.injectionSuccessful).length;
const endTime = Date.now();
const summary = {
totalSpawned: this.spawnResults.length,
injectionSuccessCount,
injectionFailureCount: this.spawnResults.length - injectionSuccessCount,
spawnResults: this.spawnResults,
startTime,
endTime,
duration: endTime - startTime
};
this.logger.info(`Agent spawning complete: ${summary.totalSpawned} agents spawned`);
this.logger.info(`Context injection success rate: ${injectionSuccessCount}/${summary.totalSpawned}`);
return summary;
} catch (error) {
this.logger.error('Agent spawning failed', error);
throw error;
}
}
/**
* Spawn a single agent
*/ async spawnSingleAgent(agentType) {
try {
// Get or increment instance count
const instanceNum = this.getNextInstanceNumber(agentType);
const agentId = `${agentType}-${this.config.iteration}-${instanceNum}`;
this.logger.info(`Spawning agent: ${agentType} (ID: ${agentId})`);
// Sanitize inputs
const safeAgentType = this.sanitizer.sanitize(agentType);
const safeTaskId = this.sanitizer.sanitize(this.config.taskId);
const safeAgentId = this.sanitizer.sanitize(agentId);
if (!this.sanitizer.validateAgentType(safeAgentType)) {
throw new Error(`Invalid agent type: ${agentType}`);
}
if (!this.sanitizer.validateTaskId(safeTaskId)) {
throw new Error(`Invalid task ID: ${this.config.taskId}`);
}
// Enrich context
const enriched = await this.contextEnricher.enrich(safeTaskId, safeAgentType, this.config.originalContext);
const contextToUse = enriched.originalContext;
// Get memory tier
const memoryTier = this.waveManager.getTier(safeAgentType);
// Spawn agent
const pid = await this.spawnAgentProcess(safeAgentType, safeAgentId, safeTaskId, contextToUse, memoryTier);
// Store agent PID in Redis if available
if (this.redisClient) {
await this.storeAgentInRedis(safeTaskId, safeAgentId, pid);
}
// Record spawn result
this.spawnResults.push({
agentId: safeAgentId,
agentType: safeAgentType,
pid,
success: true,
injectionSuccessful: enriched.success,
injectionTime: enriched.injectionTime,
contextSize: contextToUse.length
});
this.logger.info(`Agent ${safeAgentType} spawned (PID: ${pid})`);
} catch (error) {
const safeType = this.sanitizer.sanitize(agentType);
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
this.spawnResults.push({
agentId: `${safeType}-${this.config.iteration}-unknown`,
agentType: safeType,
success: false,
injectionSuccessful: false,
error: errorMsg
});
this.logger.error(`Failed to spawn agent ${safeType}: ${errorMsg}`);
}
}
/**
* Spawn agent process (CLI command)
*/ async spawnAgentProcess(agentType, agentId, taskId, context, memoryTier) {
return new Promise((resolve, reject)=>{
try {
// Get provider environment variables
const env = {
...process.env,
MEMORY_TIER: memoryTier,
TASK_ID: taskId,
AGENT_ID: agentId,
AGENT_TYPE: agentType
};
// Spawn agent in background
const child = spawn('npx', [
'claude-flow-novice',
'agent',
agentType,
'--task-id',
taskId,
'--agent-id',
agentId,
'--iteration',
String(this.config.iteration),
'--context',
context
], {
env,
detached: true,
stdio: 'ignore'
});
const pid = child.pid;
if (!pid) {
reject(new Error(`Failed to get PID for agent ${agentType}`));
return;
}
// Detach from parent process
child.unref();
resolve(pid);
} catch (error) {
reject(error);
}
});
}
/**
* Store agent info in Redis
*/ async storeAgentInRedis(taskId, agentId, pid) {
if (!this.redisClient) {
return;
}
try {
const key = `swarm:${taskId}:${agentId}:pid`;
const value = JSON.stringify({
pid,
timestamp: Date.now()
});
await this.redisClient.set(key, value);
// Also add to agent ID set for this iteration
const setKey = `swarm:${taskId}:loop3:agent_ids:iteration${this.config.iteration}`;
await this.redisClient.sadd(setKey, agentId);
} catch (error) {
this.logger.warn('Failed to store agent info in Redis', error);
}
}
/**
* Validate input arguments
*/ validateInputs() {
if (!this.sanitizer.validateTaskId(this.config.taskId)) {
throw new Error(`Invalid task ID format: ${this.config.taskId}`);
}
for (const agent of this.config.agents){
if (!this.sanitizer.validateAgentType(agent)) {
throw new Error(`Invalid agent type format: ${agent}`);
}
}
}
/**
* Get next instance number for agent type
*/ instanceCounters = {};
getNextInstanceNumber(agentType) {
if (!this.instanceCounters[agentType]) {
this.instanceCounters[agentType] = 0;
}
return ++this.instanceCounters[agentType];
}
/**
* Ensure log directory exists
*/ async ensureLogDir() {
try {
await fs.mkdir(this.config.logDir, {
recursive: true
});
} catch (error) {
this.logger.warn('Failed to create log directory', error);
}
}
/**
* Get spawn results for testing
*/ getResults() {
return this.spawnResults;
}
/**
* Reset state (for testing)
*/ reset() {
this.spawnResults = [];
this.instanceCounters = {};
this.waveManager.reset();
}
}
/**
* Convenience function for spawning agents
*/ export async function spawnAgents(config) {
const spawner = new AgentSpawner(config);
return spawner.spawn();
}
export { MemoryTierAnalyzer, WaveManager, InputSanitizer, DefaultContextEnricher };
//# sourceMappingURL=agent-spawner.js.map