@probelabs/probe
Version: 
Node.js wrapper for the probe code search tool
1,108 lines (958 loc) • 59.2 kB
JavaScript
// Core ProbeAgent class adapted from examples/chat/probeChat.js
import { createAnthropic } from '@ai-sdk/anthropic';
import { createOpenAI } from '@ai-sdk/openai';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { streamText } from 'ai';
import { randomUUID } from 'crypto';
import { EventEmitter } from 'events';
import { TokenCounter } from './tokenCounter.js';
import { 
  createTools,
  searchToolDefinition,
  queryToolDefinition,
  extractToolDefinition,
  listFilesToolDefinition,
  searchFilesToolDefinition,
  attemptCompletionToolDefinition,
  implementToolDefinition,
  attemptCompletionSchema,
  parseXmlToolCallWithThinking
} from './tools.js';
import { createMessagePreview } from '../tools/common.js';
import { 
  createWrappedTools, 
  listFilesToolInstance, 
  searchFilesToolInstance,
  clearToolExecutionData 
} from './probeTool.js';
import { listFilesByLevel } from '../index.js';
import { 
  cleanSchemaResponse,
  isJsonSchema,
  validateJsonResponse,
  createJsonCorrectionPrompt,
  isJsonSchemaDefinition,
  createSchemaDefinitionCorrectionPrompt,
  validateAndFixMermaidResponse
} from './schemaUtils.js';
// Maximum tool iterations to prevent infinite loops - configurable via MAX_TOOL_ITERATIONS env var
const MAX_TOOL_ITERATIONS = parseInt(process.env.MAX_TOOL_ITERATIONS || '30', 10);
const MAX_HISTORY_MESSAGES = 100;
/**
 * ProbeAgent class to handle AI interactions with code search capabilities
 */
