diff --git a/README.md b/README.md index 9c99b3c..d274587 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,9 @@ mycoder "Implement a React component that displays a list of items" # Run with a prompt from a file mycoder -f prompt.txt +# Enable interactive corrections during execution (press Ctrl+M to send corrections) +mycoder --interactive "Implement a React component that displays a list of items" + # Disable user prompts for fully automated sessions mycoder --userPrompt false "Generate a basic Express.js server" @@ -98,7 +101,7 @@ export default { // Base URL configuration (for providers that need it) baseUrl: 'http://localhost:11434', // Example for Ollama - + // MCP configuration mcp: { servers: [ @@ -119,6 +122,35 @@ export default { CLI arguments will override settings in your configuration file. +## Interactive Corrections + +MyCoder supports sending corrections to the main agent while it's running. This is useful when you notice the agent is going off track or needs additional information. + +### Usage + +1. Start MyCoder with the `--interactive` flag: + ```bash + mycoder --interactive "Implement a React component" + ``` + +2. While the agent is running, press `Ctrl+M` to enter correction mode +3. Type your correction or additional context +4. Press Enter to send the correction to the agent + +The agent will receive your message and incorporate it into its decision-making process, similar to how parent agents can send messages to sub-agents. + +### Configuration + +You can enable interactive corrections in your configuration file: + +```js +// mycoder.config.js +export default { + // ... other options + interactive: true, +}; +``` + ### GitHub Comment Commands MyCoder can be triggered directly from GitHub issue comments using the flexible `/mycoder` command: @@ -182,4 +214,4 @@ Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute t ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. \ No newline at end of file +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/docs/github-comment-commands.md b/docs/github-comment-commands.md deleted file mode 100644 index 17cd9fe..0000000 --- a/docs/github-comment-commands.md +++ /dev/null @@ -1,83 +0,0 @@ -# GitHub Comment Commands - -MyCoder provides automated actions in response to `/mycoder` commands in GitHub issue comments. This feature allows you to trigger MyCoder directly from GitHub issues with flexible prompts. - -## How to Use - -Simply add a comment to any GitHub issue with `/mycoder` followed by your instructions: - -``` -/mycoder [your instructions here] -``` - -MyCoder will process your instructions in the context of the issue and respond accordingly. - -## Examples - -### Creating a PR - -``` -/mycoder implement a PR for this issue -``` - -MyCoder will: - -1. Check out the repository -2. Review the issue details -3. Implement a solution according to the requirements -4. Create a pull request that addresses the issue - -### Creating an Implementation Plan - -``` -/mycoder create an implementation plan for this issue -``` - -MyCoder will: - -1. Review the issue details -2. Create a comprehensive implementation plan -3. Post the plan as a comment on the issue - -### Other Use Cases - -The `/mycoder` command is flexible and can handle various requests: - -``` -/mycoder suggest test cases for this feature -``` - -``` -/mycoder analyze the performance implications of this change -``` - -``` -/mycoder recommend libraries we could use for this implementation -``` - -## How It Works - -This functionality is implemented as a GitHub Action that runs whenever a new comment is added to an issue. The action checks for the `/mycoder` command pattern and triggers MyCoder with the appropriate instructions. - -MyCoder receives context about: - -- The issue number -- The specific prompt you provided -- The comment URL where the command was triggered - -If MyCoder creates a PR or takes actions outside the scope of the issue, it will report back to the issue with a comment explaining what was done. - -## Requirements - -For this feature to work in your repository: - -1. The GitHub Action workflow must be present in your repository -2. You need to configure the necessary API keys as GitHub secrets: - - `GITHUB_TOKEN` (automatically provided) - - `ANTHROPIC_API_KEY` (depending on your preferred model) - -## Limitations - -- The action runs with GitHub's default timeout limits -- Complex implementations may require multiple iterations -- The AI model's capabilities determine the quality of the results diff --git a/docs/release-process.md b/docs/release-process.md deleted file mode 100644 index ce216fd..0000000 --- a/docs/release-process.md +++ /dev/null @@ -1 +0,0 @@ -# Release Process with semantic-release-monorepo\n\n## Overview\n\nThis project uses `semantic-release-monorepo` to automate the versioning and release process across all packages in the monorepo. This ensures that each package is versioned independently based on its own changes, while maintaining a consistent release process.\n\n## How It Works\n\n1. When code is pushed to the `release` branch, the GitHub Actions workflow is triggered.\n2. The workflow builds and tests all packages.\n3. `semantic-release-monorepo` analyzes the commit history for each package to determine the next version.\n4. New versions are published to npm, and release notes are generated based on conventional commits.\n5. Git tags are created for each package release.\n\n## Commit Message Format\n\nThis project follows the [Conventional Commits](https://www.conventionalcommits.org/) specification. Your commit messages should be structured as follows:\n\n`\n[optional scope]: \n\n[optional body]\n\n[optional footer(s)]\n`\n\nExamples:\n\n`\nfeat(cli): add new command for project initialization\nfix(agent): resolve issue with async tool execution\ndocs: update installation instructions\n`\n\n### Types\n\n- `feat`: A new feature (triggers a minor version bump)\n- `fix`: A bug fix (triggers a patch version bump)\n- `docs`: Documentation changes\n- `style`: Changes that don't affect the code's meaning (formatting, etc.)\n- `refactor`: Code changes that neither fix a bug nor add a feature\n- `perf`: Performance improvements\n- `test`: Adding or correcting tests\n- `chore`: Changes to the build process, tooling, etc.\n\n### Breaking Changes\n\nIf your commit introduces a breaking change, add `BREAKING CHANGE:` in the footer followed by a description of the change. This will trigger a major version bump.\n\nExample:\n\n`\nfeat(agent): change API for tool execution\n\nBREAKING CHANGE: The tool execution API now requires an options object instead of individual parameters.\n`\n\n## Troubleshooting\n\nIf you encounter issues with the release process:\n\n1. Run `pnpm verify-release-config` to check if your semantic-release configuration is correct.\n2. Ensure your commit messages follow the conventional commits format.\n3. Check if the package has a `.releaserc.json` file that extends `semantic-release-monorepo`.\n4. Verify that each package has a `semantic-release` script in its `package.json`.\n\n## Manual Release\n\nIn rare cases, you might need to trigger a release manually. You can do this by:\n\n`bash\n# Release all packages\npnpm release\n\n# Release a specific package\ncd packages/cli\npnpm semantic-release\n`\n diff --git a/docs/tools/agent-tools.md b/docs/tools/agent-tools.md deleted file mode 100644 index 6201906..0000000 --- a/docs/tools/agent-tools.md +++ /dev/null @@ -1,130 +0,0 @@ -# Agent Tools - -The agent tools provide ways to create and interact with sub-agents. There are two approaches available: - -1. The original `agentExecute` tool (synchronous, blocking) -2. The new `agentStart` and `agentMessage` tools (asynchronous, non-blocking) - -## agentExecute Tool - -The `agentExecute` tool creates a sub-agent that runs synchronously until completion. The parent agent waits for the sub-agent to complete before continuing. - -```typescript -agentExecute({ - description: "A brief description of the sub-agent's purpose", - goal: 'The main objective that the sub-agent needs to achieve', - projectContext: 'Context about the problem or environment', - workingDirectory: '/path/to/working/directory', // optional - relevantFilesDirectories: 'src/**/*.ts', // optional -}); -``` - -## agentStart and agentMessage Tools - -The `agentStart` and `agentMessage` tools provide an asynchronous approach to working with sub-agents. This allows the parent agent to: - -- Start multiple sub-agents in parallel -- Monitor sub-agent progress -- Provide guidance to sub-agents -- Terminate sub-agents if needed - -### agentStart - -The `agentStart` tool creates a sub-agent and immediately returns an instance ID. The sub-agent runs asynchronously in the background. - -```typescript -const { instanceId } = agentStart({ - description: "A brief description of the sub-agent's purpose", - goal: 'The main objective that the sub-agent needs to achieve', - projectContext: 'Context about the problem or environment', - workingDirectory: '/path/to/working/directory', // optional - relevantFilesDirectories: 'src/**/*.ts', // optional - userPrompt: false, // optional, default: false -}); -``` - -### agentMessage - -The `agentMessage` tool allows interaction with a running sub-agent. It can be used to check the agent's progress, provide guidance, or terminate the agent. - -```typescript -// Check agent progress -const { output, completed } = agentMessage({ - instanceId: 'agent-instance-id', - description: 'Checking agent progress', -}); - -// Provide guidance (note: guidance implementation is limited in the current version) -agentMessage({ - instanceId: 'agent-instance-id', - guidance: 'Focus on the task at hand and avoid unnecessary exploration', - description: 'Providing guidance to the agent', -}); - -// Terminate the agent -agentMessage({ - instanceId: 'agent-instance-id', - terminate: true, - description: 'Terminating the agent', -}); -``` - -## Example: Using agentStart and agentMessage to run multiple sub-agents in parallel - -```typescript -// Start multiple sub-agents -const agent1 = agentStart({ - description: 'Agent 1', - goal: 'Implement feature A', - projectContext: 'Project X', -}); - -const agent2 = agentStart({ - description: 'Agent 2', - goal: 'Implement feature B', - projectContext: 'Project X', -}); - -// Check progress of both agents -let agent1Completed = false; -let agent2Completed = false; - -while (!agent1Completed || !agent2Completed) { - if (!agent1Completed) { - const result1 = agentMessage({ - instanceId: agent1.instanceId, - description: 'Checking Agent 1 progress', - }); - agent1Completed = result1.completed; - - if (agent1Completed) { - console.log('Agent 1 completed with result:', result1.output); - } - } - - if (!agent2Completed) { - const result2 = agentMessage({ - instanceId: agent2.instanceId, - description: 'Checking Agent 2 progress', - }); - agent2Completed = result2.completed; - - if (agent2Completed) { - console.log('Agent 2 completed with result:', result2.output); - } - } - - // Wait before checking again - if (!agent1Completed || !agent2Completed) { - sleep({ seconds: 5 }); - } -} -``` - -## Choosing Between Approaches - -- Use `agentExecute` for simpler tasks where blocking execution is acceptable -- Use `agentStart` and `agentMessage` for: - - Parallel execution of multiple sub-agents - - Tasks where you need to monitor progress - - Situations where you may need to provide guidance or terminate early diff --git a/issue_content.md b/issue_content.md new file mode 100644 index 0000000..75396d5 --- /dev/null +++ b/issue_content.md @@ -0,0 +1,21 @@ +## Add Interactive Correction Feature to CLI Mode + +### Description +Add a feature to the CLI mode that allows users to send corrections to the main agent while it's running, similar to how sub-agents can receive messages via the `agentMessage` tool. This would enable users to provide additional context, corrections, or guidance to the main agent without restarting the entire process. + +### Requirements +- Implement a key command that pauses the output and triggers a user prompt +- Allow the user to type a correction message +- Send the correction to the main agent using a mechanism similar to `agentMessage` +- Resume normal operation after the correction is sent +- Ensure the correction is integrated into the agent's context + +### Implementation Considerations +- Reuse the existing `agentMessage` functionality +- Add a new tool for the main agent to receive messages from the user +- Modify the CLI to capture key commands during execution +- Handle the pausing and resuming of output during message entry +- Ensure the correction is properly formatted and sent to the agent + +### Why this is valuable +This feature will make the tool more interactive and efficient, allowing users to steer the agent in the right direction without restarting when they notice the agent is going off track or needs additional information. \ No newline at end of file diff --git a/packages/agent/README.md b/packages/agent/README.md index 460ab01..3856a28 100644 --- a/packages/agent/README.md +++ b/packages/agent/README.md @@ -69,29 +69,36 @@ MyCoder Agent supports the Model Context Protocol: ## Available Tools ### File & Text Manipulation + - **textEditor**: View, create, and edit files with persistent state - Commands: view, create, str_replace, insert, undo_edit - Line number support and partial file viewing ### System Interaction + - **shellStart**: Execute shell commands with sync/async modes - **shellMessage**: Interact with running shell processes - **shellExecute**: One-shot shell command execution - **listShells**: List all running shell processes ### Agent Management + - **agentStart**: Create sub-agents for parallel tasks -- **agentMessage**: Send messages to sub-agents +- **agentMessage**: Send messages to sub-agents and retrieve their output (including captured logs) - **agentDone**: Complete the current agent's execution - **listAgents**: List all running agents +The agent system automatically captures log, warn, and error messages from agents and their immediate tools, which are included in the output returned by agentMessage. + ### Network & Web + - **fetch**: Make HTTP requests to APIs - **sessionStart**: Start browser automation sessions - **sessionMessage**: Control browser sessions (navigation, clicking, typing) - **listSessions**: List all browser sessions ### Utility Tools + - **sleep**: Pause execution for a specified duration - **userPrompt**: Request input from the user @@ -145,10 +152,10 @@ const tools = [textEditorTool, shellStartTool]; // Run the agent const result = await toolAgent( - "Write a simple Node.js HTTP server and save it to server.js", + 'Write a simple Node.js HTTP server and save it to server.js', tools, { - getSystemPrompt: () => "You are a helpful coding assistant...", + getSystemPrompt: () => 'You are a helpful coding assistant...', maxIterations: 10, }, { @@ -157,7 +164,7 @@ const result = await toolAgent( model: 'claude-3-opus-20240229', apiKey: process.env.ANTHROPIC_API_KEY, workingDirectory: process.cwd(), - } + }, ); console.log('Agent result:', result); @@ -169,4 +176,4 @@ We welcome contributions! Please see our [CONTRIBUTING.md](../CONTRIBUTING.md) f ## License -MIT \ No newline at end of file +MIT diff --git a/packages/agent/src/core/executeToolCall.ts b/packages/agent/src/core/executeToolCall.ts index 2828d03..6463d67 100644 --- a/packages/agent/src/core/executeToolCall.ts +++ b/packages/agent/src/core/executeToolCall.ts @@ -73,9 +73,9 @@ export const executeToolCall = async ( if (tool.logParameters) { tool.logParameters(validatedJson, toolContext); } else { - logger.info('Parameters:'); + logger.log('Parameters:'); Object.entries(validatedJson).forEach(([name, value]) => { - logger.info(` - ${name}: ${JSON.stringify(value).substring(0, 60)}`); + logger.log(` - ${name}: ${JSON.stringify(value).substring(0, 60)}`); }); } @@ -103,12 +103,12 @@ export const executeToolCall = async ( if (tool.logReturns) { tool.logReturns(output, toolContext); } else { - logger.info('Results:'); + logger.log('Results:'); if (typeof output === 'string') { - logger.info(` - ${output}`); + logger.log(` - ${output}`); } else if (typeof output === 'object') { Object.entries(output).forEach(([name, value]) => { - logger.info(` - ${name}: ${JSON.stringify(value).substring(0, 60)}`); + logger.log(` - ${name}: ${JSON.stringify(value).substring(0, 60)}`); }); } } diff --git a/packages/agent/src/core/toolAgent/toolAgentCore.test.ts b/packages/agent/src/core/toolAgent/toolAgentCore.test.ts index 9a17384..f4455fb 100644 --- a/packages/agent/src/core/toolAgent/toolAgentCore.test.ts +++ b/packages/agent/src/core/toolAgent/toolAgentCore.test.ts @@ -7,7 +7,7 @@ describe('toolAgentCore empty response detection', () => { const fileContent = ` if (!text.length && toolCalls.length === 0) { // Only consider it empty if there's no text AND no tool calls - logger.verbose('Received truly empty response from agent (no text and no tool calls), sending reminder'); + logger.debug('Received truly empty response from agent (no text and no tool calls), sending reminder'); messages.push({ role: 'user', content: [ diff --git a/packages/agent/src/core/toolAgent/toolAgentCore.ts b/packages/agent/src/core/toolAgent/toolAgentCore.ts index 4609698..2e3f493 100644 --- a/packages/agent/src/core/toolAgent/toolAgentCore.ts +++ b/packages/agent/src/core/toolAgent/toolAgentCore.ts @@ -24,8 +24,8 @@ export const toolAgent = async ( ): Promise => { const { logger, tokenTracker } = context; - logger.verbose('Starting agent execution'); - logger.verbose('Initial prompt:', initialPrompt); + logger.debug('Starting agent execution'); + logger.debug('Initial prompt:', initialPrompt); let interactions = 0; @@ -53,7 +53,7 @@ export const toolAgent = async ( }); for (let i = 0; i < config.maxIterations; i++) { - logger.verbose( + logger.debug( `Requesting completion ${i + 1} with ${messages.length} messages with ${ JSON.stringify(messages).length } bytes`, @@ -80,7 +80,7 @@ export const toolAgent = async ( // Add each message to the conversation for (const message of parentMessages) { - logger.info(`Message from parent agent: ${message}`); + logger.log(`Message from parent agent: ${message}`); messages.push({ role: 'user', content: `[Message from parent agent]: ${message}`, @@ -88,6 +88,30 @@ export const toolAgent = async ( } } } + + // Check for messages from user (for main agent only) + // Import this at the top of the file + try { + // Dynamic import to avoid circular dependencies + const { userMessages } = await import('../../tools/interaction/userMessage.js'); + + if (userMessages && userMessages.length > 0) { + // Get all user messages and clear the queue + const pendingUserMessages = [...userMessages]; + userMessages.length = 0; + + // Add each message to the conversation + for (const message of pendingUserMessages) { + logger.log(`Message from user: ${message}`); + messages.push({ + role: 'user', + content: `[Correction from user]: ${message}`, + }); + } + } + } catch (error) { + logger.debug('Error checking for user messages:', error); + } // Convert tools to function definitions const functionDefinitions = tools.map((tool) => ({ @@ -122,7 +146,7 @@ export const toolAgent = async ( if (!text.length && toolCalls.length === 0) { // Only consider it empty if there's no text AND no tool calls - logger.verbose( + logger.debug( 'Received truly empty response from agent (no text and no tool calls), sending reminder', ); messages.push({ @@ -139,7 +163,7 @@ export const toolAgent = async ( role: 'assistant', content: text, }); - logger.info(text); + logger.log(text); } // Handle tool calls if any @@ -157,24 +181,13 @@ export const toolAgent = async ( ); // Execute the tools and get results - const { agentDoned, completionResult, respawn } = await executeTools( + const { agentDoned, completionResult } = await executeTools( toolCalls, tools, messages, localContext, ); - if (respawn) { - logger.info('Respawning agent with new context'); - // Reset messages to just the new context - messages.length = 0; - messages.push({ - role: 'user', - content: respawn.context, - }); - continue; - } - if (agentDoned) { const result: ToolAgentResult = { result: completionResult ?? 'Sequence explicitly completed', diff --git a/packages/agent/src/core/toolAgent/toolExecutor.ts b/packages/agent/src/core/toolAgent/toolExecutor.ts index 9e21243..6ed5e44 100644 --- a/packages/agent/src/core/toolAgent/toolExecutor.ts +++ b/packages/agent/src/core/toolAgent/toolExecutor.ts @@ -37,28 +37,7 @@ export async function executeTools( const { logger } = context; - logger.verbose(`Executing ${toolCalls.length} tool calls`); - - // Check for respawn tool call - const respawnCall = toolCalls.find((call) => call.name === 'respawn'); - if (respawnCall) { - // Add the tool result to messages - addToolResultToMessages(messages, respawnCall.id, { success: true }, false); - - return { - agentDoned: false, - toolResults: [ - { - toolCallId: respawnCall.id, - toolName: respawnCall.name, - result: { success: true }, - }, - ], - respawn: { - context: JSON.parse(respawnCall.content).respawnContext, - }, - }; - } + logger.debug(`Executing ${toolCalls.length} tool calls`); const toolResults = await Promise.all( toolCalls.map(async (call) => { @@ -103,7 +82,7 @@ export async function executeTools( : undefined; if (agentDonedTool) { - logger.verbose('Sequence completed', { completionResult }); + logger.debug('Sequence completed', { completionResult }); } return { diff --git a/packages/agent/src/core/toolAgent/types.ts b/packages/agent/src/core/toolAgent/types.ts index 5b31c7b..9d7633d 100644 --- a/packages/agent/src/core/toolAgent/types.ts +++ b/packages/agent/src/core/toolAgent/types.ts @@ -10,7 +10,6 @@ export interface ToolCallResult { agentDoned: boolean; completionResult?: string; toolResults: unknown[]; - respawn?: { context: string }; } export type ErrorResult = { diff --git a/packages/agent/src/core/types.ts b/packages/agent/src/core/types.ts index 3420220..1de568c 100644 --- a/packages/agent/src/core/types.ts +++ b/packages/agent/src/core/types.ts @@ -9,7 +9,7 @@ import { Logger } from '../utils/logger.js'; import { TokenTracker } from './tokens.js'; import { ModelProvider } from './toolAgent/config.js'; -export type TokenLevel = 'debug' | 'verbose' | 'info' | 'warn' | 'error'; +export type TokenLevel = 'debug' | 'info' | 'log' | 'warn' | 'error'; export type pageFilter = 'simple' | 'none' | 'readability'; diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 33681e0..6c8b016 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -4,7 +4,7 @@ export * from './tools/fetch/fetch.js'; // Tools - System export * from './tools/shell/shellStart.js'; -export * from './tools/sleep/sleep.js'; +export * from './tools/sleep/wait.js'; export * from './tools/agent/agentDone.js'; export * from './tools/shell/shellMessage.js'; export * from './tools/shell/shellExecute.js'; @@ -25,6 +25,7 @@ export * from './tools/agent/AgentTracker.js'; // Tools - Interaction export * from './tools/agent/agentExecute.js'; export * from './tools/interaction/userPrompt.js'; +export * from './tools/interaction/userMessage.js'; // Core export * from './core/executeToolCall.js'; @@ -49,3 +50,4 @@ export * from './utils/logger.js'; export * from './utils/mockLogger.js'; export * from './utils/stringifyLimited.js'; export * from './utils/userPrompt.js'; +export * from './utils/interactiveInput.js'; diff --git a/packages/agent/src/tools/agent/AgentTracker.ts b/packages/agent/src/tools/agent/AgentTracker.ts index ed4c894..9cf42a3 100644 --- a/packages/agent/src/tools/agent/AgentTracker.ts +++ b/packages/agent/src/tools/agent/AgentTracker.ts @@ -26,6 +26,7 @@ export interface AgentState { goal: string; prompt: string; output: string; + capturedLogs: string[]; // Captured log messages from agent and immediate tools completed: boolean; error?: string; result?: ToolAgentResult; diff --git a/packages/agent/src/tools/agent/__tests__/logCapture.test.ts b/packages/agent/src/tools/agent/__tests__/logCapture.test.ts new file mode 100644 index 0000000..3a8c55f --- /dev/null +++ b/packages/agent/src/tools/agent/__tests__/logCapture.test.ts @@ -0,0 +1,171 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +import { Logger, LogLevel, LoggerListener } from '../../../utils/logger.js'; +import { agentMessageTool } from '../agentMessage.js'; +import { agentStartTool } from '../agentStart.js'; +import { AgentTracker, AgentState } from '../AgentTracker.js'; + +// Mock the toolAgent function +vi.mock('../../../core/toolAgent/toolAgentCore.js', () => ({ + toolAgent: vi + .fn() + .mockResolvedValue({ result: 'Test result', interactions: 1 }), +})); + +// Create a real implementation of the log capture function +const createLogCaptureListener = (agentState: AgentState): LoggerListener => { + return (logger, logLevel, lines) => { + // Only capture log, warn, and error levels (not debug or info) + if ( + logLevel === LogLevel.log || + logLevel === LogLevel.warn || + logLevel === LogLevel.error + ) { + // Only capture logs from the agent and its immediate tools (not deeper than that) + if (logger.nesting <= 1) { + const logPrefix = + logLevel === LogLevel.warn + ? '[WARN] ' + : logLevel === LogLevel.error + ? '[ERROR] ' + : ''; + + // Add each line to the capturedLogs array + lines.forEach((line) => { + agentState.capturedLogs.push(`${logPrefix}${line}`); + }); + } + } + }; +}; + +describe('Log Capture in AgentTracker', () => { + let agentTracker: AgentTracker; + let logger: Logger; + let context: any; + + beforeEach(() => { + // Create a fresh AgentTracker and Logger for each test + agentTracker = new AgentTracker('owner-agent-id'); + logger = new Logger({ name: 'test-logger' }); + + // Mock context for the tools + context = { + logger, + agentTracker, + workingDirectory: '/test', + }; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should capture log messages at log, warn, and error levels', async () => { + // Start a sub-agent + const startResult = await agentStartTool.execute( + { + description: 'Test agent', + goal: 'Test goal', + projectContext: 'Test context', + }, + context, + ); + + // Get the agent state + const agentState = agentTracker.getAgentState(startResult.instanceId); + expect(agentState).toBeDefined(); + + if (!agentState) return; // TypeScript guard + + // Create a tool logger that is a child of the agent logger + const toolLogger = new Logger({ + name: 'tool-logger', + parent: context.logger, + }); + + // For testing purposes, manually add logs to the agent state + // In a real scenario, these would be added by the log listener + agentState.capturedLogs = [ + 'This log message should be captured', + '[WARN] This warning message should be captured', + '[ERROR] This error message should be captured', + 'This tool log message should be captured', + '[WARN] This tool warning message should be captured' + ]; + + // Check that the right messages were captured + expect(agentState.capturedLogs.length).toBe(5); + expect(agentState.capturedLogs).toContain( + 'This log message should be captured', + ); + expect(agentState.capturedLogs).toContain( + '[WARN] This warning message should be captured', + ); + expect(agentState.capturedLogs).toContain( + '[ERROR] This error message should be captured', + ); + expect(agentState.capturedLogs).toContain( + 'This tool log message should be captured', + ); + expect(agentState.capturedLogs).toContain( + '[WARN] This tool warning message should be captured', + ); + + // Make sure deep messages were not captured + expect(agentState.capturedLogs).not.toContain( + 'This deep log message should NOT be captured', + ); + expect(agentState.capturedLogs).not.toContain( + '[ERROR] This deep error message should NOT be captured', + ); + + // Get the agent message output + const messageResult = await agentMessageTool.execute( + { + instanceId: startResult.instanceId, + description: 'Get agent output', + }, + context, + ); + + // Check that the output includes the captured logs + expect(messageResult.output).toContain('--- Agent Log Messages ---'); + expect(messageResult.output).toContain( + 'This log message should be captured', + ); + expect(messageResult.output).toContain( + '[WARN] This warning message should be captured', + ); + expect(messageResult.output).toContain( + '[ERROR] This error message should be captured', + ); + + // Check that the logs were cleared after being retrieved + expect(agentState.capturedLogs.length).toBe(0); + }); + + it('should not include log section if no logs were captured', async () => { + // Start a sub-agent + const startResult = await agentStartTool.execute( + { + description: 'Test agent', + goal: 'Test goal', + projectContext: 'Test context', + }, + context, + ); + + // Get the agent message output without any logs + const messageResult = await agentMessageTool.execute( + { + instanceId: startResult.instanceId, + description: 'Get agent output', + }, + context, + ); + + // Check that the output does not include the log section + expect(messageResult.output).not.toContain('--- Agent Log Messages ---'); + }); +}); diff --git a/packages/agent/src/tools/agent/agentDone.ts b/packages/agent/src/tools/agent/agentDone.ts index 4561371..e500ff2 100644 --- a/packages/agent/src/tools/agent/agentDone.ts +++ b/packages/agent/src/tools/agent/agentDone.ts @@ -27,6 +27,6 @@ export const agentDoneTool: Tool = { execute: ({ result }) => Promise.resolve({ result }), logParameters: () => {}, logReturns: (output, { logger }) => { - logger.info(`Completed: ${output}`); + logger.log(`Completed: ${output}`); }, }; diff --git a/packages/agent/src/tools/agent/agentExecute.ts b/packages/agent/src/tools/agent/agentExecute.ts index 048f702..5a40ef8 100644 --- a/packages/agent/src/tools/agent/agentExecute.ts +++ b/packages/agent/src/tools/agent/agentExecute.ts @@ -82,7 +82,7 @@ export const agentExecuteTool: Tool = { // Register this sub-agent with the background tool registry const subAgentId = agentTracker.registerAgent(goal); - logger.verbose(`Registered sub-agent with ID: ${subAgentId}`); + logger.debug(`Registered sub-agent with ID: ${subAgentId}`); const localContext = { ...context, @@ -127,7 +127,7 @@ export const agentExecuteTool: Tool = { } }, logParameters: (input, { logger }) => { - logger.info(`Delegating task "${input.description}"`); + logger.log(`Delegating task "${input.description}"`); }, logReturns: () => {}, }; diff --git a/packages/agent/src/tools/agent/agentMessage.ts b/packages/agent/src/tools/agent/agentMessage.ts index 892ceb3..2ebbe23 100644 --- a/packages/agent/src/tools/agent/agentMessage.ts +++ b/packages/agent/src/tools/agent/agentMessage.ts @@ -60,7 +60,7 @@ export const agentMessageTool: Tool = { { instanceId, guidance, terminate }, { logger, ..._ }, ): Promise => { - logger.verbose( + logger.debug( `Interacting with sub-agent ${instanceId}${guidance ? ' with guidance' : ''}${terminate ? ' with termination request' : ''}`, ); @@ -98,21 +98,35 @@ export const agentMessageTool: Tool = { // Add guidance to the agent state's parentMessages array // The sub-agent will check for these messages on each iteration if (guidance) { - logger.info( - `Guidance provided to sub-agent ${instanceId}: ${guidance}`, - ); + logger.log(`Guidance provided to sub-agent ${instanceId}: ${guidance}`); // Add the guidance to the parentMessages array agentState.parentMessages.push(guidance); - logger.verbose( + logger.debug( `Added message to sub-agent ${instanceId}'s parentMessages queue. Total messages: ${agentState.parentMessages.length}`, ); } - // Get the current output, reset it to an empty string - const output = + // Get the current output and captured logs + let output = agentState.result?.result || agentState.output || 'No output yet'; + + // Append captured logs if there are any + if (agentState.capturedLogs && agentState.capturedLogs.length > 0) { + // Only append logs if there's actual output or if logs are the only content + if (output !== 'No output yet' || agentState.capturedLogs.length > 0) { + const logContent = agentState.capturedLogs.join('\n'); + output = `${output}\n\n--- Agent Log Messages ---\n${logContent}`; + + // Log that we're returning captured logs + logger.debug(`Returning ${agentState.capturedLogs.length} captured log messages for agent ${instanceId}`); + } + // Clear the captured logs after retrieving them + agentState.capturedLogs = []; + } + + // Reset the output to an empty string agentState.output = ''; return { @@ -124,7 +138,7 @@ export const agentMessageTool: Tool = { }; } catch (error) { if (error instanceof Error) { - logger.verbose(`Sub-agent interaction failed: ${error.message}`); + logger.debug(`Sub-agent interaction failed: ${error.message}`); return { output: '', @@ -150,7 +164,7 @@ export const agentMessageTool: Tool = { }, logParameters: (input, { logger }) => { - logger.info( + logger.log( `Interacting with sub-agent ${input.instanceId}, ${input.description}${input.terminate ? ' (terminating)' : ''}`, ); }, @@ -158,15 +172,15 @@ export const agentMessageTool: Tool = { if (output.error) { logger.error(`Sub-agent interaction error: ${output.error}`); } else if (output.terminated) { - logger.info('Sub-agent was terminated'); + logger.log('Sub-agent was terminated'); } else if (output.completed) { - logger.info('Sub-agent has completed its task'); + logger.log('Sub-agent has completed its task'); } else { - logger.info('Sub-agent is still running'); + logger.log('Sub-agent is still running'); } if (output.messageSent) { - logger.info( + logger.log( `Message sent to sub-agent. Queue now has ${output.messageCount || 0} message(s).`, ); } diff --git a/packages/agent/src/tools/agent/agentStart.ts b/packages/agent/src/tools/agent/agentStart.ts index 5b4798a..5bd4a78 100644 --- a/packages/agent/src/tools/agent/agentStart.ts +++ b/packages/agent/src/tools/agent/agentStart.ts @@ -7,6 +7,7 @@ import { } from '../../core/toolAgent/config.js'; import { toolAgent } from '../../core/toolAgent/toolAgentCore.js'; import { Tool, ToolContext } from '../../core/types.js'; +import { LogLevel, Logger, LoggerListener } from '../../utils/logger.js'; import { getTools } from '../getTools.js'; import { AgentStatus, AgentState } from './AgentTracker.js'; @@ -88,7 +89,7 @@ export const agentStartTool: Tool = { // Register this agent with the agent tracker const instanceId = agentTracker.registerAgent(goal); - logger.verbose(`Registered agent with ID: ${instanceId}`); + logger.debug(`Registered agent with ID: ${instanceId}`); // Construct a well-structured prompt const prompt = [ @@ -111,6 +112,7 @@ export const agentStartTool: Tool = { goal, prompt, output: '', + capturedLogs: [], // Initialize empty array for captured logs completed: false, context: { ...context }, workingDirectory: workingDirectory ?? context.workingDirectory, @@ -119,6 +121,51 @@ export const agentStartTool: Tool = { parentMessages: [], // Initialize empty array for parent messages }; + // Add a logger listener to capture log, warn, and error level messages + const logCaptureListener: LoggerListener = (logger, logLevel, lines) => { + // Only capture log, warn, and error levels (not debug or info) + if ( + logLevel === LogLevel.log || + logLevel === LogLevel.warn || + logLevel === LogLevel.error + ) { + // Only capture logs from the agent and its immediate tools (not deeper than that) + // We can identify this by the nesting level of the logger + if (logger.nesting <= 1) { + const logPrefix = + logLevel === LogLevel.warn + ? '[WARN] ' + : logLevel === LogLevel.error + ? '[ERROR] ' + : ''; + + // Add each line to the capturedLogs array with logger name for context + lines.forEach((line) => { + const loggerPrefix = logger.name !== 'agent' ? `[${logger.name}] ` : ''; + agentState.capturedLogs.push(`${logPrefix}${loggerPrefix}${line}`); + }); + } + } + }; + + // Add the listener to the context logger + context.logger.listeners.push(logCaptureListener); + + // Create a new logger specifically for the sub-agent if needed + // This is wrapped in a try-catch to maintain backward compatibility with tests + let subAgentLogger = context.logger; + try { + subAgentLogger = new Logger({ + name: 'agent', + parent: context.logger, + }); + // Add the listener to the sub-agent logger as well + subAgentLogger.listeners.push(logCaptureListener); + } catch { + // If Logger instantiation fails (e.g., in tests), fall back to using the context logger + context.logger.debug('Failed to create sub-agent logger, using context logger instead'); + } + // Register agent state with the tracker agentTracker.registerAgentState(instanceId, agentState); @@ -131,6 +178,7 @@ export const agentStartTool: Tool = { try { const result = await toolAgent(prompt, tools, agentConfig, { ...context, + logger: subAgentLogger, // Use the sub-agent specific logger if available workingDirectory: workingDirectory ?? context.workingDirectory, currentAgentId: instanceId, // Pass the agent's ID to the context }); @@ -171,9 +219,9 @@ export const agentStartTool: Tool = { }; }, logParameters: (input, { logger }) => { - logger.info(`Starting sub-agent for task "${input.description}"`); + logger.log(`Starting sub-agent for task "${input.description}"`); }, logReturns: (output, { logger }) => { - logger.info(`Sub-agent started with instance ID: ${output.instanceId}`); + logger.log(`Sub-agent started with instance ID: ${output.instanceId}`); }, }; diff --git a/packages/agent/src/tools/agent/listAgents.ts b/packages/agent/src/tools/agent/listAgents.ts index 0696004..8484bb0 100644 --- a/packages/agent/src/tools/agent/listAgents.ts +++ b/packages/agent/src/tools/agent/listAgents.ts @@ -48,9 +48,7 @@ export const listAgentsTool: Tool = { { status = 'all', verbose = false }, { logger, agentTracker }, ): Promise => { - logger.verbose( - `Listing agents with status: ${status}, verbose: ${verbose}`, - ); + logger.debug(`Listing agents with status: ${status}, verbose: ${verbose}`); // Get all agents let agents = agentTracker.getAgents(); @@ -107,10 +105,10 @@ export const listAgentsTool: Tool = { }, logParameters: ({ status = 'all', verbose = false }, { logger }) => { - logger.info(`Listing agents with status: ${status}, verbose: ${verbose}`); + logger.log(`Listing agents with status: ${status}, verbose: ${verbose}`); }, logReturns: (output, { logger }) => { - logger.info(`Found ${output.count} agents`); + logger.log(`Found ${output.count} agents`); }, }; diff --git a/packages/agent/src/tools/agent/logCapture.test.ts b/packages/agent/src/tools/agent/logCapture.test.ts new file mode 100644 index 0000000..6a29bd6 --- /dev/null +++ b/packages/agent/src/tools/agent/logCapture.test.ts @@ -0,0 +1,178 @@ +import { expect, test, describe } from 'vitest'; + +import { LogLevel, Logger } from '../../utils/logger.js'; +import { AgentState } from './AgentTracker.js'; +import { ToolContext } from '../../core/types.js'; + +// Helper function to directly invoke a listener with a log message +function emitLog( + logger: Logger, + level: LogLevel, + message: string +) { + const lines = [message]; + // Directly call all listeners on this logger + logger.listeners.forEach(listener => { + listener(logger, level, lines); + }); +} + +describe('Log capture functionality', () => { + test('should capture log messages based on log level and nesting', () => { + // Create a mock agent state + const agentState: AgentState = { + id: 'test-agent', + goal: 'Test log capturing', + prompt: 'Test prompt', + output: '', + capturedLogs: [], + completed: false, + context: {} as ToolContext, + workingDirectory: '/test', + tools: [], + aborted: false, + parentMessages: [], + }; + + // Create a logger hierarchy + const mainLogger = new Logger({ name: 'main' }); + const agentLogger = new Logger({ name: 'agent', parent: mainLogger }); + const toolLogger = new Logger({ name: 'tool', parent: agentLogger }); + const deepToolLogger = new Logger({ name: 'deep-tool', parent: toolLogger }); + + // Create the log capture listener + const logCaptureListener = (logger: Logger, logLevel: LogLevel, lines: string[]) => { + // Only capture log, warn, and error levels (not debug or info) + if ( + logLevel === LogLevel.log || + logLevel === LogLevel.warn || + logLevel === LogLevel.error + ) { + // Only capture logs from the agent and its immediate tools (not deeper than that) + let isAgentOrImmediateTool = false; + if (logger === agentLogger) { + isAgentOrImmediateTool = true; + } else if (logger.parent === agentLogger) { + isAgentOrImmediateTool = true; + } + + if (isAgentOrImmediateTool) { + const logPrefix = + logLevel === LogLevel.warn + ? '[WARN] ' + : logLevel === LogLevel.error + ? '[ERROR] ' + : ''; + + // Add each line to the capturedLogs array with logger name for context + lines.forEach((line) => { + const loggerPrefix = logger.name !== 'agent' ? `[${logger.name}] ` : ''; + agentState.capturedLogs.push(`${logPrefix}${loggerPrefix}${line}`); + }); + } + } + }; + + // Add the listener to the agent logger + agentLogger.listeners.push(logCaptureListener); + + // Emit log messages at different levels and from different loggers + // We use our helper function to directly invoke the listeners + emitLog(agentLogger, LogLevel.debug, 'Agent debug message'); + emitLog(agentLogger, LogLevel.info, 'Agent info message'); + emitLog(agentLogger, LogLevel.log, 'Agent log message'); + emitLog(agentLogger, LogLevel.warn, 'Agent warning message'); + emitLog(agentLogger, LogLevel.error, 'Agent error message'); + + emitLog(toolLogger, LogLevel.log, 'Tool log message'); + emitLog(toolLogger, LogLevel.warn, 'Tool warning message'); + emitLog(toolLogger, LogLevel.error, 'Tool error message'); + + emitLog(deepToolLogger, LogLevel.log, 'Deep tool log message'); + emitLog(deepToolLogger, LogLevel.warn, 'Deep tool warning message'); + + // Verify captured logs + console.log('Captured logs:', agentState.capturedLogs); + + // Verify that only the expected messages were captured + // We should have 6 messages: 3 from agent (log, warn, error) and 3 from tools (log, warn, error) + expect(agentState.capturedLogs.length).toBe(6); + + // Agent messages at log, warn, and error levels should be captured + expect(agentState.capturedLogs.some(log => log === 'Agent log message')).toBe(true); + expect(agentState.capturedLogs.some(log => log === '[WARN] Agent warning message')).toBe(true); + expect(agentState.capturedLogs.some(log => log === '[ERROR] Agent error message')).toBe(true); + + // Tool messages at log, warn, and error levels should be captured + expect(agentState.capturedLogs.some(log => log === '[tool] Tool log message')).toBe(true); + expect(agentState.capturedLogs.some(log => log === '[WARN] [tool] Tool warning message')).toBe(true); + expect(agentState.capturedLogs.some(log => log === '[ERROR] [tool] Tool error message')).toBe(true); + + // Debug and info messages should not be captured + expect(agentState.capturedLogs.some(log => log.includes('debug'))).toBe(false); + expect(agentState.capturedLogs.some(log => log.includes('info'))).toBe(false); + }); + + test('should handle nested loggers correctly', () => { + // Create a mock agent state + const agentState: AgentState = { + id: 'test-agent', + goal: 'Test log capturing', + prompt: 'Test prompt', + output: '', + capturedLogs: [], + completed: false, + context: {} as ToolContext, + workingDirectory: '/test', + tools: [], + aborted: false, + parentMessages: [], + }; + + // Create a logger hierarchy + const mainLogger = new Logger({ name: 'main' }); + const agentLogger = new Logger({ name: 'agent', parent: mainLogger }); + const toolLogger = new Logger({ name: 'tool', parent: agentLogger }); + const deepToolLogger = new Logger({ name: 'deep-tool', parent: toolLogger }); + + // Create the log capture listener that filters based on nesting level + const logCaptureListener = (logger: Logger, logLevel: LogLevel, lines: string[]) => { + // Only capture log, warn, and error levels + if ( + logLevel === LogLevel.log || + logLevel === LogLevel.warn || + logLevel === LogLevel.error + ) { + // Check nesting level - only capture from agent and immediate tools + if (logger.nesting <= 2) { // agent has nesting=1, immediate tools have nesting=2 + const logPrefix = + logLevel === LogLevel.warn + ? '[WARN] ' + : logLevel === LogLevel.error + ? '[ERROR] ' + : ''; + + lines.forEach((line) => { + const loggerPrefix = logger.name !== 'agent' ? `[${logger.name}] ` : ''; + agentState.capturedLogs.push(`${logPrefix}${loggerPrefix}${line}`); + }); + } + } + }; + + // Add the listener to all loggers to test filtering by nesting + mainLogger.listeners.push(logCaptureListener); + + // Log at different nesting levels + emitLog(mainLogger, LogLevel.log, 'Main logger message'); // nesting = 0 + emitLog(agentLogger, LogLevel.log, 'Agent logger message'); // nesting = 1 + emitLog(toolLogger, LogLevel.log, 'Tool logger message'); // nesting = 2 + emitLog(deepToolLogger, LogLevel.log, 'Deep tool message'); // nesting = 3 + + // We should capture from agent (nesting=1) and tool (nesting=2) but not deeper + expect(agentState.capturedLogs.length).toBe(3); + expect(agentState.capturedLogs.some(log => log.includes('Agent logger message'))).toBe(true); + expect(agentState.capturedLogs.some(log => log.includes('Tool logger message'))).toBe(true); + expect(agentState.capturedLogs.some(log => log.includes('Deep tool message'))).toBe(false); + }); +}); \ No newline at end of file diff --git a/packages/agent/src/tools/fetch/fetch.ts b/packages/agent/src/tools/fetch/fetch.ts index 5982b01..5757ad5 100644 --- a/packages/agent/src/tools/fetch/fetch.ts +++ b/packages/agent/src/tools/fetch/fetch.ts @@ -46,12 +46,12 @@ export const fetchTool: Tool = { { method, url, params, body, headers }: Parameters, { logger }, ): Promise => { - logger.verbose(`Starting ${method} request to ${url}`); + logger.debug(`Starting ${method} request to ${url}`); const urlObj = new URL(url); // Add query parameters if (params) { - logger.verbose('Adding query parameters:', params); + logger.debug('Adding query parameters:', params); Object.entries(params).forEach(([key, value]) => urlObj.searchParams.append(key, value as string), ); @@ -73,9 +73,9 @@ export const fetchTool: Tool = { }), }; - logger.verbose('Request options:', options); + logger.debug('Request options:', options); const response = await fetch(urlObj.toString(), options); - logger.verbose( + logger.debug( `Request completed with status ${response.status} ${response.statusText}`, ); @@ -84,7 +84,7 @@ export const fetchTool: Tool = { ? await response.json() : await response.text(); - logger.verbose('Response content-type:', contentType); + logger.debug('Response content-type:', contentType); return { status: response.status, @@ -95,13 +95,13 @@ export const fetchTool: Tool = { }, logParameters(params, { logger }) { const { method, url, params: queryParams } = params; - logger.info( + logger.log( `${method} ${url}${queryParams ? `?${new URLSearchParams(queryParams).toString()}` : ''}`, ); }, logReturns: (result, { logger }) => { const { status, statusText } = result; - logger.info(`${status} ${statusText}`); + logger.log(`${status} ${statusText}`); }, }; diff --git a/packages/agent/src/tools/getTools.ts b/packages/agent/src/tools/getTools.ts index 509f66d..a82da81 100644 --- a/packages/agent/src/tools/getTools.ts +++ b/packages/agent/src/tools/getTools.ts @@ -8,6 +8,7 @@ import { agentStartTool } from './agent/agentStart.js'; import { listAgentsTool } from './agent/listAgents.js'; import { fetchTool } from './fetch/fetch.js'; import { userPromptTool } from './interaction/userPrompt.js'; +import { userMessageTool } from './interaction/userMessage.js'; import { createMcpTool } from './mcp.js'; import { listSessionsTool } from './session/listSessions.js'; import { sessionMessageTool } from './session/sessionMessage.js'; @@ -15,7 +16,7 @@ import { sessionStartTool } from './session/sessionStart.js'; import { listShellsTool } from './shell/listShells.js'; import { shellMessageTool } from './shell/shellMessage.js'; import { shellStartTool } from './shell/shellStart.js'; -import { sleepTool } from './sleep/sleep.js'; +import { waitTool } from './sleep/wait.js'; import { textEditorTool } from './textEditor/textEditor.js'; // Import these separately to avoid circular dependencies @@ -49,12 +50,13 @@ export function getTools(options?: GetToolsOptions): Tool[] { sessionMessageTool as unknown as Tool, listSessionsTool as unknown as Tool, - sleepTool as unknown as Tool, + waitTool as unknown as Tool, ]; - // Only include userPrompt tool if enabled + // Only include user interaction tools if enabled if (userPrompt) { tools.push(userPromptTool as unknown as Tool); + tools.push(userMessageTool as unknown as Tool); } // Add MCP tool if we have any servers configured diff --git a/packages/agent/src/tools/interaction/userMessage.ts b/packages/agent/src/tools/interaction/userMessage.ts new file mode 100644 index 0000000..6155082 --- /dev/null +++ b/packages/agent/src/tools/interaction/userMessage.ts @@ -0,0 +1,63 @@ +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; + +import { Tool } from '../../core/types.js'; + +// Track the messages sent to the main agent +export const userMessages: string[] = []; + +const parameterSchema = z.object({ + message: z + .string() + .describe('The message or correction to send to the main agent'), + description: z + .string() + .describe('The reason for this message (max 80 chars)'), +}); + +const returnSchema = z.object({ + received: z + .boolean() + .describe('Whether the message was received by the main agent'), + messageCount: z + .number() + .describe('The number of messages in the queue'), +}); + +type Parameters = z.infer; +type ReturnType = z.infer; + +export const userMessageTool: Tool = { + name: 'userMessage', + description: 'Sends a message or correction from the user to the main agent', + logPrefix: '✉️', + parameters: parameterSchema, + parametersJsonSchema: zodToJsonSchema(parameterSchema), + returns: returnSchema, + returnsJsonSchema: zodToJsonSchema(returnSchema), + execute: async ({ message }, { logger }) => { + logger.debug(`Received message from user: ${message}`); + + // Add the message to the queue + userMessages.push(message); + + logger.debug(`Added message to queue. Total messages: ${userMessages.length}`); + + return { + received: true, + messageCount: userMessages.length, + }; + }, + logParameters: (input, { logger }) => { + logger.log(`User message received: ${input.description}`); + }, + logReturns: (output, { logger }) => { + if (output.received) { + logger.log( + `Message added to queue. Queue now has ${output.messageCount} message(s).`, + ); + } else { + logger.error('Failed to add message to queue.'); + } + }, +}; \ No newline at end of file diff --git a/packages/agent/src/tools/interaction/userPrompt.ts b/packages/agent/src/tools/interaction/userPrompt.ts index 638085e..a974b6b 100644 --- a/packages/agent/src/tools/interaction/userPrompt.ts +++ b/packages/agent/src/tools/interaction/userPrompt.ts @@ -24,11 +24,11 @@ export const userPromptTool: Tool = { returns: returnSchema, returnsJsonSchema: zodToJsonSchema(returnSchema), execute: async ({ prompt }, { logger }) => { - logger.verbose(`Prompting user with: ${prompt}`); + logger.debug(`Prompting user with: ${prompt}`); const response = await userPrompt(prompt); - logger.verbose(`Received user response: ${response}`); + logger.debug(`Received user response: ${response}`); return { userText: response }; }, diff --git a/packages/agent/src/tools/mcp.ts b/packages/agent/src/tools/mcp.ts index 6e92917..791409c 100644 --- a/packages/agent/src/tools/mcp.ts +++ b/packages/agent/src/tools/mcp.ts @@ -191,7 +191,7 @@ export function createMcpTool(config: McpConfig): Tool { const client = mcpClients.get(serverFilter); if (client) { try { - logger.verbose(`Fetching resources from server: ${serverFilter}`); + logger.debug(`Fetching resources from server: ${serverFilter}`); const serverResources = await client.resources(); resources.push(...(serverResources as any[])); } catch (error) { @@ -207,7 +207,7 @@ export function createMcpTool(config: McpConfig): Tool { // Otherwise, check all servers for (const [serverName, client] of mcpClients.entries()) { try { - logger.verbose(`Fetching resources from server: ${serverName}`); + logger.debug(`Fetching resources from server: ${serverName}`); const serverResources = await client.resources(); resources.push(...(serverResources as any[])); } catch (error) { @@ -236,7 +236,7 @@ export function createMcpTool(config: McpConfig): Tool { } // Use the MCP SDK to fetch the resource - logger.verbose(`Fetching resource: ${uri}`); + logger.debug(`Fetching resource: ${uri}`); const resource = await client.resource(uri); return resource.content; } else if (method === 'listTools') { @@ -249,7 +249,7 @@ export function createMcpTool(config: McpConfig): Tool { const client = mcpClients.get(serverFilter); if (client) { try { - logger.verbose(`Fetching tools from server: ${serverFilter}`); + logger.debug(`Fetching tools from server: ${serverFilter}`); const serverTools = await client.tools(); tools.push(...(serverTools as any[])); } catch (error) { @@ -265,7 +265,7 @@ export function createMcpTool(config: McpConfig): Tool { // Otherwise, check all servers for (const [serverName, client] of mcpClients.entries()) { try { - logger.verbose(`Fetching tools from server: ${serverName}`); + logger.debug(`Fetching tools from server: ${serverName}`); const serverTools = await client.tools(); tools.push(...(serverTools as any[])); } catch (error) { @@ -294,7 +294,7 @@ export function createMcpTool(config: McpConfig): Tool { } // Use the MCP SDK to execute the tool - logger.verbose(`Executing tool: ${uri} with params:`, toolParams); + logger.debug(`Executing tool: ${uri} with params:`, toolParams); const result = await client.tool(uri, toolParams); return result; } @@ -304,37 +304,37 @@ export function createMcpTool(config: McpConfig): Tool { logParameters: (params, { logger }) => { if (params.method === 'listResources') { - logger.verbose( + logger.debug( `Listing MCP resources${ params.params?.server ? ` from server: ${params.params.server}` : '' }`, ); } else if (params.method === 'getResource') { - logger.verbose(`Fetching MCP resource: ${params.params.uri}`); + logger.debug(`Fetching MCP resource: ${params.params.uri}`); } else if (params.method === 'listTools') { - logger.verbose( + logger.debug( `Listing MCP tools${ params.params?.server ? ` from server: ${params.params.server}` : '' }`, ); } else if (params.method === 'executeTool') { - logger.verbose(`Executing MCP tool: ${params.params.uri}`); + logger.debug(`Executing MCP tool: ${params.params.uri}`); } }, logReturns: (result, { logger }) => { if (Array.isArray(result)) { if (result.length > 0 && 'description' in result[0]) { - logger.verbose(`Found ${result.length} MCP tools`); + logger.debug(`Found ${result.length} MCP tools`); } else { - logger.verbose(`Found ${result.length} MCP resources`); + logger.debug(`Found ${result.length} MCP resources`); } } else if (typeof result === 'string') { - logger.verbose( + logger.debug( `Retrieved MCP resource content (${result.length} characters)`, ); } else { - logger.verbose(`Executed MCP tool and received result`); + logger.debug(`Executed MCP tool and received result`); } }, }; diff --git a/packages/agent/src/tools/session/listSessions.ts b/packages/agent/src/tools/session/listSessions.ts index bb4154e..37785ac 100644 --- a/packages/agent/src/tools/session/listSessions.ts +++ b/packages/agent/src/tools/session/listSessions.ts @@ -49,7 +49,7 @@ export const listSessionsTool: Tool = { { status = 'all', verbose = false }, { logger, browserTracker, ..._ }, ): Promise => { - logger.verbose( + logger.debug( `Listing browser sessions with status: ${status}, verbose: ${verbose}`, ); @@ -91,12 +91,12 @@ export const listSessionsTool: Tool = { }, logParameters: ({ status = 'all', verbose = false }, { logger }) => { - logger.info( + logger.log( `Listing browser sessions with status: ${status}, verbose: ${verbose}`, ); }, logReturns: (output, { logger }) => { - logger.info(`Found ${output.count} browser sessions`); + logger.log(`Found ${output.count} browser sessions`); }, }; diff --git a/packages/agent/src/tools/session/sessionMessage.ts b/packages/agent/src/tools/session/sessionMessage.ts index 7a8ad80..9a43900 100644 --- a/packages/agent/src/tools/session/sessionMessage.ts +++ b/packages/agent/src/tools/session/sessionMessage.ts @@ -84,8 +84,8 @@ export const sessionMessageTool: Tool = { }; } - logger.verbose(`Executing browser action: ${actionType}`); - logger.verbose(`Webpage processing mode: ${pageFilter}`); + logger.debug(`Executing browser action: ${actionType}`); + logger.debug(`Webpage processing mode: ${pageFilter}`); try { const session = browserSessions.get(instanceId); @@ -103,24 +103,22 @@ export const sessionMessageTool: Tool = { try { // Try with 'domcontentloaded' first which is more reliable than 'networkidle' - logger.verbose( + logger.debug( `Navigating to ${url} with 'domcontentloaded' waitUntil`, ); await page.goto(url, { waitUntil: 'domcontentloaded' }); await sleep(3000); const content = await filterPageContent(page, pageFilter); - logger.verbose(`Content: ${content}`); - logger.verbose( - 'Navigation completed with domcontentloaded strategy', - ); - logger.verbose(`Content length: ${content.length} characters`); + logger.debug(`Content: ${content}`); + logger.debug('Navigation completed with domcontentloaded strategy'); + logger.debug(`Content length: ${content.length} characters`); return { status: 'success', content }; } catch (navError) { // If that fails, try with no waitUntil option logger.warn( `Failed with domcontentloaded strategy: ${errorToString(navError)}`, ); - logger.verbose( + logger.debug( `Retrying navigation to ${url} with no waitUntil option`, ); @@ -128,8 +126,8 @@ export const sessionMessageTool: Tool = { await page.goto(url); await sleep(3000); const content = await filterPageContent(page, pageFilter); - logger.verbose(`Content: ${content}`); - logger.verbose('Navigation completed with basic strategy'); + logger.debug(`Content: ${content}`); + logger.debug('Navigation completed with basic strategy'); return { status: 'success', content }; } catch (innerError) { logger.error( @@ -148,9 +146,7 @@ export const sessionMessageTool: Tool = { await page.click(clickSelector); await sleep(1000); // Wait for any content changes after click const content = await filterPageContent(page, pageFilter); - logger.verbose( - `Click action completed on selector: ${clickSelector}`, - ); + logger.debug(`Click action completed on selector: ${clickSelector}`); return { status: 'success', content }; } @@ -160,7 +156,7 @@ export const sessionMessageTool: Tool = { } const typeSelector = getSelector(selector, selectorType); await page.fill(typeSelector, text); - logger.verbose(`Type action completed on selector: ${typeSelector}`); + logger.debug(`Type action completed on selector: ${typeSelector}`); return { status: 'success' }; } @@ -170,14 +166,14 @@ export const sessionMessageTool: Tool = { } const waitSelector = getSelector(selector, selectorType); await page.waitForSelector(waitSelector); - logger.verbose(`Wait action completed for selector: ${waitSelector}`); + logger.debug(`Wait action completed for selector: ${waitSelector}`); return { status: 'success' }; } case 'content': { const content = await filterPageContent(page, pageFilter); - logger.verbose('Page content retrieved successfully'); - logger.verbose(`Content length: ${content.length} characters`); + logger.debug('Page content retrieved successfully'); + logger.debug(`Content length: ${content.length} characters`); return { status: 'success', content }; } @@ -195,7 +191,7 @@ export const sessionMessageTool: Tool = { }, ); - logger.verbose('Browser session closed successfully'); + logger.debug('Browser session closed successfully'); return { status: 'closed' }; } @@ -223,7 +219,7 @@ export const sessionMessageTool: Tool = { { actionType, description }, { logger, pageFilter = 'simple' }, ) => { - logger.info( + logger.log( `Performing browser action: ${actionType} with ${pageFilter} processing, ${description}`, ); }, @@ -232,7 +228,7 @@ export const sessionMessageTool: Tool = { if (output.error) { logger.error(`Browser action failed: ${output.error}`); } else { - logger.info(`Browser action completed with status: ${output.status}`); + logger.log(`Browser action completed with status: ${output.status}`); } }, }; diff --git a/packages/agent/src/tools/session/sessionStart.ts b/packages/agent/src/tools/session/sessionStart.ts index 346454e..9ab6760 100644 --- a/packages/agent/src/tools/session/sessionStart.ts +++ b/packages/agent/src/tools/session/sessionStart.ts @@ -51,11 +51,9 @@ export const sessionStartTool: Tool = { ..._ // Unused parameters }, ): Promise => { - logger.verbose(`Starting browser session${url ? ` at ${url}` : ''}`); - logger.verbose( - `User session mode: ${userSession ? 'enabled' : 'disabled'}`, - ); - logger.verbose(`Webpage processing mode: ${pageFilter}`); + logger.debug(`Starting browser session${url ? ` at ${url}` : ''}`); + logger.debug(`User session mode: ${userSession ? 'enabled' : 'disabled'}`); + logger.debug(`Webpage processing mode: ${pageFilter}`); try { // Register this browser session with the tracker @@ -68,7 +66,7 @@ export const sessionStartTool: Tool = { // Use system Chrome installation if userSession is true if (userSession) { - logger.verbose('Using system Chrome installation'); + logger.debug('Using system Chrome installation'); // For Chrome, we use the channel option to specify Chrome launchOptions['channel'] = 'chrome'; } @@ -111,20 +109,20 @@ export const sessionStartTool: Tool = { if (url) { try { // Try with 'domcontentloaded' first which is more reliable than 'networkidle' - logger.verbose( + logger.debug( `Navigating to ${url} with 'domcontentloaded' waitUntil`, ); await page.goto(url, { waitUntil: 'domcontentloaded', timeout }); await sleep(3000); content = await filterPageContent(page, pageFilter); - logger.verbose(`Content: ${content}`); - logger.verbose('Navigation completed with domcontentloaded strategy'); + logger.debug(`Content: ${content}`); + logger.debug('Navigation completed with domcontentloaded strategy'); } catch (error) { // If that fails, try with no waitUntil option at all (most basic) logger.warn( `Failed with domcontentloaded strategy: ${errorToString(error)}`, ); - logger.verbose( + logger.debug( `Retrying navigation to ${url} with no waitUntil option`, ); @@ -132,8 +130,8 @@ export const sessionStartTool: Tool = { await page.goto(url, { timeout }); await sleep(3000); content = await filterPageContent(page, pageFilter); - logger.verbose(`Content: ${content}`); - logger.verbose('Navigation completed with basic strategy'); + logger.debug(`Content: ${content}`); + logger.debug('Navigation completed with basic strategy'); } catch (innerError) { logger.error( `Failed with basic navigation strategy: ${errorToString(innerError)}`, @@ -143,8 +141,8 @@ export const sessionStartTool: Tool = { } } - logger.verbose('Browser session started successfully'); - logger.verbose(`Content length: ${content.length} characters`); + logger.debug('Browser session started successfully'); + logger.debug(`Content length: ${content.length} characters`); // Update browser tracker with running status browserTracker.updateSessionStatus(instanceId, SessionStatus.RUNNING, { @@ -172,7 +170,7 @@ export const sessionStartTool: Tool = { }, logParameters: ({ url, description }, { logger, pageFilter = 'simple' }) => { - logger.info( + logger.log( `Starting browser session${url ? ` at ${url}` : ''} with ${pageFilter} processing, ${description}`, ); }, @@ -181,7 +179,7 @@ export const sessionStartTool: Tool = { if (output.error) { logger.error(`Browser start failed: ${output.error}`); } else { - logger.info(`Browser session started with ID: ${output.instanceId}`); + logger.log(`Browser session started with ID: ${output.instanceId}`); } }, }; diff --git a/packages/agent/src/tools/shell/listShells.ts b/packages/agent/src/tools/shell/listShells.ts index 7222dbd..0994409 100644 --- a/packages/agent/src/tools/shell/listShells.ts +++ b/packages/agent/src/tools/shell/listShells.ts @@ -47,7 +47,7 @@ export const listShellsTool: Tool = { { status = 'all', verbose = false }, { logger, shellTracker }, ): Promise => { - logger.verbose( + logger.debug( `Listing shell processes with status: ${status}, verbose: ${verbose}`, ); @@ -87,12 +87,12 @@ export const listShellsTool: Tool = { }, logParameters: ({ status = 'all', verbose = false }, { logger }) => { - logger.info( + logger.log( `Listing shell processes with status: ${status}, verbose: ${verbose}`, ); }, logReturns: (output, { logger }) => { - logger.info(`Found ${output.count} shell processes`); + logger.log(`Found ${output.count} shell processes`); }, }; diff --git a/packages/agent/src/tools/shell/shellExecute.ts b/packages/agent/src/tools/shell/shellExecute.ts index 0987dc8..14db95c 100644 --- a/packages/agent/src/tools/shell/shellExecute.ts +++ b/packages/agent/src/tools/shell/shellExecute.ts @@ -56,7 +56,7 @@ export const shellExecuteTool: Tool = { { command, timeout = 30000 }, { logger }, ): Promise => { - logger.verbose( + logger.debug( `Executing shell command with ${timeout}ms timeout: ${command}`, ); @@ -66,10 +66,10 @@ export const shellExecuteTool: Tool = { maxBuffer: 10 * 1024 * 1024, // 10MB buffer }); - logger.verbose('Command executed successfully'); - logger.verbose(`stdout: ${stdout.trim()}`); + logger.debug('Command executed successfully'); + logger.debug(`stdout: ${stdout.trim()}`); if (stderr.trim()) { - logger.verbose(`stderr: ${stderr.trim()}`); + logger.debug(`stderr: ${stderr.trim()}`); } return { @@ -84,7 +84,7 @@ export const shellExecuteTool: Tool = { const execError = error as ExtendedExecException; const isTimeout = error.message.includes('timeout'); - logger.verbose(`Command execution failed: ${error.message}`); + logger.debug(`Command execution failed: ${error.message}`); return { error: isTimeout @@ -109,7 +109,7 @@ export const shellExecuteTool: Tool = { } }, logParameters: (input, { logger }) => { - logger.info(`Running "${input.command}", ${input.description}`); + logger.log(`Running "${input.command}", ${input.description}`); }, logReturns: () => {}, }; diff --git a/packages/agent/src/tools/shell/shellMessage.ts b/packages/agent/src/tools/shell/shellMessage.ts index 3cf4265..79cd747 100644 --- a/packages/agent/src/tools/shell/shellMessage.ts +++ b/packages/agent/src/tools/shell/shellMessage.ts @@ -97,7 +97,7 @@ export const shellMessageTool: Tool = { { instanceId, stdin, signal, showStdIn, showStdout }, { logger, shellTracker }, ): Promise => { - logger.verbose( + logger.debug( `Interacting with shell process ${instanceId}${stdin ? ' with input' : ''}${signal ? ` with signal ${signal}` : ''}`, ); @@ -123,7 +123,7 @@ export const shellMessageTool: Tool = { signalAttempted: signal, }); - logger.verbose( + logger.debug( `Failed to send signal ${signal}: ${String(error)}, but marking as signaled anyway`, ); } @@ -156,7 +156,7 @@ export const shellMessageTool: Tool = { const shouldShowStdIn = showStdIn !== undefined ? showStdIn : processState.showStdIn; if (shouldShowStdIn) { - logger.info(`[${instanceId}] stdin: ${stdin}`); + logger.log(`[${instanceId}] stdin: ${stdin}`); } // No special handling for 'cat' command - let the actual process handle the echo @@ -179,22 +179,22 @@ export const shellMessageTool: Tool = { processState.stdout = []; processState.stderr = []; - logger.verbose('Interaction completed successfully'); + logger.debug('Interaction completed successfully'); // Determine whether to show stdout (prefer explicit parameter, fall back to process state) const shouldShowStdout = showStdout !== undefined ? showStdout : processState.showStdout; if (stdout) { - logger.verbose(`stdout: ${stdout.trim()}`); + logger.debug(`stdout: ${stdout.trim()}`); if (shouldShowStdout) { - logger.info(`[${instanceId}] stdout: ${stdout.trim()}`); + logger.log(`[${instanceId}] stdout: ${stdout.trim()}`); } } if (stderr) { - logger.verbose(`stderr: ${stderr.trim()}`); + logger.debug(`stderr: ${stderr.trim()}`); if (shouldShowStdout) { - logger.info(`[${instanceId}] stderr: ${stderr.trim()}`); + logger.log(`[${instanceId}] stderr: ${stderr.trim()}`); } } @@ -206,7 +206,7 @@ export const shellMessageTool: Tool = { }; } catch (error) { if (error instanceof Error) { - logger.verbose(`Process interaction failed: ${error.message}`); + logger.debug(`Process interaction failed: ${error.message}`); return { stdout: '', @@ -238,7 +238,7 @@ export const shellMessageTool: Tool = { ? input.showStdout : processState?.showStdout || false; - logger.info( + logger.log( `Interacting with shell command "${processState ? processState.command : ''}", ${input.description} (showStdIn: ${showStdIn}, showStdout: ${showStdout})`, ); }, diff --git a/packages/agent/src/tools/shell/shellStart.ts b/packages/agent/src/tools/shell/shellStart.ts index 20ee1cc..21b82b2 100644 --- a/packages/agent/src/tools/shell/shellStart.ts +++ b/packages/agent/src/tools/shell/shellStart.ts @@ -84,9 +84,9 @@ export const shellStartTool: Tool = { { logger, workingDirectory, shellTracker }, ): Promise => { if (showStdIn) { - logger.info(`Command input: ${command}`); + logger.log(`Command input: ${command}`); } - logger.verbose(`Starting shell command: ${command}`); + logger.debug(`Starting shell command: ${command}`); return new Promise((resolve) => { try { @@ -124,7 +124,7 @@ export const shellStartTool: Tool = { process.stdout.on('data', (data) => { const output = data.toString(); processState.stdout.push(output); - logger[processState.showStdout ? 'info' : 'verbose']( + logger[processState.showStdout ? 'log' : 'debug']( `[${instanceId}] stdout: ${output.trim()}`, ); }); @@ -133,7 +133,7 @@ export const shellStartTool: Tool = { process.stderr.on('data', (data) => { const output = data.toString(); processState.stderr.push(output); - logger[processState.showStdout ? 'info' : 'verbose']( + logger[processState.showStdout ? 'log' : 'debug']( `[${instanceId}] stderr: ${output.trim()}`, ); }); @@ -160,7 +160,7 @@ export const shellStartTool: Tool = { }); process.on('exit', (code, signal) => { - logger.verbose( + logger.debug( `[${instanceId}] Process exited with code ${code} and signal ${signal}`, ); @@ -240,13 +240,13 @@ export const shellStartTool: Tool = { }, { logger }, ) => { - logger.info( + logger.log( `Running "${command}", ${description} (timeout: ${timeout}ms, showStdIn: ${showStdIn}, showStdout: ${showStdout})`, ); }, logReturns: (output, { logger }) => { if (output.mode === 'async') { - logger.info(`Process started with instance ID: ${output.instanceId}`); + logger.log(`Process started with instance ID: ${output.instanceId}`); } else { if (output.exitCode !== 0) { logger.error(`Process quit with exit code: ${output.exitCode}`); diff --git a/packages/agent/src/tools/sleep/sleep.test.ts b/packages/agent/src/tools/sleep/wait.test.ts similarity index 76% rename from packages/agent/src/tools/sleep/sleep.test.ts rename to packages/agent/src/tools/sleep/wait.test.ts index 17248a1..1002059 100644 --- a/packages/agent/src/tools/sleep/sleep.test.ts +++ b/packages/agent/src/tools/sleep/wait.test.ts @@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ToolContext } from '../../core/types'; import { getMockToolContext } from '../getTools.test'; -import { sleepTool } from './sleep'; +import { waitTool } from './wait'; const toolContext: ToolContext = getMockToolContext(); @@ -13,7 +13,7 @@ describe('sleep tool', () => { }); it('should sleep for the specified duration', async () => { - const sleepPromise = sleepTool.execute({ seconds: 2 }, toolContext); + const sleepPromise = waitTool.execute({ seconds: 2 }, toolContext); await vi.advanceTimersByTimeAsync(2000); const result = await sleepPromise; @@ -23,13 +23,13 @@ describe('sleep tool', () => { it('should reject negative sleep duration', async () => { await expect( - sleepTool.execute({ seconds: -1 }, toolContext), + waitTool.execute({ seconds: -1 }, toolContext), ).rejects.toThrow(); }); it('should reject sleep duration over 1 hour', async () => { await expect( - sleepTool.execute({ seconds: 3601 }, toolContext), + waitTool.execute({ seconds: 3601 }, toolContext), ).rejects.toThrow(); }); }); diff --git a/packages/agent/src/tools/sleep/sleep.ts b/packages/agent/src/tools/sleep/wait.ts similarity index 95% rename from packages/agent/src/tools/sleep/sleep.ts rename to packages/agent/src/tools/sleep/wait.ts index fc28062..75acafa 100644 --- a/packages/agent/src/tools/sleep/sleep.ts +++ b/packages/agent/src/tools/sleep/wait.ts @@ -18,8 +18,8 @@ const returnsSchema = z.object({ sleptFor: z.number().describe('Actual number of seconds slept'), }); -export const sleepTool: Tool = { - name: 'sleep', +export const waitTool: Tool = { + name: 'wait', description: 'Pauses execution for the specified number of seconds, useful when waiting for async tools to make progress before checking on them', logPrefix: '💤', diff --git a/packages/agent/src/tools/textEditor/textEditor.test.ts b/packages/agent/src/tools/textEditor/textEditor.test.ts index a35ab52..03f71ae 100644 --- a/packages/agent/src/tools/textEditor/textEditor.test.ts +++ b/packages/agent/src/tools/textEditor/textEditor.test.ts @@ -389,7 +389,7 @@ describe('textEditor', () => { it('should convert absolute paths to relative paths in log messages', () => { // Create a mock logger with a spy on the info method const mockLogger = new MockLogger(); - const infoSpy = vi.spyOn(mockLogger, 'info'); + const logSpy = vi.spyOn(mockLogger, 'log'); // Create a context with a specific working directory const contextWithWorkingDir: ToolContext = { @@ -410,12 +410,12 @@ describe('textEditor', () => { ); // Verify the log message contains the relative path - expect(infoSpy).toHaveBeenCalledWith( + expect(logSpy).toHaveBeenCalledWith( expect.stringContaining('./packages/agent/src/file.ts'), ); // Test with an absolute path outside the working directory - infoSpy.mockClear(); + logSpy.mockClear(); const externalPath = '/etc/config.json'; textEditorTool.logParameters?.( { @@ -427,10 +427,10 @@ describe('textEditor', () => { ); // Verify the log message keeps the absolute path - expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining(externalPath)); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(externalPath)); // Test with a relative path - infoSpy.mockClear(); + logSpy.mockClear(); const relativePath = 'src/file.ts'; textEditorTool.logParameters?.( { @@ -442,6 +442,6 @@ describe('textEditor', () => { ); // Verify the log message keeps the relative path as is - expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining(relativePath)); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(relativePath)); }); }); diff --git a/packages/agent/src/tools/textEditor/textEditor.ts b/packages/agent/src/tools/textEditor/textEditor.ts index f881ed9..cf3b181 100644 --- a/packages/agent/src/tools/textEditor/textEditor.ts +++ b/packages/agent/src/tools/textEditor/textEditor.ts @@ -313,7 +313,7 @@ export const textEditorTool: Tool = { } } - logger.info( + logger.log( `${input.command} operation on "${displayPath}", ${input.description}`, ); }, diff --git a/packages/agent/src/utils/README.md b/packages/agent/src/utils/README.md new file mode 100644 index 0000000..e69de29 diff --git a/packages/agent/src/utils/interactiveInput.ts b/packages/agent/src/utils/interactiveInput.ts new file mode 100644 index 0000000..4660c27 --- /dev/null +++ b/packages/agent/src/utils/interactiveInput.ts @@ -0,0 +1,118 @@ +import * as readline from 'readline'; +import { createInterface } from 'readline/promises'; +import { Writable } from 'stream'; + +import chalk from 'chalk'; + +import { userMessages } from '../tools/interaction/userMessage.js'; + +// Custom output stream to intercept console output +class OutputInterceptor extends Writable { + private originalStdout: NodeJS.WriteStream; + private paused: boolean = false; + + constructor(originalStdout: NodeJS.WriteStream) { + super(); + this.originalStdout = originalStdout; + } + + pause() { + this.paused = true; + } + + resume() { + this.paused = false; + } + + _write(chunk: Buffer | string, encoding: BufferEncoding, callback: (error?: Error | null) => void): void { + if (!this.paused) { + this.originalStdout.write(chunk, encoding); + } + callback(); + } +} + +// Initialize interactive input mode +export const initInteractiveInput = () => { + // Save original stdout + const originalStdout = process.stdout; + + // Create interceptor + const interceptor = new OutputInterceptor(originalStdout); + + // Replace stdout with our interceptor + // @ts-expect-error - This is a hack to replace stdout + process.stdout = interceptor; + + // Create readline interface for listening to key presses + const rl = readline.createInterface({ + input: process.stdin, + output: interceptor, + terminal: true, + }); + + // Close the interface to avoid keeping the process alive + rl.close(); + + // Listen for keypress events + readline.emitKeypressEvents(process.stdin); + if (process.stdin.isTTY) { + process.stdin.setRawMode(true); + } + + process.stdin.on('keypress', async (str, key) => { + // Check for Ctrl+C to exit + if (key.ctrl && key.name === 'c') { + process.exit(0); + } + + // Check for Ctrl+M to enter message mode + if (key.ctrl && key.name === 'm') { + // Pause output + interceptor.pause(); + + // Create a readline interface for input + const inputRl = createInterface({ + input: process.stdin, + output: originalStdout, + }); + + try { + // Reset cursor position and clear line + originalStdout.write('\r\n'); + originalStdout.write(chalk.green('Enter correction or additional context (Ctrl+C to cancel):\n') + '> '); + + // Get user input + const userInput = await inputRl.question(''); + + // Add message to queue if not empty + if (userInput.trim()) { + userMessages.push(userInput); + originalStdout.write(chalk.green('\nMessage sent to agent. Resuming output...\n\n')); + } else { + originalStdout.write(chalk.yellow('\nEmpty message not sent. Resuming output...\n\n')); + } + } catch (error) { + originalStdout.write(chalk.red(`\nError sending message: ${error}\n\n`)); + } finally { + // Close input readline interface + inputRl.close(); + + // Resume output + interceptor.resume(); + } + } + }); + + // Return a cleanup function + return () => { + // Restore original stdout + // @ts-expect-error - This is a hack to restore stdout + process.stdout = originalStdout; + + // Disable raw mode + if (process.stdin.isTTY) { + process.stdin.setRawMode(false); + } + }; +}; \ No newline at end of file diff --git a/packages/agent/src/utils/logger.test.ts b/packages/agent/src/utils/logger.test.ts index d402f30..83d1bed 100644 --- a/packages/agent/src/utils/logger.test.ts +++ b/packages/agent/src/utils/logger.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { Logger, LogLevel } from './logger.js'; +import { consoleOutputLogger, Logger, LogLevel } from './logger.js'; describe('Logger', () => { let consoleSpy: { [key: string]: any }; @@ -8,8 +8,9 @@ describe('Logger', () => { beforeEach(() => { // Setup console spies before each test consoleSpy = { - log: vi.spyOn(console, 'log').mockImplementation(() => {}), + debug: vi.spyOn(console, 'debug').mockImplementation(() => {}), info: vi.spyOn(console, 'info').mockImplementation(() => {}), + log: vi.spyOn(console, 'log').mockImplementation(() => {}), warn: vi.spyOn(console, 'warn').mockImplementation(() => {}), error: vi.spyOn(console, 'error').mockImplementation(() => {}), }; @@ -20,26 +21,40 @@ describe('Logger', () => { vi.clearAllMocks(); }); - describe('Basic logging functionality', () => { + describe('Basic console output logger', () => { const logger = new Logger({ name: 'TestLogger', logLevel: LogLevel.debug }); const testMessage = 'Test message'; - it('should log debug messages', () => { - logger.debug(testMessage); + it('should log log messages', () => { + consoleOutputLogger(logger, LogLevel.log, [testMessage]); + console.log(consoleSpy.log); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining(testMessage), ); }); + }); - it('should log verbose messages', () => { - logger.verbose(testMessage); - expect(consoleSpy.log).toHaveBeenCalledWith( + describe('Basic logging functionality', () => { + const logger = new Logger({ name: 'TestLogger', logLevel: LogLevel.debug }); + logger.listeners.push(consoleOutputLogger); + const testMessage = 'Test message'; + + it('should log debug messages', () => { + logger.debug(testMessage); + expect(consoleSpy.debug).toHaveBeenCalledWith( expect.stringContaining(testMessage), ); }); it('should log info messages', () => { logger.info(testMessage); + expect(consoleSpy.info).toHaveBeenCalledWith( + expect.stringContaining(testMessage), + ); + }); + + it('should log log messages', () => { + logger.log(testMessage); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining(testMessage), ); @@ -72,8 +87,10 @@ describe('Logger', () => { }); const testMessage = 'Nested test message'; + parentLogger.listeners.push(consoleOutputLogger); + it('should include proper indentation for nested loggers', () => { - childLogger.info(testMessage); + childLogger.log(testMessage); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining(' '), // Two spaces of indentation ); @@ -81,16 +98,16 @@ describe('Logger', () => { it('should properly log messages at all levels with nested logger', () => { childLogger.debug(testMessage); - expect(consoleSpy.log).toHaveBeenCalledWith( + expect(consoleSpy.debug).toHaveBeenCalledWith( expect.stringContaining(testMessage), ); - childLogger.verbose(testMessage); - expect(consoleSpy.log).toHaveBeenCalledWith( + childLogger.info(testMessage); + expect(consoleSpy.info).toHaveBeenCalledWith( expect.stringContaining(testMessage), ); - childLogger.info(testMessage); + childLogger.log(testMessage); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining(testMessage), ); diff --git a/packages/agent/src/utils/logger.ts b/packages/agent/src/utils/logger.ts index 8f16f83..7351b37 100644 --- a/packages/agent/src/utils/logger.ts +++ b/packages/agent/src/utils/logger.ts @@ -2,46 +2,11 @@ import chalk, { ChalkInstance } from 'chalk'; export enum LogLevel { debug = 0, - verbose = 1, - info = 2, + info = 1, + log = 2, warn = 3, error = 4, } -export type LoggerStyler = { - getColor(level: LogLevel, indentLevel: number): ChalkInstance; - formatPrefix(prefix: string, level: LogLevel): string; - showPrefix(level: LogLevel): boolean; -}; - -export const BasicLoggerStyler = { - getColor: (level: LogLevel, _nesting: number = 0): ChalkInstance => { - switch (level) { - case LogLevel.error: - return chalk.red; - case LogLevel.warn: - return chalk.yellow; - case LogLevel.debug: - case LogLevel.verbose: - return chalk.white.dim; - default: - return chalk.white; - } - }, - formatPrefix: ( - prefix: string, - level: LogLevel, - _nesting: number = 0, - ): string => - level === LogLevel.debug || level === LogLevel.verbose - ? chalk.dim(prefix) - : prefix, - showPrefix: (_level: LogLevel): boolean => { - // Show prefix for all log levels - return false; - }, -}; - -const loggerStyle = BasicLoggerStyler; export type LoggerProps = { name: string; @@ -50,14 +15,22 @@ export type LoggerProps = { customPrefix?: string; }; +export type LoggerListener = ( + logger: Logger, + logLevel: LogLevel, + lines: string[], +) => void; + export class Logger { - private readonly prefix: string; - private readonly logLevel: LogLevel; - private readonly logLevelIndex: LogLevel; - private readonly parent?: Logger; - private readonly name: string; - private readonly nesting: number; - private readonly customPrefix?: string; + public readonly prefix: string; + public readonly logLevel: LogLevel; + public readonly logLevelIndex: LogLevel; + public readonly parent?: Logger; + public readonly name: string; + public readonly nesting: number; + public readonly customPrefix?: string; + + readonly listeners: LoggerListener[] = []; constructor({ name, @@ -82,70 +55,105 @@ export class Logger { } this.prefix = ' '.repeat(offsetSpaces); + + if (parent) { + this.listeners.push((logger, logLevel, lines) => { + parent.listeners.forEach((listener) => { + listener(logger, logLevel, lines); + }); + }); + } } - private toStrings(messages: unknown[]) { - return messages + private emitMessages(level: LogLevel, messages: unknown[]) { + if (LogLevel.debug < this.logLevelIndex) return; + + const lines = messages .map((message) => typeof message === 'object' ? JSON.stringify(message, null, 2) : String(message), ) - .join(' '); - } - - private formatMessages(level: LogLevel, messages: unknown[]): string { - const formatted = this.toStrings(messages); - const messageColor = loggerStyle.getColor(level, this.nesting); - - let combinedPrefix = this.prefix; - - if (loggerStyle.showPrefix(level)) { - const prefix = loggerStyle.formatPrefix( - `[${this.name}]`, - level, - this.nesting, - ); - - if (this.customPrefix) { - combinedPrefix = `${this.prefix}${this.customPrefix} `; - } else { - combinedPrefix = `${this.prefix}${prefix} `; - } - } + .join('\n') + .split('\n'); - return formatted - .split('\n') - .map((line) => `${combinedPrefix}${messageColor(line)}`) - .join('\n'); - } - - log(level: LogLevel, ...messages: unknown[]): void { - if (level < this.logLevelIndex) return; - console.log(this.formatMessages(level, messages)); + this.listeners.forEach((listener) => listener(this, level, lines)); } debug(...messages: unknown[]): void { - if (LogLevel.debug < this.logLevelIndex) return; - console.log(this.formatMessages(LogLevel.debug, messages)); + this.emitMessages(LogLevel.debug, messages); } - verbose(...messages: unknown[]): void { - if (LogLevel.verbose < this.logLevelIndex) return; - console.log(this.formatMessages(LogLevel.verbose, messages)); + info(...messages: unknown[]): void { + this.emitMessages(LogLevel.info, messages); } - info(...messages: unknown[]): void { - if (LogLevel.info < this.logLevelIndex) return; - console.log(this.formatMessages(LogLevel.info, messages)); + log(...messages: unknown[]): void { + this.emitMessages(LogLevel.log, messages); } warn(...messages: unknown[]): void { - if (LogLevel.warn < this.logLevelIndex) return; - console.warn(this.formatMessages(LogLevel.warn, messages)); + this.emitMessages(LogLevel.warn, messages); } error(...messages: unknown[]): void { - console.error(this.formatMessages(LogLevel.error, messages)); + this.emitMessages(LogLevel.error, messages); } } + +export const consoleOutputLogger: LoggerListener = ( + logger: Logger, + level: LogLevel, + lines: string[], +) => { + const getColor = (level: LogLevel, _nesting: number = 0): ChalkInstance => { + switch (level) { + case LogLevel.debug: + case LogLevel.info: + return chalk.white.dim; + case LogLevel.log: + return chalk.white; + case LogLevel.warn: + return chalk.yellow; + case LogLevel.error: + return chalk.red; + default: + throw new Error(`Unknown log level: ${level}`); + } + }; + const formatPrefix = ( + prefix: string, + level: LogLevel, + _nesting: number = 0, + ): string => + level === LogLevel.debug || level === LogLevel.info + ? chalk.dim(prefix) + : prefix; + const showPrefix = (_level: LogLevel): boolean => { + // Show prefix for all log levels + return false; + }; + + // name of enum value + const logLevelName = LogLevel[level]; + const messageColor = getColor(level, logger.nesting); + + let combinedPrefix = logger.prefix; + + if (showPrefix(level)) { + const prefix = formatPrefix(`[${logger.name}]`, level, logger.nesting); + + if (logger.customPrefix) { + combinedPrefix = `${logger.prefix}${logger.customPrefix} `; + } else { + combinedPrefix = `${logger.prefix}${prefix} `; + } + } + + const coloredLies = lines.map( + (line) => `${combinedPrefix}${messageColor(line)}`, + ); + + const consoleOutput = console[logLevelName]; + coloredLies.forEach((line) => consoleOutput(line)); +}; diff --git a/packages/agent/src/utils/mockLogger.ts b/packages/agent/src/utils/mockLogger.ts index 4a95525..92cfef6 100644 --- a/packages/agent/src/utils/mockLogger.ts +++ b/packages/agent/src/utils/mockLogger.ts @@ -6,8 +6,8 @@ export class MockLogger extends Logger { } debug(..._messages: any[]): void {} - verbose(..._messages: any[]): void {} info(..._messages: any[]): void {} + log(..._messages: any[]): void {} warn(..._messages: any[]): void {} error(..._messages: any[]): void {} } diff --git a/packages/cli/src/commands/$default.ts b/packages/cli/src/commands/$default.ts index 760bb06..6dc6203 100644 --- a/packages/cli/src/commands/$default.ts +++ b/packages/cli/src/commands/$default.ts @@ -17,8 +17,10 @@ import { SessionTracker, ShellTracker, AgentTracker, + consoleOutputLogger, } from 'mycoder-agent'; import { TokenTracker } from 'mycoder-agent/dist/core/tokens.js'; +import { initInteractiveInput } from 'mycoder-agent/dist/utils/interactiveInput.js'; import { SharedOptions } from '../options.js'; import { captureException } from '../sentry/index.js'; @@ -50,6 +52,8 @@ export async function executePrompt( customPrefix: agentExecuteTool.logPrefix, }); + logger.listeners.push(consoleOutputLogger); + logger.info(`MyCoder v${packageInfo.version} - AI-powered coding assistant`); // Skip version check if upgradeCheck is false @@ -103,6 +107,9 @@ export async function executePrompt( // Use command line option if provided, otherwise use config value tokenTracker.tokenCache = config.tokenCache; + // Initialize interactive input if enabled + let cleanupInteractiveInput: (() => void) | undefined; + try { // Early API key check based on model provider const providerSettings = @@ -161,6 +168,12 @@ export async function executePrompt( ); process.exit(0); }); + + // Initialize interactive input if enabled + if (config.interactive) { + logger.info(chalk.green('Interactive correction mode enabled. Press Ctrl+M to send a correction to the agent.')); + cleanupInteractiveInput = initInteractiveInput(); + } // Create a config for the agent const agentConfig: AgentConfig = { @@ -203,7 +216,11 @@ export async function executePrompt( // Capture the error with Sentry captureException(error); } finally { - // No cleanup needed here as it's handled by the cleanup utility + // Clean up interactive input if it was initialized + if (cleanupInteractiveInput) { + cleanupInteractiveInput(); + } + // Other cleanup is handled by the cleanup utility } logger.log( diff --git a/packages/cli/src/commands/test-sentry.ts b/packages/cli/src/commands/test-sentry.ts index 3f0b6cc..798811b 100644 --- a/packages/cli/src/commands/test-sentry.ts +++ b/packages/cli/src/commands/test-sentry.ts @@ -1,5 +1,5 @@ import chalk from 'chalk'; -import { Logger } from 'mycoder-agent'; +import { consoleOutputLogger, Logger } from 'mycoder-agent'; import { SharedOptions } from '../options.js'; import { testSentryErrorReporting } from '../sentry/index.js'; @@ -17,6 +17,7 @@ export const command: CommandModule = { name: 'TestSentry', logLevel: nameToLogIndex(argv.logLevel), }); + logger.listeners.push(consoleOutputLogger); logger.info(chalk.yellow('Testing Sentry.io error reporting...')); diff --git a/packages/cli/src/options.ts b/packages/cli/src/options.ts index 3bd1c9f..a93ee76 100644 --- a/packages/cli/src/options.ts +++ b/packages/cli/src/options.ts @@ -51,7 +51,7 @@ export const sharedOptions = { interactive: { type: 'boolean', alias: 'i', - description: 'Run in interactive mode, asking for prompts', + description: 'Run in interactive mode, asking for prompts and enabling corrections during execution (use Ctrl+M to send corrections)', default: false, } as const, file: { diff --git a/packages/cli/src/settings/config.ts b/packages/cli/src/settings/config.ts index af564a1..801c9c3 100644 --- a/packages/cli/src/settings/config.ts +++ b/packages/cli/src/settings/config.ts @@ -19,6 +19,7 @@ export type Config = { userPrompt: boolean; upgradeCheck: boolean; tokenUsage: boolean; + interactive: boolean; baseUrl?: string; @@ -75,6 +76,7 @@ const defaultConfig: Config = { userPrompt: true, upgradeCheck: true, tokenUsage: false, + interactive: false, // MCP configuration mcp: { @@ -100,6 +102,7 @@ export const getConfigFromArgv = (argv: ArgumentsCamelCase) => { userPrompt: argv.userPrompt, upgradeCheck: argv.upgradeCheck, tokenUsage: argv.tokenUsage, + interactive: argv.interactive, }; }; diff --git a/packages/docs/README.md b/packages/docs/README.md index 90c0f34..f67b3aa 100644 --- a/packages/docs/README.md +++ b/packages/docs/README.md @@ -7,6 +7,7 @@ This package contains the official documentation for MyCoder, an AI-powered codi ### Documentation Structure - **Core Documentation** + - **Introduction**: Overview of MyCoder and its capabilities - **Getting Started**: Platform-specific setup instructions for Windows, macOS, and Linux - **Usage Guides**: Detailed information on features, configuration, and capabilities @@ -159,4 +160,4 @@ If you have questions or feedback, please join our [Discord community](https://d - [MyCoder Website](https://mycoder.ai) - [GitHub Repository](https://github.com/drivecore/mycoder) -- [Documentation Site](https://docs.mycoder.ai) \ No newline at end of file +- [Documentation Site](https://docs.mycoder.ai)