UNPKG

@continue-reasoning/agent

Version:

A platform-agnostic AI agent framework for building autonomous AI agents with tool execution capabilities

523 lines 19 kB
/** * @fileoverview Core Tool Scheduler Implementation * * This module provides the core tool scheduling and execution system for AI agents. * It manages tool lifecycle, execution state, confirmation workflows, and error handling. * The implementation references the core package patterns but uses our interface system. */ import { ToolCallStatus, ToolConfirmationOutcome, } from './interfaces.js'; /** * Core tool scheduler implementation * * This class manages the complete lifecycle of tool execution including: * - Request validation and scheduling * - Confirmation workflows for destructive operations * - Parallel and sequential execution management * - Error handling and retry logic * - State tracking and event emission * * The implementation follows these principles: * - Non-blocking operation with async/await patterns * - Comprehensive state management for all tool calls * - Flexible confirmation system for user safety * - Robust error handling with proper cleanup * - Event-driven architecture for monitoring * * @example * ```typescript * const scheduler = new CoreToolScheduler({ * toolRegistry: toolRegistryPromise, * outputUpdateHandler: (id, output) => console.log(`Tool ${id}: ${output}`), * onAllToolCallsComplete: (completed) => console.log('All tools done'), * }); * * await scheduler.schedule([toolCallRequest], abortSignal); * ``` */ export class CoreToolScheduler { config; /** Map of tool call IDs to their current state */ toolCalls = new Map(); /** Current execution state */ isCurrentlyRunning = false; /** Available tools registry */ toolRegistry = new Map(); /** Promise that resolves when registry is ready */ registryReady; /** Abort controller for canceling all operations */ abortController; constructor(config) { this.config = config; // Support both new interface (tools array) and old interface (toolRegistry promise) if (config.tools) { this.toolRegistry = new Map(config.tools.map(tool => [tool.name, tool])); this.registryReady = Promise.resolve(); } else if (config.toolRegistry) { // Initialize async for backward compatibility this.registryReady = config.toolRegistry.then(registry => { this.toolRegistry = registry; }); } else { this.registryReady = Promise.resolve(); } } /** * Schedule tool call(s) for execution * * This is the main entry point for tool execution. It processes requests, * validates tools, handles confirmations, and manages execution lifecycle. * * @param request - Single tool call or array of tool calls * @param signal - Abort signal for cancellation */ async schedule(request, signal) { const requests = Array.isArray(request) ? request : [request]; if (requests.length === 0) { return; } this.isCurrentlyRunning = true; this.abortController = new AbortController(); // Link external abort signal to internal controller if (signal.aborted) { this.abortController.abort(); return; } signal.addEventListener('abort', () => { this.abortController?.abort(); }); try { // Phase 1: Validate all tool calls await this.validateToolCalls(requests); // Phase 2: Handle confirmations for tools that require them await this.handleConfirmations(); // Phase 3: Execute approved tools await this.executeApprovedTools(); // Phase 4: Wait for completion and cleanup await this.waitForCompletion(); } catch (error) { // Handle errors and cleanup await this.handleSchedulingError(error); } finally { this.isCurrentlyRunning = false; this.notifyCompletion(); } } /** * Handle confirmation response for a tool call * * Processes user responses to confirmation prompts and updates * tool call state accordingly. * * @param callId - Tool call identifier * @param outcome - User's confirmation decision * @param payload - Optional payload for modifications */ async handleConfirmationResponse(_callId, outcome, payload) { const callId = _callId; const toolCall = this.toolCalls.get(callId); if (!toolCall || toolCall.status !== ToolCallStatus.AwaitingApproval) { console.warn(`Cannot handle confirmation for tool call ${callId}: invalid state`); return; } const waitingCall = toolCall; try { switch (outcome) { case ToolConfirmationOutcome.ProceedOnce: case ToolConfirmationOutcome.ProceedAlways: case ToolConfirmationOutcome.ProceedAlwaysServer: case ToolConfirmationOutcome.ProceedAlwaysTool: // Approve and execute await this.approveAndExecuteToolCall(waitingCall, outcome); break; case ToolConfirmationOutcome.ModifyWithEditor: // Handle modification with editor await this.handleModificationRequest(waitingCall, payload); break; case ToolConfirmationOutcome.Cancel: // Cancel the tool call await this.cancelToolCall(waitingCall, 'User cancelled'); break; } } catch (error) { await this.handleToolCallError(waitingCall, error); } this.notifyUpdate(); } registerTool(tool) { this.toolRegistry.set(tool.name, tool); } removeTool(toolName) { return this.toolRegistry.delete(toolName); } getTool(name) { // Note: This is synchronous, so for backward compatibility we return undefined if registry not ready // In practice, this should be called after initialization const tool = this.toolRegistry.get(name); return tool; } getToolList() { return Array.from(this.toolRegistry.values()); } /** * Get current tool calls * * @returns Array of all current tool calls */ getCurrentToolCalls() { return Array.from(this.toolCalls.values()); } /** * Check if scheduler is currently running * * @returns True if scheduler is processing tool calls */ isRunning() { return this.isCurrentlyRunning; } /** * Cancel all pending tool calls * * Cancels all tool calls that are not yet completed and cleans up resources. * * @param reason - Reason for cancellation */ cancelAll(reason) { for (const [_callId, toolCall] of this.toolCalls) { if (this.isCompletedToolCall(toolCall)) { continue; // Don't cancel completed calls } this.cancelToolCallSync(toolCall, reason); } this.abortController?.abort(); this.notifyUpdate(); } // ============================================================================ // PRIVATE IMPLEMENTATION METHODS // ============================================================================ /** * Validate all tool call requests */ async validateToolCalls(requests) { for (const request of requests) { await this.validateSingleToolCall(request); } } /** * Validate a single tool call request */ async validateSingleToolCall(request) { // Create validating tool call - will populate tool in try block let toolCall; try { // Resolve tool first const tool = await this.resolveToolForRequest(request); toolCall = { status: ToolCallStatus.Validating, request, startTime: Date.now(), tool, }; this.toolCalls.set(request.callId, toolCall); this.notifyUpdate(); // Validate tool parameters const validationError = toolCall.tool.validateToolParams(request.args); if (validationError) { throw new Error(`Tool parameter validation failed: ${validationError}`); } // Check if tool requires confirmation const confirmationDetails = await toolCall.tool.shouldConfirmExecute(request.args, this.abortController?.signal || new AbortController().signal); if (confirmationDetails) { // Move to awaiting approval state const waitingCall = { ...toolCall, status: ToolCallStatus.AwaitingApproval, confirmationDetails, }; this.toolCalls.set(request.callId, waitingCall); this.notifyUpdate(); } else { // Move to scheduled state const scheduledCall = { ...toolCall, status: ToolCallStatus.Scheduled, }; this.toolCalls.set(request.callId, scheduledCall); this.notifyUpdate(); } } catch (error) { // If toolCall wasn't created yet (tool not found), create a minimal one for error handling if (!toolCall) { toolCall = { status: ToolCallStatus.Validating, request, startTime: Date.now(), tool: {}, // Dummy tool for error case }; this.toolCalls.set(request.callId, toolCall); } await this.handleToolCallError(toolCall, error); } } /** * Resolve tool instance for a request */ async resolveToolForRequest(request) { // Ensure registry is loaded for backward compatibility await this.registryReady; const tool = this.toolRegistry.get(request.name); if (!tool) { throw new Error(`Tool '${request.name}' not found in registry`); } return tool; } /** * Handle confirmations for tools that require them */ async handleConfirmations() { const waitingCalls = this.getToolCallsByStatus(ToolCallStatus.AwaitingApproval); if (waitingCalls.length === 0) { return; } // Handle confirmations based on approval mode switch (this.config.approvalMode) { case 'yolo': // Auto-approve all tools for (const call of waitingCalls) { await this.approveAndExecuteToolCall(call, ToolConfirmationOutcome.ProceedAlways); } break; case 'always': // Wait for manual confirmation of each tool // In a real implementation, this would trigger UI prompts // For now, we'll wait for external confirmation via handleConfirmationResponse break; case 'default': default: // Use tool-specific confirmation logic // For now, similar to 'always' mode break; } } /** * Execute all approved (scheduled) tools */ async executeApprovedTools() { const scheduledCalls = this.getToolCallsByStatus(ToolCallStatus.Scheduled); // Execute tools in parallel (could be made configurable) const executionPromises = scheduledCalls.map(call => this.executeToolCall(call)); await Promise.allSettled(executionPromises); } /** * Execute a single tool call */ async executeToolCall(scheduledCall) { // Move to executing state const executingCall = { ...scheduledCall, status: ToolCallStatus.Executing, liveOutput: '', }; this.toolCalls.set(scheduledCall.request.callId, executingCall); this.notifyUpdate(); try { // Set up output update handler const updateOutput = (output) => { executingCall.liveOutput = (executingCall.liveOutput || '') + output; this.notifyOutputUpdate(scheduledCall.request.callId, output); this.notifyUpdate(); }; // Execute the tool const result = await executingCall.tool.execute(scheduledCall.request.args, this.abortController?.signal || new AbortController().signal, updateOutput); // Move to success state const successCall = { ...executingCall, status: ToolCallStatus.Success, response: { callId: scheduledCall.request.callId, responseParts: result.llmContent, resultDisplay: result.returnDisplay, }, durationMs: Date.now() - (scheduledCall.startTime || Date.now()), }; this.toolCalls.set(scheduledCall.request.callId, successCall); this.notifyUpdate(); } catch (error) { await this.handleToolCallError(executingCall, error); } } /** * Approve and execute a waiting tool call */ async approveAndExecuteToolCall(waitingCall, outcome) { // Move to scheduled state first const scheduledCall = { ...waitingCall, status: ToolCallStatus.Scheduled, outcome, }; this.toolCalls.set(waitingCall.request.callId, scheduledCall); this.notifyUpdate(); // Then execute await this.executeToolCall(scheduledCall); } /** * Handle modification request from user */ async handleModificationRequest(waitingCall, payload) { if (!payload?.newContent) { await this.cancelToolCall(waitingCall, 'No modification content provided'); return; } // Create modified scheduled call (skip confirmation since user already approved modification) const scheduledCall = { ...waitingCall, status: ToolCallStatus.Scheduled, request: { ...waitingCall.request, args: { ...waitingCall.request.args, // Apply the modification (this would depend on tool type) content: payload.newContent, }, }, outcome: ToolConfirmationOutcome.ModifyWithEditor, }; this.toolCalls.set(waitingCall.request.callId, scheduledCall); this.notifyUpdate(); // Execute the modified tool call await this.executeToolCall(scheduledCall); } /** * Cancel a tool call */ async cancelToolCall(toolCall, reason) { this.cancelToolCallSync(toolCall, reason); this.notifyUpdate(); } /** * Cancel tool call synchronously */ cancelToolCallSync(toolCall, reason) { const cancelledCall = { ...toolCall, status: ToolCallStatus.Cancelled, tool: 'tool' in toolCall ? toolCall.tool : {}, response: { callId: toolCall.request.callId, responseParts: `Tool call cancelled: ${reason}`, error: new Error(reason), }, durationMs: Date.now() - (toolCall.startTime || Date.now()), }; this.toolCalls.set(toolCall.request.callId, cancelledCall); } /** * Handle tool call error */ async handleToolCallError(toolCall, error) { const errorMessage = error instanceof Error ? error.message : String(error); const erroredCall = { ...toolCall, status: ToolCallStatus.Error, response: { callId: toolCall.request.callId, responseParts: `Tool execution failed: ${errorMessage}`, error: error instanceof Error ? error : new Error(errorMessage), }, durationMs: Date.now() - (toolCall.startTime || Date.now()), }; this.toolCalls.set(toolCall.request.callId, erroredCall); this.notifyUpdate(); } /** * Handle scheduling error */ async handleSchedulingError(error) { console.error('Tool scheduling error:', error); // Cancel any incomplete tool calls for (const [, toolCall] of this.toolCalls) { if (!this.isCompletedToolCall(toolCall)) { this.cancelToolCallSync(toolCall, 'Scheduling error'); } } } /** * Wait for all tool calls to complete */ async waitForCompletion() { const maxWaitTime = 60000; // 60 seconds const startTime = Date.now(); while (this.hasIncompleteToolCalls() && !this.abortController?.signal.aborted) { if (Date.now() - startTime > maxWaitTime) { this.cancelAll('Timeout waiting for tool completion'); break; } await this.wait(100); } } /** * Check if there are incomplete tool calls */ hasIncompleteToolCalls() { for (const toolCall of this.toolCalls.values()) { if (!this.isCompletedToolCall(toolCall)) { return true; } } return false; } /** * Check if tool call is completed */ isCompletedToolCall(toolCall) { return toolCall.status === ToolCallStatus.Success || toolCall.status === ToolCallStatus.Error || toolCall.status === ToolCallStatus.Cancelled; } /** * Get tool calls by status */ getToolCallsByStatus(status) { return Array.from(this.toolCalls.values()).filter(call => call.status === status); } /** * Get completed tool calls */ getCompletedToolCalls() { return Array.from(this.toolCalls.values()).filter(this.isCompletedToolCall); } /** * Notify about output updates */ notifyOutputUpdate(callId, output) { this.config.outputUpdateHandler?.(callId, output); } /** * Notify about tool calls update */ notifyUpdate() { this.config.onToolCallsUpdate?.(this.getCurrentToolCalls()); } /** * Notify about completion */ notifyCompletion() { const completedCalls = this.getCompletedToolCalls(); this.config.onAllToolCallsComplete?.(completedCalls); } /** * Wait for specified time */ wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } //# sourceMappingURL=coreToolScheduler.js.map