export class ProbeAgent {
  /**
   * Create a new ProbeAgent instance
   * @param {Object} options - Configuration options
   * @param {string} [options.sessionId] - Optional session ID
   * @param {string} [options.customPrompt] - Custom prompt to replace the default system message
   * @param {string} [options.promptType] - Predefined prompt type (architect, code-review, support)
   * @param {boolean} [options.allowEdit=false] - Allow the use of the 'implement' tool
   * @param {string} [options.path] - Search directory path
   * @param {string} [options.provider] - Force specific AI provider
   * @param {string} [options.model] - Override model name
   * @param {boolean} [options.debug] - Enable debug mode
   * @param {boolean} [options.outline] - Enable outline-xml format for search results
   * @param {number} [options.maxResponseTokens] - Maximum tokens for AI responses
   * @param {boolean} [options.disableMermaidValidation=false] - Disable automatic mermaid diagram validation and fixing
   */
  constructor(options = {}) {
    // Basic configuration
    this.sessionId = options.sessionId || randomUUID();
    this.customPrompt = options.customPrompt || null;
    this.promptType = options.promptType || 'code-explorer';
    this.allowEdit = !!options.allowEdit;
    this.debug = options.debug || process.env.DEBUG === '1';
    this.cancelled = false;
    this.tracer = options.tracer || null;
    this.outline = !!options.outline;
    this.maxResponseTokens = options.maxResponseTokens || parseInt(process.env.MAX_RESPONSE_TOKENS || '0', 10) || null;
    this.disableMermaidValidation = !!options.disableMermaidValidation;
    // Search configuration
    this.allowedFolders = options.path ? [options.path] : [process.cwd()];
    // API configuration
    this.clientApiProvider = options.provider || null;
    this.clientApiKey = null; // Will be set from environment
    this.clientApiUrl = null;
    // Initialize token counter
    this.tokenCounter = new TokenCounter();
    if (this.debug) {
      console.log(`[DEBUG] Generated session ID for agent: ${this.sessionId}`);
      console.log(`[DEBUG] Maximum tool iterations configured: ${MAX_TOOL_ITERATIONS}`);
      console.log(`[DEBUG] Allow Edit (implement tool): ${this.allowEdit}`);
    }
    // Initialize tools
    this.initializeTools();
    // Initialize the AI model
    this.initializeModel();
    // Initialize chat history
    this.history = [];
    
    // Initialize event emitter for tool execution updates
    this.events = new EventEmitter();
  }
  /**
   * Initialize tools with configuration
   */
  initializeTools() {
    const configOptions = {
      sessionId: this.sessionId,
      debug: this.debug,
      defaultPath: this.allowedFolders.length > 0 ? this.allowedFolders[0] : process.cwd(),
      allowedFolders: this.allowedFolders,
      outline: this.outline
    };
    // Create base tools
    const baseTools = createTools(configOptions);
    
    // Create wrapped tools with event emission
    const wrappedTools = createWrappedTools(baseTools);
    // Store tool instances for execution
    this.toolImplementations = {
      search: wrappedTools.searchToolInstance,
      query: wrappedTools.queryToolInstance,
      extract: wrappedTools.extractToolInstance,
      delegate: wrappedTools.delegateToolInstance,
      listFiles: listFilesToolInstance,
      searchFiles: searchFilesToolInstance,
    };
    
    // Store wrapped tools for ACP system
    this.wrappedTools = wrappedTools;
  }
  /**
   * Initialize the AI model based on available API keys and forced provider setting
   */
  initializeModel() {
    // Get API keys from environment variables
    const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
    const openaiApiKey = process.env.OPENAI_API_KEY;
    const googleApiKey = process.env.GOOGLE_API_KEY;
    // Get custom API URLs if provided
    const llmBaseUrl = process.env.LLM_BASE_URL;
    const anthropicApiUrl = process.env.ANTHROPIC_API_URL || llmBaseUrl;
    const openaiApiUrl = process.env.OPENAI_API_URL || llmBaseUrl;
    const googleApiUrl = process.env.GOOGLE_API_URL || llmBaseUrl;
    // Get model override if provided
    const modelName = process.env.MODEL_NAME;
    // Use client-forced provider or environment variable
    const forceProvider = this.clientApiProvider || (process.env.FORCE_PROVIDER ? process.env.FORCE_PROVIDER.toLowerCase() : null);
    if (this.debug) {
      console.log(`[DEBUG] Available API keys: Anthropic=${!!anthropicApiKey}, OpenAI=${!!openaiApiKey}, Google=${!!googleApiKey}`);
      console.log(`[DEBUG] Force provider: ${forceProvider || '(not set)'}`);
      if (modelName) console.log(`[DEBUG] Model override: ${modelName}`);
    }
    // Check if a specific provider is forced
    if (forceProvider) {
      if (forceProvider === 'anthropic' && anthropicApiKey) {
        this.initializeAnthropicModel(anthropicApiKey, anthropicApiUrl, modelName);
        return;
      } else if (forceProvider === 'openai' && openaiApiKey) {
        this.initializeOpenAIModel(openaiApiKey, openaiApiUrl, modelName);
        return;
      } else if (forceProvider === 'google' && googleApiKey) {
        this.initializeGoogleModel(googleApiKey, googleApiUrl, modelName);
        return;
      }
      console.warn(`WARNING: Forced provider "${forceProvider}" selected but required API key is missing or invalid! Falling back to auto-detection.`);
    }
    // If no provider is forced or forced provider failed, use the first available API key
    if (anthropicApiKey) {
      this.initializeAnthropicModel(anthropicApiKey, anthropicApiUrl, modelName);
    } else if (openaiApiKey) {
      this.initializeOpenAIModel(openaiApiKey, openaiApiUrl, modelName);
    } else if (googleApiKey) {
      this.initializeGoogleModel(googleApiKey, googleApiUrl, modelName);
    } else {
      throw new Error('No API key provided. Please set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY environment variable.');
    }
  }
  /**
   * Initialize Anthropic model
   */
  initializeAnthropicModel(apiKey, apiUrl, modelName) {
    this.provider = createAnthropic({
      apiKey: apiKey,
      ...(apiUrl && { baseURL: apiUrl }),
    });
    this.model = modelName || 'claude-opus-4-1-20250805';
    this.apiType = 'anthropic';
    
    if (this.debug) {
      console.log(`Using Anthropic API with model: ${this.model}${apiUrl ? ` (URL: ${apiUrl})` : ''}`);
    }
  }
  /**
   * Initialize OpenAI model
   */
  initializeOpenAIModel(apiKey, apiUrl, modelName) {
    this.provider = createOpenAI({
      compatibility: 'strict',
      apiKey: apiKey,
      ...(apiUrl && { baseURL: apiUrl }),
    });
    this.model = modelName || 'gpt-5-thinking';
    this.apiType = 'openai';
    
    if (this.debug) {
      console.log(`Using OpenAI API with model: ${this.model}${apiUrl ? ` (URL: ${apiUrl})` : ''}`);
    }
  }
  /**
   * Initialize Google model
   */
  initializeGoogleModel(apiKey, apiUrl, modelName) {
    this.provider = createGoogleGenerativeAI({
      apiKey: apiKey,
      ...(apiUrl && { baseURL: apiUrl }),
    });
    this.model = modelName || 'gemini-2.5-pro';
    this.apiType = 'google';
    
    if (this.debug) {
      console.log(`Using Google API with model: ${this.model}${apiUrl ? ` (URL: ${apiUrl})` : ''}`);
    }
  }
  /**
   * Get the system message with instructions for the AI (XML Tool Format)
   */
  async getSystemMessage() {
    // Build tool definitions
    let toolDefinitions = `
${searchToolDefinition}
${queryToolDefinition}
${extractToolDefinition}
${listFilesToolDefinition}
${searchFilesToolDefinition}
${attemptCompletionToolDefinition}
`;
    if (this.allowEdit) {
      toolDefinitions += `${implementToolDefinition}\n`;
    }
    // Build XML tool guidelines
    let xmlToolGuidelines = `
# Tool Use Formatting
Tool use MUST be formatted using XML-style tags. Each tool call requires BOTH opening and closing tags with the exact tool name. Each parameter is similarly enclosed within its own set of opening and closing tags. You MUST use exactly ONE tool call per message until you are ready to complete the task.
**CRITICAL: Every XML tag MUST have both opening <tag> and closing </tag> parts.**
Structure (note the closing tags):
<tool_name>
<parameter1_name>value1</parameter1_name>
<parameter2_name>value2</parameter2_name>
...
</tool_name>
Examples:
<search>
<query>error handling</query>
<path>src/search</path>
</search>
<extract>
<path>src/config.js</path>
<start_line>15</start_line>
<end_line>25</end_line>
</extract>
<attempt_completion>
<result>The configuration is loaded from src/config.js lines 15-25 which contains the database settings.</result>
</attempt_completion>
# Special Case: Quick Completion
If your previous response was already correct and complete, you may respond with just:
<attempt_complete>
This signals to use your previous response as the final answer without repeating content.
# Thinking Process
Before using a tool, analyze the situation within <thinking></thinking> tags. This helps you organize your thoughts and make better decisions.
Example:
<thinking>
I need to find code related to error handling in the search module. The most appropriate tool for this is the search tool, which requires a query parameter and a path parameter. I have both the query ("error handling") and the path ("src/search"), so I can proceed with the search.
</thinking>
# Tool Use Guidelines
1. Think step-by-step about how to achieve the user's goal.
2. Use <thinking></thinking> tags to analyze the situation and determine the appropriate tool.
3. Choose **one** tool that helps achieve the current step.
4. Format the tool call using the specified XML format with BOTH opening and closing tags. Ensure all required parameters are included.
5. **You MUST respond with exactly one tool call in the specified XML format in each turn.**
6. Wait for the tool execution result, which will be provided in the next message (within a <tool_result> block).
7. Analyze the tool result and decide the next step. If more tool calls are needed, repeat steps 2-6.
8. If the task is fully complete and all previous steps were successful, use the \`<attempt_completion>\` tool to provide the final answer. This is the ONLY way to finish the task.
9. If you cannot proceed (e.g., missing information, invalid request), explain the issue clearly before using \`<attempt_completion>\` with an appropriate message in the \`<result>\` tag.
10. If your previous response was already correct and complete, you may use \`<attempt_complete>\` as a shorthand.
Available Tools:
- search: Search code using keyword queries.
- query: Search code using structural AST patterns.
- extract: Extract specific code blocks or lines from files.
- listFiles: List files and directories in a specified location.
- searchFiles: Find files matching a glob pattern with recursive search capability.
${this.allowEdit ? '- implement: Implement a feature or fix a bug using aider.\n' : ''}
- attempt_completion: Finalize the task and provide the result to the user.
- attempt_complete: Quick completion using previous response (shorthand).
`;
    // Common instructions
    const commonInstructions = `<instructions>
Follow these instructions carefully:
1. Analyze the user's request.
2. Use <thinking></thinking> tags to analyze the situation and determine the appropriate tool for each step.
3. Use the available tools step-by-step to fulfill the request.
4. You should always prefer the \`search\` tool for code-related questions. Read full files only if really necessary.
5. Ensure to get really deep and understand the full picture before answering.
6. You MUST respond with exactly ONE tool call per message, using the specified XML format, until the task is complete.
7. Wait for the tool execution result (provided in the next user message in a <tool_result> block) before proceeding to the next step.
8. Once the task is fully completed, use the '<attempt_completion>' tool to provide the final result. This is the ONLY way to signal completion.
9. Prefer concise and focused search queries. Use specific keywords and phrases to narrow down results.
</instructions>
`;
    // Define predefined prompts (without the common instructions)
    const predefinedPrompts = {
      'code-explorer': `You are ProbeChat Code Explorer, a specialized AI assistant focused on helping developers, product managers, and QAs understand and navigate codebases. Your primary function is to answer questions based on code, explain how systems work, and provide insights into code functionality using the provided code analysis tools.
When exploring code:
- Provide clear, concise explanations based on user request
- Find and highlight the most relevant code snippets, if required
- Trace function calls and data flow through the system
- Try to understand the user's intent and provide relevant information
- Understand high level picture
- Balance detail with clarity in your explanations`,
      'architect': `You are ProbeChat Architect, a specialized AI assistant focused on software architecture and design. Your primary function is to help users understand, analyze, and design software systems using the provided code analysis tools.
When analyzing code:
- Focus on high-level design patterns and system organization
- Identify architectural patterns and component relationships
- Evaluate system structure and suggest architectural improvements
- Consider scalability, maintainability, and extensibility in your analysis`,
      'code-review': `You are ProbeChat Code Reviewer, a specialized AI assistant focused on code quality and best practices. Your primary function is to help users identify issues, suggest improvements, and ensure code follows best practices using the provided code analysis tools.
When reviewing code:
- Look for bugs, edge cases, and potential issues
- Identify performance bottlenecks and optimization opportunities
- Check for security vulnerabilities and best practices
- Evaluate code style and consistency
- Provide specific, actionable suggestions with code examples where appropriate`,
      'code-review-template': `You are going to perform code review according to provided user rules. Ensure to review only code provided in diff and latest commit, if provided. However you still need to fully understand how modified code works, and read dependencies if something is not clear.`,
      'engineer': `You are senior engineer focused on software architecture and design.
Before jumping on the task you first, in details analyse user request, and try to provide elegant and concise solution.
If solution is clear, you can jump to implementation right away, if not, you can ask user a clarification question, by calling attempt_completion tool, with required details.
Before jumping to implementation:
- Focus on high-level design patterns and system organization
- Identify architectural patterns and component relationships
- Evaluate system structure and suggest architectural improvements
- Focus on backward compatibility.
- Consider scalability, maintainability, and extensibility in your analysis
During the implementation:
- Avoid implementing special cases
- Do not forget to add the tests`,
      'support': `You are ProbeChat Support, a specialized AI assistant focused on helping developers troubleshoot issues and solve problems. Your primary function is to help users diagnose errors, understand unexpected behaviors, and find solutions using the provided code analysis tools.
When troubleshooting:
- Focus on finding root causes, not just symptoms
- Explain concepts clearly with appropriate context
- Provide step-by-step guidance to solve problems
- Suggest diagnostic steps to verify solutions
- Consider edge cases and potential complications
- Be empathetic and patient in your explanations`
    };
    let systemMessage = '';
    // Use custom prompt if provided
    if (this.customPrompt) {
      systemMessage = "<role>" + this.customPrompt + "</role>";
      if (this.debug) {
        console.log(`[DEBUG] Using custom prompt`);
      }
    }
    // Use predefined prompt if specified
    else if (this.promptType && predefinedPrompts[this.promptType]) {
      systemMessage = "<role>" + predefinedPrompts[this.promptType] + "</role>";
      if (this.debug) {
        console.log(`[DEBUG] Using predefined prompt: ${this.promptType}`);
      }
      // Add common instructions to predefined prompts
      systemMessage += commonInstructions;
    } else {
      // Use the default prompt (code explorer) if no prompt type is specified
      systemMessage = "<role>" + predefinedPrompts['code-explorer'] + "</role>";
      if (this.debug) {
        console.log(`[DEBUG] Using default prompt: code explorer`);
      }
      // Add common instructions to the default prompt
      systemMessage += commonInstructions;
    }
    // Add XML Tool Guidelines
    systemMessage += `\n${xmlToolGuidelines}\n`;
    // Add Tool Definitions
    systemMessage += `\n# Tools Available\n${toolDefinitions}\n`;
    // Add folder information
    const searchDirectory = this.allowedFolders.length > 0 ? this.allowedFolders[0] : process.cwd();
    if (this.debug) {
      console.log(`[DEBUG] Generating file list for base directory: ${searchDirectory}...`);
    }
    try {
      const files = await listFilesByLevel({
        directory: searchDirectory,
        maxFiles: 100,
        respectGitignore: !process.env.PROBE_NO_GITIGNORE || process.env.PROBE_NO_GITIGNORE === '',
        cwd: process.cwd()
      });
      systemMessage += `\n# Repository Structure\n\nYou are working with a repository located at: ${searchDirectory}\n\nHere's an overview of the repository structure (showing up to 100 most relevant files):\n\n\`\`\`\n${files}\n\`\`\`\n\n`;
    } catch (error) {
      if (this.debug) {
        console.log(`[DEBUG] Could not generate file list: ${error.message}`);
      }
      systemMessage += `\n# Repository Structure\n\nYou are working with a repository located at: ${searchDirectory}\n\n`;
    }
    if (this.allowedFolders.length > 0) {
      systemMessage += `\n**Important**: For security reasons, you can only search within these allowed folders: ${this.allowedFolders.join(', ')}\n\n`;
    }
    return systemMessage;
  }
  /**
   * Answer a question using the agentic flow
   * @param {string} message - The user's question
   * @param {Array} [images] - Optional array of image data (base64 strings or URLs)
   * @param {Object|string} [schemaOrOptions] - Can be either:
   *   - A string: JSON schema for structured output (backwards compatible)
   *   - An object: Options object with schema and other options
   * @param {string} [schemaOrOptions.schema] - JSON schema string for structured output
   * @returns {Promise<string>} - The final answer
   */
  async answer(message, images = [], schemaOrOptions = {}) {
    if (!message || typeof message !== 'string' || message.trim().length === 0) {
      throw new Error('Message is required and must be a non-empty string');
    }
    // Handle backwards compatibility - if third argument is a string, treat it as schema
    let options = {};
    if (typeof schemaOrOptions === 'string') {
      options = { schema: schemaOrOptions };
    } else {
      options = schemaOrOptions || {};
    }
    try {
      // Generate system message
      const systemMessage = await this.getSystemMessage();
      // Create user message with optional image support
      let userMessage = { role: 'user', content: message.trim() };
      
      // If images are provided, use multi-modal message format
      if (images && images.length > 0) {
        userMessage.content = [
          { type: 'text', text: message.trim() },
          ...images.map(image => ({
            type: 'image',
            image: image
          }))
        ];
      }
      // Initialize conversation with existing history + new user message
      let currentMessages = [
        { role: 'system', content: systemMessage },
        ...this.history, // Include previous conversation history
        userMessage
      ];
      let currentIteration = 0;
      let completionAttempted = false;
      let finalResult = 'I was unable to complete your request due to reaching the maximum number of tool iterations.';
      // Adjust max iterations if schema is provided
      // +1 for schema formatting
      // +2 for potential Mermaid validation retries (can be multiple diagrams)
      // +1 for potential JSON correction
      const maxIterations = options.schema ? MAX_TOOL_ITERATIONS + 4 : MAX_TOOL_ITERATIONS;
      if (this.debug) {
        console.log(`[DEBUG] Starting agentic flow for question: ${message.substring(0, 100)}...`);
        if (options.schema) {
          console.log(`[DEBUG] Schema provided, using extended iteration limit: ${maxIterations} (base: ${MAX_TOOL_ITERATIONS})`);
        }
      }
      // Tool iteration loop
      while (currentIteration < maxIterations && !completionAttempted) {
        currentIteration++;
        if (this.cancelled) throw new Error('Request was cancelled by the user');
        if (this.debug) {
          console.log(`\n[DEBUG] --- Tool Loop Iteration ${currentIteration}/${maxIterations} ---`);
          console.log(`[DEBUG] Current messages count for AI call: ${currentMessages.length}`);
          
          // Log preview of the latest user message (helpful for debugging loops)
          const lastUserMessage = [...currentMessages].reverse().find(msg => msg.role === 'user');
          if (lastUserMessage && lastUserMessage.content) {
            const userPreview = createMessagePreview(lastUserMessage.content);
            console.log(`[DEBUG] Latest user message (${lastUserMessage.content.length} chars): ${userPreview}`);
          }
        }
        // Add iteration tracing event
        if (this.tracer) {
          this.tracer.addEvent('iteration.start', {
            'iteration': currentIteration,
            'max_iterations': maxIterations,
            'message_count': currentMessages.length
          });
        }
        // Add warning message when reaching the last iteration
        if (currentIteration === maxIterations) {
          const warningMessage = `⚠️ WARNING: You have reached the maximum tool iterations limit (${maxIterations}). This is your final message. Please respond with the data you have so far. If something was not completed, honestly state what was not done and provide any partial results or recommendations you can offer.`;
          
          currentMessages.push({
            role: 'user',
            content: warningMessage
          });
          
          if (this.debug) {
            console.log(`[DEBUG] Added max iterations warning message at iteration ${currentIteration}`);
          }
        }
        // Calculate context size
        this.tokenCounter.calculateContextSize(currentMessages);
        if (this.debug) {
          console.log(`[DEBUG] Estimated context tokens BEFORE LLM call (Iter ${currentIteration}): ${this.tokenCounter.contextSize}`);
        }
        let maxResponseTokens = this.maxResponseTokens;
        if (!maxResponseTokens) {
          // Use model-based defaults if not explicitly configured
          maxResponseTokens = 4000;
          if (this.model.includes('opus') || this.model.includes('sonnet') || this.model.startsWith('gpt-4-')) {
            maxResponseTokens = 8192;
          } else if (this.model.startsWith('gpt-4o')) {
            maxResponseTokens = 8192;
          } else if (this.model.startsWith('gemini')) {
            maxResponseTokens = 32000;
          }
        }
        // Make AI request
        let assistantResponseContent = '';
        try {
          // Wrap AI request with tracing if available
          const executeAIRequest = async () => {
            const result = await streamText({
              model: this.provider(this.model),
              messages: currentMessages,
              maxTokens: maxResponseTokens,
              temperature: 0.3,
            });
            // Collect the streamed response
            for await (const delta of result.textStream) {
              assistantResponseContent += delta;
            }
            // Record token usage
            const usage = await result.usage;
            if (usage) {
              this.tokenCounter.recordUsage(usage, result.experimental_providerMetadata);
            }
            return result;
          };
          if (this.tracer) {
            await this.tracer.withSpan('ai.request', executeAIRequest, {
              'ai.model': this.model,
              'ai.provider': this.clientApiProvider || 'auto',
              'iteration': currentIteration,
              'max_tokens': maxResponseTokens,
              'temperature': 0.3,
              'message_count': currentMessages.length
            });
          } else {
            await executeAIRequest();
          }
        } catch (error) {
          console.error(`Error during streamText (Iter ${currentIteration}):`, error);
          finalResult = `Error: Failed to get response from AI model during iteration ${currentIteration}. ${error.message}`;
          throw new Error(finalResult);
        }
        // Log preview of assistant response for debugging loops
        if (this.debug && assistantResponseContent) {
          const assistantPreview = createMessagePreview(assistantResponseContent);
          console.log(`[DEBUG] Assistant response (${assistantResponseContent.length} chars): ${assistantPreview}`);
        }
        // Parse tool call from response with valid tools list
        const validTools = [
          'search', 'query', 'extract', 'listFiles', 'searchFiles', 'attempt_completion'
        ];
        if (this.allowEdit) {
          validTools.push('implement');
        }
        
        const parsedTool = parseXmlToolCallWithThinking(assistantResponseContent, validTools);
        if (parsedTool) {
          const { toolName, params } = parsedTool;
          if (this.debug) console.log(`[DEBUG] Parsed tool call: ${toolName} with params:`, params);
          if (toolName === 'attempt_completion') {
            completionAttempted = true;
            // Handle attempt_complete shorthand - use previous response
            if (params.result === '__PREVIOUS_RESPONSE__') {
              // Find the last assistant message with actual content (not tool calls)
              const lastAssistantMessage = [...currentMessages].reverse().find(msg =>
                msg.role === 'assistant' &&
                msg.content &&
                !parseXmlToolCallWithThinking(msg.content, validTools)
              );
              if (lastAssistantMessage) {
                finalResult = lastAssistantMessage.content;
                if (this.debug) console.log(`[DEBUG] Using previous response as completion: ${finalResult.substring(0, 100)}...`);
              } else {
                finalResult = 'Error: No previous response found to use as completion.';
                if (this.debug) console.log(`[DEBUG] No suitable previous response found for attempt_complete shorthand`);
              }
            } else {
              // Standard attempt_completion handling
              const validation = attemptCompletionSchema.safeParse(params);
              if (validation.success) {
                finalResult = validation.data.result;
                if (this.debug) console.log(`[DEBUG] Task completed successfully with result: ${finalResult.substring(0, 100)}...`);
              } else {
                console.error(`[ERROR] Invalid attempt_completion parameters:`, validation.error);
                finalResult = 'Error: Invalid completion attempt. The task could not be completed properly.';
              }
            }
            break;
          } else {
            // Execute the tool
            if (this.toolImplementations[toolName]) {
              try {
                // Add sessionId to params for tool execution
                const toolParams = { ...params, sessionId: this.sessionId };
                
                // Emit tool start event
                this.events.emit('toolCall', {
                  timestamp: new Date().toISOString(),
                  name: toolName,
                  args: toolParams,
                  status: 'started'
                });
                
                // Execute tool with tracing if available
                const executeToolCall = async () => {
                  // For delegate tool, pass current iteration and max iterations
                  if (toolName === 'delegate') {
                    const enhancedParams = {
                      ...toolParams,
                      currentIteration,
                      maxIterations,
                      debug: this.debug,
                      tracer: this.tracer
                    };
                    
                    if (this.debug) {
                      console.log(`[DEBUG] Executing delegate tool at iteration ${currentIteration}/${maxIterations}`);
                      console.log(`[DEBUG] Delegate task: ${toolParams.task?.substring(0, 100)}...`);
                    }
                    
                    // Record delegation start in telemetry
                    if (this.tracer) {
                      this.tracer.recordDelegationEvent('tool_started', {
                        'delegation.iteration': currentIteration,
                        'delegation.max_iterations': maxIterations,
                        'delegation.task_preview': toolParams.task?.substring(0, 200) + (toolParams.task?.length > 200 ? '...' : '')
                      });
                    }
                    
                    return await this.toolImplementations[toolName].execute(enhancedParams);
                  }
                  return await this.toolImplementations[toolName].execute(toolParams);
                };
                let toolResult;
                try {
                  if (this.tracer) {
                    toolResult = await this.tracer.withSpan('tool.call', executeToolCall, {
                      'tool.name': toolName,
                      'tool.params': JSON.stringify(toolParams).substring(0, 500),
                      'iteration': currentIteration
                    });
                  } else {
                    toolResult = await executeToolCall();
                  }
                  
                  // Emit tool success event
                  this.events.emit('toolCall', {
                    timestamp: new Date().toISOString(),
                    name: toolName,
                    args: toolParams,
                    resultPreview: typeof toolResult === 'string'
                      ? (toolResult.length > 200 ? toolResult.substring(0, 200) + '...' : toolResult)
                      : (toolResult ? JSON.stringify(toolResult).substring(0, 200) + '...' : 'No Result'),
                    status: 'completed'
                  });
                  
                } catch (toolError) {
                  // Emit tool error event
                  this.events.emit('toolCall', {
                    timestamp: new Date().toISOString(),
                    name: toolName,
                    args: toolParams,
                    error: toolError.message || 'Unknown error',
                    status: 'error'
                  });
                  throw toolError; // Re-throw to be handled by outer catch
                }
                
                // Add assistant response and tool result to conversation
                currentMessages.push({ role: 'assistant', content: assistantResponseContent });
                currentMessages.push({
                  role: 'user',
                  content: `<tool_result>\n${typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult, null, 2)}\n</tool_result>`
                });
                if (this.debug) {
                  console.log(`[DEBUG] Tool ${toolName} executed successfully. Result length: ${typeof toolResult === 'string' ? toolResult.length : JSON.stringify(toolResult).length}`);
                }
              } catch (error) {
                console.error(`[ERROR] Tool execution failed for ${toolName}:`, error);
                currentMessages.push({ role: 'assistant', content: assistantResponseContent });
                currentMessages.push({
                  role: 'user', 
                  content: `<tool_result>\nError: ${error.message}\n</tool_result>`
                });
              }
            } else {
              console.error(`[ERROR] Unknown tool: ${toolName}`);
              currentMessages.push({ role: 'assistant', content: assistantResponseContent });
              currentMessages.push({
                role: 'user',
                content: `<tool_result>\nError: Unknown tool '${toolName}'. Available tools: ${Object.keys(this.toolImplementations).join(', ')}\n</tool_result>`
              });
            }
          }
        } else {
          // No tool call found, add assistant response and ask for tool usage
          currentMessages.push({ role: 'assistant', content: assistantResponseContent });
          // Build appropriate reminder message based on whether schema is provided
          let reminderContent;
          if (options.schema) {  // Apply for ANY schema, not just JSON schemas
            // When schema is provided, give specific instructions
            reminderContent = `Please use one of the available tools to help answer the question, or use attempt_completion if you have enough information to provide a final answer.
Remember: Use proper XML format with BOTH opening and closing tags:
<tool_name>
<parameter>value</parameter>
</tool_name>
IMPORTANT: A schema was provided. You MUST respond with data that matches this schema.
Use attempt_completion with your response directly inside the tags:
<attempt_completion>
{"key": "value", "field": "your actual data here matching the schema"}
</attempt_completion>
Your response must conform to this schema:
${options.schema}`;
          } else {
            // Standard reminder without schema
            reminderContent = `Please use one of the available tools to help answer the question, or use attempt_completion if you have enough information to provide a final answer.
Remember: Use proper XML format with BOTH opening and closing tags:
<tool_name>
<parameter>value</parameter>
</tool_name>
Or for quick completion if your previous response was already correct:
<attempt_complete>`;
          }
          currentMessages.push({
            role: 'user',
            content: reminderContent
          });
          if (this.debug) {
            console.log(`[DEBUG] No tool call detected in assistant response. Prompting for tool use.`);
          }
        }
        // Keep message history manageable
        if (currentMessages.length > MAX_HISTORY_MESSAGES) {
          const messagesBefore = currentMessages.length;
          const systemMsg = currentMessages[0]; // Keep system message
          const recentMessages = currentMessages.slice(-MAX_HISTORY_MESSAGES + 1);
          currentMessages = [systemMsg, ...recentMessages];
          
          if (this.debug) {
            console.log(`[DEBUG] Trimmed message history from ${messagesBefore} to ${currentMessages.length} messages`);
          }
        }
      }
      if (currentIteration >= maxIterations && !completionAttempted) {
        console.warn(`[WARN] Max tool iterations (${maxIterations}) reached for session ${this.sessionId}. Returning current error state.`);
      }
      // Store final history
      this.history = currentMessages.map(msg => ({ ...msg }));
      if (this.history.length > MAX_HISTORY_MESSAGES) {
        const messagesBefore = this.history.length;
        this.history = this.history.slice(-MAX_HISTORY_MESSAGES);
        if (this.debug) {
          console.log(`[DEBUG] Trimmed stored history from ${messagesBefore} to ${this.history.length} messages`);
        }
      }
      // Update token counter with final history
      this.tokenCounter.updateHistory(this.history);
      // Schema handling - format response according to provided schema
      // Skip schema processing if result came from attempt_completion tool
      // Don't apply schema formatting if we failed due to max iterations
      const reachedMaxIterations = currentIteration >= maxIterations && !completionAttempted;
      if (options.schema && !options._schemaFormatted && !completionAttempted && !reachedMaxIterations) {
        if (this.debug) {
          console.log('[DEBUG] Schema provided, applying automatic formatting...');
        }
        
        try {
          // Step 1: Make a follow-up call to format according to schema
          const schemaPrompt = `CRITICAL: You MUST respond with ONLY valid JSON DATA that conforms to this schema structure. DO NOT return the schema definition itself.
Schema to follow (this is just the structure - provide ACTUAL DATA):
${options.schema}
REQUIREMENTS:
- Return ONLY the JSON object/array with REAL DATA that matches the schema structure
- DO NOT return the schema definition itself (no "$schema", "$id", "type", "properties", etc.)
- NO additional text, explanations, or markdown formatting
- NO code blocks or backticks
- The JSON must be parseable by JSON.parse()
- Fill in actual values that make sense based on your previous response content
EXAMPLE:
If schema defines {type: "object", properties: {name: {type: "string"}, age: {type: "number"}}}
Return: {"name": "John Doe", "age": 25}
NOT: {"type": "object", "properties": {"name": {"type": "string"}}}
Convert your previous response content into actual JSON data that follows this schema structure.`;
          
          // Call answer recursively with _schemaFormatted flag to prevent infinite loop
          finalResult = await this.answer(schemaPrompt, [], { 
            ...options, 
            _schemaFormatted: true 
          });
          
          // Step 2: Clean the response (remove code blocks)
          finalResult = cleanSchemaResponse(finalResult);
          
          // Step 3: Validate and fix Mermaid diagrams if present
          if (!this.disableMermaidValidation) {
            try {
              if (this.debug) {
                console.log(`[DEBUG] Mermaid validation: Starting enhanced mermaid validation...`);
              }
              
              // Record mermaid validation start in telemetry
              if (this.tracer) {
                this.tracer.recordMermaidValidationEvent('schema_processing_started', {
                  'mermaid_validation.context': 'schema_processing',
                  'mermaid_validation.response_length': finalResult.length
                });
              }
              
              const mermaidValidation = await validateAndFixMermaidResponse(finalResult, {
                debug: this.debug,
                path: this.allowedFolders[0],
                provider: this.clientApiProvider,
                model: this.model,
                tracer: this.tracer
              });
              
              if (mermaidValidation.wasFixed) {
                finalResult = mermaidValidation.fixedResponse;
                if (this.debug) {
                  console.log(`[DEBUG] Mermaid validation: Diagrams successfully fixed`);
                  
                  if (mermaidValidation.performanceMetrics) {
                    const metrics = mermaidValidation.performanceMetrics;
                    console.log(`[DEBUG] Mermaid validation: Performance - total: ${metrics.totalTimeMs}ms, AI fixing: ${metrics.aiFixingTimeMs}ms`);
                    console.log(`[DEBUG] Mermaid validation: Results - ${metrics.diagramsFixed}/${metrics.diagramsProcessed} diagrams fixed`);
                  }
                  
                  if (mermaidValidation.fixingResults) {
                    mermaidValidation.fixingResults.forEach((fixResult, index) => {
                      if (fixResult.wasFixed) {
                        const method = fixResult.fixedWithHtmlDecoding ? 'HTML entity decoding' : 'AI correction';
                        const time = fixResult.aiFixingTimeMs ? ` in ${fixResult.aiFixingTimeMs}ms` : '';
                        console.log(`[DEBUG] Mermaid validation: Fixed diagram ${fixResult.diagramIndex + 1} with ${method}${time}`);
                        console.log(`[DEBUG] Mermaid validation: Original error: ${fixResult.originalError}`);
                      } else {
                        console.log(`[DEBUG] Mermaid validation: Failed to fix diagram ${fixResult.diagramIndex + 1}: ${fixResult.fixingError}`);
                      }
                    });
                  }
                }
              } else if (this.debug) {
                console.log(`[DEBUG] Mermaid validation: No fixes needed or fixes unsuccessful`);
                if (mermaidValidation.diagrams?.length > 0) {
                  console.log(`[DEBUG] Mermaid validation: Found ${mermaidValidation.diagrams.length} diagrams, all valid: ${mermaidValidation.isValid}`);
                }
              }
            } catch (error) {
              if (this.debug) {
                console.log(`[DEBUG] Mermaid validation: Process failed with error: ${error.message}`);
                console.log(`[DEBUG] Mermaid validation: Stack trace: ${error.stack}`);
              }
            }
          } else if (this.debug) {
            console.log(`[DEBUG] Mermaid validation: Skipped due to disableMermaidValidation option`);
          }
          
          // Step 4: Validate and potentially correct JSON responses
          if (isJsonSchema(options.schema)) {
            if (this.debug) {
              console.log(`[DEBUG] JSON validation: Starting validation process for schema response`);
              console.log(`[DEBUG] JSON validation: Response length: ${finalResult.length} chars`);
            }
            
            // Record JSON validation start in telemetry
            if (this.tracer) {
              this.tracer.recordJsonValidationEvent('started', {
                'json_validation.response_length': finalResult.length,
                'json_validation.schema_type': 'JSON'
              });
            }
            
            let validation = validateJsonResponse(finalResult, { debug: this.debug });
            let retryCount = 0;
            const maxRetries = 3;
            
            // First check if the response is valid JSON but is actually a schema definition
            if (validation.isValid && isJsonSchemaDefinition(finalResult, { debug: this.debug })) {
              if (this.debug) {
                console.log(`[DEBUG] JSON validation: Response is a JSON schema definition instead of data, correcting...`);
              }
              
              // Use specialized correction prompt for schema definition confusion
              const schemaDefinitionPrompt = createSchemaDefinitionCorrectionPrompt(
                finalResult,
                options.schema,
                0
              );
              
              finalResult = await this.answer(schemaDefinitionPrompt, [], { 
                ...options, 
                _schemaFormatted: true 
              });
              finalResult = cleanSchemaResponse(finalResult);
              validation = validateJsonResponse(finalResult);
              retryCount = 1; // Start at 1 since we already did one correction
            }
            
            while (!validation.isValid && retryCount < maxRetries) {
              if (this.debug) {
                console.log(`[DEBUG] JSON validation: Validation failed (attempt ${retryCount + 1}/${maxRetries}):`, validation.error);
                console.log(`[DEBUG] JSON validation: Invalid response sample: ${finalResult.substring(0, 300)}${finalResult.length > 300 ? '...' : ''}`);
              }
              
              // Check if the invalid response is actually a schema definition
              let correctionPrompt;
              try {
                if (isJsonSchemaDefinition(finalResult, { debug: this.debug })) {
                  if (this.debug) {
                    console.log(`[DEBUG] JSON validation: Response is still a schema definition, using specialized correction`);
                  }
                  correctionPrompt = createSchemaDefinitionCorrectionPrompt(
                    finalResult,
                    options.schema,
                    retryCount
                  );
                } else {
                  correctionPrompt = createJsonCorrectionPrompt(
                    finalResult, 
                    options.schema, 
                    validation.error,
                    retryCount
                  );
                }
              } catch (error) {
                // If we can't parse to check if it's a schema definition, use regular correction
                correctionPrompt = createJsonCorrectionPrompt(
                  finalResult, 
                  options.schema, 
                  validation.error,
                  retryCount
                );
              }
              
              finalResult = await this.answer(correctionPrompt, [], { 
                ...options, 
                _schemaFormatted: true 
              });
              finalResult = cleanSchemaResponse(finalResult);
              
              // Validate the corrected response
              validation = validateJsonResponse(finalResult, { debug: this.debug });
              retryCount++;
              
              if (this.debug) {
                if (!validation.isValid && retryCount < maxRetries) {
                  console.log(`[DEBUG] JSON validation: Still invalid after correction ${retryCount}, retrying...`);
                  console.log(`[DEBUG] JSON validation: Corrected response sample: ${finalResult.substring(0, 300)}${finalResult.length > 300 ? '...' : ''}`);
                } else if (validation.isValid) {
                  console.log(`[DEBUG] JSON validation: Successfully corrected after ${retryCount} attempts`);
                }
              }
            }
            
            if (!validation.isValid && this.debug) {
              console.log(`[DEBUG] JSON validation: Still invalid after ${maxRetries} correction attempts:`, validation.error);
              console.log(`[DEBUG] JSON validation: Final invalid response: ${finalResult.substring(0, 500)}${finalResult.length > 500 ? '...' : ''}`);
            } else if (validation.isValid && this.debug) {
              console.log(`[DEBUG] JSON validation: Final validation successful`);
            }
            
            // Record JSON validation completion in telemetry
            if (this.tracer) {
              this.tracer.recordJsonValidationEvent('completed', {
                'json_validation.success': validation.isValid,
                'json_validation.retry_count': retryCount,
                'json_validation.max_retries': maxRetries,
                'json_validation.final_response_length': finalResult.length,
                'json_validation.error': validation.isValid ? null : validation.error
              });
            }
          }
        } catch (error) {
          console.error('[ERROR] Schema formatting failed:', error);
          // Return the original result if schema formatting fails
        }
      } else if (reachedMaxIterations && options.schema && this.debug) {
        console.log('[DEBUG] Skipping schema formatting due to max iterations reached without completion');
      } else if (completionAttempted && options.schema) {
        // For attempt_completion results with schema, still clean markdown if needed
        try {
          finalResult = cleanSchemaResponse(finalResult);
          
          // Validate and fix Mermaid diagrams if present
          if (!this.disableMermaidValidation) {
            if (this.debug) {
              console.log(`[DEBUG] Mermaid validation: Validating attempt_completion result...`);
            }
            
            const mermaidValidation = await validateAndFixMermaidResponse(finalResult, {
              debug: this.debug,
              path: this.allowedFolders[0],
              provider: this.clientApiProvider,
              model: this.model,
              tracer: this.tracer
            });
            
            if (mermaidValidat