UNPKG

@alvinveroy/codecompass

Version:

AI-powered MCP server for codebase navigation and LLM prompt optimization

674 lines (663 loc) 44.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.toolRegistry = void 0; exports.getProcessedDiff = getProcessedDiff; exports.processSnippet = processSnippet; exports.createAgentState = createAgentState; exports.generateAgentSystemPrompt = generateAgentSystemPrompt; exports.parseToolCalls = parseToolCalls; exports.parseCapabilityCall = parseCapabilityCall; exports.executeToolCall = executeToolCall; exports.runAgentLoop = runAgentLoop; const config_service_1 = require("./config-service"); const llm_provider_1 = require("./llm-provider"); const state_1 = require("./state"); const repository_1 = require("./repository"); const path_1 = __importDefault(require("path")); const zod_1 = require("zod"); const capabilities = __importStar(require("./agent_capabilities")); // Import all capabilities // Define Zod schema for agent_query tool's parameters const AgentQueryToolParamsSchema = zod_1.z.object({ user_query: zod_1.z.string().describe("The user's detailed question or task regarding the codebase."), session_id: zod_1.z.string().optional().describe("The session ID for maintaining context.") }); // Define Zod schema for parsing capability calls from LLM output // This is what the orchestrator LLM should output. const CapabilityCallSchema = zod_1.z.object({ capability: zod_1.z.string().describe("The name of the internal capability to call."), parameters: zod_1.z.record(zod_1.z.unknown()).describe("The parameters for the capability."), reasoning: zod_1.z.string().optional().describe("The reasoning for choosing this capability and parameters.") }); // Add this schema definition near the other Zod schemas at the top of the file, // or just before `capabilityDefinitions` array. const FormattedSearchResultSchema = zod_1.z.object({ filepath: zod_1.z.string(), snippet: zod_1.z.string(), last_modified: zod_1.z.string().optional(), relevance: zod_1.z.number().optional(), is_chunked: zod_1.z.boolean().optional(), original_filepath: zod_1.z.string().optional(), chunk_index: zod_1.z.number().int().optional(), total_chunks: zod_1.z.number().int().optional(), }); // Define Parameter Schemas for Each Capability const CapabilitySearchCodeSnippetsParamsSchema = zod_1.z.object({ query: zod_1.z.string().describe("The search query string."), }); const CapabilityGetRepositoryOverviewParamsSchema = zod_1.z.object({ query: zod_1.z.string().describe("The query string to find relevant context and snippets."), }); const CapabilityGetChangelogParamsSchema = zod_1.z.object({}).describe("No parameters needed."); // Empty object for no params const CapabilityFetchMoreSearchResultsParamsSchema = zod_1.z.object({ query: zod_1.z.string().describe("The original or refined query string for which more results are needed."), }); const CapabilityGetFullFileContentParamsSchema = zod_1.z.object({ filepath: zod_1.z.string().describe("The path to the file within the repository."), }); const CapabilityListDirectoryParamsSchema = zod_1.z.object({ dirPath: zod_1.z.string().describe("The path to the directory within the repository."), }); const CapabilityGetAdjacentFileChunksParamsSchema = zod_1.z.object({ filepath: zod_1.z.string().describe("The path to the chunked file."), currentChunkIndex: zod_1.z.number().int().min(0).describe("The 0-based index of the current chunk."), }); const CapabilityGenerateSuggestionWithContextParamsSchema = zod_1.z.object({ query: zod_1.z.string().describe("The user's original query or goal for the suggestion."), repoPathName: zod_1.z.string().describe("The name of the repository (e.g., basename)."), filesContextString: zod_1.z.string().describe("A string summarizing the relevant files or file list."), diffSummary: zod_1.z.string().describe("A summary of recent repository changes (git diff)."), recentQueriesStrings: zod_1.z.array(zod_1.z.string()).describe("A list of recent related queries, if any."), relevantSnippets: zod_1.z.array(FormattedSearchResultSchema).describe("An array of relevant code snippets and their metadata."), }); const CapabilityAnalyzeCodeProblemWithContextParamsSchema = zod_1.z.object({ problemQuery: zod_1.z.string().describe("The user's description of the code problem."), relevantSnippets: zod_1.z.array(FormattedSearchResultSchema).describe("An array of code snippets relevant to the problem."), }); // Helper function for robust stringification of unknown step output function stringifyStepOutput(output) { if (typeof output === 'string') { return output; } if (output === null || output === undefined) { return String(output); // Handles null and undefined correctly } // Explicitly handle arrays and objects for JSON.stringify if (Array.isArray(output) || (typeof output === 'object' && output !== null)) { try { return JSON.stringify(output, null, 2); } catch { // Fallback for non-serializable objects or objects with circular refs return '[Unserializable Object]'; } } // For other primitives (number, boolean, symbol, bigint), String() is safe. // This addresses the new error at line 27 if 'output' was an unhandled object type. // eslint-disable-next-line @typescript-eslint/no-base-to-string return String(output); } // Tool registry with descriptions for the agent exports.toolRegistry = [ { name: "agent_query", description: "Processes the user's complex query about the codebase. Analyzes the query, formulates a plan using available internal capabilities (like code search, file reading, history analysis), executes the plan step-by-step, and synthesizes the information to provide a comprehensive answer to the user's original request.", parameters: { user_query: "string - The user's detailed question or task regarding the codebase.", session_id: "string (optional) - The session ID for maintaining context across interactions." }, requiresModel: true // The agent's planning and synthesis steps require an LLM. } ]; // Helper function to get processed diff (summarized or truncated if necessary) // Exported for spying async function getProcessedDiff(repoPath, suggestionModelAvailable) { const diffContent = await (0, repository_1.getRepositoryDiff)(repoPath); // Check if diffContent is one of the "no useful diff" messages const noUsefulDiffMessages = [ "No Git repository found", "No changes found in the last two commits.", "Not enough commits to compare.", // Add any other similar messages from getRepositoryDiff ]; const isEffectivelyEmptyDiff = !diffContent || noUsefulDiffMessages.includes(diffContent); if (!isEffectivelyEmptyDiff) { const MAX_DIFF_LENGTH = config_service_1.configService.MAX_DIFF_LENGTH_FOR_CONTEXT_TOOL; if (diffContent.length > MAX_DIFF_LENGTH) { if (suggestionModelAvailable) { try { const llmProvider = await (0, llm_provider_1.getLLMProvider)(); const summaryPrompt = `Summarize the following git diff concisely, focusing on the most significant changes, additions, and deletions (e.g., 3-5 key bullet points or a short paragraph). The project is "${path_1.default.basename(repoPath)}".\n\nGit Diff:\n${diffContent}`; const summarizedDiff = await llmProvider.generateText(summaryPrompt); config_service_1.logger.info(`Summarized large diff content for repository: ${repoPath}`); return summarizedDiff; // Return the summary } catch (summaryError) { const sErr = summaryError instanceof Error ? summaryError : new Error(String(summaryError)); config_service_1.logger.warn(`Failed to summarize diff for ${repoPath}. Using truncated diff. Error: ${sErr.message}`); return `Diff is large. Summary attempt failed. Truncated diff:\n${diffContent.substring(0, MAX_DIFF_LENGTH)}...`; } } else { config_service_1.logger.warn(`Suggestion model not available to summarize large diff for ${repoPath}. Using truncated diff.`); return `Diff is large. Full content omitted as suggestion model is offline. Truncated diff:\n${diffContent.substring(0, MAX_DIFF_LENGTH)}...`; } } // If diff is not too long, return it as is return diffContent; } else if (!diffContent) { return "No diff information available."; } // If it's one of the noUsefulDiffMessages, return it as is return diffContent; } // Helper function to process (summarize if needed) a single snippet // Exported for spying async function processSnippet(snippet, query, // The user's query for context-aware summarization filepath, // Filepath for context suggestionModelAvailable) { const MAX_LENGTH = config_service_1.configService.MAX_SNIPPET_LENGTH_FOR_CONTEXT_NO_SUMMARY; if (snippet.length > MAX_LENGTH) { if (suggestionModelAvailable) { try { const llmProvider = await (0, llm_provider_1.getLLMProvider)(); const summaryPrompt = `The user's query is: "${query}". Concisely summarize the following code snippet from file "${filepath}", focusing on its relevance to the query. Aim for 2-4 key points or a short paragraph. Retain important identifiers or logic if possible. Snippet:\n\n\`\`\`\n${snippet}\n\`\`\``; const summarizedSnippet = await llmProvider.generateText(summaryPrompt); config_service_1.logger.info(`Summarized long snippet from ${filepath} for query "${query}". Original length: ${snippet.length}, Summary length: ${summarizedSnippet.length}`); return summarizedSnippet; } catch (summaryError) { const sErr = summaryError instanceof Error ? summaryError : new Error(String(summaryError)); config_service_1.logger.warn(`Failed to summarize snippet from ${filepath} for query "${query}". Using truncated snippet. Error: ${sErr.message}`); return `${snippet.substring(0, MAX_LENGTH)}... (summary failed, snippet truncated)`; } } else { config_service_1.logger.warn(`Suggestion model not available to summarize long snippet from ${filepath}. Using truncated snippet.`); return `${snippet.substring(0, MAX_LENGTH)}... (snippet truncated, summary unavailable)`; } } return snippet; // Return original snippet if not too long } // Create a new agent state function createAgentState(sessionId, query) { return { sessionId: sessionId, query: query, steps: [], context: [], isComplete: false }; } // Generate the agent system prompt // Exported for testing function generateAgentSystemPrompt(availableCapabilities // Changed parameter ) { return `You are CodeCompass Orchestrator, an AI assistant that helps developers by understanding their queries about a codebase and breaking them down into a series of steps using available internal capabilities. Your goal is to gather information piece by piece and then synthesize it to provide a comprehensive answer to the original user query. You have access to the following internal capabilities: ${availableCapabilities.map(cap => ` Capability: ${cap.name} Description: ${cap.description} Parameters (JSON Schema): ${JSON.stringify(cap.parameters_schema?._def || { description: cap.parameters_schema?.description }, null, 2)} `).join('\n')} When responding to the user's main query, follow these steps: 1. Analyze the user's query to understand their intent and what information is needed. 2. Formulate a plan. Think step-by-step. 3. Choose the most appropriate capability to execute next to gather a piece of information for your plan. 4. Explain your reasoning for choosing this capability and specify the exact parameters to use. 5. **CRITICAL**: Format your chosen capability call as a single, valid JSON object. This JSON object **MUST BE THE ONLY CONTENT** in your response. Do not include any other text before or after the JSON object. Example JSON format: {"capability": "capability_name", "parameters": {...parameters_object...}, "reasoning": "Your reasoning here..."} 6. After receiving the results from the capability, analyze them. 7. Decide if you have enough information to answer the user's original query. - If yes, provide a comprehensive final answer to the user. Do NOT use the JSON capability call format for the final answer. Just provide the answer as plain text. - If no, repeat from step 3, choosing the next best capability. 8. If you believe you are making progress on a complex task but require more processing steps than initially allocated, you can output a special JSON object: {"capability": "request_more_processing_time", "parameters": {"reasoning": "Your reason for needing more time..."}, "reasoning": "Need more iterations."} This may allow you additional interactions. Use this judiciously. Important guidelines: - Break down complex queries into multiple capability calls. - Accumulate context from the results of each capability call. - Be concise in your reasoning for choosing a capability. - Only use capabilities that are relevant to gathering information for the user's query. - Ensure parameters match the schema for the chosen capability. - If a capability requires context gathered by previous capabilities (e.g., 'capability_generateSuggestionWithContext' needs 'relevantSnippets'), ensure you have gathered that context first. - If after using available capabilities you still lack sufficient information, clearly state in your final answer that it's based on limited information and specify what was lacking. - Do not hallucinate. If you cannot answer confidently, explain what's missing. Example of choosing a capability: User Query: "Find functions related to user authentication in 'src/auth.ts'" Your thought process: 1. The user wants to find functions in a specific file related to a topic. 2. First, I should get the content of 'src/auth.ts'. 3. Then, I can analyze that content for "user authentication" functions. (Or, if a search capability is very good, I might try searching directly). Let's start by getting the file content. Your output (JSON only): {"capability": "capability_getFullFileContent", "parameters": {"filepath": "src/auth.ts"}, "reasoning": "Need to retrieve the content of 'src/auth.ts' to analyze it for authentication functions."} After getting the file content, you might then decide you have enough information to answer, or you might use another capability (e.g., a hypothetical 'analyze_code_for_topic' if it existed, or simply use your own intelligence to parse the retrieved content for the final answer). If providing the final answer, your output would be plain text, e.g.: "In 'src/auth.ts', the following functions appear related to user authentication: \`loginUser()\`, \`verifyToken()\`, ..." `; } // Parse tool calls from LLM output function parseToolCalls(output) { // Log the output for debugging config_service_1.logger.debug("Parsing tool calls from output", { outputLength: output.length }); // Split the output by lines and look for lines starting with TOOL_CALL: const lines = output.split('\n'); const results = []; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('TOOL_CALL:')) { try { // Extract the JSON part const jsonPart = line.substring('TOOL_CALL:'.length).trim(); config_service_1.logger.debug("Found potential tool call", { jsonPart }); const parsedJson = JSON.parse(jsonPart); config_service_1.logger.debug("Successfully parsed JSON", { parsedJson }); // Type guard for ParsedToolCall const isParsedToolCall = (item) => { if (typeof item !== 'object' || item === null) { return false; } // Now item is confirmed to be an object and not null const p = item; // Cast to Record for safer 'in' checks if preferred, or use item directly return ('tool' in p && typeof p.tool === 'string' && 'parameters' in p && typeof p.parameters === 'object' && p.parameters !== null); }; if (isParsedToolCall(parsedJson)) { results.push({ tool: parsedJson.tool, // Access directly after type guard parameters: parsedJson.parameters // Access directly after type guard }); } else { config_service_1.logger.warn("Parsed JSON part does not match expected tool call structure", { parsedJsonPart: jsonPart }); } } catch (error) { const _err = error instanceof Error ? error : new Error(String(error)); config_service_1.logger.error("Failed to parse tool call", { line, error: _err }); } } } config_service_1.logger.debug(`Found ${results.length} valid tool calls`); return results; } // Add this new function: function parseCapabilityCall(llmOutput) { try { // Assuming the LLM outputs *only* the JSON object for a capability call. const trimmedOutput = llmOutput.trim(); // Basic check to see if it looks like a JSON object if (trimmedOutput.startsWith("{") && trimmedOutput.endsWith("}")) { //eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const parsedJson = JSON.parse(trimmedOutput); const validationResult = CapabilityCallSchema.safeParse(parsedJson); if (validationResult.success) { config_service_1.logger.debug("Successfully parsed capability call", { data: validationResult.data }); return validationResult.data; } else { config_service_1.logger.warn("Parsed JSON does not match CapabilityCallSchema", { errors: validationResult.error.issues, json: parsedJson }); } } } catch (error) { const _err = error instanceof Error ? error : new Error(String(error)); config_service_1.logger.warn("Failed to parse LLM output as a capability call JSON", { output: llmOutput, error: _err.message }); } return null; // Return null if not a valid capability call } // Add this new async function: async function runAgentQueryOrchestrator(params, qdrantClient, repoPath, suggestionModelAvailable // This indicates if LLM-dependent capabilities can be used ) { const { user_query, session_id } = params; config_service_1.logger.info(`Agent Query Orchestrator started for user query: "${user_query}" (Session: ${session_id || 'new'})`); const session = (0, state_1.getOrCreateSession)(session_id, repoPath); const agentState = createAgentState(session.id, user_query); // createAgentState might need adjustment if it sets up a plan // Define available capabilities for the orchestrator's prompt // This list needs to be maintained and schemas defined for each capability's parameters. const capabilityDefinitions = [ { name: "capability_searchCodeSnippets", description: "Searches for code snippets in the repository based on a query string.", parameters_schema: CapabilitySearchCodeSnippetsParamsSchema }, { name: "capability_getRepositoryOverview", description: "Gets an overview of the repository including recent changes (diff summary) and relevant code snippets for a query.", parameters_schema: CapabilityGetRepositoryOverviewParamsSchema }, { name: "capability_getChangelog", description: "Retrieves the project's CHANGELOG.md file.", parameters_schema: CapabilityGetChangelogParamsSchema }, { name: "capability_fetchMoreSearchResults", description: "Fetches more search results for a given query, typically used if initial results are insufficient.", parameters_schema: CapabilityFetchMoreSearchResultsParamsSchema }, { name: "capability_getFullFileContent", description: "Retrieves the full content of a specified file.", parameters_schema: CapabilityGetFullFileContentParamsSchema }, { name: "capability_listDirectory", description: "Lists the contents (files and subdirectories) of a specified directory.", parameters_schema: CapabilityListDirectoryParamsSchema }, { name: "capability_getAdjacentFileChunks", description: "Retrieves code chunks adjacent to a previously identified chunk of a file.", parameters_schema: CapabilityGetAdjacentFileChunksParamsSchema }, // LLM-dependent capabilities - orchestrator should gather context first, then LLM synthesizes. // Or, these could be called by the orchestrator if the LLM explicitly plans to use them for final synthesis. { name: "capability_generateSuggestionWithContext", description: "Generates a code suggestion based on a query and extensive provided context (files, diff, snippets). Call this after gathering sufficient context.", parameters_schema: CapabilityGenerateSuggestionWithContextParamsSchema }, { name: "capability_analyzeCodeProblemWithContext", description: "Analyzes a code problem based on a query and provided relevant code snippets. Call this after gathering snippets.", parameters_schema: CapabilityAnalyzeCodeProblemWithContextParamsSchema }, ]; const orchestratorSystemPrompt = generateAgentSystemPrompt(capabilityDefinitions); let currentPromptContent = `Original User Query: ${user_query}\n\nAnalyze this query and formulate a plan. Then, choose your first capability call or provide a direct answer if no capabilities are needed.`; let orchestratorSteps = 0; const maxOrchestratorSteps = config_service_1.configService.AGENT_ABSOLUTE_MAX_STEPS; // Use absolute max for the orchestrator's internal loop const capabilityContext = { qdrantClient, repoPath, suggestionModelAvailable, }; while (orchestratorSteps < maxOrchestratorSteps && !agentState.isComplete) { orchestratorSteps++; config_service_1.logger.info(`Orchestrator Step ${orchestratorSteps}/${maxOrchestratorSteps} for query: "${user_query}"`); const fullPrompt = `${orchestratorSystemPrompt}\n\n${currentPromptContent}`; const llmProvider = await (0, llm_provider_1.getLLMProvider)(); // Get provider inside loop if it can change, or outside if static config_service_1.logger.debug(`Orchestrator (step ${orchestratorSteps}) sending prompt to LLM. Length: ${fullPrompt.length}`); // logger.silly("Orchestrator prompt content:", { prompt: fullPrompt }); // Potentially very verbose let llmResponseText; try { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error("Orchestrator LLM call timed out")), config_service_1.configService.AGENT_QUERY_TIMEOUT); }); llmResponseText = await Promise.race([ llmProvider.generateText(fullPrompt), timeoutPromise ]); config_service_1.logger.debug(`Orchestrator (step ${orchestratorSteps}) LLM response received. Length: ${llmResponseText.length}`); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); config_service_1.logger.error(`Orchestrator LLM call failed or timed out: ${err.message}`); agentState.finalResponse = `Error during orchestration: LLM call failed. ${err.message}`; agentState.isComplete = true; break; } const parsedCapability = parseCapabilityCall(llmResponseText); if (parsedCapability) { if (parsedCapability.capability === "request_more_processing_time") { // This special capability doesn't actually exist in agent_capabilities.ts // It's a signal from the LLM. The loop condition already uses absolute max. // We could potentially extend a softer limit here if we had one. config_service_1.logger.info("Orchestrator: LLM requested more processing time.", { reasoning: parsedCapability.parameters.reasoning }); currentPromptContent += `\n\nThought: Processing time extension acknowledged. Current step ${orchestratorSteps}/${maxOrchestratorSteps}. Continue planning.`; // Add a step to agentState to record this request agentState.steps.push({ tool: "internal_request_more_time", // Use a distinct name input: parsedCapability.parameters, output: { status: "Acknowledged, loop continues up to absolute max steps." }, reasoning: parsedCapability.reasoning || "LLM requested more processing time." }); if (orchestratorSteps >= maxOrchestratorSteps - 1) { // -1 because step will increment config_service_1.logger.warn("Orchestrator: LLM requested more time, but already at absolute max steps. Will terminate after this."); } continue; // Continue to the next iteration, allowing LLM to make another decision } const rawCapabilityName = parsedCapability.capability; // Type guard to check if rawCapabilityName is a valid key of capabilities if (Object.prototype.hasOwnProperty.call(capabilities, rawCapabilityName)) { const capabilityName = rawCapabilityName; // Now safer const capabilityFunc = capabilities[capabilityName]; if (typeof capabilityFunc === 'function') { const capabilityDef = capabilityDefinitions.find(cd => cd.name === capabilityName); if (!capabilityDef) { // This case should ideally not be hit if capabilityName is derived from `keyof typeof capabilities` // and capabilityDefinitions is comprehensive. config_service_1.logger.error(`Orchestrator: Capability definition not found for known capability "${capabilityName}"`); currentPromptContent += `\n\nInternal Error: Capability definition missing for "${capabilityName}". Please report this.`; agentState.steps.push({ tool: "internal_error", input: { capability_name: capabilityName }, output: { error: `Capability definition for "${capabilityName}" not found internally.` }, reasoning: "Internal error during capability definition lookup." }); } else { // Validate parameters const validationResult = capabilityDef.parameters_schema.safeParse(parsedCapability.parameters); if (!validationResult.success) { config_service_1.logger.warn(`Orchestrator: Invalid parameters for capability ${capabilityName}. Errors:`, { errors: validationResult.error.issues, providedParams: parsedCapability.parameters }); currentPromptContent += `\n\nError: Invalid parameters provided for capability "${capabilityName}". Expected schema: ${JSON.stringify(capabilityDef.parameters_schema?._def || { description: capabilityDef.parameters_schema?.description }, null, 2)} Errors: ${validationResult.error.issues.map(issue => `${issue.path.join('.')}: ${issue.message}`).join(', ')} Please correct the parameters and try again.`; agentState.steps.push({ tool: capabilityName, input: parsedCapability.parameters, output: { error: "Invalid parameters", details: validationResult.error.issues }, reasoning: parsedCapability.reasoning || "Attempted to call capability with invalid parameters." }); } else { // Parameters are valid, proceed with execution try { config_service_1.logger.info(`Orchestrator executing capability: ${capabilityName}`, { params: validationResult.data }); // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument const capabilityResult = await capabilityFunc(capabilityContext, validationResult.data); agentState.steps.push({ tool: capabilityName, input: validationResult.data, // Log validated and potentially transformed data output: capabilityResult, reasoning: parsedCapability.reasoning || llmResponseText }); // NEW: Add significant results to agentState.context // Store the output of successful capability calls. agentState.context.push({ sourceCapability: capabilityName, timestamp: new Date().toISOString(), data: capabilityResult }); currentPromptContent += `\n\nExecuted Capability: ${capabilityName}\nParameters: ${JSON.stringify(validationResult.data)}\nResults: ${stringifyStepOutput(capabilityResult)}\n\nWhat is your next step or final answer?`; } catch (capError) { const cErr = capError instanceof Error ? capError : new Error(String(capError)); config_service_1.logger.error(`Orchestrator: Error executing capability ${capabilityName}: ${cErr.message}`, { stack: cErr.stack }); currentPromptContent += `\n\nError executing capability ${capabilityName}: ${cErr.message}. Please try a different approach or provide a response with the information you have.`; agentState.steps.push({ tool: capabilityName, input: validationResult.data, output: { error: `Failed to execute: ${cErr.message}` }, reasoning: parsedCapability.reasoning || "Attempted to call capability, but execution failed." }); } } } } else { // This case should ideally not be hit if all entries in 'capabilities' are functions. config_service_1.logger.warn(`Orchestrator: Capability "${capabilityName}" found but is not a function.`); currentPromptContent += `\n\nInternal Error: Capability "${capabilityName}" is not executable.`; agentState.steps.push({ tool: "internal_error_non_function_capability", input: { capability_name: capabilityName }, output: { error: `Capability "${capabilityName}" is not executable.` }, reasoning: "Internal error: capability entry is not a function." }); } } else { // This 'else' handles the case where rawCapabilityName is not a key of 'capabilities' // This replaces the old 'else' block that handled unknown capabilities. config_service_1.logger.warn(`Orchestrator: LLM tried to call unknown capability "${rawCapabilityName}"`); currentPromptContent += `\n\nError: You tried to call an unknown capability: "${rawCapabilityName}". Please choose from the available capabilities.`; agentState.steps.push({ tool: "unknown_capability_call", input: { capability_name: rawCapabilityName, parameters: parsedCapability.parameters }, output: { error: `Capability "${rawCapabilityName}" not found.` }, reasoning: parsedCapability.reasoning || "LLM attempted to call an unknown capability." }); } } else { // Not a capability call, assume it's the final answer config_service_1.logger.info("Orchestrator: LLM provided a final answer."); agentState.finalResponse = llmResponseText; agentState.isComplete = true; } } // End of while loop if (!agentState.isComplete) { config_service_1.logger.warn(`Orchestrator reached max steps (${maxOrchestratorSteps}) without a final answer. Synthesizing a fallback response.`); // Ask LLM to synthesize based on current state if no explicit final answer was given const fallbackPrompt = `${orchestratorSystemPrompt}\n\n${currentPromptContent}\n\nYou have reached the maximum number of steps. Please provide your final answer to the user based on the information collected so far.`; const llmProvider = await (0, llm_provider_1.getLLMProvider)(); try { agentState.finalResponse = await Promise.race([ llmProvider.generateText(fallbackPrompt), new Promise((_, reject) => setTimeout(() => reject(new Error("Fallback response generation timed out")), config_service_1.configService.AGENT_QUERY_TIMEOUT / 2)) // Shorter timeout ]); } catch (fallbackError) { const fbErr = fallbackError instanceof Error ? fallbackError : new Error(String(fallbackError)); config_service_1.logger.error(`Orchestrator: Fallback response generation failed: ${fbErr.message}`); agentState.finalResponse = "The agent reached its processing limit and could not generate a final summary. Please try rephrasing your query or contact support if this persists."; } agentState.isComplete = true; } // Persist final state to session (optional, depending on how session state is used later) // For now, addSuggestion handles adding the final response. // updateContext(session.id, repoPath, undefined, agentState); // If we want to save full agent state // Persist the final response as a suggestion (0, state_1.addSuggestion)(session.id, user_query, agentState.finalResponse || "No final response generated."); // NEW: Persist the full agent state to the session // The `context` field within `agentState` is now populated with capability outputs. // The third argument to updateContext (for general context items) can be undefined // as we are managing context within agentState. (0, state_1.addAgentSteps)(session.id, agentState.query, agentState.steps, agentState.finalResponse || "No final response was generated."); config_service_1.logger.info(`Orchestrator finished. Full agent state for session ${session.id} persisted. Final response added.`); return agentState.finalResponse || "No final response was generated by the orchestrator."; } // Execute a tool call async function executeToolCall(toolCall, qdrantClient, repoPath, suggestionModelAvailable) { const { tool, parameters } = toolCall; const toolInfo = exports.toolRegistry.find(t => t.name === tool); if (!toolInfo) { config_service_1.logger.error(`Tool not found: ${tool}`); throw new Error(`Tool not found: ${tool}`); } // This check is important as agent_query (and thus orchestration) requires an LLM. if (toolInfo.requiresModel && !suggestionModelAvailable) { config_service_1.logger.warn(`Attempt to use model-dependent tool '${tool}' when model is unavailable.`); throw new Error(`Tool ${tool} requires the suggestion model which is not available`); } switch (tool) { case "agent_query": { // Validate parameters for agent_query const validationResult = AgentQueryToolParamsSchema.safeParse(parameters); if (!validationResult.success) { config_service_1.logger.error("Invalid parameters for agent_query tool", { errors: validationResult.error.issues, params: parameters }); throw new Error(`Invalid parameters for agent_query: ${validationResult.error.message}`); } // Call the orchestrator return await runAgentQueryOrchestrator(validationResult.data, qdrantClient, repoPath, suggestionModelAvailable); } // Cases for old tools (search_code, get_repository_context, etc.) should be removed // as they are no longer directly callable tools. // If they are still present, ensure they throw an error indicating they are refactored. case "search_code": // Example of how to mark old tools case "get_repository_context": case "generate_suggestion": case "get_changelog": case "analyze_code_problem": case "request_additional_context": case "request_more_processing_steps": config_service_1.logger.error(`Attempted to call refactored tool '${tool}' directly.`); throw new Error(`Tool '${tool}' is an internal capability and cannot be called directly. Use 'agent_query'.`); default: config_service_1.logger.error(`Tool execution not implemented: ${tool}`); throw new Error(`Tool execution not implemented: ${tool}`); } } // Run the agent loop async function runAgentLoop(query, sessionId, qdrantClient, repoPath, suggestionModelAvailable) { config_service_1.logger.info(`Outer Agent Loop started for query: "${query}" (Session: ${sessionId || 'new'})`); // Ensure provider is ready (existing logic from your file) config_service_1.logger.info(`Agent running with provider: ${config_service_1.configService.SUGGESTION_PROVIDER}, model: ${config_service_1.configService.SUGGESTION_MODEL}`); const currentProvider = await (0, llm_provider_1.getLLMProvider)(); // Force refresh handled by getLLMProvider if needed const isConnected = await currentProvider.checkConnection(); config_service_1.logger.info(`Agent confirmed provider: ${isConnected ? "connected" : "disconnected"}`); if (!isConnected && suggestionModelAvailable) { // suggestionModelAvailable implies we expect a connection config_service_1.logger.error(`Agent provider ${config_service_1.configService.SUGGESTION_PROVIDER} is not connected. Aborting.`); return "Error: The AI suggestion provider is not connected. Please check your configuration and network."; } // Test generation (existing logic) if (suggestionModelAvailable) { try { const _testResult = await currentProvider.generateText("Test message"); config_service_1.logger.info(`Agent verified provider ${config_service_1.configService.SUGGESTION_PROVIDER} is working`); } catch (error) { config_service_1.logger.error(`Agent failed to verify provider ${config_service_1.configService.SUGGESTION_PROVIDER}: ${error instanceof Error ? error.message : String(error)}`); return `Error: Failed to verify the AI suggestion provider. ${error instanceof Error ? error.message : String(error)}`; } } else { config_service_1.logger.info("Suggestion model is not available. Agent will operate in a limited mode if possible, or fail if agent_query requires it."); // agent_query requires a model, so this path will likely lead to an error in executeToolCall if suggestionModelAvailable is false. } const session = (0, state_1.getOrCreateSession)(sessionId, repoPath); // The outer loop's system prompt is now very simple, guiding towards agent_query. // Or, we can assume the LLM will always pick agent_query if it's the only one. // For robustness, let's provide a minimal system prompt. const outerSystemPrompt = `You are a helpful assistant. To answer any user query about the codebase, you MUST use the "agent_query" tool. Tool: agent_query Description: ${exports.toolRegistry[0].description} Parameters: ${JSON.stringify(exports.toolRegistry[0].parameters, null, 2)}`; const initialUserPromptForOuterLoop = `User query: ${query}\n\nPlease use the "agent_query" tool to process this query.`; const agentPrompt = `${outerSystemPrompt}\n\n${initialUserPromptForOuterLoop}`; let finalAnswer; try { config_service_1.logger.info("Outer loop: Requesting LLM to invoke agent_query tool."); const llmProvider = await (0, llm_provider_1.getLLMProvider)(); const llmOutput = await llmProvider.generateText(agentPrompt); // LLM should output a TOOL_CALL for agent_query const toolCalls = parseToolCalls(llmOutput); // Existing parseToolCalls if (toolCalls.length > 0 && toolCalls[0].tool === "agent_query") { config_service_1.logger.info("Outer loop: LLM correctly chose agent_query. Executing..."); // Execute agent_query, which now contains the main orchestration logic const orchestratorResponse = await executeToolCall(toolCalls[0], // Assuming the first (and only) call is agent_query qdrantClient, repoPath, suggestionModelAvailable); finalAnswer = typeof orchestratorResponse === 'string' ? orchestratorResponse : JSON.stringify(orchestratorResponse); } else { config_service_1.logger.warn("Outer loop: LLM did not call agent_query as expected. Output was:", { llmOutput }); finalAnswer = "The agent did not follow instructions to use the 'agent_query' tool. Raw LLM output: " + llmOutput; (0, state_1.addSuggestion)(session.id, query, finalAnswer); // Log this unexpected response } } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); config_service_1.logger.error(`Error in outer agent loop: ${err.message}`, { stack: err.stack }); finalAnswer = `An error occurred while processing your request: ${err.message}`; (0, state_1.addSuggestion)(session.id, query, finalAnswer); // Log error response } const formattedResponse = `# CodeCompass Agent Response ${finalAnswer} Session ID: ${session.id} (Use this ID in future requests to maintain context)`; return formattedResponse; }