@alvinveroy/codecompass
Version:
AI-powered MCP server for codebase navigation and LLM prompt optimization
674 lines (663 loc) • 44.7 kB
JavaScript
;
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;
}