From 4ae6a76b8a9b1e41ff15c8aac61ede432c8727e4 Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Wed, 26 Feb 2025 21:29:36 -0500 Subject: [PATCH 1/6] central user prompts with consistent formatting into a function. --- packages/agent/src/index.ts | 1 + .../agent/src/tools/interaction/userPrompt.ts | 22 ++-------------- packages/agent/src/utils/userPrompt.ts | 25 +++++++++++++++++++ packages/cli/src/commands/$default.ts | 20 +++------------ 4 files changed, 32 insertions(+), 36 deletions(-) create mode 100644 packages/agent/src/utils/userPrompt.ts diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index bb716a4..b86b6ce 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -36,3 +36,4 @@ export * from './utils/errorToString.js'; export * from './utils/logger.js'; export * from './utils/mockLogger.js'; export * from './utils/stringifyLimited.js'; +export * from './utils/userPrompt.js'; diff --git a/packages/agent/src/tools/interaction/userPrompt.ts b/packages/agent/src/tools/interaction/userPrompt.ts index c735b68..25b99a1 100644 --- a/packages/agent/src/tools/interaction/userPrompt.ts +++ b/packages/agent/src/tools/interaction/userPrompt.ts @@ -1,10 +1,8 @@ -import * as readline from 'readline'; - -import chalk from 'chalk'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { Tool } from '../../core/types.js'; +import { userPrompt } from '../../utils/userPrompt.js'; const parameterSchema = z.object({ prompt: z.string().describe('The prompt message to display to the user'), @@ -23,26 +21,10 @@ export const userPromptTool: Tool = { execute: async ({ prompt }, { logger }) => { logger.verbose(`Prompting user with: ${prompt}`); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - terminal: true, - }); - - // Disable the readline interface's internal input processing - if (rl.terminal) { - process.stdin.setRawMode(false); - } - - const response = await new Promise((resolve) => { - rl.question(chalk.green(prompt + ' '), (answer) => { - resolve(answer); - }); - }); + const response = await userPrompt(prompt); logger.verbose(`Received user response: ${response}`); - rl.close(); return response; }, logParameters: () => {}, diff --git a/packages/agent/src/utils/userPrompt.ts b/packages/agent/src/utils/userPrompt.ts new file mode 100644 index 0000000..b9f3219 --- /dev/null +++ b/packages/agent/src/utils/userPrompt.ts @@ -0,0 +1,25 @@ +import * as readline from 'readline'; + +import chalk from 'chalk'; + +export const userPrompt = async (prompt: string): Promise => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: true, + }); + + try { + // Disable the readline interface's internal input processing + if (rl.terminal) { + process.stdin.setRawMode(false); + } + return await new Promise((resolve) => { + rl.question(chalk.green('\n' + prompt + '\n') + '\n> ', (answer) => { + resolve(answer); + }); + }); + } finally { + rl.close(); + } +}; diff --git a/packages/cli/src/commands/$default.ts b/packages/cli/src/commands/$default.ts index 9e8712b..65e9656 100644 --- a/packages/cli/src/commands/$default.ts +++ b/packages/cli/src/commands/$default.ts @@ -1,13 +1,13 @@ import * as fs from 'fs/promises'; import { createInterface } from 'readline/promises'; -import chalk from 'chalk'; import { toolAgent, Logger, getTools, getAnthropicApiKeyError, TokenLevel, + userPrompt, } from 'mycoder-agent'; import { SharedOptions } from '../options.js'; @@ -85,21 +85,9 @@ export const command: CommandModule = { // If interactive mode if (argv.interactive) { - const readline = createInterface({ - input: process.stdin, - output: process.stdout, - }); - - try { - console.log( - chalk.green( - "Type your request below or 'help' for usage information. Use Ctrl+C to exit.", - ), - ); - prompt = await readline.question('\n> '); - } finally { - readline.close(); - } + prompt = await userPrompt( + "Type your request below or 'help' for usage information. Use Ctrl+C to exit.", + ); } else if (!prompt) { // Use command line prompt if provided prompt = argv.prompt; From c0c8e50c07b15678028dc0a1c30d1bc4636dd669 Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Thu, 27 Feb 2025 08:10:23 -0500 Subject: [PATCH 2/6] better terminal compatibility. --- packages/agent/src/utils/userPrompt.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/agent/src/utils/userPrompt.ts b/packages/agent/src/utils/userPrompt.ts index b9f3219..3da50da 100644 --- a/packages/agent/src/utils/userPrompt.ts +++ b/packages/agent/src/utils/userPrompt.ts @@ -1,24 +1,15 @@ -import * as readline from 'readline'; +import { createInterface } from 'readline/promises'; import chalk from 'chalk'; export const userPrompt = async (prompt: string): Promise => { - const rl = readline.createInterface({ + const rl = createInterface({ input: process.stdin, output: process.stdout, - terminal: true, }); try { - // Disable the readline interface's internal input processing - if (rl.terminal) { - process.stdin.setRawMode(false); - } - return await new Promise((resolve) => { - rl.question(chalk.green('\n' + prompt + '\n') + '\n> ', (answer) => { - resolve(answer); - }); - }); + return await rl.question(chalk.green('\n' + prompt + '\n') + '\n> '); } finally { rl.close(); } From 0689d0ff8be978662c5b5d603ba27ad1b68cd672 Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Thu, 27 Feb 2025 10:13:08 -0500 Subject: [PATCH 3/6] started on emoji log prefixes --- packages/agent/src/core/tokens.ts | 135 ++++++++++++------ .../agent/src/core/toolAgent.cache.test.ts | 102 ------------- .../agent/src/core/toolAgent.respawn.test.ts | 15 +- packages/agent/src/core/toolAgent.test.ts | 45 ++---- packages/agent/src/core/toolAgent.ts | 47 +++--- packages/agent/src/core/types.ts | 5 +- .../agent/src/tools/browser/BrowserManager.ts | 1 + .../agent/src/tools/browser/browseMessage.ts | 2 +- .../agent/src/tools/browser/browseStart.ts | 2 +- .../agent/src/tools/interaction/subAgent.ts | 2 +- packages/agent/src/tools/io/fetch.ts | 7 +- packages/agent/src/tools/io/readFile.test.ts | 14 +- packages/agent/src/tools/io/readFile.ts | 2 +- .../agent/src/tools/io/updateFile.test.ts | 34 +++-- packages/agent/src/tools/io/updateFile.ts | 2 +- .../agent/src/tools/system/respawn.test.ts | 18 +-- .../src/tools/system/shellExecute.test.ts | 12 +- .../agent/src/tools/system/shellExecute.ts | 2 +- .../src/tools/system/shellMessage.test.ts | 34 +++-- .../agent/src/tools/system/shellMessage.ts | 2 +- .../agent/src/tools/system/shellStart.test.ts | 23 +-- packages/agent/src/tools/system/shellStart.ts | 6 +- packages/agent/src/tools/system/sleep.test.ts | 23 ++- packages/agent/src/tools/system/sleep.ts | 2 +- packages/agent/src/utils/logger.ts | 5 + packages/cli/src/commands/$default.ts | 37 +++-- packages/cli/src/options.ts | 6 + 27 files changed, 270 insertions(+), 315 deletions(-) delete mode 100644 packages/agent/src/core/toolAgent.cache.test.ts diff --git a/packages/agent/src/core/tokens.ts b/packages/agent/src/core/tokens.ts index ae2874e..2d7ba0f 100644 --- a/packages/agent/src/core/tokens.ts +++ b/packages/agent/src/core/tokens.ts @@ -1,53 +1,100 @@ import Anthropic from '@anthropic-ai/sdk'; -export type TokenUsage = { - input: number; - inputCacheWrites: number; - inputCacheReads: number; - output: number; -}; - -export const getTokenUsage = (response: Anthropic.Message): TokenUsage => { - return { - input: response.usage.input_tokens, - inputCacheWrites: response.usage.cache_creation_input_tokens ?? 0, - inputCacheReads: response.usage.cache_read_input_tokens ?? 0, - output: response.usage.output_tokens, - }; -}; - -export const addTokenUsage = (a: TokenUsage, b: TokenUsage): TokenUsage => { - return { - input: a.input + b.input, - inputCacheWrites: a.inputCacheWrites + b.inputCacheWrites, - inputCacheReads: a.inputCacheReads + b.inputCacheReads, - output: a.output + b.output, - }; -}; +import { LogLevel } from '../utils/logger.js'; const PER_MILLION = 1 / 1000000; -const TOKEN_COST: TokenUsage = { +const TOKEN_COST = { input: 3 * PER_MILLION, - inputCacheWrites: 3.75 * PER_MILLION, - inputCacheReads: 0.3 * PER_MILLION, + cacheWrites: 3.75 * PER_MILLION, + cacheReads: 0.3 * PER_MILLION, output: 15 * PER_MILLION, }; -const formatter = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 2, -}); - -export const getTokenCost = (usage: TokenUsage): string => { - return formatter.format( - usage.input * TOKEN_COST.input + - usage.inputCacheWrites * TOKEN_COST.inputCacheWrites + - usage.inputCacheReads * TOKEN_COST.inputCacheReads + - usage.output * TOKEN_COST.output, - ); -}; +export class TokenUsage { + public input: number = 0; + public cacheWrites: number = 0; + public cacheReads: number = 0; + public output: number = 0; -export const getTokenString = (usage: TokenUsage): string => { - return `input: ${usage.input} input-cache-writes: ${usage.inputCacheWrites} input-cache-reads: ${usage.inputCacheReads} output: ${usage.output} COST: ${getTokenCost(usage)}`; -}; + constructor() {} + + add(usage: TokenUsage) { + this.input += usage.input; + this.cacheWrites += usage.cacheWrites; + this.cacheReads += usage.cacheReads; + this.output += usage.output; + } + + clone() { + const usage = new TokenUsage(); + usage.input = this.input; + usage.cacheWrites = this.cacheWrites; + usage.cacheReads = this.cacheReads; + usage.output = this.output; + return usage; + } + + static fromMessage(message: Anthropic.Message) { + const usage = new TokenUsage(); + usage.input = message.usage.input_tokens; + usage.cacheWrites = message.usage.cache_creation_input_tokens ?? 0; + usage.cacheReads = message.usage.cache_read_input_tokens ?? 0; + usage.output = message.usage.output_tokens; + return usage; + } + + static sum(usages: TokenUsage[]) { + const usage = new TokenUsage(); + usages.forEach((u) => usage.add(u)); + return usage; + } + + getCost() { + const formatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + }); + + return formatter.format( + this.input * TOKEN_COST.input + + this.cacheWrites * TOKEN_COST.cacheWrites + + this.cacheReads * TOKEN_COST.cacheReads + + this.output * TOKEN_COST.output, + ); + } + + toString() { + return `input: ${this.input} cache-writes: ${this.cacheWrites} cache-reads: ${this.cacheReads} output: ${this.output} COST: ${this.getCost()}`; + } +} + +export class TokenTracker { + public tokenUsage = new TokenUsage(); + public children: TokenTracker[] = []; + + constructor( + public readonly name: string = 'unnamed', + public readonly parent: TokenTracker | undefined = undefined, + public readonly logLevel: LogLevel = parent?.logLevel ?? LogLevel.debug, + ) { + if (parent) { + parent.children.push(this); + } + } + + getTotalUsage() { + const usage = this.tokenUsage.clone(); + this.children.forEach((child) => usage.add(child.getTotalUsage())); + return usage; + } + + getTotalCost() { + const usage = this.getTotalUsage(); + return usage.getCost(); + } + + toString() { + return `${this.name}: ${this.tokenUsage.toString()}`; + } +} diff --git a/packages/agent/src/core/toolAgent.cache.test.ts b/packages/agent/src/core/toolAgent.cache.test.ts deleted file mode 100644 index 735012e..0000000 --- a/packages/agent/src/core/toolAgent.cache.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { MockLogger } from '../utils/mockLogger.js'; - -import { toolAgent } from './toolAgent.js'; - -const logger = new MockLogger(); - -process.env.ANTHROPIC_API_KEY = 'sk-ant-api03-1234567890'; - -// Mock Anthropic client -vi.mock('@anthropic-ai/sdk', () => { - return { - default: class MockAnthropic { - messages = { - create: vi.fn().mockImplementation(() => { - return { - id: 'msg_123', - model: 'claude-3-7-sonnet-latest', - type: 'message', - role: 'assistant', - content: [ - { - type: 'text', - text: 'I will help with that.', - }, - { - type: 'tool_use', - id: 'tu_123', - name: 'sequenceComplete', - input: { - result: 'Test complete', - }, - }, - ], - usage: { - input_tokens: 100, - output_tokens: 50, - // Simulating cached tokens - cache_read_input_tokens: 30, - cache_creation_input_tokens: 70, - }, - }; - }), - }; - constructor() {} - }, - }; -}); - -// Mock tool -const mockTool = { - name: 'sequenceComplete', - description: 'Completes the sequence', - parameters: { - type: 'object' as const, - properties: { - result: { - type: 'string' as const, - }, - }, - additionalProperties: false, - required: ['result'], - }, - returns: { - type: 'string' as const, - }, - execute: vi.fn().mockImplementation(async (params) => { - console.log(' Parameters:'); - Object.entries(params).forEach(([key, value]) => { - console.log(` - ${key}: ${JSON.stringify(value)}`); - }); - console.log(); - console.log(' Results:'); - console.log(` - ${params.result}`); - console.log(); - return params.result; - }), -}; - -describe('toolAgent input token caching', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should track cached tokens in the result', async () => { - const result = await toolAgent('test prompt', [mockTool], undefined, { - logger, - headless: true, - workingDirectory: '.', - tokenLevel: 'debug', - }); - - // Verify that cached tokens are tracked - expect(result.tokens.inputCacheReads).toBeDefined(); - expect(result.tokens.inputCacheReads).toBe(30); - - // Verify total token counts - expect(result.tokens.input).toBe(100); - expect(result.tokens.output).toBe(50); - }); -}); diff --git a/packages/agent/src/core/toolAgent.respawn.test.ts b/packages/agent/src/core/toolAgent.respawn.test.ts index e5e69d8..d857732 100644 --- a/packages/agent/src/core/toolAgent.respawn.test.ts +++ b/packages/agent/src/core/toolAgent.respawn.test.ts @@ -4,8 +4,14 @@ import { toolAgent } from '../../src/core/toolAgent.js'; import { getTools } from '../../src/tools/getTools.js'; import { MockLogger } from '../utils/mockLogger.js'; -const logger = new MockLogger(); +import { TokenTracker } from './tokens.js'; +const toolContext = { + logger: new MockLogger(), + headless: true, + workingDirectory: '.', + tokenTracker: new TokenTracker(), +}; // Mock Anthropic SDK vi.mock('@anthropic-ai/sdk', () => { return { @@ -52,12 +58,7 @@ describe('toolAgent respawn functionality', () => { temperature: 0, getSystemPrompt: () => 'test system prompt', }, - { - logger, - headless: true, - workingDirectory: '.', - tokenLevel: 'debug', - }, + toolContext, ); expect(result.result).toBe( diff --git a/packages/agent/src/core/toolAgent.test.ts b/packages/agent/src/core/toolAgent.test.ts index ee21e85..26f2af5 100644 --- a/packages/agent/src/core/toolAgent.test.ts +++ b/packages/agent/src/core/toolAgent.test.ts @@ -3,10 +3,16 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { MockLogger } from '../utils/mockLogger.js'; import { executeToolCall } from './executeToolCall.js'; +import { TokenTracker } from './tokens.js'; import { toolAgent } from './toolAgent.js'; import { Tool } from './types.js'; -const logger = new MockLogger(); +const toolContext = { + logger: new MockLogger(), + headless: true, + workingDirectory: '.', + tokenTracker: new TokenTracker(), +}; // Mock configuration for testing const testConfig = { @@ -101,12 +107,7 @@ describe('toolAgent', () => { input: { input: 'test' }, }, [mockTool], - { - logger, - headless: true, - workingDirectory: '.', - tokenLevel: 'debug', - }, + toolContext, ); expect(result.includes('Processed: test')).toBeTruthy(); @@ -121,12 +122,7 @@ describe('toolAgent', () => { input: {}, }, [mockTool], - { - logger, - headless: true, - workingDirectory: '.', - tokenLevel: 'debug', - }, + toolContext, ), ).rejects.toThrow("No tool with the name 'nonexistentTool' exists."); }); @@ -157,12 +153,7 @@ describe('toolAgent', () => { input: {}, }, [errorTool], - { - logger, - headless: true, - workingDirectory: '.', - tokenLevel: 'debug', - }, + toolContext, ), ).rejects.toThrow('Deliberate failure'); }); @@ -182,12 +173,7 @@ describe('toolAgent', () => { 'Test prompt', [sequenceCompleteTool], testConfig, - { - logger, - headless: true, - workingDirectory: '.', - tokenLevel: 'debug', - }, + toolContext, ); // Verify that create was called twice (once for empty response, once for completion) @@ -205,16 +191,9 @@ describe('toolAgent', () => { 'Test prompt', [sequenceCompleteTool], testConfig, - { - logger, - headless: true, - workingDirectory: '.', - tokenLevel: 'debug', - }, + toolContext, ); expect(result.result).toBe('Test complete'); - expect(result.tokens.input).toBe(10); - expect(result.tokens.output).toBe(10); }); }); diff --git a/packages/agent/src/core/toolAgent.ts b/packages/agent/src/core/toolAgent.ts index d2af2c4..9b9f86e 100644 --- a/packages/agent/src/core/toolAgent.ts +++ b/packages/agent/src/core/toolAgent.ts @@ -7,12 +7,7 @@ import chalk from 'chalk'; import { getAnthropicApiKeyError } from '../utils/errors.js'; import { executeToolCall } from './executeToolCall.js'; -import { - addTokenUsage, - getTokenString, - getTokenUsage, - TokenUsage, -} from './tokens.js'; +import { TokenTracker, TokenUsage } from './tokens.js'; import { Tool, TextContent, @@ -24,7 +19,6 @@ import { export interface ToolAgentResult { result: string; - tokens: TokenUsage; interactions: number; } @@ -153,7 +147,10 @@ async function executeTools( toolCalls.map(async (call) => { let toolResult = ''; try { - toolResult = await executeToolCall(call, tools, context); + toolResult = await executeToolCall(call, tools, { + ...context, + tokenTracker: new TokenTracker(call.name, context.tokenTracker), + }); } catch (error: any) { toolResult = `Error: Exception thrown during tool execution. Type: ${error.constructor.name}, Message: ${error.message}`; } @@ -238,17 +235,11 @@ export const toolAgent = async ( config = CONFIG, context: ToolContext, ): Promise => { - const { logger, tokenLevel, tokenUsage } = context; + const { logger, tokenTracker } = context; logger.verbose('Starting agent execution'); logger.verbose('Initial prompt:', initialPrompt); - let tokens: TokenUsage = { - input: 0, - inputCacheWrites: 0, - inputCacheReads: 0, - output: 0, - }; let interactions = 0; const apiKey = process.env.ANTHROPIC_API_KEY; @@ -317,8 +308,8 @@ export const toolAgent = async ( } // Track both regular and cached token usage - const tokenPerMessage = getTokenUsage(response); - tokens = addTokenUsage(tokens, tokenPerMessage); + const tokenUsagePerMessage = TokenUsage.fromMessage(response); + tokenTracker.tokenUsage.add(tokenUsagePerMessage); const { content, toolCalls } = processResponse(response); messages.push({ @@ -335,10 +326,9 @@ export const toolAgent = async ( logger.info(assistantMessage); } - // Use the appropriate log level based on tokenUsage flag - const logLevel = tokenUsage ? 'info' : tokenLevel; - logger[logLevel]( - chalk.blue(`[Token Usage/Message] ${getTokenString(tokenPerMessage)}`), + logger.log( + tokenTracker.logLevel, + chalk.blue(`[Token Usage/Message] ${tokenUsagePerMessage.toString()}`), ); const { sequenceCompleted, completionResult, respawn } = await executeTools( @@ -364,13 +354,11 @@ export const toolAgent = async ( result: completionResult ?? 'Sequence explicitly completed with an empty result', - tokens, interactions, }; - // Use the appropriate log level based on tokenUsage flag - const logLevel = tokenUsage ? 'info' : tokenLevel; - logger[logLevel]( - chalk.blueBright(`[Token Usage/Agent] ${getTokenString(tokens)}`), + logger.log( + tokenTracker.logLevel, + chalk.blueBright(`[Token Usage/Agent] ${tokenTracker.toString()}`), ); return result; } @@ -379,13 +367,12 @@ export const toolAgent = async ( logger.warn('Maximum iterations reached'); const result = { result: 'Maximum sub-agent iterations reach without successful completion', - tokens, interactions, }; // Use the appropriate log level based on tokenUsage flag - const logLevel = tokenUsage ? 'info' : tokenLevel; - logger[logLevel]( - chalk.blueBright(`[Token Usage/Agent] ${getTokenString(tokens)}`), + logger.log( + tokenTracker.logLevel, + chalk.blueBright(`[Token Usage/Agent] ${tokenTracker.toString()}`), ); return result; }; diff --git a/packages/agent/src/core/types.ts b/packages/agent/src/core/types.ts index a976ab6..8f859ad 100644 --- a/packages/agent/src/core/types.ts +++ b/packages/agent/src/core/types.ts @@ -2,14 +2,15 @@ import { JsonSchema7Type } from 'zod-to-json-schema'; import { Logger } from '../utils/logger.js'; +import { TokenTracker } from './tokens.js'; + export type TokenLevel = 'debug' | 'verbose' | 'info' | 'warn' | 'error'; export type ToolContext = { logger: Logger; workingDirectory: string; headless: boolean; - tokenLevel: TokenLevel; - tokenUsage?: boolean; + tokenTracker: TokenTracker; }; export type Tool, TReturn = any> = { diff --git a/packages/agent/src/tools/browser/BrowserManager.ts b/packages/agent/src/tools/browser/BrowserManager.ts index a136e8a..4723f53 100644 --- a/packages/agent/src/tools/browser/BrowserManager.ts +++ b/packages/agent/src/tools/browser/BrowserManager.ts @@ -18,6 +18,7 @@ export class BrowserManager { async createSession(config?: BrowserConfig): Promise { try { const sessionConfig = { ...this.defaultConfig, ...config }; + console.log('sessionConfig', sessionConfig); const browser = await chromium.launch({ headless: sessionConfig.headless, }); diff --git a/packages/agent/src/tools/browser/browseMessage.ts b/packages/agent/src/tools/browser/browseMessage.ts index 53b8aa9..8875a46 100644 --- a/packages/agent/src/tools/browser/browseMessage.ts +++ b/packages/agent/src/tools/browser/browseMessage.ts @@ -165,7 +165,7 @@ export const browseMessageTool: Tool = { logParameters: ({ action, description }, { logger }) => { logger.info( - `Performing browser action: ${action.actionType}, ${description}`, + `🏄 Performing browser action: ${action.actionType}, ${description}`, ); }, diff --git a/packages/agent/src/tools/browser/browseStart.ts b/packages/agent/src/tools/browser/browseStart.ts index 566cc59..53f69fd 100644 --- a/packages/agent/src/tools/browser/browseStart.ts +++ b/packages/agent/src/tools/browser/browseStart.ts @@ -102,7 +102,7 @@ export const browseStartTool: Tool = { logParameters: ({ url, description }, { logger }) => { logger.info( - `Starting browser session${url ? ` at ${url}` : ''}, ${description}`, + `🏄 Starting browser session${url ? ` at ${url}` : ''}, ${description}`, ); }, diff --git a/packages/agent/src/tools/interaction/subAgent.ts b/packages/agent/src/tools/interaction/subAgent.ts index c2a830b..4e126f5 100644 --- a/packages/agent/src/tools/interaction/subAgent.ts +++ b/packages/agent/src/tools/interaction/subAgent.ts @@ -108,7 +108,7 @@ export const subAgentTool: Tool = { return result.result; // Return the result string directly }, logParameters: (input, { logger }) => { - logger.info(`Delegating task "${input.description}"`); + logger.info(`🤖 Delegating task "${input.description}"`); }, logReturns: () => {}, }; diff --git a/packages/agent/src/tools/io/fetch.ts b/packages/agent/src/tools/io/fetch.ts index 68027f8..adb1119 100644 --- a/packages/agent/src/tools/io/fetch.ts +++ b/packages/agent/src/tools/io/fetch.ts @@ -93,7 +93,12 @@ export const fetchTool: Tool = { logParameters(params, { logger }) { const { method, url, params: queryParams } = params; logger.info( - `${method} ${url}${queryParams ? `?${new URLSearchParams(queryParams).toString()}` : ''}`, + `🌐 ${method} ${url}${queryParams ? `?${new URLSearchParams(queryParams).toString()}` : ''} ==>`, ); }, + + logReturns: (result, { logger }) => { + const { status, statusText } = result; + logger.info(`🌐 ==> ${status} ${statusText}`); + }, }; diff --git a/packages/agent/src/tools/io/readFile.test.ts b/packages/agent/src/tools/io/readFile.test.ts index 2f89eec..cfb68a7 100644 --- a/packages/agent/src/tools/io/readFile.test.ts +++ b/packages/agent/src/tools/io/readFile.test.ts @@ -1,16 +1,22 @@ import { describe, it, expect } from 'vitest'; -import { MockLogger } from '../../utils/mockLogger.js'; +import { TokenTracker } from '../../core/tokens.js'; +import { MockLogger } from '../../utils/mockLogger'; import { readFileTool } from './readFile.js'; -const logger = new MockLogger(); +const toolContext = { + logger: new MockLogger(), + headless: true, + workingDirectory: '.', + tokenTracker: new TokenTracker(), +}; describe('readFile', () => { it('should read a file', async () => { const { content } = await readFileTool.execute( { path: 'package.json', description: 'test' }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(content).toContain('mycoder'); }); @@ -19,7 +25,7 @@ describe('readFile', () => { try { await readFileTool.execute( { path: 'nonexistent.txt', description: 'test' }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(true).toBe(false); // Should not reach here } catch (error: any) { diff --git a/packages/agent/src/tools/io/readFile.ts b/packages/agent/src/tools/io/readFile.ts index d80d6c2..e1a56e1 100644 --- a/packages/agent/src/tools/io/readFile.ts +++ b/packages/agent/src/tools/io/readFile.ts @@ -96,7 +96,7 @@ export const readFileTool: Tool = { }; }, logParameters: (input, { logger }) => { - logger.info(`Looking at "${input.path}", ${input.description}`); + logger.info(` 📖 Reading "${input.path}", ${input.description}`); }, logReturns: () => {}, }; diff --git a/packages/agent/src/tools/io/updateFile.test.ts b/packages/agent/src/tools/io/updateFile.test.ts index 3791ddf..ed79a83 100644 --- a/packages/agent/src/tools/io/updateFile.test.ts +++ b/packages/agent/src/tools/io/updateFile.test.ts @@ -5,13 +5,19 @@ import { join } from 'path'; import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { TokenTracker } from '../../core/tokens.js'; import { MockLogger } from '../../utils/mockLogger.js'; import { shellExecuteTool } from '../system/shellExecute.js'; import { readFileTool } from './readFile.js'; import { updateFileTool } from './updateFile.js'; -const logger = new MockLogger(); +const toolContext = { + logger: new MockLogger(), + headless: true, + workingDirectory: '.', + tokenTracker: new TokenTracker(), +}; describe('updateFile', () => { let testDir: string; @@ -23,7 +29,7 @@ describe('updateFile', () => { afterEach(async () => { await shellExecuteTool.execute( { command: `rm -rf "${testDir}"`, description: 'test' }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); }); @@ -41,7 +47,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); // Verify return value @@ -51,7 +57,7 @@ describe('updateFile', () => { // Verify content const readResult = await readFileTool.execute( { path: testPath, description: 'test' }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(readResult.content).toBe(testContent); }); @@ -72,7 +78,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); // Append content @@ -85,7 +91,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); // Verify return value @@ -95,7 +101,7 @@ describe('updateFile', () => { // Verify content const readResult = await readFileTool.execute( { path: testPath, description: 'test' }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(readResult.content).toBe(expectedContent); }); @@ -117,7 +123,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); // Update specific text @@ -131,7 +137,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); // Verify return value @@ -141,7 +147,7 @@ describe('updateFile', () => { // Verify content const readResult = await readFileTool.execute( { path: testPath, description: 'test' }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(readResult.content).toBe(expectedContent); }); @@ -162,7 +168,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); // Attempt update that should fail @@ -177,7 +183,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ), ).rejects.toThrow('Found 2 occurrences of oldStr, expected exactly 1'); }); @@ -196,7 +202,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); // Verify return value @@ -206,7 +212,7 @@ describe('updateFile', () => { // Verify content const readResult = await readFileTool.execute( { path: nestedPath, description: 'test' }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(readResult.content).toBe(testContent); }); diff --git a/packages/agent/src/tools/io/updateFile.ts b/packages/agent/src/tools/io/updateFile.ts index cba36d3..8fd647d 100644 --- a/packages/agent/src/tools/io/updateFile.ts +++ b/packages/agent/src/tools/io/updateFile.ts @@ -85,7 +85,7 @@ export const updateFileTool: Tool = { logParameters: (input, { logger }) => { const isFile = fs.existsSync(input.path); logger.info( - `${isFile ? 'Modifying' : 'Creating'} "${input.path}", ${input.description}`, + `${isFile ? '✏️ Modifying' : '✏️ Creating'} "${input.path}", ${input.description}`, ); }, logReturns: () => {}, diff --git a/packages/agent/src/tools/system/respawn.test.ts b/packages/agent/src/tools/system/respawn.test.ts index 2d396e6..7e0431f 100644 --- a/packages/agent/src/tools/system/respawn.test.ts +++ b/packages/agent/src/tools/system/respawn.test.ts @@ -1,12 +1,17 @@ import { describe, it, expect } from 'vitest'; -import { Logger } from '../../utils/logger'; +import { TokenTracker } from '../../core/tokens'; +import { MockLogger } from '../../utils/mockLogger'; import { respawnTool } from './respawn'; +const toolContext = { + logger: new MockLogger(), + headless: true, + workingDirectory: '.', + tokenTracker: new TokenTracker(), +}; describe('respawnTool', () => { - const mockLogger = new Logger({ name: 'test' }); - it('should have correct name and description', () => { expect(respawnTool.name).toBe('respawn'); expect(respawnTool.description).toContain('Resets the agent context'); @@ -15,12 +20,7 @@ describe('respawnTool', () => { it('should execute and return confirmation message', async () => { const result = await respawnTool.execute( { respawnContext: 'new context' }, - { - logger: mockLogger, - headless: true, - workingDirectory: '.', - tokenLevel: 'debug', - }, + toolContext, ); expect(result).toBe('Respawn initiated'); }); diff --git a/packages/agent/src/tools/system/shellExecute.test.ts b/packages/agent/src/tools/system/shellExecute.test.ts index f3eee7b..dddfc0a 100644 --- a/packages/agent/src/tools/system/shellExecute.test.ts +++ b/packages/agent/src/tools/system/shellExecute.test.ts @@ -1,16 +1,22 @@ import { describe, it, expect } from 'vitest'; +import { TokenTracker } from '../../core/tokens.js'; import { MockLogger } from '../../utils/mockLogger.js'; import { shellExecuteTool } from './shellExecute.js'; -const logger = new MockLogger(); +const toolContext = { + logger: new MockLogger(), + headless: true, + workingDirectory: '.', + tokenTracker: new TokenTracker(), +}; describe('shellExecute', () => { it('should execute shell commands', async () => { const { stdout } = await shellExecuteTool.execute( { command: "echo 'test'", description: 'test' }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(stdout).toContain('test'); }); @@ -18,7 +24,7 @@ describe('shellExecute', () => { it('should handle command errors', async () => { const { error } = await shellExecuteTool.execute( { command: 'nonexistentcommand', description: 'test' }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(error).toContain('Command failed:'); }); diff --git a/packages/agent/src/tools/system/shellExecute.ts b/packages/agent/src/tools/system/shellExecute.ts index ae93154..3ad5c68 100644 --- a/packages/agent/src/tools/system/shellExecute.ts +++ b/packages/agent/src/tools/system/shellExecute.ts @@ -107,7 +107,7 @@ export const shellExecuteTool: Tool = { } }, logParameters: (input, { logger }) => { - logger.info(`Running "${input.command}", ${input.description}`); + logger.info(`🖥️ Running "${input.command}", ${input.description}`); }, logReturns: () => {}, }; diff --git a/packages/agent/src/tools/system/shellMessage.test.ts b/packages/agent/src/tools/system/shellMessage.test.ts index dee3ab2..390918b 100644 --- a/packages/agent/src/tools/system/shellMessage.test.ts +++ b/packages/agent/src/tools/system/shellMessage.test.ts @@ -1,12 +1,18 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { TokenTracker } from '../../core/tokens.js'; import { MockLogger } from '../../utils/mockLogger.js'; import { sleep } from '../../utils/sleep.js'; import { shellMessageTool, NodeSignals } from './shellMessage.js'; import { processStates, shellStartTool } from './shellStart.js'; -const logger = new MockLogger(); +const toolContext = { + logger: new MockLogger(), + headless: true, + workingDirectory: '.', + tokenTracker: new TokenTracker(), +}; // Helper function to get instanceId from shellStart result const getInstanceId = ( @@ -40,7 +46,7 @@ describe('shellMessageTool', () => { description: 'Test interactive process', timeout: 50, // Force async mode for interactive process }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); testInstanceId = getInstanceId(startResult); @@ -52,7 +58,7 @@ describe('shellMessageTool', () => { stdin: 'hello world', description: 'Test interaction', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(result.stdout).toBe('hello world'); @@ -66,7 +72,7 @@ describe('shellMessageTool', () => { instanceId: 'nonexistent-id', description: 'Test invalid process', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(result.error).toBeDefined(); @@ -81,7 +87,7 @@ describe('shellMessageTool', () => { description: 'Test completion', timeout: 0, // Force async mode }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); const instanceId = getInstanceId(startResult); @@ -94,7 +100,7 @@ describe('shellMessageTool', () => { instanceId, description: 'Check completion', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(result.completed).toBe(true); @@ -110,7 +116,7 @@ describe('shellMessageTool', () => { description: 'Test SIGTERM handling', timeout: 0, // Force async mode }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); const instanceId = getInstanceId(startResult); @@ -121,7 +127,7 @@ describe('shellMessageTool', () => { signal: NodeSignals.SIGTERM, description: 'Send SIGTERM', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(result.signaled).toBe(true); @@ -132,7 +138,7 @@ describe('shellMessageTool', () => { instanceId, description: 'Check on status', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(result2.completed).toBe(true); @@ -147,7 +153,7 @@ describe('shellMessageTool', () => { description: 'Test signal handling on terminated process', timeout: 0, // Force async mode }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); const instanceId = getInstanceId(startResult); @@ -159,7 +165,7 @@ describe('shellMessageTool', () => { signal: NodeSignals.SIGTERM, description: 'Send signal to terminated process', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(result.signaled).toBe(true); @@ -174,7 +180,7 @@ describe('shellMessageTool', () => { description: 'Test signal flag verification', timeout: 0, // Force async mode }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); const instanceId = getInstanceId(startResult); @@ -186,7 +192,7 @@ describe('shellMessageTool', () => { signal: NodeSignals.SIGTERM, description: 'Send SIGTERM', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); await sleep(50); @@ -197,7 +203,7 @@ describe('shellMessageTool', () => { instanceId, description: 'Check signal state', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(checkResult.signaled).toBe(true); diff --git a/packages/agent/src/tools/system/shellMessage.ts b/packages/agent/src/tools/system/shellMessage.ts index bf75c7c..c1747a2 100644 --- a/packages/agent/src/tools/system/shellMessage.ts +++ b/packages/agent/src/tools/system/shellMessage.ts @@ -167,7 +167,7 @@ export const shellMessageTool: Tool = { logParameters: (input, { logger }) => { const processState = processStates.get(input.instanceId); logger.info( - `Interacting with shell command "${processState ? processState.command : ''}", ${input.description}`, + `🖥️ Interacting with shell command "${processState ? processState.command : ''}", ${input.description}`, ); }, logReturns: () => {}, diff --git a/packages/agent/src/tools/system/shellStart.test.ts b/packages/agent/src/tools/system/shellStart.test.ts index ce0b5f0..72d0f33 100644 --- a/packages/agent/src/tools/system/shellStart.test.ts +++ b/packages/agent/src/tools/system/shellStart.test.ts @@ -1,12 +1,17 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { TokenTracker } from '../../core/tokens.js'; import { MockLogger } from '../../utils/mockLogger.js'; import { sleep } from '../../utils/sleep.js'; import { processStates, shellStartTool } from './shellStart.js'; -const logger = new MockLogger(); - +const toolContext = { + logger: new MockLogger(), + headless: true, + workingDirectory: '.', + tokenTracker: new TokenTracker(), +}; describe('shellStartTool', () => { beforeEach(() => { processStates.clear(); @@ -26,7 +31,7 @@ describe('shellStartTool', () => { description: 'Test process', timeout: 500, // Generous timeout to ensure sync mode }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(result.mode).toBe('sync'); @@ -44,7 +49,7 @@ describe('shellStartTool', () => { description: 'Slow command test', timeout: 50, // Short timeout to force async mode }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(result.mode).toBe('async'); @@ -60,7 +65,7 @@ describe('shellStartTool', () => { command: 'nonexistentcommand', description: 'Invalid command test', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(result.mode).toBe('sync'); @@ -78,7 +83,7 @@ describe('shellStartTool', () => { description: 'Sync completion test', timeout: 500, }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); // Even sync results should be in processStates @@ -96,7 +101,7 @@ describe('shellStartTool', () => { description: 'Async completion test', timeout: 50, }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); if (asyncResult.mode === 'async') { @@ -111,7 +116,7 @@ describe('shellStartTool', () => { description: 'Pipe test', timeout: 50, // Force async for interactive command }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(result.mode).toBe('async'); @@ -144,7 +149,7 @@ describe('shellStartTool', () => { command: 'sleep 1', description: 'Default timeout test', }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + toolContext, ); expect(result.mode).toBe('sync'); diff --git a/packages/agent/src/tools/system/shellStart.ts b/packages/agent/src/tools/system/shellStart.ts index d13210d..ec4160a 100644 --- a/packages/agent/src/tools/system/shellStart.ts +++ b/packages/agent/src/tools/system/shellStart.ts @@ -188,14 +188,14 @@ export const shellStartTool: Tool = { { logger }, ) => { logger.info( - `Starting "${command}", ${description} (timeout: ${timeout}ms)`, + `🖥️ Starting "${command}", ${description} (timeout: ${timeout}ms)`, ); }, logReturns: (output, { logger }) => { if (output.mode === 'async') { - logger.info(`Process started with instance ID: ${output.instanceId}`); + logger.info(`🖥️ Process started with instance ID: ${output.instanceId}`); } else { - logger.info(`Process completed with exit code: ${output.exitCode}`); + logger.info(`🖥️ Process completed with exit code: ${output.exitCode}`); } }, }; diff --git a/packages/agent/src/tools/system/sleep.test.ts b/packages/agent/src/tools/system/sleep.test.ts index f151f9c..514e774 100644 --- a/packages/agent/src/tools/system/sleep.test.ts +++ b/packages/agent/src/tools/system/sleep.test.ts @@ -1,10 +1,16 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { TokenTracker } from '../../core/tokens'; import { MockLogger } from '../../utils/mockLogger'; import { sleepTool } from './sleep'; -const logger = new MockLogger(); +const toolContext = { + logger: new MockLogger(), + headless: true, + workingDirectory: '.', + tokenTracker: new TokenTracker(), +}; describe('sleep tool', () => { beforeEach(() => { @@ -12,10 +18,7 @@ describe('sleep tool', () => { }); it('should sleep for the specified duration', async () => { - const sleepPromise = sleepTool.execute( - { seconds: 2 }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, - ); + const sleepPromise = sleepTool.execute({ seconds: 2 }, toolContext); await vi.advanceTimersByTimeAsync(2000); const result = await sleepPromise; @@ -25,19 +28,13 @@ describe('sleep tool', () => { it('should reject negative sleep duration', async () => { await expect( - sleepTool.execute( - { seconds: -1 }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, - ), + sleepTool.execute({ seconds: -1 }, toolContext), ).rejects.toThrow(); }); it('should reject sleep duration over 1 hour', async () => { await expect( - sleepTool.execute( - { seconds: 3601 }, - { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, - ), + sleepTool.execute({ seconds: 3601 }, toolContext), ).rejects.toThrow(); }); }); diff --git a/packages/agent/src/tools/system/sleep.ts b/packages/agent/src/tools/system/sleep.ts index ed89288..cff16f0 100644 --- a/packages/agent/src/tools/system/sleep.ts +++ b/packages/agent/src/tools/system/sleep.ts @@ -34,7 +34,7 @@ export const sleepTool: Tool = { }); }, logParameters({ seconds }) { - return `sleeping for ${seconds} seconds`; + return `💤 sleeping for ${seconds} seconds`; }, logReturns() { return ''; diff --git a/packages/agent/src/utils/logger.ts b/packages/agent/src/utils/logger.ts index 47fa443..8b9ca43 100644 --- a/packages/agent/src/utils/logger.ts +++ b/packages/agent/src/utils/logger.ts @@ -106,6 +106,11 @@ export class Logger { .join('\n'); } + log(level: LogLevel, ...messages: unknown[]): void { + if (this.logLevelIndex > level) return; + console.log(this.formatMessages(level, messages)); + } + debug(...messages: unknown[]): void { if (this.logLevelIndex > LogLevel.debug) return; console.log(this.formatMessages(LogLevel.debug, messages)); diff --git a/packages/cli/src/commands/$default.ts b/packages/cli/src/commands/$default.ts index 65e9656..149be14 100644 --- a/packages/cli/src/commands/$default.ts +++ b/packages/cli/src/commands/$default.ts @@ -6,9 +6,10 @@ import { Logger, getTools, getAnthropicApiKeyError, - TokenLevel, userPrompt, + LogLevel, } from 'mycoder-agent'; +import { TokenTracker } from 'mycoder-agent/dist/core/tokens.js'; import { SharedOptions } from '../options.js'; import { hasUserConsented, saveUserConsent } from '../settings/settings.js'; @@ -33,9 +34,6 @@ export const command: CommandModule = { const logger = new Logger({ name: 'Default' }); const packageInfo = getPackageInfo(); - // Use 'info' log level for token logging when tokenUsage is enabled, otherwise use 'debug' - const tokenLevel: TokenLevel = argv.tokenUsage ? 'info' : 'debug'; - logger.info( `MyCoder v${packageInfo.version} - AI-powered coding assistant`, ); @@ -59,28 +57,28 @@ export const command: CommandModule = { saveUserConsent(); } else { logger.info('User did not consent. Exiting.'); - process.exit(0); + throw new Error('User did not consent'); } } + + const tokenTracker = new TokenTracker( + 'Root', + undefined, + argv.tokenUsage ? LogLevel.info : LogLevel.debug, + ); + try { // Early API key check if (!process.env.ANTHROPIC_API_KEY) { logger.error(getAnthropicApiKeyError()); - process.exit(1); + throw new Error('Anthropic API key not found'); } let prompt: string | undefined; // If promptFile is specified, read from file if (argv.file) { - try { - prompt = await fs.readFile(argv.file, 'utf-8'); - } catch (error: any) { - logger.error( - `Failed to read prompt file: ${argv.file}, ${error?.message}`, - ); - process.exit(1); - } + prompt = await fs.readFile(argv.file, 'utf-8'); } // If interactive mode @@ -97,7 +95,7 @@ export const command: CommandModule = { logger.error( 'No prompt provided. Either specify a prompt, use --promptFile, or run in --interactive mode.', ); - process.exit(1); + throw new Error('No prompt provided'); } // Add the standard suffix to all prompts @@ -111,11 +109,11 @@ export const command: CommandModule = { const result = await toolAgent(prompt, tools, undefined, { logger, - headless: true, + headless: argv.headless ?? true, workingDirectory: '.', - tokenLevel, - tokenUsage: argv.tokenUsage, + tokenTracker, }); + const output = typeof result.result === 'string' ? result.result @@ -123,7 +121,8 @@ export const command: CommandModule = { logger.info('\n=== Result ===\n', output); } catch (error) { logger.error('An error occurred:', error); - process.exit(1); } + + logger.info(`Token Usage: ${tokenTracker.toString()}`); }, }; diff --git a/packages/cli/src/options.ts b/packages/cli/src/options.ts index bbe9a3c..1927a68 100644 --- a/packages/cli/src/options.ts +++ b/packages/cli/src/options.ts @@ -5,6 +5,7 @@ export type SharedOptions = { readonly interactive: boolean; readonly file?: string; readonly tokenUsage?: boolean; + readonly headless?: boolean; }; export const sharedOptions = { @@ -31,4 +32,9 @@ export const sharedOptions = { description: 'Output token usage at info log level', default: false, } as const, + headless: { + type: 'boolean', + description: 'Use browser in headless mode with no UI showing', + default: true, + } as const, }; From b6c997a6376e9a78d24b9d7c788f3433d9384822 Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Thu, 27 Feb 2025 11:05:37 -0500 Subject: [PATCH 4/6] more emoji prefixes into the logger. --- packages/agent/src/core/executeToolCall.ts | 11 ++++++----- packages/agent/src/core/toolAgent.ts | 2 +- packages/agent/src/core/types.ts | 1 + packages/agent/src/tools/browser/browseMessage.ts | 3 ++- packages/agent/src/tools/browser/browseStart.ts | 3 ++- packages/agent/src/tools/interaction/subAgent.ts | 3 ++- packages/agent/src/tools/interaction/userPrompt.ts | 1 + packages/agent/src/tools/io/fetch.ts | 5 +++-- packages/agent/src/tools/io/readFile.ts | 3 ++- packages/agent/src/tools/io/updateFile.ts | 3 ++- packages/agent/src/tools/system/respawn.ts | 1 + packages/agent/src/tools/system/sequenceComplete.ts | 1 + packages/agent/src/tools/system/shellExecute.ts | 3 ++- packages/agent/src/tools/system/shellMessage.ts | 3 ++- packages/agent/src/tools/system/shellStart.ts | 5 +++-- packages/agent/src/tools/system/sleep.ts | 3 ++- packages/agent/src/utils/logger.ts | 8 +++++++- 17 files changed, 40 insertions(+), 19 deletions(-) diff --git a/packages/agent/src/core/executeToolCall.ts b/packages/agent/src/core/executeToolCall.ts index 7a96223..3c7dab4 100644 --- a/packages/agent/src/core/executeToolCall.ts +++ b/packages/agent/src/core/executeToolCall.ts @@ -12,16 +12,17 @@ export const executeToolCall = async ( tools: Tool[], context: ToolContext, ): Promise => { - const logger = new Logger({ - name: `Tool:${toolCall.name}`, - parent: context.logger, - }); - const tool = tools.find((t) => t.name === toolCall.name); if (!tool) { throw new Error(`No tool with the name '${toolCall.name}' exists.`); } + const logger = new Logger({ + name: `Tool:${toolCall.name}`, + parent: context.logger, + customPrefix: tool.logPrefix, + }); + const toolContext = { ...context, logger, diff --git a/packages/agent/src/core/toolAgent.ts b/packages/agent/src/core/toolAgent.ts index 9b9f86e..1b06ae9 100644 --- a/packages/agent/src/core/toolAgent.ts +++ b/packages/agent/src/core/toolAgent.ts @@ -23,7 +23,7 @@ export interface ToolAgentResult { } const CONFIG = { - maxIterations: 50, + maxIterations: 200, model: 'claude-3-7-sonnet-latest', maxTokens: 4096, temperature: 0.7, diff --git a/packages/agent/src/core/types.ts b/packages/agent/src/core/types.ts index 8f859ad..efa2c27 100644 --- a/packages/agent/src/core/types.ts +++ b/packages/agent/src/core/types.ts @@ -18,6 +18,7 @@ export type Tool, TReturn = any> = { description: string; parameters: JsonSchema7Type; returns: JsonSchema7Type; + logPrefix?: string; logParameters?: (params: TParams, context: ToolContext) => void; logReturns?: (returns: TReturn, context: ToolContext) => void; diff --git a/packages/agent/src/tools/browser/browseMessage.ts b/packages/agent/src/tools/browser/browseMessage.ts index 8875a46..bd89fcc 100644 --- a/packages/agent/src/tools/browser/browseMessage.ts +++ b/packages/agent/src/tools/browser/browseMessage.ts @@ -66,6 +66,7 @@ const getSelector = (selector: string, type?: SelectorType): string => { export const browseMessageTool: Tool = { name: 'browseMessage', + logPrefix: '🏄', description: 'Performs actions in an active browser session', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), @@ -165,7 +166,7 @@ export const browseMessageTool: Tool = { logParameters: ({ action, description }, { logger }) => { logger.info( - `🏄 Performing browser action: ${action.actionType}, ${description}`, + `Performing browser action: ${action.actionType}, ${description}`, ); }, diff --git a/packages/agent/src/tools/browser/browseStart.ts b/packages/agent/src/tools/browser/browseStart.ts index 53f69fd..d1d2ff3 100644 --- a/packages/agent/src/tools/browser/browseStart.ts +++ b/packages/agent/src/tools/browser/browseStart.ts @@ -33,6 +33,7 @@ type ReturnType = z.infer; export const browseStartTool: Tool = { name: 'browseStart', + logPrefix: '🏄', description: 'Starts a new browser session with optional initial URL', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), @@ -102,7 +103,7 @@ export const browseStartTool: Tool = { logParameters: ({ url, description }, { logger }) => { logger.info( - `🏄 Starting browser session${url ? ` at ${url}` : ''}, ${description}`, + `Starting browser session${url ? ` at ${url}` : ''}, ${description}`, ); }, diff --git a/packages/agent/src/tools/interaction/subAgent.ts b/packages/agent/src/tools/interaction/subAgent.ts index 4e126f5..18f8715 100644 --- a/packages/agent/src/tools/interaction/subAgent.ts +++ b/packages/agent/src/tools/interaction/subAgent.ts @@ -65,6 +65,7 @@ export const subAgentTool: Tool = { name: 'subAgent', description: 'Creates a sub-agent that has access to all tools to solve a specific task', + logPrefix: '🤖', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), execute: async (params, context) => { @@ -108,7 +109,7 @@ export const subAgentTool: Tool = { return result.result; // Return the result string directly }, logParameters: (input, { logger }) => { - logger.info(`🤖 Delegating task "${input.description}"`); + logger.info(`Delegating task "${input.description}"`); }, logReturns: () => {}, }; diff --git a/packages/agent/src/tools/interaction/userPrompt.ts b/packages/agent/src/tools/interaction/userPrompt.ts index 25b99a1..ebeaa14 100644 --- a/packages/agent/src/tools/interaction/userPrompt.ts +++ b/packages/agent/src/tools/interaction/userPrompt.ts @@ -16,6 +16,7 @@ type ReturnType = z.infer; export const userPromptTool: Tool = { name: 'userPrompt', description: 'Prompts the user for input and returns their response', + logPrefix: '🗣️', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), execute: async ({ prompt }, { logger }) => { diff --git a/packages/agent/src/tools/io/fetch.ts b/packages/agent/src/tools/io/fetch.ts index adb1119..b98b9dc 100644 --- a/packages/agent/src/tools/io/fetch.ts +++ b/packages/agent/src/tools/io/fetch.ts @@ -37,6 +37,7 @@ export const fetchTool: Tool = { name: 'fetch', description: 'Executes HTTP requests using native Node.js fetch API, for using APIs, not for browsing the web.', + logPrefix: '🌐', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), execute: async ( @@ -93,12 +94,12 @@ export const fetchTool: Tool = { logParameters(params, { logger }) { const { method, url, params: queryParams } = params; logger.info( - `🌐 ${method} ${url}${queryParams ? `?${new URLSearchParams(queryParams).toString()}` : ''} ==>`, + `${method} ${url}${queryParams ? `?${new URLSearchParams(queryParams).toString()}` : ''}`, ); }, logReturns: (result, { logger }) => { const { status, statusText } = result; - logger.info(`🌐 ==> ${status} ${statusText}`); + logger.info(`${status} ${statusText}`); }, }; diff --git a/packages/agent/src/tools/io/readFile.ts b/packages/agent/src/tools/io/readFile.ts index e1a56e1..2edb166 100644 --- a/packages/agent/src/tools/io/readFile.ts +++ b/packages/agent/src/tools/io/readFile.ts @@ -47,6 +47,7 @@ type ReturnType = z.infer; export const readFileTool: Tool = { name: 'readFile', description: 'Reads file content within size limits and optional range', + logPrefix: '📖', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), execute: async ( @@ -96,7 +97,7 @@ export const readFileTool: Tool = { }; }, logParameters: (input, { logger }) => { - logger.info(` 📖 Reading "${input.path}", ${input.description}`); + logger.info(`Reading "${input.path}", ${input.description}`); }, logReturns: () => {}, }; diff --git a/packages/agent/src/tools/io/updateFile.ts b/packages/agent/src/tools/io/updateFile.ts index 8fd647d..3355344 100644 --- a/packages/agent/src/tools/io/updateFile.ts +++ b/packages/agent/src/tools/io/updateFile.ts @@ -44,6 +44,7 @@ export const updateFileTool: Tool = { name: 'updateFile', description: 'Creates a file or updates a file by rewriting, patching, or appending content', + logPrefix: '📝', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), execute: async ( @@ -85,7 +86,7 @@ export const updateFileTool: Tool = { logParameters: (input, { logger }) => { const isFile = fs.existsSync(input.path); logger.info( - `${isFile ? '✏️ Modifying' : '✏️ Creating'} "${input.path}", ${input.description}`, + `${isFile ? 'Modifying' : '✏️ Creating'} "${input.path}", ${input.description}`, ); }, logReturns: () => {}, diff --git a/packages/agent/src/tools/system/respawn.ts b/packages/agent/src/tools/system/respawn.ts index e0158bd..6b0030e 100644 --- a/packages/agent/src/tools/system/respawn.ts +++ b/packages/agent/src/tools/system/respawn.ts @@ -8,6 +8,7 @@ export const respawnTool: Tool = { name: 'respawn', description: 'Resets the agent context to just the system prompt and provided context', + logPrefix: '🔄', parameters: { type: 'object', properties: { diff --git a/packages/agent/src/tools/system/sequenceComplete.ts b/packages/agent/src/tools/system/sequenceComplete.ts index 5c9073a..037ef5b 100644 --- a/packages/agent/src/tools/system/sequenceComplete.ts +++ b/packages/agent/src/tools/system/sequenceComplete.ts @@ -17,6 +17,7 @@ type ReturnType = z.infer; export const sequenceCompleteTool: Tool = { name: 'sequenceComplete', description: 'Completes the tool use sequence and returns the final result', + logPrefix: '✅', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), execute: ({ result }) => Promise.resolve(result), diff --git a/packages/agent/src/tools/system/shellExecute.ts b/packages/agent/src/tools/system/shellExecute.ts index 3ad5c68..d042734 100644 --- a/packages/agent/src/tools/system/shellExecute.ts +++ b/packages/agent/src/tools/system/shellExecute.ts @@ -45,6 +45,7 @@ interface ExtendedExecException extends ExecException { export const shellExecuteTool: Tool = { name: 'shellExecute', + logPrefix: '💻', description: 'Executes a bash shell command and returns its output, can do amazing things if you are a shell scripting wizard', parameters: zodToJsonSchema(parameterSchema), @@ -107,7 +108,7 @@ export const shellExecuteTool: Tool = { } }, logParameters: (input, { logger }) => { - logger.info(`🖥️ Running "${input.command}", ${input.description}`); + logger.info(`Running "${input.command}", ${input.description}`); }, logReturns: () => {}, }; diff --git a/packages/agent/src/tools/system/shellMessage.ts b/packages/agent/src/tools/system/shellMessage.ts index c1747a2..c774500 100644 --- a/packages/agent/src/tools/system/shellMessage.ts +++ b/packages/agent/src/tools/system/shellMessage.ts @@ -76,6 +76,7 @@ export const shellMessageTool: Tool = { name: 'shellMessage', description: 'Interacts with a running shell process, sending input and receiving output', + logPrefix: '💻', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), @@ -167,7 +168,7 @@ export const shellMessageTool: Tool = { logParameters: (input, { logger }) => { const processState = processStates.get(input.instanceId); logger.info( - `🖥️ Interacting with shell command "${processState ? processState.command : ''}", ${input.description}`, + `Interacting with shell command "${processState ? processState.command : ''}", ${input.description}`, ); }, logReturns: () => {}, diff --git a/packages/agent/src/tools/system/shellStart.ts b/packages/agent/src/tools/system/shellStart.ts index ec4160a..e78304b 100644 --- a/packages/agent/src/tools/system/shellStart.ts +++ b/packages/agent/src/tools/system/shellStart.ts @@ -71,6 +71,7 @@ export const shellStartTool: Tool = { name: 'shellStart', description: 'Starts a shell command with fast sync mode (default 100ms timeout) that falls back to async mode for longer-running commands', + logPrefix: '💻', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), @@ -193,9 +194,9 @@ export const shellStartTool: Tool = { }, logReturns: (output, { logger }) => { if (output.mode === 'async') { - logger.info(`🖥️ Process started with instance ID: ${output.instanceId}`); + logger.info(`Process started with instance ID: ${output.instanceId}`); } else { - logger.info(`🖥️ Process completed with exit code: ${output.exitCode}`); + logger.info(`Process completed with exit code: ${output.exitCode}`); } }, }; diff --git a/packages/agent/src/tools/system/sleep.ts b/packages/agent/src/tools/system/sleep.ts index cff16f0..5ff84f2 100644 --- a/packages/agent/src/tools/system/sleep.ts +++ b/packages/agent/src/tools/system/sleep.ts @@ -22,6 +22,7 @@ export const sleepTool: Tool = { name: 'sleep', description: 'Pauses execution for the specified number of seconds, useful when waiting for async tools to make progress before checking on them', + logPrefix: '💤', parameters: zodToJsonSchema(parametersSchema), returns: zodToJsonSchema(returnsSchema), async execute(params) { @@ -34,7 +35,7 @@ export const sleepTool: Tool = { }); }, logParameters({ seconds }) { - return `💤 sleeping for ${seconds} seconds`; + return `sleeping for ${seconds} seconds`; }, logReturns() { return ''; diff --git a/packages/agent/src/utils/logger.ts b/packages/agent/src/utils/logger.ts index 8b9ca43..d3c8c6a 100644 --- a/packages/agent/src/utils/logger.ts +++ b/packages/agent/src/utils/logger.ts @@ -43,6 +43,7 @@ export type LoggerProps = { name: string; logLevel?: LogLevel; parent?: Logger; + customPrefix?: string; }; export class Logger { @@ -52,12 +53,15 @@ export class Logger { private readonly parent?: Logger; private readonly name: string; private readonly nesting: number; + private readonly customPrefix?: string; constructor({ name, parent = undefined, logLevel = parent?.logLevel ?? LogLevel.info, + customPrefix, }: LoggerProps) { + this.customPrefix = customPrefix; this.name = name; this.parent = parent; this.logLevel = logLevel; @@ -96,7 +100,9 @@ export class Logger { ); let combinedPrefix = `${this.prefix}${prefix}`; - if (combinedPrefix.length > 0) { + if (this.customPrefix) { + combinedPrefix = `${this.prefix}${this.customPrefix} `; + } else if (combinedPrefix.length > 0) { combinedPrefix += ' '; } From e1b1836e2b19575e08b6c50b276b03be80f8bc40 Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Thu, 27 Feb 2025 11:57:04 -0500 Subject: [PATCH 5/6] improved token-caching --- packages/agent/src/core/tokens.ts | 2 +- packages/agent/src/core/toolAgent.ts | 17 ++++++++++++++--- packages/agent/src/tools/system/shellStart.ts | 2 +- packages/cli/src/commands/$default.ts | 15 ++++++++++++++- packages/cli/src/index.ts | 6 ------ 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/agent/src/core/tokens.ts b/packages/agent/src/core/tokens.ts index 2d7ba0f..e1d99da 100644 --- a/packages/agent/src/core/tokens.ts +++ b/packages/agent/src/core/tokens.ts @@ -95,6 +95,6 @@ export class TokenTracker { } toString() { - return `${this.name}: ${this.tokenUsage.toString()}`; + return `${this.name}: ${this.getTotalUsage().toString()}`; } } diff --git a/packages/agent/src/core/toolAgent.ts b/packages/agent/src/core/toolAgent.ts index 1b06ae9..4a7c9ae 100644 --- a/packages/agent/src/core/toolAgent.ts +++ b/packages/agent/src/core/toolAgent.ts @@ -199,7 +199,15 @@ function addCacheControlToContentBlocks( ): ContentBlockParam[] { return content.map((c, i) => { if (i === content.length - 1) { - if (c.type === 'text' || c.type === 'document' || c.type === 'image') { + if ( + c.type === 'text' || + c.type === 'document' || + c.type === 'image' || + c.type === 'tool_use' || + c.type === 'tool_result' || + c.type === 'thinking' || + c.type === 'redacted_thinking' + ) { return { ...c, cache_control: { type: 'ephemeral' } }; } } @@ -209,7 +217,7 @@ function addCacheControlToContentBlocks( function addCacheControlToMessages( messages: Anthropic.Messages.MessageParam[], ): Anthropic.Messages.MessageParam[] { - return messages.map((m) => { + return messages.map((m, i) => { if (typeof m.content === 'string') { return { ...m, @@ -224,7 +232,10 @@ function addCacheControlToMessages( } return { ...m, - content: addCacheControlToContentBlocks(m.content), + content: + i >= messages.length - 2 + ? addCacheControlToContentBlocks(m.content) + : m.content, }; }); } diff --git a/packages/agent/src/tools/system/shellStart.ts b/packages/agent/src/tools/system/shellStart.ts index e78304b..810e7f4 100644 --- a/packages/agent/src/tools/system/shellStart.ts +++ b/packages/agent/src/tools/system/shellStart.ts @@ -189,7 +189,7 @@ export const shellStartTool: Tool = { { logger }, ) => { logger.info( - `🖥️ Starting "${command}", ${description} (timeout: ${timeout}ms)`, + `Starting "${command}", ${description} (timeout: ${timeout}ms)`, ); }, logReturns: (output, { logger }) => { diff --git a/packages/cli/src/commands/$default.ts b/packages/cli/src/commands/$default.ts index 149be14..6341e99 100644 --- a/packages/cli/src/commands/$default.ts +++ b/packages/cli/src/commands/$default.ts @@ -1,6 +1,7 @@ import * as fs from 'fs/promises'; import { createInterface } from 'readline/promises'; +import chalk from 'chalk'; import { toolAgent, Logger, @@ -107,6 +108,15 @@ export const command: CommandModule = { const tools = getTools(); + // Error handling + process.on('SIGINT', () => { + logger.log( + tokenTracker.logLevel, + chalk.blueBright(`[Token Usage Total] ${tokenTracker.toString()}`), + ); + process.exit(0); + }); + const result = await toolAgent(prompt, tools, undefined, { logger, headless: argv.headless ?? true, @@ -123,6 +133,9 @@ export const command: CommandModule = { logger.error('An error occurred:', error); } - logger.info(`Token Usage: ${tokenTracker.toString()}`); + logger.log( + tokenTracker.logLevel, + chalk.blueBright(`[Token Usage Total] ${tokenTracker.toString()}`), + ); }, }; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 5757401..a3919ee 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -35,12 +35,6 @@ const main = async () => { console.log(); } - // Error handling - process.on('SIGINT', () => { - logger.warn('\nGracefully shutting down...'); - process.exit(0); - }); - process.on('uncaughtException', (error) => { logger.error( 'Fatal error:', From 352ba5b2c31a7723df24b4c221f36c16ea047d33 Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Thu, 27 Feb 2025 12:00:22 -0500 Subject: [PATCH 6/6] changeset --- packages/agent/CHANGELOG.md | 6 ++++++ packages/agent/package.json | 2 +- packages/cli/CHANGELOG.md | 11 +++++++++++ packages/cli/package.json | 2 +- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 52c71c2..3c95c49 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,11 @@ # mycoder-agent +## 0.2.0 + +### Minor Changes + +- Add token caching, better user input handling, token usage logging (--tokenUsage), the ability to see the browser (--headless=false), and log prefixes with emojis. + ## 0.1.3 ### Patch Changes diff --git a/packages/agent/package.json b/packages/agent/package.json index e454de2..c710a30 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "mycoder-agent", - "version": "0.1.4", + "version": "0.2.0", "description": "Agent module for mycoder - an AI-powered software development assistant", "type": "module", "main": "dist/index.js", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 8a8b851..809ff7f 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,16 @@ # mycoder +## 0.2.0 + +### Minor Changes + +- Add token caching, better user input handling, token usage logging (--tokenUsage), the ability to see the browser (--headless=false), and log prefixes with emojis. + +### Patch Changes + +- Updated dependencies + - mycoder-agent@0.2.0 + ## 0.2.2 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 22878c9..f09f9ea 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": "0.1.4", + "version": "0.2.0", "type": "module", "bin": "./bin/cli.js", "main": "./dist/index.js",