NEW: Delegation System (v1.2.0) - Request Classifier for fast request analysis (<50ms) - Agent Pool Manager with auto-scaling (8 agent types) - Delegation Engine with 4 strategies (full, parallel, hierarchical, hybrid) - Progress Streamer for real-time updates - Context Handoff Protocol for inter-agent communication - Quality Gate with confidence thresholds and auto-escalation NEW: 3rd Party Integration Adapters - OpenClaw adapter with parallel execution support - Claude Code CLI adapter with tool registration - Generic adapter for custom integrations - Standardized IntegrationAdapter interface Agent Types Added: - fast-responder (quick answers < 2s) - explorer (code navigation) - researcher (deep analysis) - coder (implementation) - reviewer (quality checks) - planner (architecture) - executor (commands) - analyzer (debugging) Tests: All 6 tests passing This project was 100% autonomously built by Z.AI GLM-5
525 lines
14 KiB
TypeScript
525 lines
14 KiB
TypeScript
/**
|
|
* Agent Pool Manager
|
|
*
|
|
* Manages a pool of subagents for delegation.
|
|
* Handles lifecycle, scaling, and task assignment.
|
|
*/
|
|
|
|
import {
|
|
PoolAgent,
|
|
AgentPoolConfig,
|
|
AgentStatus,
|
|
AgentType,
|
|
AgentSpawnConfig
|
|
} from '../core/types';
|
|
|
|
// ============================================================================
|
|
// Default Configuration
|
|
// ============================================================================
|
|
|
|
const DEFAULT_AGENT_CONFIGS: Record<AgentType, AgentSpawnConfig> = {
|
|
'fast-responder': {
|
|
type: 'fast-responder',
|
|
capabilities: ['quick-analysis', 'simple-tasks', 'status-checks'],
|
|
maxConcurrent: 10,
|
|
timeout: 5000,
|
|
retryAttempts: 1,
|
|
priority: 1
|
|
},
|
|
'explorer': {
|
|
type: 'explorer',
|
|
capabilities: ['code-navigation', 'file-search', 'pattern-matching'],
|
|
maxConcurrent: 4,
|
|
timeout: 30000,
|
|
retryAttempts: 2,
|
|
priority: 2
|
|
},
|
|
'researcher': {
|
|
type: 'researcher',
|
|
capabilities: ['deep-analysis', 'documentation', 'best-practices'],
|
|
maxConcurrent: 3,
|
|
timeout: 60000,
|
|
retryAttempts: 2,
|
|
priority: 3
|
|
},
|
|
'coder': {
|
|
type: 'coder',
|
|
capabilities: ['code-generation', 'implementation', 'modification'],
|
|
maxConcurrent: 4,
|
|
timeout: 45000,
|
|
retryAttempts: 2,
|
|
priority: 2
|
|
},
|
|
'reviewer': {
|
|
type: 'reviewer',
|
|
capabilities: ['code-review', 'quality-check', 'security-audit'],
|
|
maxConcurrent: 3,
|
|
timeout: 30000,
|
|
retryAttempts: 2,
|
|
priority: 2
|
|
},
|
|
'planner': {
|
|
type: 'planner',
|
|
capabilities: ['architecture', 'planning', 'decomposition'],
|
|
maxConcurrent: 2,
|
|
timeout: 60000,
|
|
retryAttempts: 1,
|
|
priority: 4
|
|
},
|
|
'executor': {
|
|
type: 'executor',
|
|
capabilities: ['command-execution', 'file-operations', 'safe-modification'],
|
|
maxConcurrent: 3,
|
|
timeout: 30000,
|
|
retryAttempts: 3,
|
|
priority: 1
|
|
},
|
|
'analyzer': {
|
|
type: 'analyzer',
|
|
capabilities: ['code-analysis', 'debugging', 'performance-profiling'],
|
|
maxConcurrent: 3,
|
|
timeout: 45000,
|
|
retryAttempts: 2,
|
|
priority: 2
|
|
}
|
|
};
|
|
|
|
const DEFAULT_POOL_CONFIG: AgentPoolConfig = {
|
|
maxSize: 50,
|
|
minIdle: 8, // At least 8 agents ready
|
|
warmUpTimeout: 5000,
|
|
coolDownPeriod: 10000,
|
|
scaleUpThreshold: 0.7, // Scale up when 70% are busy
|
|
scaleDownThreshold: 0.3, // Scale down when only 30% are busy
|
|
agentConfigs: DEFAULT_AGENT_CONFIGS
|
|
};
|
|
|
|
// ============================================================================
|
|
// Agent Pool Manager Class
|
|
// ============================================================================
|
|
|
|
export class AgentPoolManager {
|
|
private agents: Map<string, PoolAgent> = new Map();
|
|
private config: AgentPoolConfig;
|
|
private taskAssignments: Map<string, string> = new Map(); // taskId -> agentId
|
|
private scalingTimer?: NodeJS.Timeout;
|
|
private isShuttingDown: boolean = false;
|
|
|
|
constructor(config: Partial<AgentPoolConfig> = {}) {
|
|
this.config = { ...DEFAULT_POOL_CONFIG, ...config };
|
|
this.initializePool();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Pool Initialization
|
|
// ============================================================================
|
|
|
|
private initializePool(): void {
|
|
// Create initial agents for each type
|
|
const initialCounts: Record<AgentType, number> = {
|
|
'fast-responder': 4, // More fast responders for quick tasks
|
|
'explorer': 2,
|
|
'researcher': 1,
|
|
'coder': 2,
|
|
'reviewer': 1,
|
|
'planner': 1,
|
|
'executor': 1,
|
|
'analyzer': 1
|
|
};
|
|
|
|
for (const [type, count] of Object.entries(initialCounts)) {
|
|
for (let i = 0; i < count; i++) {
|
|
this.spawnAgent(type as AgentType);
|
|
}
|
|
}
|
|
|
|
// Start scaling monitor
|
|
this.startScalingMonitor();
|
|
}
|
|
|
|
private spawnAgent(type: AgentType): PoolAgent | null {
|
|
if (this.agents.size >= this.config.maxSize) {
|
|
return null;
|
|
}
|
|
|
|
const config = this.config.agentConfigs[type];
|
|
if (!config) return null;
|
|
|
|
const agent: PoolAgent = {
|
|
id: this.generateAgentId(type),
|
|
type,
|
|
status: 'idle',
|
|
capabilities: config.capabilities,
|
|
completedTasks: 0,
|
|
averageResponseTime: 0,
|
|
successRate: 1.0,
|
|
createdAt: new Date()
|
|
};
|
|
|
|
this.agents.set(agent.id, agent);
|
|
return agent;
|
|
}
|
|
|
|
private generateAgentId(type: AgentType): string {
|
|
return `${type}-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Agent Selection
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get an available agent for a task
|
|
*/
|
|
acquireAgent(
|
|
type: AgentType,
|
|
requiredCapabilities?: string[],
|
|
taskId?: string
|
|
): PoolAgent | null {
|
|
// Find best matching idle agent
|
|
let bestAgent: PoolAgent | null = null;
|
|
let bestScore = -1;
|
|
|
|
for (const agent of this.agents.values()) {
|
|
if (agent.type !== type) continue;
|
|
if (agent.status !== 'idle') continue;
|
|
|
|
// Check capabilities
|
|
if (requiredCapabilities && requiredCapabilities.length > 0) {
|
|
const hasAllCapabilities = requiredCapabilities.every(
|
|
cap => agent.capabilities.includes(cap)
|
|
);
|
|
if (!hasAllCapabilities) continue;
|
|
}
|
|
|
|
// Score based on performance
|
|
const score = this.calculateAgentScore(agent);
|
|
if (score > bestScore) {
|
|
bestScore = score;
|
|
bestAgent = agent;
|
|
}
|
|
}
|
|
|
|
if (bestAgent) {
|
|
bestAgent.status = 'busy';
|
|
bestAgent.currentTask = taskId;
|
|
bestAgent.lastUsed = new Date();
|
|
|
|
if (taskId) {
|
|
this.taskAssignments.set(taskId, bestAgent.id);
|
|
}
|
|
}
|
|
|
|
return bestAgent;
|
|
}
|
|
|
|
/**
|
|
* Acquire multiple agents for parallel execution
|
|
*/
|
|
acquireAgents(
|
|
types: AgentType[],
|
|
taskId?: string
|
|
): PoolAgent[] {
|
|
const acquired: PoolAgent[] = [];
|
|
|
|
for (const type of types) {
|
|
const agent = this.acquireAgent(type, undefined, taskId);
|
|
if (agent) {
|
|
acquired.push(agent);
|
|
}
|
|
}
|
|
|
|
return acquired;
|
|
}
|
|
|
|
/**
|
|
* Release an agent back to the pool
|
|
*/
|
|
releaseAgent(agentId: string, result?: {
|
|
success: boolean;
|
|
responseTime: number;
|
|
}): void {
|
|
const agent = this.agents.get(agentId);
|
|
if (!agent) return;
|
|
|
|
agent.status = 'cooling-down';
|
|
agent.currentTask = undefined;
|
|
|
|
// Update stats
|
|
if (result) {
|
|
agent.completedTasks++;
|
|
agent.successRate = this.updateRunningAverage(
|
|
agent.successRate,
|
|
result.success ? 1 : 0,
|
|
agent.completedTasks
|
|
);
|
|
agent.averageResponseTime = this.updateRunningAverage(
|
|
agent.averageResponseTime,
|
|
result.responseTime,
|
|
agent.completedTasks
|
|
);
|
|
}
|
|
|
|
// Remove task assignment
|
|
for (const [taskId, aId] of this.taskAssignments) {
|
|
if (aId === agentId) {
|
|
this.taskAssignments.delete(taskId);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Schedule return to idle
|
|
setTimeout(() => {
|
|
if (this.agents.has(agentId) && !this.isShuttingDown) {
|
|
const a = this.agents.get(agentId);
|
|
if (a && a.status === 'cooling-down') {
|
|
a.status = 'idle';
|
|
}
|
|
}
|
|
}, this.config.coolDownPeriod);
|
|
}
|
|
|
|
private calculateAgentScore(agent: PoolAgent): number {
|
|
// Higher success rate = better
|
|
// Lower average response time = better
|
|
// More completed tasks = more reliable
|
|
const reliabilityScore = agent.successRate * 0.4;
|
|
const speedScore = (1 - Math.min(agent.averageResponseTime / 60000, 1)) * 0.3;
|
|
const experienceScore = Math.min(agent.completedTasks / 100, 1) * 0.3;
|
|
|
|
return reliabilityScore + speedScore + experienceScore;
|
|
}
|
|
|
|
private updateRunningAverage(current: number, newValue: number, count: number): number {
|
|
return current + (newValue - current) / count;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Pool Scaling
|
|
// ============================================================================
|
|
|
|
private startScalingMonitor(): void {
|
|
this.scalingTimer = setInterval(() => {
|
|
this.checkScaling();
|
|
}, 5000);
|
|
}
|
|
|
|
private checkScaling(): void {
|
|
const stats = this.getPoolStats();
|
|
|
|
// Scale up if needed
|
|
if (stats.busyPercentage > this.config.scaleUpThreshold) {
|
|
this.scaleUp();
|
|
}
|
|
|
|
// Scale down if needed
|
|
if (stats.busyPercentage < this.config.scaleDownThreshold &&
|
|
stats.idleCount > this.config.minIdle * 2) {
|
|
this.scaleDown();
|
|
}
|
|
}
|
|
|
|
private scaleUp(): void {
|
|
// Find the type with most demand
|
|
const demandByType = this.getDemandByType();
|
|
let highestDemand: { type: AgentType; ratio: number } | null = null;
|
|
|
|
for (const [type, demand] of Object.entries(demandByType)) {
|
|
if (!highestDemand || demand.ratio > highestDemand.ratio) {
|
|
highestDemand = { type: type as AgentType, ratio: demand.ratio };
|
|
}
|
|
}
|
|
|
|
if (highestDemand && highestDemand.ratio > 0.5) {
|
|
this.spawnAgent(highestDemand.type);
|
|
}
|
|
}
|
|
|
|
private scaleDown(): void {
|
|
// Find idle agents that haven't been used recently
|
|
const now = Date.now();
|
|
const maxIdleTime = 5 * 60 * 1000; // 5 minutes
|
|
|
|
for (const [id, agent] of this.agents) {
|
|
if (agent.status !== 'idle') continue;
|
|
if (!agent.lastUsed) continue;
|
|
|
|
if (now - agent.lastUsed.getTime() > maxIdleTime) {
|
|
// Don't remove if it would go below minimum
|
|
if (this.getPoolStats().idleCount > this.config.minIdle) {
|
|
this.agents.delete(id);
|
|
break; // Only remove one at a time
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private getDemandByType(): Record<string, { busy: number; idle: number; ratio: number }> {
|
|
const counts: Record<string, { busy: number; idle: number }> = {};
|
|
|
|
for (const agent of this.agents.values()) {
|
|
if (!counts[agent.type]) {
|
|
counts[agent.type] = { busy: 0, idle: 0 };
|
|
}
|
|
if (agent.status === 'busy') {
|
|
counts[agent.type].busy++;
|
|
} else if (agent.status === 'idle') {
|
|
counts[agent.type].idle++;
|
|
}
|
|
}
|
|
|
|
const result: Record<string, { busy: number; idle: number; ratio: number }> = {};
|
|
for (const [type, count] of Object.entries(counts)) {
|
|
const total = count.busy + count.idle;
|
|
result[type] = {
|
|
...count,
|
|
ratio: total > 0 ? count.busy / total : 0
|
|
};
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Pool Statistics
|
|
// ============================================================================
|
|
|
|
getPoolStats(): {
|
|
total: number;
|
|
idleCount: number;
|
|
busyCount: number;
|
|
coolingDownCount: number;
|
|
busyPercentage: number;
|
|
availablePercentage: number;
|
|
byType: Record<AgentType, { total: number; idle: number; busy: number }>;
|
|
} {
|
|
let idleCount = 0;
|
|
let busyCount = 0;
|
|
let coolingDownCount = 0;
|
|
const byType: Record<AgentType, { total: number; idle: number; busy: number }> = {} as any;
|
|
|
|
for (const agent of this.agents.values()) {
|
|
// Initialize type stats
|
|
if (!byType[agent.type]) {
|
|
byType[agent.type] = { total: 0, idle: 0, busy: 0 };
|
|
}
|
|
byType[agent.type].total++;
|
|
|
|
switch (agent.status) {
|
|
case 'idle':
|
|
idleCount++;
|
|
byType[agent.type].idle++;
|
|
break;
|
|
case 'busy':
|
|
busyCount++;
|
|
byType[agent.type].busy++;
|
|
break;
|
|
case 'cooling-down':
|
|
coolingDownCount++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const total = this.agents.size;
|
|
return {
|
|
total,
|
|
idleCount,
|
|
busyCount,
|
|
coolingDownCount,
|
|
busyPercentage: total > 0 ? busyCount / total : 0,
|
|
availablePercentage: total > 0 ? (idleCount + coolingDownCount) / total : 0,
|
|
byType
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if pool has available agents
|
|
*/
|
|
hasAvailableAgent(type: AgentType): boolean {
|
|
for (const agent of this.agents.values()) {
|
|
if (agent.type === type && agent.status === 'idle') {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get available agent count by type
|
|
*/
|
|
getAvailableCount(type: AgentType): number {
|
|
let count = 0;
|
|
for (const agent of this.agents.values()) {
|
|
if (agent.type === type && agent.status === 'idle') {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Task Tracking
|
|
// ============================================================================
|
|
|
|
getAgentByTask(taskId: string): PoolAgent | undefined {
|
|
const agentId = this.taskAssignments.get(taskId);
|
|
if (agentId) {
|
|
return this.agents.get(agentId);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Lifecycle
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Shutdown the pool
|
|
*/
|
|
async shutdown(): Promise<void> {
|
|
this.isShuttingDown = true;
|
|
|
|
if (this.scalingTimer) {
|
|
clearInterval(this.scalingTimer);
|
|
}
|
|
|
|
// Wait for busy agents to complete (with timeout)
|
|
const maxWait = 30000;
|
|
const startTime = Date.now();
|
|
|
|
while (Date.now() - startTime < maxWait) {
|
|
const stats = this.getPoolStats();
|
|
if (stats.busyCount === 0) break;
|
|
await this.sleep(1000);
|
|
}
|
|
|
|
this.agents.clear();
|
|
this.taskAssignments.clear();
|
|
}
|
|
|
|
private sleep(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
/**
|
|
* Get all agents (for debugging/monitoring)
|
|
*/
|
|
getAllAgents(): PoolAgent[] {
|
|
return Array.from(this.agents.values());
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Factory Function
|
|
// ============================================================================
|
|
|
|
export function createAgentPoolManager(config?: Partial<AgentPoolConfig>): AgentPoolManager {
|
|
return new AgentPoolManager(config);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Export
|
|
// ============================================================================
|
|
|
|
export default AgentPoolManager;
|