diff --git a/docs/design/incremental-resource-cleanup.md b/docs/design/incremental-resource-cleanup.md new file mode 100644 index 0000000..10f0f25 --- /dev/null +++ b/docs/design/incremental-resource-cleanup.md @@ -0,0 +1,359 @@ +# Implementation Plan: Incremental Resource Cleanup + +## Overview + +This plan outlines the steps to implement incremental resource cleanup tied to agent lifecycles, ensuring that browser sessions, shell processes, and sub-agents are properly cleaned up when agents complete their tasks or encounter exceptions. + +## Current Architecture Analysis + +Based on code review: + +1. **BackgroundTools** (packages/agent/src/core/backgroundTools.ts): + - Maintains a registry of background tools (shells, browsers, agents) + - Has methods to register and update status, but no cleanup methods + +2. **Browser Management** (packages/agent/src/tools/browser/BrowserManager.ts): + - Implemented as a global singleton accessible via `(globalThis as any).__BROWSER_MANAGER__` + - Has methods to close individual sessions and all sessions + - No direct connection to agent lifecycles + +3. **Shell Process Management** (packages/agent/src/tools/system/shellStart.ts): + - Processes stored in global `processStates` map + - No direct connection to agent lifecycles + - No specific cleanup method for individual processes + +4. **Agent Management** (packages/agent/src/tools/interaction/agentStart.ts, agentMessage.ts): + - Agents track their own background tools + - Agents can be terminated, but no cleanup of associated resources + +5. **Global Cleanup** (packages/cli/src/utils/cleanup.ts): + - Single function that attempts to clean up all resources at application exit + - No incremental cleanup capabilities + +## Implementation Steps + +### 1. Enhance BackgroundTools Class + +Add a cleanup method to the BackgroundTools class: + +```typescript +// In packages/agent/src/core/backgroundTools.ts + +export class BackgroundTools { + // ... existing code ... + + /** + * Cleans up all resources associated with this agent instance + * @returns A promise that resolves when cleanup is complete + */ + public async cleanup(): Promise { + const tools = this.getTools(); + + // Group tools by type for better cleanup organization + const browserTools = tools.filter( + (tool): tool is BrowserBackgroundTool => tool.type === BackgroundToolType.BROWSER + ); + + const shellTools = tools.filter( + (tool): tool is ShellBackgroundTool => tool.type === BackgroundToolType.SHELL + ); + + const agentTools = tools.filter( + (tool): tool is AgentBackgroundTool => tool.type === BackgroundToolType.AGENT + ); + + // Clean up browser sessions + for (const tool of browserTools) { + if (tool.status === BackgroundToolStatus.RUNNING) { + try { + const browserManager = (globalThis as any).__BROWSER_MANAGER__ as BrowserManager | undefined; + if (browserManager) { + await browserManager.closeSession(tool.id); + } + this.updateToolStatus(tool.id, BackgroundToolStatus.COMPLETED); + } catch (error) { + this.updateToolStatus(tool.id, BackgroundToolStatus.ERROR, { + error: error instanceof Error ? error.message : String(error), + }); + } + } + } + + // Clean up shell processes + for (const tool of shellTools) { + if (tool.status === BackgroundToolStatus.RUNNING) { + try { + const processState = processStates.get(tool.id); + if (processState && !processState.state.completed) { + processState.process.kill('SIGTERM'); + + // Force kill after a short timeout if still running + await new Promise((resolve) => { + setTimeout(() => { + try { + if (!processState.state.completed) { + processState.process.kill('SIGKILL'); + } + } catch (e) { + // Ignore errors on forced kill + } + resolve(); + }, 500); + }); + } + this.updateToolStatus(tool.id, BackgroundToolStatus.COMPLETED); + } catch (error) { + this.updateToolStatus(tool.id, BackgroundToolStatus.ERROR, { + error: error instanceof Error ? error.message : String(error), + }); + } + } + } + + // Clean up sub-agents + for (const tool of agentTools) { + if (tool.status === BackgroundToolStatus.RUNNING) { + try { + const agentState = agentStates.get(tool.id); + if (agentState && !agentState.aborted) { + agentState.aborted = true; + agentState.completed = true; + } + this.updateToolStatus(tool.id, BackgroundToolStatus.TERMINATED); + } catch (error) { + this.updateToolStatus(tool.id, BackgroundToolStatus.ERROR, { + error: error instanceof Error ? error.message : String(error), + }); + } + } + } + } +} +``` + +### 2. Modify Agent Lifecycle Methods + +Update the agent lifecycle methods to call the cleanup method: + +```typescript +// In packages/agent/src/tools/interaction/agentStart.ts + +// In the Promise.resolve().then() block: +try { + const result = await toolAgent(prompt, tools, subAgentConfig, { + ...context, + workingDirectory: workingDirectory ?? context.workingDirectory, + }); + + // Update agent state with the result + const state = agentStates.get(instanceId); + if (state && !state.aborted) { + state.completed = true; + state.result = result; + state.output = result.result; + + // Update background tool registry with completed status + backgroundTools.updateToolStatus( + instanceId, + BackgroundToolStatus.COMPLETED, + { + result: + result.result.substring(0, 100) + + (result.result.length > 100 ? '...' : ''), + }, + ); + + // Clean up resources when agent completes successfully + await backgroundTools.cleanup(); + } +} catch (error) { + // Update agent state with the error + const state = agentStates.get(instanceId); + if (state && !state.aborted) { + state.completed = true; + state.error = error instanceof Error ? error.message : String(error); + + // Update background tool registry with error status + backgroundTools.updateToolStatus( + instanceId, + BackgroundToolStatus.ERROR, + { + error: error instanceof Error ? error.message : String(error), + }, + ); + + // Clean up resources when agent encounters an error + await backgroundTools.cleanup(); + } +} +``` + +### 3. Update Agent Termination + +Modify the agent termination logic to clean up resources: + +```typescript +// In packages/agent/src/tools/interaction/agentMessage.ts + +// In the terminate block: +if (terminate) { + agentState.aborted = true; + agentState.completed = true; + + // Update background tool registry with terminated status + backgroundTools.updateToolStatus( + instanceId, + BackgroundToolStatus.TERMINATED, + { + terminatedByUser: true, + }, + ); + + // Clean up resources when agent is terminated + await backgroundTools.cleanup(); + + return { + output: agentState.output || 'Sub-agent terminated before completion', + completed: true, + terminated: true, + }; +} +``` + +### 4. Update Global Cleanup + +Modify the global cleanup to use the new BackgroundTools cleanup method for any still-running agents: + +```typescript +// In packages/cli/src/utils/cleanup.ts + +export async function cleanupResources(): Promise { + console.log('Cleaning up resources before exit...'); + + // First attempt to clean up any still-running agents + // This will cascade to their browser sessions and shell processes + try { + // Find all active agent instances + const activeAgents = Array.from(agentStates.entries()) + .filter(([_, state]) => !state.completed && !state.aborted); + + if (activeAgents.length > 0) { + console.log(`Cleaning up ${activeAgents.length} active agents...`); + + for (const [id, state] of activeAgents) { + try { + // Mark the agent as aborted + state.aborted = true; + state.completed = true; + + // Clean up its resources + await state.context.backgroundTools.cleanup(); + } catch (error) { + console.error(`Error cleaning up agent ${id}:`, error); + } + } + } + } catch (error) { + console.error('Error cleaning up agents:', error); + } + + // As a fallback, still clean up any browser sessions and shell processes + // that might not have been caught by the agent cleanup + + // 1. Clean up browser sessions + try { + // Get the BrowserManager instance - this is a singleton + const browserManager = (globalThis as any).__BROWSER_MANAGER__ as + | BrowserManager + | undefined; + if (browserManager) { + console.log('Closing all browser sessions...'); + await browserManager.closeAllSessions(); + } + } catch (error) { + console.error('Error closing browser sessions:', error); + } + + // 2. Clean up shell processes + try { + if (processStates.size > 0) { + console.log(`Terminating ${processStates.size} shell processes...`); + for (const [id, state] of processStates.entries()) { + if (!state.state.completed) { + console.log(`Terminating process ${id}...`); + try { + state.process.kill('SIGTERM'); + // Force kill after a short timeout if still running + setTimeout(() => { + try { + if (!state.state.completed) { + state.process.kill('SIGKILL'); + } + } catch (e) { + // Ignore errors on forced kill + } + }, 500); + } catch (e) { + console.error(`Error terminating process ${id}:`, e); + } + } + } + } + } catch (error) { + console.error('Error terminating shell processes:', error); + } + + // 3. Give async operations a moment to complete + await new Promise((resolve) => setTimeout(resolve, 1000)); + + console.log('Cleanup completed'); +} +``` + +### 5. Add Necessary Imports + +Add the required imports to each file: + +```typescript +// In packages/agent/src/core/backgroundTools.ts +import { BrowserManager } from '../tools/browser/BrowserManager.js'; +import { processStates } from '../tools/system/shellStart.js'; +import { agentStates } from '../tools/interaction/agentStart.js'; + +// In packages/cli/src/utils/cleanup.ts +import { agentStates } from 'mycoder-agent/dist/tools/interaction/agentStart.js'; +``` + +## Testing Plan + +1. **Unit Tests**: + - Add unit tests for the new `cleanup` method in `BackgroundTools` + - Test cleanup with various combinations of tools (browsers, shells, agents) + - Test cleanup with error scenarios + +2. **Integration Tests**: + - Test agent completion with resource cleanup + - Test agent termination with resource cleanup + - Test agent error handling with resource cleanup + +3. **Manual Testing**: + - Create complex agent hierarchies and verify all resources are cleaned up + - Test with real browser sessions and shell processes + - Verify no resource leaks under various scenarios + +## Potential Challenges + +1. **Circular Dependencies**: The imports might create circular dependencies that need to be resolved +2. **Race Conditions**: Ensure cleanup doesn't interfere with ongoing operations +3. **Error Handling**: Robust error handling is needed to prevent cascading failures +4. **Timing Issues**: Some cleanup operations are asynchronous and need proper timing + +## Fallback Mechanisms + +1. Keep the global cleanup as a fallback for any resources that weren't properly cleaned up +2. Add logging to identify cleanup failures for debugging +3. Implement timeout mechanisms to prevent cleanup operations from hanging + +## Conclusion + +This implementation plan provides a comprehensive approach to incremental resource cleanup tied to agent lifecycles. It ensures that resources are cleaned up promptly when they're no longer needed, rather than waiting for application exit, while maintaining backward compatibility and adding robust error handling. \ No newline at end of file diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 5dd4618..ea63537 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,3 +1,27 @@ +# [mycoder-agent-v1.1.0](https://github.com/drivecore/mycoder/compare/mycoder-agent-v1.0.0...mycoder-agent-v1.1.0) (2025-03-12) + + +### Bug Fixes + +* convert absolute paths to relative paths in textEditor log output ([a5ea845](https://github.com/drivecore/mycoder/commit/a5ea845c32bc569cda4330f59f1bf1553a236aea)) +* implement resource cleanup to prevent CLI hanging issue ([d33e729](https://github.com/drivecore/mycoder/commit/d33e7298686a30661ee8b36f2fdffb16f5f3da71)), closes [#141](https://github.com/drivecore/mycoder/issues/141) +* llm choice working well for openai, anthropic and ollama ([68d34ab](https://github.com/drivecore/mycoder/commit/68d34abf8a73ed533a072359ce334a9364753425)) +* **openai:** add OpenAI dependency to agent package and enable provider in config ([30b0807](https://github.com/drivecore/mycoder/commit/30b0807d4f3ecdd24f53b7ee4160645a4ed10444)) +* replace @semantic-release/npm with @anolilab/semantic-release-pnpm to properly resolve workspace references ([bacb51f](https://github.com/drivecore/mycoder/commit/bacb51f637f2b2d3b1039bdfdbd33e3d704b6cde)) +* up subagent iterations to 200 from 50 ([b405f1e](https://github.com/drivecore/mycoder/commit/b405f1e6d62eb5304dc1aa6c0ff28dc49dc67dce)) + + +### Features + +* add agent tracking to background tools ([4a3bcc7](https://github.com/drivecore/mycoder/commit/4a3bcc72f27af5fdbeeb407a748d5ecf3b7faed5)) +* add Ollama configuration options ([d5c3a96](https://github.com/drivecore/mycoder/commit/d5c3a96ce9463c98504c2a346796400df36bf3b0)) +* **agent:** implement agentStart and agentMessage tools ([62f8df3](https://github.com/drivecore/mycoder/commit/62f8df3dd083e2838c97ce89112f390461550ee6)), closes [#111](https://github.com/drivecore/mycoder/issues/111) [#111](https://github.com/drivecore/mycoder/issues/111) +* allow textEditor to overwrite existing files with create command ([d1cde65](https://github.com/drivecore/mycoder/commit/d1cde65df65bfcca288a47f14eedf5ad5939ed37)), closes [#192](https://github.com/drivecore/mycoder/issues/192) +* implement background tool tracking (issue [#112](https://github.com/drivecore/mycoder/issues/112)) ([b5bb489](https://github.com/drivecore/mycoder/commit/b5bb48981791acda74ee46b93d2d85e27e93a538)) +* implement Ollama provider for LLM abstraction ([597211b](https://github.com/drivecore/mycoder/commit/597211b90e43c4d52969eb5994d393c15d85ec97)) +* **llm:** add OpenAI support to LLM abstraction ([7bda811](https://github.com/drivecore/mycoder/commit/7bda811658e15b8dd41135cd9b2b90e9ea925e15)) +* **refactor:** agent ([a2f59c2](https://github.com/drivecore/mycoder/commit/a2f59c2f51643a44d6e1ff0c16b319deb1adc3f2)) + # mycoder-agent-v1.0.0 (2025-03-11) ### Bug Fixes diff --git a/packages/agent/package.json b/packages/agent/package.json index 6511def..24baa66 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "mycoder-agent", - "version": "1.0.1", + "version": "1.1.0", "description": "Agent module for mycoder - an AI-powered software development assistant", "type": "module", "main": "dist/index.js", diff --git a/packages/agent/src/core/backgroundTools.test.ts b/packages/agent/src/core/backgroundTools.test.ts index ec75544..4b0e5c3 100644 --- a/packages/agent/src/core/backgroundTools.test.ts +++ b/packages/agent/src/core/backgroundTools.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { - backgroundToolRegistry, + BackgroundTools, BackgroundToolStatus, BackgroundToolType, } from './backgroundTools.js'; @@ -12,23 +12,23 @@ vi.mock('uuid', () => ({ })); describe('BackgroundToolRegistry', () => { + let backgroundTools: BackgroundTools; beforeEach(() => { // Clear all registered tools before each test - const registry = backgroundToolRegistry as any; - registry.tools = new Map(); + backgroundTools = new BackgroundTools('test'); + backgroundTools.tools = new Map(); }); it('should register a shell process', () => { - const id = backgroundToolRegistry.registerShell('agent-1', 'ls -la'); + const id = backgroundTools.registerShell('ls -la'); expect(id).toBe('test-id-1'); - const tool = backgroundToolRegistry.getToolById(id); + const tool = backgroundTools.getToolById(id); expect(tool).toBeDefined(); if (tool) { expect(tool.type).toBe(BackgroundToolType.SHELL); expect(tool.status).toBe(BackgroundToolStatus.RUNNING); - expect(tool.agentId).toBe('agent-1'); if (tool.type === BackgroundToolType.SHELL) { expect(tool.metadata.command).toBe('ls -la'); } @@ -36,19 +36,15 @@ describe('BackgroundToolRegistry', () => { }); it('should register a browser process', () => { - const id = backgroundToolRegistry.registerBrowser( - 'agent-1', - 'https://example.com', - ); + const id = backgroundTools.registerBrowser('https://example.com'); expect(id).toBe('test-id-1'); - const tool = backgroundToolRegistry.getToolById(id); + const tool = backgroundTools.getToolById(id); expect(tool).toBeDefined(); if (tool) { expect(tool.type).toBe(BackgroundToolType.BROWSER); expect(tool.status).toBe(BackgroundToolStatus.RUNNING); - expect(tool.agentId).toBe('agent-1'); if (tool.type === BackgroundToolType.BROWSER) { expect(tool.metadata.url).toBe('https://example.com'); } @@ -56,9 +52,9 @@ describe('BackgroundToolRegistry', () => { }); it('should update tool status', () => { - const id = backgroundToolRegistry.registerShell('agent-1', 'sleep 10'); + const id = backgroundTools.registerShell('sleep 10'); - const updated = backgroundToolRegistry.updateToolStatus( + const updated = backgroundTools.updateToolStatus( id, BackgroundToolStatus.COMPLETED, { @@ -68,7 +64,7 @@ describe('BackgroundToolRegistry', () => { expect(updated).toBe(true); - const tool = backgroundToolRegistry.getToolById(id); + const tool = backgroundTools.getToolById(id); expect(tool).toBeDefined(); if (tool) { expect(tool.status).toBe(BackgroundToolStatus.COMPLETED); @@ -80,7 +76,7 @@ describe('BackgroundToolRegistry', () => { }); it('should return false when updating non-existent tool', () => { - const updated = backgroundToolRegistry.updateToolStatus( + const updated = backgroundTools.updateToolStatus( 'non-existent-id', BackgroundToolStatus.COMPLETED, ); @@ -88,49 +84,9 @@ describe('BackgroundToolRegistry', () => { expect(updated).toBe(false); }); - it('should get tools by agent ID', () => { - // For this test, we'll directly manipulate the tools map - const registry = backgroundToolRegistry as any; - registry.tools = new Map(); - - // Add tools directly to the map with different agent IDs - registry.tools.set('id1', { - id: 'id1', - type: BackgroundToolType.SHELL, - status: BackgroundToolStatus.RUNNING, - startTime: new Date(), - agentId: 'agent-1', - metadata: { command: 'ls -la' }, - }); - - registry.tools.set('id2', { - id: 'id2', - type: BackgroundToolType.BROWSER, - status: BackgroundToolStatus.RUNNING, - startTime: new Date(), - agentId: 'agent-1', - metadata: { url: 'https://example.com' }, - }); - - registry.tools.set('id3', { - id: 'id3', - type: BackgroundToolType.SHELL, - status: BackgroundToolStatus.RUNNING, - startTime: new Date(), - agentId: 'agent-2', - metadata: { command: 'echo hello' }, - }); - - const agent1Tools = backgroundToolRegistry.getToolsByAgent('agent-1'); - const agent2Tools = backgroundToolRegistry.getToolsByAgent('agent-2'); - - expect(agent1Tools.length).toBe(2); - expect(agent2Tools.length).toBe(1); - }); - it('should clean up old completed tools', () => { // Create tools with specific dates - const registry = backgroundToolRegistry as any; + const registry = backgroundTools as any; // Add a completed tool from 25 hours ago const oldTool = { @@ -167,19 +123,5 @@ describe('BackgroundToolRegistry', () => { registry.tools.set('old-tool', oldTool); registry.tools.set('recent-tool', recentTool); registry.tools.set('old-running-tool', oldRunningTool); - - // Clean up tools older than 24 hours - backgroundToolRegistry.cleanupOldTools(24); - - // Old completed tool should be removed - expect(backgroundToolRegistry.getToolById('old-tool')).toBeUndefined(); - - // Recent completed tool should remain - expect(backgroundToolRegistry.getToolById('recent-tool')).toBeDefined(); - - // Old running tool should remain (not completed) - expect( - backgroundToolRegistry.getToolById('old-running-tool'), - ).toBeDefined(); }); }); diff --git a/packages/agent/src/core/backgroundTools.ts b/packages/agent/src/core/backgroundTools.ts index ebe850b..8e034c3 100644 --- a/packages/agent/src/core/backgroundTools.ts +++ b/packages/agent/src/core/backgroundTools.ts @@ -22,7 +22,6 @@ export interface BackgroundTool { status: BackgroundToolStatus; startTime: Date; endTime?: Date; - agentId: string; // To track which agent created this process metadata: Record; // Additional tool-specific information } @@ -61,30 +60,20 @@ export type AnyBackgroundTool = /** * Registry to keep track of all background processes */ -export class BackgroundToolRegistry { - private static instance: BackgroundToolRegistry; - private tools: Map = new Map(); +export class BackgroundTools { + tools: Map = new Map(); // Private constructor for singleton pattern - private constructor() {} - - // Get the singleton instance - public static getInstance(): BackgroundToolRegistry { - if (!BackgroundToolRegistry.instance) { - BackgroundToolRegistry.instance = new BackgroundToolRegistry(); - } - return BackgroundToolRegistry.instance; - } + constructor(readonly ownerName: string) {} // Register a new shell process - public registerShell(agentId: string, command: string): string { + public registerShell(command: string): string { const id = uuidv4(); const tool: ShellBackgroundTool = { id, type: BackgroundToolType.SHELL, status: BackgroundToolStatus.RUNNING, startTime: new Date(), - agentId, metadata: { command, }, @@ -94,14 +83,13 @@ export class BackgroundToolRegistry { } // Register a new browser process - public registerBrowser(agentId: string, url?: string): string { + public registerBrowser(url?: string): string { const id = uuidv4(); const tool: BrowserBackgroundTool = { id, type: BackgroundToolType.BROWSER, status: BackgroundToolStatus.RUNNING, startTime: new Date(), - agentId, metadata: { url, }, @@ -111,14 +99,13 @@ export class BackgroundToolRegistry { } // Register a new agent process (for future use) - public registerAgent(agentId: string, goal?: string): string { + public registerAgent(goal?: string): string { const id = uuidv4(); const tool: AgentBackgroundTool = { id, type: BackgroundToolType.AGENT, status: BackgroundToolStatus.RUNNING, startTime: new Date(), - agentId, metadata: { goal, }, @@ -155,13 +142,10 @@ export class BackgroundToolRegistry { return true; } - // Get all processes for a specific agent - public getToolsByAgent(agentId: string): AnyBackgroundTool[] { + public getTools(): AnyBackgroundTool[] { const result: AnyBackgroundTool[] = []; for (const tool of this.tools.values()) { - if (tool.agentId === agentId) { - result.push(tool); - } + result.push(tool); } return result; } @@ -170,25 +154,4 @@ export class BackgroundToolRegistry { public getToolById(id: string): AnyBackgroundTool | undefined { return this.tools.get(id); } - - // Clean up completed processes (optional, for maintenance) - public cleanupOldTools(olderThanHours: number = 24): void { - const cutoffTime = new Date(Date.now() - olderThanHours * 60 * 60 * 1000); - - for (const [id, tool] of this.tools.entries()) { - // Remove if it's completed/error/terminated AND older than cutoff - if ( - tool.endTime && - tool.endTime < cutoffTime && - (tool.status === BackgroundToolStatus.COMPLETED || - tool.status === BackgroundToolStatus.ERROR || - tool.status === BackgroundToolStatus.TERMINATED) - ) { - this.tools.delete(id); - } - } - } } - -// Export singleton instance -export const backgroundToolRegistry = BackgroundToolRegistry.getInstance(); diff --git a/packages/agent/src/core/toolAgent/toolAgentCore.ts b/packages/agent/src/core/toolAgent/toolAgentCore.ts index da00326..057bb5a 100644 --- a/packages/agent/src/core/toolAgent/toolAgentCore.ts +++ b/packages/agent/src/core/toolAgent/toolAgentCore.ts @@ -39,11 +39,15 @@ export const toolAgent = async ( logger.debug('User message:', initialPrompt); + const localContext = { + ...context, + }; + // Get the system prompt once at the start - const systemPrompt = config.getSystemPrompt(context); + const systemPrompt = config.getSystemPrompt(localContext); // Create the LLM provider - const provider = createProvider(context.provider, context.model); + const provider = createProvider(localContext.provider, localContext.model); for (let i = 0; i < config.maxIterations; i++) { logger.verbose( @@ -74,8 +78,8 @@ export const toolAgent = async ( const generateOptions = { messages: messagesWithSystem, functions: functionDefinitions, - temperature: context.temperature, - maxTokens: context.maxTokens, + temperature: localContext.temperature, + maxTokens: localContext.maxTokens, }; const { text, toolCalls, tokenUsage } = await generateText( @@ -123,7 +127,7 @@ export const toolAgent = async ( // Execute the tools and get results const { sequenceCompleted, completionResult, respawn } = - await executeTools(toolCalls, tools, messages, context); + await executeTools(toolCalls, tools, messages, localContext); if (respawn) { logger.info('Respawning agent with new context'); diff --git a/packages/agent/src/core/types.ts b/packages/agent/src/core/types.ts index 59c70d0..a1871f0 100644 --- a/packages/agent/src/core/types.ts +++ b/packages/agent/src/core/types.ts @@ -3,6 +3,7 @@ import { JsonSchema7Type } from 'zod-to-json-schema'; import { Logger } from '../utils/logger.js'; +import { BackgroundTools } from './backgroundTools.js'; import { TokenTracker } from './tokens.js'; import { ModelProvider } from './toolAgent/config.js'; @@ -26,6 +27,7 @@ export type ToolContext = { model: string; maxTokens: number; temperature: number; + backgroundTools: BackgroundTools; }; export type Tool, TReturn = any> = { diff --git a/packages/agent/src/tools/browser/browseMessage.ts b/packages/agent/src/tools/browser/browseMessage.ts index abe07c3..a6b35d5 100644 --- a/packages/agent/src/tools/browser/browseMessage.ts +++ b/packages/agent/src/tools/browser/browseMessage.ts @@ -1,10 +1,7 @@ import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; -import { - backgroundToolRegistry, - BackgroundToolStatus, -} from '../../core/backgroundTools.js'; +import { BackgroundToolStatus } from '../../core/backgroundTools.js'; import { Tool } from '../../core/types.js'; import { errorToString } from '../../utils/errorToString.js'; import { sleep } from '../../utils/sleep.js'; @@ -75,7 +72,7 @@ export const browseMessageTool: Tool = { execute: async ( { instanceId, actionType, url, selector, selectorType, text }, - { logger, pageFilter }, + { logger, pageFilter, backgroundTools }, ): Promise => { // Validate action format @@ -190,7 +187,7 @@ export const browseMessageTool: Tool = { browserSessions.delete(instanceId); // Update background tool registry when browser is explicitly closed - backgroundToolRegistry.updateToolStatus( + backgroundTools.updateToolStatus( instanceId, BackgroundToolStatus.COMPLETED, { @@ -210,14 +207,10 @@ export const browseMessageTool: Tool = { logger.error('Browser action failed:', { error }); // Update background tool registry with error status if action fails - backgroundToolRegistry.updateToolStatus( - instanceId, - BackgroundToolStatus.ERROR, - { - error: errorToString(error), - actionType, - }, - ); + backgroundTools.updateToolStatus(instanceId, BackgroundToolStatus.ERROR, { + error: errorToString(error), + actionType, + }); return { status: 'error', diff --git a/packages/agent/src/tools/browser/browseStart.ts b/packages/agent/src/tools/browser/browseStart.ts index 8d95000..9498cb1 100644 --- a/packages/agent/src/tools/browser/browseStart.ts +++ b/packages/agent/src/tools/browser/browseStart.ts @@ -3,10 +3,7 @@ import { v4 as uuidv4 } from 'uuid'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; -import { - backgroundToolRegistry, - BackgroundToolStatus, -} from '../../core/backgroundTools.js'; +import { BackgroundToolStatus } from '../../core/backgroundTools.js'; import { Tool } from '../../core/types.js'; import { errorToString } from '../../utils/errorToString.js'; import { sleep } from '../../utils/sleep.js'; @@ -46,7 +43,7 @@ export const browseStartTool: Tool = { execute: async ( { url, timeout = 30000 }, - { logger, headless, userSession, pageFilter, agentId }, + { logger, headless, userSession, pageFilter, agentId, backgroundTools }, ): Promise => { logger.verbose(`Starting browser session${url ? ` at ${url}` : ''}`); logger.verbose( @@ -58,7 +55,7 @@ export const browseStartTool: Tool = { const instanceId = uuidv4(); // Register this browser session with the background tool registry - backgroundToolRegistry.registerBrowser(agentId || 'unknown', url); + backgroundTools.registerBrowser(url); // Launch browser const launchOptions = { @@ -99,7 +96,7 @@ export const browseStartTool: Tool = { browser.on('disconnected', () => { browserSessions.delete(instanceId); // Update background tool registry when browser disconnects - backgroundToolRegistry.updateToolStatus( + backgroundTools.updateToolStatus( instanceId, BackgroundToolStatus.TERMINATED, ); @@ -146,7 +143,7 @@ export const browseStartTool: Tool = { logger.verbose(`Content length: ${content.length} characters`); // Update background tool registry with running status - backgroundToolRegistry.updateToolStatus( + backgroundTools.updateToolStatus( instanceId, BackgroundToolStatus.RUNNING, { diff --git a/packages/agent/src/tools/getTools.test.ts b/packages/agent/src/tools/getTools.test.ts index 45965fe..211e116 100644 --- a/packages/agent/src/tools/getTools.test.ts +++ b/packages/agent/src/tools/getTools.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest'; +import { BackgroundTools } from '../core/backgroundTools.js'; import { TokenTracker } from '../core/tokens.js'; import { ToolContext } from '../core/types.js'; import { MockLogger } from '../utils/mockLogger.js'; @@ -19,6 +20,7 @@ export const getMockToolContext = (): ToolContext => ({ model: 'claude-3-7-sonnet-20250219', maxTokens: 4096, temperature: 0.7, + backgroundTools: new BackgroundTools('test'), }); describe('getTools', () => { diff --git a/packages/agent/src/tools/interaction/agentMessage.ts b/packages/agent/src/tools/interaction/agentMessage.ts index 91e0afd..00a5ff4 100644 --- a/packages/agent/src/tools/interaction/agentMessage.ts +++ b/packages/agent/src/tools/interaction/agentMessage.ts @@ -1,10 +1,7 @@ import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; -import { - backgroundToolRegistry, - BackgroundToolStatus, -} from '../../core/backgroundTools.js'; +import { BackgroundToolStatus } from '../../core/backgroundTools.js'; import { Tool } from '../../core/types.js'; import { agentStates } from './agentStart.js'; @@ -54,7 +51,7 @@ export const agentMessageTool: Tool = { execute: async ( { instanceId, guidance, terminate }, - { logger }, + { logger, backgroundTools }, ): Promise => { logger.verbose( `Interacting with sub-agent ${instanceId}${guidance ? ' with guidance' : ''}${terminate ? ' with termination request' : ''}`, @@ -81,7 +78,7 @@ export const agentMessageTool: Tool = { agentState.completed = true; // Update background tool registry with terminated status - backgroundToolRegistry.updateToolStatus( + backgroundTools.updateToolStatus( instanceId, BackgroundToolStatus.TERMINATED, { diff --git a/packages/agent/src/tools/interaction/agentStart.ts b/packages/agent/src/tools/interaction/agentStart.ts index ec106f3..8b06295 100644 --- a/packages/agent/src/tools/interaction/agentStart.ts +++ b/packages/agent/src/tools/interaction/agentStart.ts @@ -2,10 +2,7 @@ import { v4 as uuidv4 } from 'uuid'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; -import { - backgroundToolRegistry, - BackgroundToolStatus, -} from '../../core/backgroundTools.js'; +import { BackgroundToolStatus } from '../../core/backgroundTools.js'; import { getDefaultSystemPrompt, AgentConfig, @@ -91,7 +88,7 @@ export const agentStartTool: Tool = { returns: returnSchema, returnsJsonSchema: zodToJsonSchema(returnSchema), execute: async (params, context) => { - const { logger, agentId } = context; + const { logger, backgroundTools } = context; // Validate parameters const { @@ -107,7 +104,7 @@ export const agentStartTool: Tool = { const instanceId = uuidv4(); // Register this agent with the background tool registry - backgroundToolRegistry.registerAgent(agentId || 'unknown', goal); + backgroundTools.registerAgent(goal); logger.verbose(`Registered agent with ID: ${instanceId}`); // Construct a well-structured prompt @@ -156,7 +153,7 @@ export const agentStartTool: Tool = { state.output = result.result; // Update background tool registry with completed status - backgroundToolRegistry.updateToolStatus( + backgroundTools.updateToolStatus( instanceId, BackgroundToolStatus.COMPLETED, { @@ -174,7 +171,7 @@ export const agentStartTool: Tool = { state.error = error instanceof Error ? error.message : String(error); // Update background tool registry with error status - backgroundToolRegistry.updateToolStatus( + backgroundTools.updateToolStatus( instanceId, BackgroundToolStatus.ERROR, { diff --git a/packages/agent/src/tools/interaction/agentTools.test.ts b/packages/agent/src/tools/interaction/agentTools.test.ts index 9b0531e..6e1c26f 100644 --- a/packages/agent/src/tools/interaction/agentTools.test.ts +++ b/packages/agent/src/tools/interaction/agentTools.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; +import { BackgroundTools } from '../../core/backgroundTools.js'; import { TokenTracker } from '../../core/tokens.js'; import { ToolContext } from '../../core/types.js'; import { MockLogger } from '../../utils/mockLogger.js'; @@ -28,6 +29,7 @@ const mockContext: ToolContext = { model: 'claude-3-7-sonnet-20250219', maxTokens: 4096, temperature: 0.7, + backgroundTools: new BackgroundTools('test'), }; describe('Agent Tools', () => { diff --git a/packages/agent/src/tools/interaction/subAgent.test.ts b/packages/agent/src/tools/interaction/subAgent.test.ts index 4b4df8e..6b0dff7 100644 --- a/packages/agent/src/tools/interaction/subAgent.test.ts +++ b/packages/agent/src/tools/interaction/subAgent.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; +import { BackgroundTools } from '../../core/backgroundTools.js'; import { TokenTracker } from '../../core/tokens.js'; import { ToolContext } from '../../core/types.js'; import { MockLogger } from '../../utils/mockLogger.js'; @@ -32,6 +33,7 @@ const mockContext: ToolContext = { model: 'claude-3-7-sonnet-20250219', maxTokens: 4096, temperature: 0.7, + backgroundTools: new BackgroundTools('test'), }; describe('subAgentTool', () => { diff --git a/packages/agent/src/tools/interaction/subAgent.ts b/packages/agent/src/tools/interaction/subAgent.ts index 3f66ae2..ac32616 100644 --- a/packages/agent/src/tools/interaction/subAgent.ts +++ b/packages/agent/src/tools/interaction/subAgent.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { - backgroundToolRegistry, + BackgroundTools, BackgroundToolStatus, } from '../../core/backgroundTools.js'; import { @@ -69,7 +69,7 @@ export const subAgentTool: Tool = { returns: returnSchema, returnsJsonSchema: zodToJsonSchema(returnSchema), execute: async (params, context) => { - const { logger, agentId } = context; + const { logger, backgroundTools } = context; // Validate parameters const { @@ -81,12 +81,15 @@ export const subAgentTool: Tool = { } = parameterSchema.parse(params); // Register this sub-agent with the background tool registry - const subAgentId = backgroundToolRegistry.registerAgent( - agentId || 'unknown', - goal, - ); + const subAgentId = backgroundTools.registerAgent(goal); logger.verbose(`Registered sub-agent with ID: ${subAgentId}`); + const localContext = { + ...context, + workingDirectory: workingDirectory ?? context.workingDirectory, + backgroundTools: new BackgroundTools(`subAgent: ${goal}`), + }; + // Construct a well-structured prompt const prompt = [ `Description: ${description}`, @@ -108,13 +111,10 @@ export const subAgentTool: Tool = { }; try { - const result = await toolAgent(prompt, tools, config, { - ...context, - workingDirectory: workingDirectory ?? context.workingDirectory, - }); + const result = await toolAgent(prompt, tools, config, localContext); // Update background tool registry with completed status - backgroundToolRegistry.updateToolStatus( + backgroundTools.updateToolStatus( subAgentId, BackgroundToolStatus.COMPLETED, { @@ -127,13 +127,9 @@ export const subAgentTool: Tool = { return { response: result.result }; } catch (error) { // Update background tool registry with error status - backgroundToolRegistry.updateToolStatus( - subAgentId, - BackgroundToolStatus.ERROR, - { - error: error instanceof Error ? error.message : String(error), - }, - ); + backgroundTools.updateToolStatus(subAgentId, BackgroundToolStatus.ERROR, { + error: error instanceof Error ? error.message : String(error), + }); throw error; } diff --git a/packages/agent/src/tools/system/listBackgroundTools.test.ts b/packages/agent/src/tools/system/listBackgroundTools.test.ts index 5f9fddf..7435062 100644 --- a/packages/agent/src/tools/system/listBackgroundTools.test.ts +++ b/packages/agent/src/tools/system/listBackgroundTools.test.ts @@ -5,7 +5,7 @@ import { listBackgroundToolsTool } from './listBackgroundTools.js'; // Mock the entire background tools module vi.mock('../../core/backgroundTools.js', () => { return { - backgroundToolRegistry: { + backgroundTools: { getToolsByAgent: vi.fn().mockReturnValue([ { id: 'shell-1', diff --git a/packages/agent/src/tools/system/listBackgroundTools.ts b/packages/agent/src/tools/system/listBackgroundTools.ts index 83eff8f..bc7608e 100644 --- a/packages/agent/src/tools/system/listBackgroundTools.ts +++ b/packages/agent/src/tools/system/listBackgroundTools.ts @@ -1,10 +1,7 @@ import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; -import { - backgroundToolRegistry, - BackgroundToolStatus, -} from '../../core/backgroundTools.js'; +import { BackgroundToolStatus } from '../../core/backgroundTools.js'; import { Tool } from '../../core/types.js'; const parameterSchema = z.object({ @@ -52,14 +49,14 @@ export const listBackgroundToolsTool: Tool = { execute: async ( { status = 'all', type = 'all', verbose = false }, - { logger, agentId }, + { logger, backgroundTools }, ): Promise => { logger.verbose( `Listing background tools with status: ${status}, type: ${type}, verbose: ${verbose}`, ); // Get all tools for this agent - const tools = backgroundToolRegistry.getToolsByAgent(agentId || 'unknown'); + const tools = backgroundTools.getTools(); // Filter by status if specified const filteredByStatus = diff --git a/packages/agent/src/tools/system/shellMessage.ts b/packages/agent/src/tools/system/shellMessage.ts index fc892ac..17655d9 100644 --- a/packages/agent/src/tools/system/shellMessage.ts +++ b/packages/agent/src/tools/system/shellMessage.ts @@ -1,10 +1,7 @@ import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; -import { - backgroundToolRegistry, - BackgroundToolStatus, -} from '../../core/backgroundTools.js'; +import { BackgroundToolStatus } from '../../core/backgroundTools.js'; import { Tool } from '../../core/types.js'; import { sleep } from '../../utils/sleep.js'; @@ -99,7 +96,7 @@ export const shellMessageTool: Tool = { execute: async ( { instanceId, stdin, signal, showStdIn, showStdout }, - { logger }, + { logger, backgroundTools }, ): Promise => { logger.verbose( `Interacting with shell process ${instanceId}${stdin ? ' with input' : ''}${signal ? ` with signal ${signal}` : ''}`, @@ -122,7 +119,7 @@ export const shellMessageTool: Tool = { processState.state.signaled = true; // Update background tool registry if signal failed - backgroundToolRegistry.updateToolStatus( + backgroundTools.updateToolStatus( instanceId, BackgroundToolStatus.ERROR, { @@ -142,7 +139,7 @@ export const shellMessageTool: Tool = { signal === 'SIGKILL' || signal === 'SIGINT' ) { - backgroundToolRegistry.updateToolStatus( + backgroundTools.updateToolStatus( instanceId, BackgroundToolStatus.TERMINATED, { @@ -151,7 +148,7 @@ export const shellMessageTool: Tool = { }, ); } else { - backgroundToolRegistry.updateToolStatus( + backgroundTools.updateToolStatus( instanceId, BackgroundToolStatus.RUNNING, { diff --git a/packages/agent/src/tools/system/shellStart.ts b/packages/agent/src/tools/system/shellStart.ts index fa8e36d..c98c7e7 100644 --- a/packages/agent/src/tools/system/shellStart.ts +++ b/packages/agent/src/tools/system/shellStart.ts @@ -4,10 +4,7 @@ import { v4 as uuidv4 } from 'uuid'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; -import { - backgroundToolRegistry, - BackgroundToolStatus, -} from '../../core/backgroundTools.js'; +import { BackgroundToolStatus } from '../../core/backgroundTools.js'; import { Tool } from '../../core/types.js'; import { errorToString } from '../../utils/errorToString.js'; @@ -102,7 +99,7 @@ export const shellStartTool: Tool = { showStdIn = false, showStdout = false, }, - { logger, workingDirectory, agentId }, + { logger, workingDirectory, backgroundTools }, ): Promise => { if (showStdIn) { logger.info(`Command input: ${command}`); @@ -115,7 +112,7 @@ export const shellStartTool: Tool = { const instanceId = uuidv4(); // Register this shell process with the background tool registry - backgroundToolRegistry.registerShell(agentId || 'unknown', command); + backgroundTools.registerShell(command); let hasResolved = false; @@ -164,7 +161,7 @@ export const shellStartTool: Tool = { processState.state.completed = true; // Update background tool registry with error status - backgroundToolRegistry.updateToolStatus( + backgroundTools.updateToolStatus( instanceId, BackgroundToolStatus.ERROR, { @@ -198,7 +195,7 @@ export const shellStartTool: Tool = { code === 0 ? BackgroundToolStatus.COMPLETED : BackgroundToolStatus.ERROR; - backgroundToolRegistry.updateToolStatus(instanceId, status, { + backgroundTools.updateToolStatus(instanceId, status, { exitCode: code, signaled: signal !== null, }); diff --git a/packages/agent/src/utils/logger.ts b/packages/agent/src/utils/logger.ts index bf38316..8f16f83 100644 --- a/packages/agent/src/utils/logger.ts +++ b/packages/agent/src/utils/logger.ts @@ -37,7 +37,7 @@ export const BasicLoggerStyler = { : prefix, showPrefix: (_level: LogLevel): boolean => { // Show prefix for all log levels - return true; + return false; }, }; diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 718d89d..2c173d3 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,3 +1,30 @@ +# [mycoder-v1.1.1](https://github.com/drivecore/mycoder/compare/mycoder-v1.1.0...mycoder-v1.1.1) (2025-03-12) + + +### Bug Fixes + +* remove userWarning option from docs and Github Action. ([35617c1](https://github.com/drivecore/mycoder/commit/35617c19a4a03ba5c170b93d035bdf99ddb81544)) + +# [mycoder-v1.1.0](https://github.com/drivecore/mycoder/compare/mycoder-v1.0.0...mycoder-v1.1.0) (2025-03-12) + + +### Bug Fixes + +* implement resource cleanup to prevent CLI hanging issue ([d33e729](https://github.com/drivecore/mycoder/commit/d33e7298686a30661ee8b36f2fdffb16f5f3da71)), closes [#141](https://github.com/drivecore/mycoder/issues/141) +* llm choice working well for openai, anthropic and ollama ([68d34ab](https://github.com/drivecore/mycoder/commit/68d34abf8a73ed533a072359ce334a9364753425)) +* remove unreliable init command and createDefaultConfigFile function ([5559567](https://github.com/drivecore/mycoder/commit/5559567d1986e828983f5975495bee89fcd91772)), closes [#225](https://github.com/drivecore/mycoder/issues/225) +* replace @semantic-release/npm with @anolilab/semantic-release-pnpm to properly resolve workspace references ([bacb51f](https://github.com/drivecore/mycoder/commit/bacb51f637f2b2d3b1039bdfdbd33e3d704b6cde)) + + +### Features + +* add git and gh CLI tools availability check ([8996f36](https://github.com/drivecore/mycoder/commit/8996f3609d3d13a62dd9943bfe2e846508a70336)), closes [#217](https://github.com/drivecore/mycoder/issues/217) +* add Ollama configuration options ([d5c3a96](https://github.com/drivecore/mycoder/commit/d5c3a96ce9463c98504c2a346796400df36bf3b0)) +* **cli:** Add checking for git and gh CLI tools in GitHub mode ([5443185](https://github.com/drivecore/mycoder/commit/54431854e1e02de2a3c6bf993b114993739dcca1)), closes [#217](https://github.com/drivecore/mycoder/issues/217) +* **llm:** add OpenAI support to LLM abstraction ([7bda811](https://github.com/drivecore/mycoder/commit/7bda811658e15b8dd41135cd9b2b90e9ea925e15)) +* **refactor:** agent ([a2f59c2](https://github.com/drivecore/mycoder/commit/a2f59c2f51643a44d6e1ff0c16b319deb1adc3f2)) +* Replace config CLI commands with config file-based approach ([#215](https://github.com/drivecore/mycoder/issues/215)) ([8dffcef](https://github.com/drivecore/mycoder/commit/8dffcef10c123c05ef6970c465c4d8b3f0475622)) + # mycoder-v1.0.0 (2025-03-11) ### Bug Fixes diff --git a/packages/cli/package.json b/packages/cli/package.json index 6f7a6c9..79374fb 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "mycoder", "description": "A command line tool using agent that can do arbitrary tasks, including coding tasks", - "version": "1.0.1", + "version": "1.1.1", "type": "module", "bin": "./bin/cli.js", "main": "./dist/index.js", diff --git a/packages/cli/src/commands/$default.ts b/packages/cli/src/commands/$default.ts index cc57b93..3680d8c 100644 --- a/packages/cli/src/commands/$default.ts +++ b/packages/cli/src/commands/$default.ts @@ -14,6 +14,7 @@ import { DEFAULT_CONFIG, AgentConfig, ModelProvider, + BackgroundTools, } from 'mycoder-agent'; import { TokenTracker } from 'mycoder-agent/dist/core/tokens.js'; @@ -208,6 +209,7 @@ export const command: CommandModule = { model: config.model, maxTokens: config.maxTokens, temperature: config.temperature, + backgroundTools: new BackgroundTools('mainAgent'), }); const output =