UNPKG

local-agent

Version:

A CLI agentic system for orchestrating tools and memory with per-folder scoping

476 lines (471 loc) 24.8 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.runInteractiveSession = runInteractiveSession; /** * @fileoverview * Provides the interactive session loop and supporting utilities for the local agent. * Handles user prompts, conversation context, multi-step task management, and dynamic model/tool selection. * Designed for extensibility and clarity for external developers. */ const readline = __importStar(require("readline")); const ai_sdk_1 = require("@agentic/ai-sdk"); const ai_1 = require("ai"); const marked_1 = require("marked"); const marked_terminal_1 = __importDefault(require("marked-terminal")); // Import new simplified systems const providers_1 = require("./providers"); const tasks_1 = require("./tasks"); // Debug logging utility const isDebug = process.argv.includes('--debug'); function debugLog(...args) { if (isDebug) console.log(...args); } const errors_1 = require("./errors"); const memory_1 = require("./memory"); const initialization_1 = require("./initialization"); /** * Starts and manages the interactive agent session in the terminal. * Handles user prompts, conversation history, multi-step task logic, and tool/model selection. * Provides a rich, markdown-enabled interface for agent interaction. * * @param {any} config - The agent configuration object. * @param {Record<string, any>} loadedTools - Loaded MCP tool instances. * @param {string} sessionFile - Path to the session memory log file. * @param {string} agentName - The display name of the agent. */ function runInteractiveSession(config, loadedTools, sessionFile, agentName, keys = {}) { const BLUE = "\x1b[34m"; const RESET = "\x1b[0m"; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: `${BLUE}$> ${RESET}` }); // Configure marked to use TerminalRenderer for markdown output marked_1.marked.setOptions({ renderer: new marked_terminal_1.default() }); // Conversation history and task management const conversationHistory = []; const taskManager = new tasks_1.TaskManager(); // Enhanced system prompt for multi-step task handling const enhancedSystemPrompt = `${config.system} IMPORTANT INSTRUCTIONS FOR MULTI-STEP TASKS: - You are capable of handling complex, multi-step tasks that require multiple tool invocations - When a user requests a complex task (like setting up a project, etc.), break it down into logical steps - After completing each step, analyze if the overall task is complete or if more steps are needed - If more steps are needed, continue working on the task WITHOUT waiting for user input - DO NOT STOP after creating just one file or folder - continue until the task is truly complete - For development tasks, ensure you create ALL necessary files, folders, and configurations - When creating projects, include package.json, proper file structure, source files, and basic functionality TASK CONTINUATION LOGIC - BE AGGRESSIVE: - If you just created a folder for a project, IMMEDIATELY continue by creating the necessary files - If you created some files but the project is incomplete, CONTINUE creating remaining files - If you set up basic structure, CONTINUE with configuration and dependencies - Create multiple files in sequence without stopping - Only consider a task complete when it's fully functional, all files exist, and ready to use - Use phrases like "Now I will", "Next I need to", "Let me now" to indicate continuation COMPLETION CRITERIA: - For projects: All source files, configuration files, and documentation must be created - The project should be immediately runnable/usable after completion - Include instructions on how to run/test the created project - Only stop when you would tell the user "you can now run this" or "everything is ready" TOOL USAGE GUIDELINES: - Use filesystem tools to create directories and write files manually - DO NOT try to run shell commands like 'npx create-react-app' - you don't have shell access - Instead, manually create all necessary files for projects (package.json, source files, etc.) - When creating React apps, manually create: package.json, src/App.js, src/index.js, public/index.html - When creating Node.js projects, manually create: package.json, server.js, routes/, etc. - If a tool fails with an error, acknowledge the error and try a different approach - Never repeat the same tool call that just failed CONTEXT AWARENESS: - Remember what you've done in previous steps of the current task - Build upon previous actions rather than starting over - Reference earlier work when explaining current actions - If you get tool errors, explain them clearly and adapt your approach`; console.log(`Type your prompt for ${agentName} (Ctrl+C to exit):`); rl.prompt(); /** * Builds a string representation of recent conversation history and current task context. * Used to provide context to the language model for more coherent responses. * * @returns {string} The formatted conversation and task context. */ function buildConversationContext() { if (conversationHistory.length === 0) return ''; const recentHistory = conversationHistory.slice(-10); // Keep last 10 messages let context = '\n\nCONVERSATION HISTORY:\n'; recentHistory.forEach((msg, index) => { const timeStr = msg.timestamp.toLocaleTimeString(); context += `[${timeStr}] ${msg.role.toUpperCase()}: ${msg.content}\n`; if (msg.toolUsed) { context += `[${timeStr}] TOOL_USED: ${msg.toolUsed}\n`; } }); // Get current task context from the task manager if (taskManager.isTaskActive()) { const taskContext = taskManager.getTaskContext(); context += `\nCURRENT TASK CONTEXT:\n`; context += `Task: ${taskContext.taskDescription}\n`; context += `Completed Steps: ${taskContext.completedSteps.join(', ')}\n`; context += `Next Steps: ${taskContext.nextSteps.join(', ')}\n`; } return context; } /** * Processes a user prompt, manages conversation flow, invokes tools/models, and handles multi-step logic. * Updates conversation history and task context as needed. * * @param {string} prompt - The user's input prompt. * @param {boolean} [isAutoContinuation=false] - Whether this is an automatic continuation of a multi-step task. * @returns {Promise<void>} */ async function processUserInput(prompt, isAutoContinuation = false) { const userTime = new Date(); // Log user prompt if (!isAutoContinuation) { (0, memory_1.logUserPrompt)(sessionFile, prompt); conversationHistory.push({ role: 'user', content: prompt, timestamp: userTime }); // Detect if this is a new multi-step task if (taskManager.isMultiStepTask(prompt)) { taskManager.startTask(prompt); } } // Add a space before the thinking/loading spinner console.log(""); // Spinner animation for "Thinking..." const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; let spinnerIndex = 0; let spinnerActive = true; const spinnerInterval = setInterval(() => { process.stdout.write(`\r${spinnerFrames[spinnerIndex]} Thinking...`); spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length; }, 100); try { // Combine all loaded MCP tools into a single array for createAISDKTools const mcpToolInstances = Object.values(loadedTools); const allTools = mcpToolInstances.length === 1 ? (0, ai_sdk_1.createAISDKTools)(mcpToolInstances[0]) : (0, ai_sdk_1.createAISDKTools)(...mcpToolInstances); // Build the full prompt with conversation context const contextualPrompt = prompt + buildConversationContext(); // Dynamically select provider and model using the factory let model; try { // Get model from the factory (handles parsing, provider loading, and initialization) model = await providers_1.ProviderFactory.getModelFromString(config.model, keys); } catch (err) { spinnerActive = false; clearInterval(spinnerInterval); process.stdout.clearLine(0); process.stdout.cursorTo(0); // Specific error handling for provider errors if (err instanceof providers_1.ProviderError) { (0, errors_1.handleError)(err, 'model selection'); } else { (0, errors_1.handleError)(err, 'model initialization'); } if (!isAutoContinuation) { rl.prompt(); } return; } // Add timeout for tool operations to prevent hanging const generateTextWithTimeout = (timeoutMs = 120000) => { return Promise.race([ (0, ai_1.generateText)({ model, tools: allTools, temperature: config.temperature, system: enhancedSystemPrompt, prompt: contextualPrompt }), new Promise((_, reject) => setTimeout(() => reject(new Error('Tool operation timeout after 2 minutes')), timeoutMs)) ]); }; const result = await generateTextWithTimeout(); // Type assertion for now spinnerActive = false; clearInterval(spinnerInterval); process.stdout.clearLine(0); process.stdout.cursorTo(0); let toolResultObj = null; let toolName = undefined; let assistantResponse = ""; let hasToolCalls = false; // Debug: Log the full result structure to understand what we're getting debugLog(`\n[DEBUG] Result structure:`, JSON.stringify({ text: result.text ? `"${result.text.substring(0, 100)}..."` : result.text, toolCalls: result.toolCalls ? result.toolCalls.length : 0, toolResults: result.toolResults ? result.toolResults.length : 0, usage: result.usage }, null, 2)); // Check for tool calls first (before results) if (result.toolCalls && Array.isArray(result.toolCalls) && result.toolCalls.length > 0) { hasToolCalls = true; const toolCall = result.toolCalls[0]; // Get first tool call toolName = toolCall.toolName; debugLog(`\n[DEBUG] Tool call detected: ${toolName} with args:`, JSON.stringify(toolCall.args, null, 2)); } // Check for tool results if (result.toolResults && Array.isArray(result.toolResults) && result.toolResults.length > 0) { toolResultObj = result.toolResults[0]; // Get first tool result if (toolResultObj) { toolName = toolResultObj.toolName || toolName || "unknown-tool"; debugLog(`\n[DEBUG] Tool result received for ${toolName}:`, JSON.stringify({ type: toolResultObj.type, isError: toolResultObj.isError, result: typeof toolResultObj.result === 'string' ? toolResultObj.result.substring(0, 200) + '...' : toolResultObj.result }, null, 2)); } } // Process normal text response (when no tools are involved) if (typeof result.text === "string" && result.text.trim() !== "" && !hasToolCalls) { assistantResponse = result.text; // Render markdown response with agent name in yellow const agentPrefix = `${initialization_1.YELLOW}${agentName}>${RESET} `; console.log((0, marked_1.marked)(`${agentPrefix}${result.text}\n`)); (0, memory_1.logAgentResponse)(sessionFile, result.text); } if (toolResultObj && toolName) { // Show yellow spinner with tool name until next LLM response const toolSpinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; let toolSpinnerIndex = 0; const toolSpinnerInterval = setInterval(() => { process.stdout.write(`\r${initialization_1.YELLOW}Tool [${toolName}] is working... ${toolSpinnerFrames[toolSpinnerIndex]}${RESET}`); toolSpinnerIndex = (toolSpinnerIndex + 1) % toolSpinnerFrames.length; }, 100); // Record tool usage in task manager taskManager.recordToolUse(toolName, toolResultObj); (0, memory_1.logToolUsed)(sessionFile, toolName, toolResultObj); try { // Build detailed tool result context let toolResultContext = ""; if (toolResultObj.isError) { // Handle tool errors explicitly toolResultContext = `TOOL ERROR - ${toolName} failed with error:\n${JSON.stringify(toolResultObj.result, null, 2)}\n\nPlease acknowledge this error to the user, explain what went wrong, and if possible, try an alternative approach or fix the issue. Do not repeat the same action that failed.`; console.log(`\n❌ [TOOL ERROR] ${toolName}:`, toolResultObj.result); } else { // Handle successful tool results let resultData = toolResultObj.result; // Make result more readable for the LLM if (typeof resultData === 'object') { resultData = JSON.stringify(resultData, null, 2); } else if (typeof resultData === 'string' && resultData.length > 1000) { // Truncate very long results resultData = resultData.substring(0, 1000) + '\n... (truncated)'; } toolResultContext = `TOOL SUCCESS - ${toolName} completed successfully with result:\n${resultData}\n\nPlease describe what happened to the user as if you performed the action yourself, in natural language. If this is part of a multi-step task and more work is needed to complete the overall goal, continue with the next logical step without waiting for user input.`; console.log(`\n✅ [TOOL SUCCESS] ${toolName}`); if (typeof toolResultObj.result === 'string' && toolResultObj.result.length < 200) { console.log(`Result: ${toolResultObj.result}`); } } const continuePrompt = toolResultContext; // Use the same dynamic model selection for summary let summaryModel; try { summaryModel = await providers_1.ProviderFactory.getModelFromString(config.model, keys); } catch (err) { clearInterval(toolSpinnerInterval); process.stdout.clearLine(0); process.stdout.cursorTo(0); (0, errors_1.handleError)(err, 'summary model selection'); return; } const summaryResult = await (0, ai_1.generateText)({ model: summaryModel, temperature: config.temperature, system: enhancedSystemPrompt, prompt: continuePrompt + buildConversationContext() }); clearInterval(toolSpinnerInterval); process.stdout.clearLine(0); process.stdout.cursorTo(0); let summaryResponse = ""; if (typeof summaryResult.text === "string" && summaryResult.text.trim() !== "") { summaryResponse = summaryResult.text; } else { summaryResponse = JSON.stringify(summaryResult, null, 2); } console.log(""); // Render markdown summary response with agent name in yellow const agentPrefix = `${initialization_1.YELLOW}${agentName}>${RESET} `; console.log((0, marked_1.marked)(`${agentPrefix}${summaryResponse}\n`)); (0, memory_1.logAgentResponse)(sessionFile, summaryResponse); assistantResponse = summaryResponse; // Add to conversation history conversationHistory.push({ role: 'assistant', content: assistantResponse, timestamp: new Date(), toolUsed: toolName }); // Process response through task manager taskManager.processResponse(assistantResponse, toolName); // Check task completion status const completionStatus = taskManager.getCompletionStatus(); if (completionStatus.isComplete) { if (completionStatus.reason) { console.log(`\n✅ ${initialization_1.YELLOW}Multi-step task completed (${completionStatus.reason}). Steps taken: ${completionStatus.stepCount}${RESET}`); } else { console.log(`\n✅ ${initialization_1.YELLOW}Multi-step task completed. Steps taken: ${completionStatus.stepCount}${RESET}`); } taskManager.resetTask(); } else if (taskManager.shouldContinueAutomatically(assistantResponse, toolName)) { console.log(`\n${initialization_1.YELLOW}Task continuation detected (step ${completionStatus.stepCount}). Press Enter to continue or modify the prompt:${RESET}`); // Pre-populate the readline with continuation prompt rl.write("continue"); rl.prompt(); return; } } catch (err) { clearInterval(toolSpinnerInterval); process.stdout.clearLine(0); process.stdout.cursorTo(0); (0, errors_1.handleError)(err, 'tool summary generation'); } } else if (assistantResponse) { // Add to conversation history for non-tool responses conversationHistory.push({ role: 'assistant', content: assistantResponse, timestamp: new Date() }); // Process response through task manager taskManager.processResponse(assistantResponse); // Check task completion status const completionStatus = taskManager.getCompletionStatus(); if (completionStatus.isComplete) { if (completionStatus.reason) { console.log(`\n✅ ${initialization_1.YELLOW}Multi-step task completed (${completionStatus.reason}). Steps taken: ${completionStatus.stepCount}${RESET}`); } else { console.log(`\n✅ ${initialization_1.YELLOW}Multi-step task completed. Steps taken: ${completionStatus.stepCount}${RESET}`); } taskManager.resetTask(); } else if (taskManager.shouldContinueAutomatically(assistantResponse)) { console.log(`\n${initialization_1.YELLOW}Task continuation detected (step ${completionStatus.stepCount}). Press Enter to continue or modify the prompt:${RESET}`); // Pre-populate the readline with continuation prompt rl.write("continue"); rl.prompt(); return; } } } catch (err) { spinnerActive = false; clearInterval(spinnerInterval); process.stdout.clearLine(0); process.stdout.cursorTo(0); (0, errors_1.handleError)(err, 'text generation'); } // Only prompt for next input if we're not auto-continuing if (!isAutoContinuation) { rl.prompt(); } } rl.on("line", async (line) => { const prompt = line.trim(); if (!prompt) { rl.prompt(); return; } // Handle special commands if (prompt === "/complete" || prompt === "/done") { if (taskManager.isTaskActive()) { taskManager.forceComplete("Manual completion by user"); console.log(`✅ ${initialization_1.YELLOW}Task manually marked as complete.${RESET}`); } else { console.log(`${initialization_1.YELLOW}No active task to complete.${RESET}`); } rl.prompt(); return; } if (prompt === "/status") { if (taskManager.isTaskActive()) { const context = taskManager.getTaskContext(); const status = taskManager.getCompletionStatus(); console.log(`\n📋 ${initialization_1.YELLOW}Current Task Status:${RESET}`); console.log(`Task: ${context.taskDescription}`); console.log(`Steps completed: ${status.stepCount}`); console.log(`Completed: ${context.completedSteps.join(', ') || 'None'}`); console.log(`Next steps: ${context.nextSteps.join(', ') || 'None'}`); } else { console.log(`${initialization_1.YELLOW}No active task.${RESET}`); } rl.prompt(); return; } if (prompt === "/help") { console.log(`\n${initialization_1.YELLOW}Available commands:${RESET}`); console.log(`/complete or /done - Manually mark current task as complete`); console.log(`/status - Show current task status`); console.log(`/help - Show this help message`); rl.prompt(); return; } await processUserInput(prompt); }); rl.on("close", () => { console.log(`\nSession saved to ${sessionFile}`); process.exit(0); }); } //# sourceMappingURL=interactions.js.map