@continue-reasoning/agent
Version:
A platform-agnostic AI agent framework for building autonomous AI agents with tool execution capabilities
624 lines • 23.9 kB
JavaScript
/**
* @fileoverview BaseAgent Implementation
*
* This file provides the BaseAgent class that connects all the interfaces
* and implements the core agent workflow. It coordinates between IChat,
* IToolScheduler, and AgentEvent system to provide a complete agent experience.
*/
import { AgentEventType, } from './interfaces.js';
import { LogLevel, createLogger } from './logger.js';
/**
* BaseAgent implementation that connects all core interfaces
*
* This class provides the main agent functionality by coordinating:
* - IChat: For conversation management and streaming responses
* - IToolScheduler: For tool execution and management
* - AgentEvent: For event emission and monitoring
*
* The agent follows this workflow:
* 1. Receive user input
* 2. Send to chat for LLM processing
* 3. Extract tool calls from response
* 4. Execute tools via scheduler
* 5. Integrate results back into conversation
* 6. Emit events throughout the process
*
* Key features:
* - Streaming-first approach for real-time responses
* - Comprehensive event emission for monitoring
* - Automatic tool call extraction and execution
* - Proper error handling and state management
* - Thread-safe operation with abort signal support
*
* @example
* ```typescript
* const agent = new BaseAgent(config, chat, toolScheduler);
* agent.onEvent('logger', (event) => console.log(event));
*
* const abortController = new AbortController();
* for await (const event of agent.process('Hello', 'session-1', abortController.signal)) {
* console.log(event);
* }
* ```
*/
export class BaseAgent {
agentConfig;
chat;
toolScheduler;
/** Map of event handler IDs to their handler functions */
eventHandlers = new Map();
/** Current conversation turn number, incremented for each user input */
currentTurn = 0;
/** Flag indicating if the agent is currently processing a request */
isRunning = false;
/** Timestamp of the last status update */
lastUpdateTime = Date.now();
/** Logger instance for this agent */
logger;
/**
* Constructor for BaseAgent
*
* @param config - Agent configuration including model, working directory, etc.
* @param chat - Chat instance for conversation management
* @param toolScheduler - Tool scheduler for executing tool calls
*/
constructor(agentConfig, chat, toolScheduler) {
this.agentConfig = agentConfig;
this.chat = chat;
this.toolScheduler = toolScheduler;
// Initialize logger
this.logger = agentConfig.logger || createLogger('BaseAgent', {
level: agentConfig.logLevel || LogLevel.INFO,
});
this.logger.debug('BaseAgent initialized', 'BaseAgent.constructor()');
this.setupEventHandlers();
}
registerTool(tool) {
this.toolScheduler.registerTool(tool);
}
removeTool(toolName) {
const removed = this.toolScheduler.removeTool(toolName);
return removed;
}
getToolList() {
return this.toolScheduler.getToolList();
}
getTool(toolName) {
return this.toolScheduler.getTool(toolName);
}
/**
* Set up internal event handlers
*/
setupEventHandlers() {
// Handle tool completion events
this.onEvent('internal-tool-completion', (event) => {
if (event.type === AgentEventType.ToolCallResponse) {
// Tool completion is handled in the main process loop
}
});
}
/**
* Main processing method - handles complete conversation flow
*
* This is the primary entry point for processing user input. It orchestrates
* the entire conversation flow including:
* - User input processing
* - LLM response generation (streaming)
* - Tool call extraction and execution
* - Result integration
* - Event emission
*
* The method is designed to be thread-safe and respects abort signals for
* graceful cancellation.
*
* @param userInput - The user's input text
* @param sessionId - Unique identifier for this conversation session
* @param abortSignal - Signal to abort the processing if needed
* @returns AsyncGenerator that yields AgentEvent objects
*
* @example
* ```typescript
* const abortController = new AbortController();
* for await (const event of agent.process('Hello', 'session-1', abortController.signal)) {
* if (event.type === AgentEventType.AssistantMessage) {
* console.log(event.data);
* }
* }
* ```
*/
async *process(userInput, sessionId, abortSignal) {
if (this.isRunning) {
this.logger.warn('Agent is already processing a request', 'BaseAgent.process()');
yield this.createErrorEvent('Agent is already processing a request');
return;
}
this.isRunning = true;
this.logger.info(`Starting to process user input: "${userInput.slice(0, 50)}${userInput.length > 50 ? '...' : ''}"`, 'BaseAgent.process()');
try {
// 1. Create initial user content
const userContent = this.createUserContent(userInput, sessionId);
yield this.createEvent(AgentEventType.UserMessage, {
type: 'user_input',
content: userInput,
sessionId,
turn: this.currentTurn,
});
// 2. Create initial chat message
let currentChatMessage = {
content: userContent.parts,
};
// 3. Process turns until no more tool calls
while (!abortSignal.aborted) {
this.logger.debug(`Processing turn ${this.currentTurn + 1}`, 'BaseAgent.process()');
// Process one turn
let hasToolCalls = false;
let toolCallResponses = [];
for await (const event of this.processOneTurn(sessionId, currentChatMessage, abortSignal)) {
yield event;
// Track if we have tool calls in this turn
if (event.type === AgentEventType.ToolCallResponse) {
hasToolCalls = true;
const eventData = event.data;
toolCallResponses.push(eventData.toolResponse);
}
}
// If no tool calls in this turn, we're done
if (!hasToolCalls) {
this.logger.debug('No tool calls in this turn, conversation complete', 'BaseAgent.process()');
break;
}
this.logger.debug(`Turn completed with ${toolCallResponses.length} tool call responses`, 'BaseAgent.process()');
// 4. Convert tool call responses to chat message for next turn
currentChatMessage = this.convertToolCallResponsesToChatMessage(toolCallResponses);
}
}
catch (error) {
this.logger.error(`Error during processing: ${error instanceof Error ? error.message : String(error)}`, 'BaseAgent.process()');
yield this.createErrorEvent(error instanceof Error ? error.message : String(error));
}
finally {
this.isRunning = false;
this.lastUpdateTime = Date.now();
this.logger.debug('Processing completed', 'BaseAgent.process()');
}
}
/**
* Process one turn of conversation
*
* This method processes a single turn of conversation, handling:
* - LLM response generation (streaming)
* - Tool call extraction and execution
* - Event emission
*
* @param sessionId - Unique identifier for this conversation session
* @param chatMessage - The chat message to process
* @param abortSignal - Signal to abort the processing if needed
* @returns AsyncGenerator that yields AgentEvent objects
*/
async *processOneTurn(sessionId, chatMessage, abortSignal) {
this.currentTurn++;
this.logger.debug(`Starting turn ${this.currentTurn}`, 'BaseAgent.processOneTurn()');
try {
const promptId = this.generatePromptId();
this.logger.debug(`Generated prompt ID: ${promptId}`, 'BaseAgent.processOneTurn()');
const responseStream = await this.chat.sendMessageStream(chatMessage, promptId);
// Process streaming response
let fullResponse = '';
const toolCalls = [];
for await (const chunk of responseStream) {
if (abortSignal.aborted)
break;
// Extract content from chunk
const chunkContent = this.extractContentFromChunk(chunk);
if (chunkContent) {
fullResponse += chunkContent;
yield this.createEvent(AgentEventType.AssistantMessage, {
type: 'assistant_chunk',
content: chunkContent,
sessionId,
turn: this.currentTurn,
});
}
// Extract tool calls from chunk
const chunkToolCalls = this.extractToolCallsFromChunk(chunk);
if (chunkToolCalls.length > 0) {
toolCalls.push(...chunkToolCalls);
}
// Emit token usage if available
if (chunk.usage) {
yield this.createEvent(AgentEventType.TokenUsage, {
usage: chunk.usage,
sessionId,
turn: this.currentTurn,
});
}
}
// Execute tools if any were found
if (toolCalls.length > 0) {
this.logger.info(`Executing ${toolCalls.length} tool calls`, 'BaseAgent.processOneTurn()');
yield* this.executeTools(toolCalls, sessionId, abortSignal);
}
// Emit completion event
this.logger.debug(`Turn ${this.currentTurn} completed with ${toolCalls.length} tool calls`, 'BaseAgent.processOneTurn()');
yield this.createEvent(AgentEventType.TurnComplete, {
type: 'turn_complete',
sessionId,
turn: this.currentTurn,
fullResponse,
toolCallsCount: toolCalls.length,
});
}
catch (error) {
this.logger.error(`Error in turn ${this.currentTurn}: ${error instanceof Error ? error.message : String(error)}`, 'BaseAgent.processOneTurn()');
yield this.createErrorEvent(error instanceof Error ? error.message : String(error));
}
finally {
this.isRunning = false;
this.lastUpdateTime = Date.now();
}
}
/**
* Execute tools and handle results
*
* This method handles the complete tool execution lifecycle:
* 1. Emit tool call request events
* 2. Schedule tools for execution via the tool scheduler
* 3. Wait for tool completion
* 4. Process and emit tool results
* 5. Integrate results back into conversation
*
* The method properly handles errors, timeouts, and abort signals.
*
* @param toolCalls - Array of tool call requests to execute
* @param sessionId - Current session identifier
* @param abortSignal - Signal to abort tool execution
* @returns AsyncGenerator yielding tool-related events
*
* @private
*/
async *executeTools(toolCalls, sessionId, abortSignal) {
this.logger.debug(`Executing ${toolCalls.length} tools: ${toolCalls.map(tc => tc.name).join(', ')}`, 'BaseAgent.executeTools()');
// Emit tool call request events
for (const toolCall of toolCalls) {
this.logger.debug(`Emitting tool call request: ${toolCall.name} (${toolCall.callId})`, 'BaseAgent.executeTools()');
const toolCallRequest = {
callId: toolCall.callId,
name: toolCall.name,
args: toolCall.args,
isClientInitiated: toolCall.isClientInitiated,
promptId: toolCall.promptId,
};
yield this.createEvent(AgentEventType.ToolCallRequest, {
toolCall: toolCallRequest,
sessionId,
turn: this.currentTurn,
});
}
// Schedule tools for execution
this.logger.debug('Scheduling tools for execution', 'BaseAgent.executeTools()');
await this.toolScheduler.schedule(toolCalls, abortSignal);
// Wait for completion and get only current turn's completed calls
this.logger.debug('Waiting for tool completion', 'BaseAgent.executeTools()');
const completedCalls = await this.waitForCurrentToolCompletion(toolCalls, abortSignal);
// Process completed tools
this.logger.info(`Processing ${completedCalls.length} completed tool calls`, 'BaseAgent.executeTools()');
for (const completedCall of completedCalls) {
const status = completedCall.response.error ? 'error' : 'success';
this.logger.debug(`Tool ${completedCall.request.name} (${completedCall.request.callId}) completed with status: ${status}`, 'BaseAgent.executeTools()');
const toolCallResponse = {
callId: completedCall.request.callId,
content: this.convertToolResultToContent(completedCall),
...(completedCall.response.resultDisplay && { display: completedCall.response.resultDisplay }),
...(completedCall.response.error && { error: completedCall.response.error.message }),
};
yield this.createEvent(AgentEventType.ToolCallResponse, {
toolResponse: toolCallResponse,
sessionId,
turn: this.currentTurn,
});
}
// Add tool results to chat history
if (completedCalls.length > 0) {
this.logger.debug(`Adding ${completedCalls.length} tool results to chat history`, 'BaseAgent.executeTools()');
const toolResultContent = this.convertToolCallsToContent(completedCalls);
this.chat.addHistory(toolResultContent);
}
}
/**
* Wait for completion of current turn's tool calls only
*
* This method waits for the specific tool calls from the current turn to complete,
* filtering out any previously completed tool calls from other turns.
*
* @param currentToolCalls - The tool calls from the current turn
* @param abortSignal - Signal to abort waiting
* @returns Promise resolving to array of completed tool calls from current turn only
*
* @private
*/
async waitForCurrentToolCompletion(currentToolCalls, abortSignal) {
const maxWaitTime = 30000; // 30 seconds
const startTime = Date.now();
const currentCallIds = new Set(currentToolCalls.map(call => call.callId));
while (this.toolScheduler.isRunning() && !abortSignal.aborted) {
if (Date.now() - startTime > maxWaitTime) {
this.toolScheduler.cancelAll('Timeout waiting for tool completion');
break;
}
await this.wait(100);
}
// Return only completed calls from the current turn
return this.toolScheduler.getCurrentToolCalls().filter(call => {
const isCompleted = call.status === 'success' || call.status === 'error' || call.status === 'cancelled';
const isCurrentTurn = currentCallIds.has(call.request.callId);
return isCompleted && isCurrentTurn;
});
}
/**
* Create user content from input
*
* Converts user input string into a ConversationContent object
* with proper metadata and formatting.
*
* @param userInput - The user's input text
* @param sessionId - Current session identifier
* @returns ConversationContent object representing the user input
*
* @private
*/
createUserContent(userInput, sessionId) {
const textPart = {
type: 'text',
text: userInput,
};
return {
role: 'user',
parts: [textPart],
metadata: {
sessionId,
timestamp: Date.now(),
turn: this.currentTurn,
},
};
}
/**
* Extract content from LLM response chunk
*
* Extracts text content from a streaming LLM response chunk.
* Filters for text-type content parts and concatenates their text.
*
* @param chunk - LLM response chunk
* @returns Extracted text content or null if no text found
*
* @private
*/
extractContentFromChunk(chunk) {
const textParts = chunk.content.parts.filter(part => part.type === 'text');
if (textParts.length > 0) {
return textParts.map(part => part.text || '').join('');
}
return null;
}
/**
* Extract tool calls from LLM response chunk
*
* Scans a streaming LLM response chunk for function call parts
* and converts them to tool call request format.
*
* @param chunk - LLM response chunk to scan
* @returns Array of tool call request info objects
*
* @private
*/
extractToolCallsFromChunk(chunk) {
const toolCalls = [];
for (const part of chunk.content.parts) {
if (part.type === 'function_call' && part.functionCall) {
toolCalls.push({
callId: part.functionCall.id,
name: part.functionCall.name,
args: part.functionCall.args,
isClientInitiated: false,
promptId: this.generatePromptId(),
});
}
}
return toolCalls;
}
/**
* Convert tool result to content parts
*/
convertToolResultToContent(completedCall) {
// Convert string response to proper object format for Gemini API
let result;
if (typeof completedCall.response.responseParts === 'string') {
// Wrap string responses in an object with a result field
result = { result: completedCall.response.responseParts };
}
else if (completedCall.response.responseParts && typeof completedCall.response.responseParts === 'object') {
// Use object responses as-is
result = completedCall.response.responseParts;
}
else {
// Fallback for other types
result = { result: String(completedCall.response.responseParts) };
}
const resultPart = {
type: 'function_response',
functionResponse: {
id: completedCall.request.callId,
name: completedCall.request.name,
result: result,
},
};
return [resultPart];
}
/**
* Convert completed tool calls to conversation content
*/
convertToolCallsToContent(completedCalls) {
const parts = [];
for (const call of completedCalls) {
parts.push(...this.convertToolResultToContent(call));
}
return {
role: 'function',
parts,
metadata: {
timestamp: Date.now(),
toolCallsCount: completedCalls.length,
turn: this.currentTurn,
},
};
}
/**
* Convert tool call responses to a single chat message for the next turn.
* This method aggregates all tool call responses into a single message.
*
* @param toolCallResponses - Array of ToolCallResponse events from the current turn.
* @returns A single ChatMessage object containing all tool call results.
*/
convertToolCallResponsesToChatMessage(toolCallResponses) {
const parts = [];
for (const toolCallResponse of toolCallResponses) {
// ToolCallResponse.content is already ContentPart[] with proper function_response structure
parts.push(...toolCallResponse.content);
}
return {
content: parts,
};
}
// ============================================================================
// EVENT SYSTEM
// ============================================================================
/**
* Register event handler
*/
onEvent(id, handler) {
this.eventHandlers.set(id, handler);
}
/**
* Remove event handler
*/
offEvent(id) {
this.eventHandlers.delete(id);
}
/**
* Create and emit event
*/
createEvent(type, data) {
const event = {
type,
data,
timestamp: Date.now(),
metadata: {
agentId: this.agentConfig.sessionId,
turn: this.currentTurn,
},
};
this.emitEvent(event);
return event;
}
/**
* Create error event
*/
createErrorEvent(message) {
return this.createEvent(AgentEventType.Error, {
message,
timestamp: Date.now(),
turn: this.currentTurn,
});
}
/**
* Emit event to all handlers
*/
emitEvent(event) {
for (const handler of this.eventHandlers.values()) {
try {
handler(event);
}
catch (error) {
console.error('Error in event handler:', error);
}
}
}
// ============================================================================
// INTERFACE IMPLEMENTATION
// ============================================================================
/**
* Get the underlying chat instance
*/
getChat() {
return this.chat;
}
/**
* Get the tool scheduler instance
*/
getToolScheduler() {
return this.toolScheduler;
}
/**
* Get current token usage
*/
getTokenUsage() {
return this.chat.getTokenUsage();
}
/**
* Clear conversation history
*/
clearHistory() {
this.chat.clearHistory();
this.currentTurn = 0;
this.emitEvent(this.createEvent(AgentEventType.HistoryCleared, {
type: 'history_cleared',
timestamp: Date.now(),
}));
}
/**
* Set system prompt
*/
setSystemPrompt(systemPrompt) {
this.chat.setSystemPrompt(systemPrompt);
this.emitEvent(this.createEvent(AgentEventType.SystemPromptSet, {
type: 'system_prompt_set',
systemPrompt,
timestamp: Date.now(),
}));
}
/**
* Get current system prompt
*/
getSystemPrompt() {
return this.chat.getSystemPrompt();
}
/**
* Get current agent status
*/
getStatus() {
return {
isRunning: this.isRunning,
currentTurn: this.currentTurn,
historySize: this.chat.getHistory().length,
config: this.agentConfig,
lastUpdateTime: this.lastUpdateTime,
tokenUsage: this.getTokenUsage(),
modelInfo: this.chat.getModelInfo(),
};
}
// ============================================================================
// UTILITY METHODS
// ============================================================================
/**
* Generate unique prompt ID
*/
generatePromptId() {
return `prompt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Wait for specified time
*/
wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
//# sourceMappingURL=baseAgent.js.map