local-agent
Version:
A CLI agentic system for orchestrating tools and memory with per-folder scoping
476 lines (471 loc) • 24.8 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.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