capsule-ai-cli
Version:
The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing
238 lines (236 loc) • 9.2 kB
JavaScript
import { v4 as uuidv4 } from 'uuid';
import { EventEmitter } from 'events';
import { ChatService } from './chat.js';
import { contextManager as globalContextManager } from './context.js';
import { providerRegistry } from '../providers/base.js';
import { stateService } from './state.js';
import { updateTodoByDescription } from '../tools/builtin/todo-list.js';
export class SubAgentManager extends EventEmitter {
tasks = new Map();
activeTasks = new Set();
constructor() {
super();
}
emit(event, ...args) {
return super.emit(event, ...args);
}
on(event, listener) {
return super.on(event, listener);
}
async spawnTask(config) {
const task = {
taskId: uuidv4().substring(0, 8),
name: config.name,
description: config.description,
status: 'pending',
provider: config.provider,
model: config.model,
toolCalls: [],
tokensUsed: 0,
cost: 0,
startTime: new Date()
};
this.tasks.set(task.taskId, task);
this.emit('task:created', task);
if (config.parallel !== false) {
this.executeSubAgent(task).catch(error => {
task.status = 'failed';
task.error = error.message;
this.emit('task:failed', task);
});
}
else {
await this.executeSubAgent(task);
}
return task;
}
async executeSubAgent(task) {
const originalContextId = globalContextManager.getCurrentContext().id;
try {
task.status = 'running';
this.activeTasks.add(task.taskId);
this.emit('task:started', task);
const context = globalContextManager.createNewContext();
globalContextManager.setCurrentContext(context.id);
const chatService = new ChatService();
const abortController = new AbortController();
const contextManager = globalContextManager;
task.agent = {
context,
contextManager,
chatService,
abortController
};
this.setupToolCallTracking(task);
this.emit('task:progress', task.taskId, 'Starting task execution...');
const contextPath = process.cwd();
updateTodoByDescription(contextPath, task.name, { status: 'in_progress' });
const agentPrompt = `You are a sub-agent tasked with the following:
${task.description}
Please complete this task using any available tools. Be thorough and provide detailed results.`;
if (task.provider) {
stateService.setProvider(task.provider);
}
if (task.model) {
stateService.setModel(task.model);
}
const response = await chatService.chat(agentPrompt, {
mode: 'agent',
provider: task.provider || stateService.getProvider(),
model: task.model || stateService.getModel(),
stream: false
});
const toolCallMatch = response.content.match(/\[Used tools: ([^\]]+)\]/);
;
if (toolCallMatch) {
const toolNames = toolCallMatch[1].split(', ');
for (const toolName of toolNames) {
if (!task.toolCalls.find(tc => tc.name === toolName)) {
task.toolCalls.push({
id: uuidv4(),
name: toolName,
arguments: {}
});
}
}
}
task.result = response.content;
task.tokensUsed = response.usage.totalTokens;
const providerName = task.provider || 'openai';
const model = task.model || 'gpt-4o';
const provider = providerRegistry.get(providerName);
if (provider) {
const cost = provider.calculateCost(response.usage, model);
task.cost = cost.amount;
}
else {
task.cost = 0;
}
task.status = 'completed';
task.endTime = new Date();
const completionPath = process.cwd();
updateTodoByDescription(completionPath, task.name, {
status: 'completed',
metadata: {
actualTime: task.endTime.getTime() - task.startTime.getTime(),
completionNotes: `Completed by sub-agent ${task.taskId}`
}
});
this.emit('task:completed', task);
}
catch (error) {
if (error.name === 'AbortError') {
task.status = 'cancelled';
const cancelPath = process.cwd();
updateTodoByDescription(cancelPath, task.name, { status: 'cancelled' });
this.emit('task:cancelled', task);
}
else {
task.status = 'failed';
task.error = error.message || 'Unknown error';
task.endTime = new Date();
const blockPath = process.cwd();
updateTodoByDescription(blockPath, task.name, {
status: 'blocked',
metadata: { blockedReason: error.message || 'Task failed' }
});
this.emit('task:failed', task);
}
}
finally {
globalContextManager.setCurrentContext(originalContextId);
this.activeTasks.delete(task.taskId);
}
}
setupToolCallTracking(task) {
const toolCallHandler = (toolCall) => {
if (toolCall && toolCall.name) {
task.toolCalls.push({
id: toolCall.id || uuidv4(),
name: toolCall.name,
arguments: toolCall.arguments || {}
});
this.emit('task:tool_call', task.taskId, toolCall);
}
};
task.toolCallHandler = toolCallHandler;
}
getTask(taskId) {
return this.tasks.get(taskId);
}
getAllTasks() {
return Array.from(this.tasks.values());
}
getActiveTasks() {
return Array.from(this.activeTasks).map(id => this.tasks.get(id));
}
async cancelTask(taskId) {
const task = this.tasks.get(taskId);
if (!task || task.status !== 'running') {
return false;
}
task.agent?.abortController.abort();
task.status = 'cancelled';
task.endTime = new Date();
this.activeTasks.delete(taskId);
this.emit('task:cancelled', task);
return true;
}
async waitForTask(taskId, timeoutMs) {
const task = this.tasks.get(taskId);
if (!task) {
throw new Error(`Task ${taskId} not found`);
}
if (task.status !== 'pending' && task.status !== 'running') {
return task;
}
return new Promise((resolve, reject) => {
const timeout = timeoutMs ? setTimeout(() => {
reject(new Error(`Timeout waiting for task ${taskId}`));
}, timeoutMs) : null;
const checkStatus = () => {
const currentTask = this.tasks.get(taskId);
if (!currentTask) {
if (timeout)
clearTimeout(timeout);
reject(new Error('Task disappeared while waiting'));
return;
}
if (currentTask.status === 'completed' ||
currentTask.status === 'failed' ||
currentTask.status === 'cancelled') {
if (timeout)
clearTimeout(timeout);
resolve(currentTask);
}
};
this.on('task:completed', (completedTask) => {
if (completedTask.taskId === taskId)
checkStatus();
});
this.on('task:failed', (failedTask) => {
if (failedTask.taskId === taskId)
checkStatus();
});
this.on('task:cancelled', (cancelledTask) => {
if (cancelledTask.taskId === taskId)
checkStatus();
});
});
}
getStats() {
const tasks = this.getAllTasks();
return {
total: tasks.length,
pending: tasks.filter(t => t.status === 'pending').length,
running: tasks.filter(t => t.status === 'running').length,
completed: tasks.filter(t => t.status === 'completed').length,
failed: tasks.filter(t => t.status === 'failed').length,
cancelled: tasks.filter(t => t.status === 'cancelled').length,
totalTokensUsed: tasks.reduce((sum, t) => sum + t.tokensUsed, 0),
totalCost: tasks.reduce((sum, t) => sum + t.cost, 0)
};
}
}
export const subAgentManager = new SubAgentManager();
//# sourceMappingURL=sub-agent-manager.js.map