- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
306 lines
9.6 KiB
TypeScript
306 lines
9.6 KiB
TypeScript
/**
|
|
* AgentRuntime - General-Purpose Agent Lifecycle Manager
|
|
*
|
|
* Manages the lifecycle of multiple agent instances. Can be used for:
|
|
* - Dashboard spawning multiple independent agents
|
|
* - Agent task delegation (parent spawns sub-agents)
|
|
* - Test harnesses managing multiple agents
|
|
* - Any scenario requiring multiple concurrent agents
|
|
*
|
|
* Key responsibilities:
|
|
* - Spawn and manage agents with configurable limits
|
|
* - Execute tasks on agents with timeout handling
|
|
* - Track agent status and lifecycle
|
|
* - Clean up agents when no longer needed
|
|
*/
|
|
|
|
import { randomUUID } from 'crypto';
|
|
import { DextoAgent, type IDextoLogger, type GenerateResponse } from '@dexto/core';
|
|
import { enrichAgentConfig } from '../config/index.js';
|
|
import { AgentPool } from './AgentPool.js';
|
|
import { RuntimeError } from './errors.js';
|
|
import type {
|
|
AgentRuntimeConfig,
|
|
SpawnConfig,
|
|
AgentHandle,
|
|
TaskResult,
|
|
AgentFilter,
|
|
} from './types.js';
|
|
import { AgentRuntimeConfigSchema, type ValidatedAgentRuntimeConfig } from './schemas.js';
|
|
|
|
/**
|
|
* Options for creating an AgentRuntime
|
|
*/
|
|
export interface AgentRuntimeOptions {
|
|
/** Runtime configuration */
|
|
config?: AgentRuntimeConfig;
|
|
/** Logger instance */
|
|
logger: IDextoLogger;
|
|
}
|
|
|
|
export class AgentRuntime {
|
|
private pool: AgentPool;
|
|
private config: ValidatedAgentRuntimeConfig;
|
|
private logger: IDextoLogger;
|
|
|
|
constructor(options: AgentRuntimeOptions) {
|
|
// Validate and apply defaults
|
|
this.config = AgentRuntimeConfigSchema.parse(options.config ?? {});
|
|
this.logger = options.logger;
|
|
this.pool = new AgentPool(this.config, this.logger);
|
|
|
|
this.logger.debug('AgentRuntime initialized', {
|
|
maxAgents: this.config.maxAgents,
|
|
defaultTaskTimeout: this.config.defaultTaskTimeout,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Spawn a new agent
|
|
*
|
|
* @param config - Configuration for the agent
|
|
* @returns Handle to the spawned agent
|
|
*/
|
|
async spawnAgent(config: SpawnConfig): Promise<AgentHandle> {
|
|
// Check global limit
|
|
if (!this.pool.canSpawn()) {
|
|
throw RuntimeError.maxAgentsExceeded(this.pool.size, this.config.maxAgents);
|
|
}
|
|
|
|
// Generate agent ID if not provided
|
|
const agentId = config.agentId ?? `agent-${randomUUID().slice(0, 8)}`;
|
|
|
|
// Check for duplicate ID
|
|
if (this.pool.has(agentId)) {
|
|
throw RuntimeError.agentAlreadyExists(agentId);
|
|
}
|
|
|
|
try {
|
|
// Enrich the config with runtime paths
|
|
// Skip plugin discovery for subagents to avoid duplicate warnings
|
|
const enrichedConfig = enrichAgentConfig(
|
|
config.agentConfig,
|
|
undefined, // No config path
|
|
{ isInteractiveCli: false, skipPluginDiscovery: true }
|
|
);
|
|
|
|
// Override agentId in enriched config
|
|
enrichedConfig.agentId = agentId;
|
|
|
|
// Create the agent
|
|
const agent = new DextoAgent(enrichedConfig);
|
|
|
|
// Create the handle (status: starting)
|
|
const sessionId = `session-${randomUUID().slice(0, 8)}`;
|
|
const handle: AgentHandle = {
|
|
agentId,
|
|
agent,
|
|
status: 'starting',
|
|
ephemeral: config.ephemeral ?? true,
|
|
createdAt: new Date(),
|
|
sessionId,
|
|
};
|
|
|
|
// Add optional fields only if defined (exactOptionalPropertyTypes)
|
|
if (config.group !== undefined) {
|
|
handle.group = config.group;
|
|
}
|
|
if (config.metadata !== undefined) {
|
|
handle.metadata = config.metadata;
|
|
}
|
|
|
|
// Add to pool
|
|
this.pool.add(handle);
|
|
|
|
// Call onBeforeStart hook if provided (e.g., to set approval handlers)
|
|
if (config.onBeforeStart) {
|
|
await config.onBeforeStart(agent);
|
|
}
|
|
|
|
// Start the agent
|
|
await agent.start();
|
|
|
|
// Update status to idle
|
|
this.pool.updateStatus(agentId, 'idle');
|
|
|
|
this.logger.info(
|
|
`Spawned agent '${agentId}'${handle.group ? ` (group: ${handle.group})` : ''} (ephemeral: ${handle.ephemeral})`
|
|
);
|
|
|
|
return handle;
|
|
} catch (error) {
|
|
// Clean up on failure
|
|
this.pool.remove(agentId);
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
throw RuntimeError.spawnFailed(errorMessage, agentId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute a task on an agent
|
|
*
|
|
* @param agentId - ID of the agent
|
|
* @param task - Task description to execute
|
|
* @param timeout - Optional timeout in milliseconds
|
|
* @returns Task result with response or error
|
|
*/
|
|
async executeTask(agentId: string, task: string, timeout?: number): Promise<TaskResult> {
|
|
const handle = this.pool.get(agentId);
|
|
if (!handle) {
|
|
throw RuntimeError.agentNotFound(agentId);
|
|
}
|
|
|
|
if (handle.status === 'stopped' || handle.status === 'error') {
|
|
throw RuntimeError.agentAlreadyStopped(agentId);
|
|
}
|
|
|
|
const taskTimeout = timeout ?? this.config.defaultTaskTimeout;
|
|
|
|
// Update status to running
|
|
this.pool.updateStatus(agentId, 'running');
|
|
|
|
try {
|
|
// Create timeout promise
|
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
setTimeout(() => {
|
|
reject(RuntimeError.taskTimeout(agentId, taskTimeout));
|
|
}, taskTimeout);
|
|
});
|
|
|
|
// Execute the task with timeout
|
|
const generatePromise = handle.agent.generate(task, handle.sessionId);
|
|
|
|
const response = (await Promise.race([
|
|
generatePromise,
|
|
timeoutPromise,
|
|
])) as GenerateResponse;
|
|
|
|
// Update status back to idle
|
|
this.pool.updateStatus(agentId, 'idle');
|
|
|
|
// Build result
|
|
const result: TaskResult = {
|
|
success: true,
|
|
response: response.content,
|
|
agentId,
|
|
tokenUsage: {
|
|
input: response.usage.inputTokens,
|
|
output: response.usage.outputTokens,
|
|
total: response.usage.totalTokens,
|
|
},
|
|
};
|
|
|
|
this.logger.debug(`Task completed for agent '${agentId}'`);
|
|
|
|
return result;
|
|
} catch (error) {
|
|
// Update status to error
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
this.pool.updateStatus(agentId, 'error', errorMessage);
|
|
|
|
// Check if it's a timeout error
|
|
if (error instanceof Error && error.message.includes('Task execution timed out')) {
|
|
return {
|
|
success: false,
|
|
error: errorMessage,
|
|
agentId,
|
|
};
|
|
}
|
|
|
|
// Re-throw unexpected errors as task failures
|
|
throw RuntimeError.taskFailed(agentId, errorMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get an agent handle by ID
|
|
*/
|
|
getAgent(agentId: string): AgentHandle | undefined {
|
|
return this.pool.get(agentId);
|
|
}
|
|
|
|
/**
|
|
* List agents matching the given filter
|
|
*/
|
|
listAgents(filter?: AgentFilter): AgentHandle[] {
|
|
return this.pool.list(filter);
|
|
}
|
|
|
|
/**
|
|
* Stop a specific agent
|
|
*/
|
|
async stopAgent(agentId: string): Promise<void> {
|
|
const handle = this.pool.get(agentId);
|
|
if (!handle) {
|
|
throw RuntimeError.agentNotFound(agentId);
|
|
}
|
|
|
|
if (handle.status === 'stopped') {
|
|
this.logger.debug(`Agent '${agentId}' already stopped`);
|
|
return;
|
|
}
|
|
|
|
// Update status
|
|
this.pool.updateStatus(agentId, 'stopping');
|
|
|
|
try {
|
|
// Cancel any pending approvals
|
|
handle.agent.services.approvalManager.cancelAllApprovals();
|
|
|
|
// Stop the agent
|
|
await handle.agent.stop();
|
|
|
|
// Update status
|
|
this.pool.updateStatus(agentId, 'stopped');
|
|
|
|
this.logger.debug(`Stopped agent '${agentId}'`);
|
|
|
|
// Remove from pool if ephemeral
|
|
if (handle.ephemeral) {
|
|
this.pool.remove(agentId);
|
|
this.logger.debug(`Removed ephemeral agent '${agentId}' from pool`);
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
this.pool.updateStatus(agentId, 'error', errorMessage);
|
|
this.logger.error(`Failed to stop agent '${agentId}': ${errorMessage}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop all agents matching the given filter
|
|
*/
|
|
async stopAll(filter?: AgentFilter): Promise<void> {
|
|
const agents = this.pool.list(filter);
|
|
|
|
this.logger.debug(`Stopping ${agents.length} agents`);
|
|
|
|
// Stop all in parallel
|
|
await Promise.allSettled(agents.map((handle) => this.stopAgent(handle.agentId)));
|
|
}
|
|
|
|
/**
|
|
* Get the runtime configuration
|
|
*/
|
|
getConfig(): ValidatedAgentRuntimeConfig {
|
|
return { ...this.config };
|
|
}
|
|
|
|
/**
|
|
* Get statistics about the runtime
|
|
*/
|
|
getStats(): { totalAgents: number; byStatus: Record<string, number> } {
|
|
const agents = this.pool.getAll();
|
|
const byStatus: Record<string, number> = {};
|
|
|
|
for (const agent of agents) {
|
|
byStatus[agent.status] = (byStatus[agent.status] ?? 0) + 1;
|
|
}
|
|
|
|
return {
|
|
totalAgents: agents.length,
|
|
byStatus,
|
|
};
|
|
}
|
|
}
|