UNPKG

local-agent

Version:

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

498 lines (495 loc) 23.6 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; // interactions.ts /** * @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"); // AI model providers const openai_1 = require("@ai-sdk/openai"); const anthropic_1 = require("@ai-sdk/anthropic"); const google_1 = require("@ai-sdk/google"); const ai_1 = require("ai"); // OpenRouter is optional, import if available in your project let openrouter = null; let openrouterModule = null; try { // @ts-ignore openrouterModule = require("@openrouter/ai-sdk-provider"); } catch (e) { openrouterModule = null; } /** * Initializes the OpenRouter client using the current API key from environment variables. * Returns the client instance if successful, or null if unavailable. * * @returns {any | null} The OpenRouter client instance, or null if not configured. */ function initializeOpenRouter() { if (!openrouterModule) return null; try { return openrouterModule.createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY || "", }); } catch (e) { return null; } } const memory_1 = require("./memory"); const initialization_1 = require("./initialization"); /** * Run the interactive prompt loop for the agent session. */ const marked_1 = require("marked"); const marked_terminal_1 = __importDefault(require("marked-terminal")); /** * Parses a model string to extract the provider and model name. * Supports formats like "openai/gpt-4" or just "gpt-4" (defaults to OpenAI). * * @param {string} modelString - The model string to parse. * @returns {{ provider: string, modelName: string }} The provider and model name. */ function parseModelString(modelString) { if (!modelString.includes("/")) { // Backward compatibility: treat as openai return { provider: "openai", modelName: modelString }; } const [provider, ...rest] = modelString.split("/"); return { provider: provider.toLowerCase(), modelName: rest.join("/") }; } /** * Returns the appropriate model client function for the specified provider. * Supports OpenAI, Anthropic, Google, and OpenRouter. * * @param {string} provider - The AI provider name (e.g., "openai", "anthropic"). * @returns {(modelName: string) => any} Function to get the model instance for the provider. * @throws {Error} If the provider is unsupported or not configured. */ function getClientForProvider(provider) { switch (provider) { case "openai": return openai_1.openai; case "anthropic": return anthropic_1.anthropic; case "google": return google_1.google; // "custom" provider is not supported due to missing SDK export case "custom": throw new Error("The 'custom' provider is not supported: no implementation available in the current SDK."); case "openrouter": // Initialize OpenRouter client with current API key openrouter = initializeOpenRouter(); if (!openrouter) throw new Error("OpenRouter client not available or not configured. Make sure you have the @openrouter/ai-sdk-provider package installed and OPENROUTER_API_KEY set."); // OpenRouter supports both chat and completion models, but for simplicity, use .chat for now return (modelName) => openrouter.chat(modelName); default: throw new Error(`Unsupported AI provider: ${provider}`); } } /** * 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) { 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 context const conversationHistory = []; let currentTaskContext = null; // 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 - Only stop and wait for user input when the entire task is genuinely complete or you need clarification - Maintain context of what you've accomplished and what still needs to be done - For development tasks, ensure you create all necessary files, folders, and configurations - When creating projects, include package.json, proper file structure, and basic functionality TASK CONTINUATION LOGIC: - If you just created a folder for a project, 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 - Only consider a task complete when it's fully functional and ready to use 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`; console.log(`Type your prompt for ${agentName} (Ctrl+C to exit):`); rl.prompt(); // Set the prompt color for user input (using type assertion for private property) rl._writeToOutput = function (stringToWrite) { // Always write user input in blue rl.output.write(BLUE + stringToWrite + RESET); }; /** * Determines if a user prompt describes a multi-step task (e.g., project creation). * * @param {string} prompt - The user prompt to analyze. * @returns {boolean} True if the prompt is likely a multi-step task, false otherwise. */ function isMultiStepTask(prompt) { const multiStepKeywords = [ 'create', 'build', 'setup', 'make', 'develop', 'implement', 'generate', 'react app', 'project', 'application', 'website', 'api', 'server', 'all files', 'complete', 'full', 'entire', 'whole' ]; const lowerPrompt = prompt.toLowerCase(); return multiStepKeywords.some(keyword => lowerPrompt.includes(keyword)); } /** * 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`; } }); if (currentTaskContext && !currentTaskContext.isComplete) { context += `\nCURRENT TASK CONTEXT:\n`; context += `Task: ${currentTaskContext.taskDescription}\n`; context += `Completed Steps: ${currentTaskContext.completedSteps.join(', ')}\n`; context += `Next Steps: ${currentTaskContext.nextSteps.join(', ')}\n`; } return context; } /** * Determines if the agent should automatically continue a multi-step task based on the last response or tool used. * * @param {string} response - The assistant's last response. * @param {string} [toolUsed] - The name of the tool used, if any. * @returns {boolean} True if the task should continue automatically, false if complete. */ function shouldContinueTask(response, toolUsed) { if (!currentTaskContext || currentTaskContext.isComplete) return false; const continuationIndicators = [ 'created folder', 'created directory', 'made folder', 'next step', 'continue', 'now i will', 'now i need to', 'partially complete', 'still need', 'remaining', 'created basic', 'initial setup', 'first step', 'proceeding', 'moving on', 'next logical step', 'will now', 'let me', 'i will create', 'i will add', 'setting up', 'configuring', 'installing' ]; const completionIndicators = [ 'task complete', 'finished', 'done', 'ready to use', 'fully functional', 'all files created', 'project is complete', 'successfully created', 'everything is set up', 'application is now ready', 'setup is complete', 'all necessary files', 'ready to run' ]; const lowerResponse = response.toLowerCase(); // Check for completion indicators first if (completionIndicators.some(indicator => lowerResponse.includes(indicator))) { currentTaskContext.isComplete = true; return false; } // Special logic for specific tools that typically indicate continuation if (toolUsed) { const continuationTools = ['create_directory', 'write_file']; if (continuationTools.some(tool => toolUsed.includes(tool))) { // If we just created a directory or wrote a file, likely need to continue return true; } } // Check for continuation indicators return continuationIndicators.some(indicator => lowerResponse.includes(indicator)); } /** * 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 (isMultiStepTask(prompt)) { currentTaskContext = { isMultiStep: true, taskDescription: prompt, completedSteps: [], nextSteps: [], isComplete: false }; } } // 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 let model; try { const { provider, modelName } = parseModelString(config.model); const client = getClientForProvider(provider); model = client(modelName); } catch (err) { spinnerActive = false; clearInterval(spinnerInterval); process.stdout.clearLine(0); process.stdout.cursorTo(0); const errMsg = err instanceof Error ? err.message : String(err); console.error(`Error: ${errMsg}`); (0, memory_1.logAgentError)(sessionFile, errMsg); if (!isAutoContinuation) { rl.prompt(); } return; } const result = await (0, ai_1.generateText)({ model, tools: allTools, temperature: config.temperature, system: enhancedSystemPrompt, prompt: contextualPrompt }); spinnerActive = false; clearInterval(spinnerInterval); process.stdout.clearLine(0); process.stdout.cursorTo(0); let toolResultObj = null; let toolName = undefined; let assistantResponse = ""; if (typeof result.text === "string" && result.text.trim() !== "") { assistantResponse = result.text; // Normal LLM response if (result.text.trim() !== "") { // 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); } } else if (result.toolResults && Array.isArray(result.toolResults) && result.toolResults.length > 0) { toolResultObj = result.toolResults.find((tr) => typeof tr === "object" && tr !== null && tr.type === "tool-result"); if (toolResultObj) { toolName = toolResultObj.toolName || "unknown-tool"; } } else if (typeof result === "object" && result !== null && result.type === "tool-result") { toolResultObj = result; toolName = result.toolName || "unknown-tool"; } 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); (0, memory_1.logToolUsed)(sessionFile, toolName); try { const continuePrompt = `Here is the result of my last action:\n\n${JSON.stringify(toolResultObj, null, 2)}\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.`; // Use the same dynamic model selection for summary let summaryModel; try { const { provider, modelName } = parseModelString(config.model); const client = getClientForProvider(provider); summaryModel = client(modelName); } catch (err) { clearInterval(toolSpinnerInterval); process.stdout.clearLine(0); process.stdout.cursorTo(0); const errMsg = err instanceof Error ? err.message : String(err); console.error(`Error: ${errMsg}`); (0, memory_1.logAgentError)(sessionFile, errMsg); 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 }); // Update task context if (currentTaskContext && !currentTaskContext.isComplete) { currentTaskContext.completedSteps.push(`Used ${toolName}: ${assistantResponse.substring(0, 100)}...`); } // Check if we should continue the task automatically if (shouldContinueTask(assistantResponse, toolName)) { console.log(`\n${initialization_1.YELLOW}Task continuation detected. 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); const errMsg = err instanceof Error ? err.message : String(err); console.error(`Error: ${errMsg}`); (0, memory_1.logAgentError)(sessionFile, errMsg); } } else if (assistantResponse) { // Add to conversation history for non-tool responses conversationHistory.push({ role: 'assistant', content: assistantResponse, timestamp: new Date() }); // Check if we should continue the task automatically if (shouldContinueTask(assistantResponse)) { console.log(`\n${initialization_1.YELLOW}Task continuation detected. 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); const errMsg = err instanceof Error ? err.message : String(err); console.error(`Error: ${errMsg}`); (0, memory_1.logAgentError)(sessionFile, errMsg); } // 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; } await processUserInput(prompt); }); rl.on("close", () => { console.log(`\nSession saved to ${sessionFile}`); process.exit(0); }); } //# sourceMappingURL=interactions.js.map