diff --git a/package.json b/package.json index ac410e2..3708189 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "mycoder-monorepo", "version": "0.0.1", "type": "module", + "private": true, "packageManager": "pnpm@10.2.1", "engines": { "node": ">=18.0.0" diff --git a/packages/agent/README.md b/packages/agent/README.md index 25d1eed..31fd71f 100644 --- a/packages/agent/README.md +++ b/packages/agent/README.md @@ -36,6 +36,7 @@ Get an API key from https://www.anthropic.com/api - Categories: Interaction, I/O, System, Data Management - Parallel execution capability - Type-safe definitions +- Input token caching to reduce API costs ### Agent System diff --git a/packages/agent/package.json b/packages/agent/package.json index 0ce6f87..e454de2 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "mycoder-agent", - "version": "0.1.3", + "version": "0.1.4", "description": "Agent module for mycoder - an AI-powered software development assistant", "type": "module", "main": "dist/index.js", diff --git a/packages/agent/src/core/executeToolCall.ts b/packages/agent/src/core/executeToolCall.ts index 10aeb30..7a96223 100644 --- a/packages/agent/src/core/executeToolCall.ts +++ b/packages/agent/src/core/executeToolCall.ts @@ -1,6 +1,6 @@ import { Logger } from '../utils/logger.js'; -import { Tool, ToolCall } from './types.js'; +import { Tool, ToolCall, ToolContext } from './types.js'; const OUTPUT_LIMIT = 12 * 1024; // 10KB limit @@ -10,12 +10,11 @@ const OUTPUT_LIMIT = 12 * 1024; // 10KB limit export const executeToolCall = async ( toolCall: ToolCall, tools: Tool[], - parentLogger: Logger, - options?: { workingDirectory?: string }, + context: ToolContext, ): Promise => { const logger = new Logger({ name: `Tool:${toolCall.name}`, - parent: parentLogger, + parent: context.logger, }); const tool = tools.find((t) => t.name === toolCall.name); @@ -23,7 +22,10 @@ export const executeToolCall = async ( throw new Error(`No tool with the name '${toolCall.name}' exists.`); } - const toolContext = { logger }; + const toolContext = { + ...context, + logger, + }; // for each parameter log it and its name if (tool.logParameters) { @@ -36,10 +38,7 @@ export const executeToolCall = async ( } // TODO: validate JSON schema for input - const output = await tool.execute(toolCall.input, { - logger, - workingDirectory: options?.workingDirectory, - }); + const output = await tool.execute(toolCall.input, toolContext); // for each result log it and its name if (tool.logReturns) { diff --git a/packages/agent/src/core/tokens.ts b/packages/agent/src/core/tokens.ts new file mode 100644 index 0000000..ae2874e --- /dev/null +++ b/packages/agent/src/core/tokens.ts @@ -0,0 +1,53 @@ +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, + }; +}; + +const PER_MILLION = 1 / 1000000; +const TOKEN_COST: TokenUsage = { + input: 3 * PER_MILLION, + inputCacheWrites: 3.75 * PER_MILLION, + inputCacheReads: 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 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)}`; +}; diff --git a/packages/agent/src/core/toolAgent.cache.test.ts b/packages/agent/src/core/toolAgent.cache.test.ts new file mode 100644 index 0000000..735012e --- /dev/null +++ b/packages/agent/src/core/toolAgent.cache.test.ts @@ -0,0 +1,102 @@ +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 d032d17..e5e69d8 100644 --- a/packages/agent/src/core/toolAgent.respawn.test.ts +++ b/packages/agent/src/core/toolAgent.respawn.test.ts @@ -2,7 +2,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { toolAgent } from '../../src/core/toolAgent.js'; import { getTools } from '../../src/tools/getTools.js'; -import { Logger } from '../../src/utils/logger.js'; +import { MockLogger } from '../utils/mockLogger.js'; + +const logger = new MockLogger(); // Mock Anthropic SDK vi.mock('@anthropic-ai/sdk', () => { @@ -32,7 +34,6 @@ vi.mock('@anthropic-ai/sdk', () => { }); describe('toolAgent respawn functionality', () => { - const mockLogger = new Logger({ name: 'test' }); const tools = getTools(); beforeEach(() => { @@ -41,13 +42,23 @@ describe('toolAgent respawn functionality', () => { }); it('should handle respawn tool calls', async () => { - const result = await toolAgent('initial prompt', tools, mockLogger, { - maxIterations: 2, // Need at least 2 iterations for respawn + empty response - model: 'test-model', - maxTokens: 100, - temperature: 0, - getSystemPrompt: () => 'test system prompt', - }); + const result = await toolAgent( + 'initial prompt', + tools, + { + maxIterations: 2, // Need at least 2 iterations for respawn + empty response + model: 'test-model', + maxTokens: 100, + temperature: 0, + getSystemPrompt: () => 'test system prompt', + }, + { + logger, + headless: true, + workingDirectory: '.', + tokenLevel: 'debug', + }, + ); expect(result.result).toBe( 'Maximum sub-agent iterations reach without successful completion', diff --git a/packages/agent/src/core/toolAgent.test.ts b/packages/agent/src/core/toolAgent.test.ts index b94db05..ee21e85 100644 --- a/packages/agent/src/core/toolAgent.test.ts +++ b/packages/agent/src/core/toolAgent.test.ts @@ -101,7 +101,12 @@ describe('toolAgent', () => { input: { input: 'test' }, }, [mockTool], - logger, + { + logger, + headless: true, + workingDirectory: '.', + tokenLevel: 'debug', + }, ); expect(result.includes('Processed: test')).toBeTruthy(); @@ -116,7 +121,12 @@ describe('toolAgent', () => { input: {}, }, [mockTool], - logger, + { + logger, + headless: true, + workingDirectory: '.', + tokenLevel: 'debug', + }, ), ).rejects.toThrow("No tool with the name 'nonexistentTool' exists."); }); @@ -147,7 +157,12 @@ describe('toolAgent', () => { input: {}, }, [errorTool], - logger, + { + logger, + headless: true, + workingDirectory: '.', + tokenLevel: 'debug', + }, ), ).rejects.toThrow('Deliberate failure'); }); @@ -166,8 +181,13 @@ describe('toolAgent', () => { const result = await toolAgent( 'Test prompt', [sequenceCompleteTool], - logger, testConfig, + { + logger, + headless: true, + workingDirectory: '.', + tokenLevel: 'debug', + }, ); // Verify that create was called twice (once for empty response, once for completion) @@ -184,8 +204,13 @@ describe('toolAgent', () => { const result = await toolAgent( 'Test prompt', [sequenceCompleteTool], - logger, testConfig, + { + logger, + headless: true, + workingDirectory: '.', + tokenLevel: 'debug', + }, ); expect(result.result).toBe('Test complete'); diff --git a/packages/agent/src/core/toolAgent.ts b/packages/agent/src/core/toolAgent.ts index 6c7260f..d2af2c4 100644 --- a/packages/agent/src/core/toolAgent.ts +++ b/packages/agent/src/core/toolAgent.ts @@ -1,25 +1,30 @@ import { execSync } from 'child_process'; import Anthropic from '@anthropic-ai/sdk'; +import { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages/messages.js'; +import chalk from 'chalk'; import { getAnthropicApiKeyError } from '../utils/errors.js'; -import { Logger } from '../utils/logger.js'; import { executeToolCall } from './executeToolCall.js'; +import { + addTokenUsage, + getTokenString, + getTokenUsage, + TokenUsage, +} from './tokens.js'; import { Tool, TextContent, ToolUseContent, ToolResultContent, Message, + ToolContext, } from './types.js'; export interface ToolAgentResult { result: string; - tokens: { - input: number; - output: number; - }; + tokens: TokenUsage; interactions: number; } @@ -116,13 +121,14 @@ async function executeTools( toolCalls: ToolUseContent[], tools: Tool[], messages: Message[], - logger: Logger, - workingDirectory?: string, + context: ToolContext, ): Promise { if (toolCalls.length === 0) { return { sequenceCompleted: false, toolResults: [] }; } + const { logger } = context; + logger.verbose(`Executing ${toolCalls.length} tool calls`); // Check for respawn tool call @@ -147,9 +153,7 @@ async function executeTools( toolCalls.map(async (call) => { let toolResult = ''; try { - toolResult = await executeToolCall(call, tools, logger, { - workingDirectory, - }); + toolResult = await executeToolCall(call, tools, context); } catch (error: any) { toolResult = `Error: Exception thrown during tool execution. Type: ${error.constructor.name}, Message: ${error.message}`; } @@ -171,7 +175,10 @@ async function executeTools( const sequenceCompleted = results.some((r) => r.isComplete); const completionResult = results.find((r) => r.isComplete)?.content; - messages.push({ role: 'user', content: toolResults }); + messages.push({ + role: 'user', + content: toolResults, + }); if (sequenceCompleted) { logger.verbose('Sequence completed', { completionResult }); @@ -180,18 +187,68 @@ async function executeTools( return { sequenceCompleted, completionResult, toolResults }; } +// a function that takes a list of messages and returns a list of messages but with the last message having a cache_control of ephemeral +function addCacheControlToTools(messages: T[]): T[] { + return messages.map((m, i) => ({ + ...m, + ...(i === messages.length - 1 + ? { cache_control: { type: 'ephemeral' } } + : {}), + })); +} + +function addCacheControlToContentBlocks( + content: ContentBlockParam[], +): ContentBlockParam[] { + return content.map((c, i) => { + if (i === content.length - 1) { + if (c.type === 'text' || c.type === 'document' || c.type === 'image') { + return { ...c, cache_control: { type: 'ephemeral' } }; + } + } + return c; + }); +} +function addCacheControlToMessages( + messages: Anthropic.Messages.MessageParam[], +): Anthropic.Messages.MessageParam[] { + return messages.map((m) => { + if (typeof m.content === 'string') { + return { + ...m, + content: [ + { + type: 'text', + text: m.content, + cache_control: { type: 'ephemeral' }, + }, + ] as ContentBlockParam[], + }; + } + return { + ...m, + content: addCacheControlToContentBlocks(m.content), + }; + }); +} + export const toolAgent = async ( initialPrompt: string, tools: Tool[], - logger: Logger, config = CONFIG, - workingDirectory?: string, + context: ToolContext, ): Promise => { + const { logger, tokenLevel, tokenUsage } = context; + logger.verbose('Starting agent execution'); logger.verbose('Initial prompt:', initialPrompt); - let totalInputTokens = 0; - let totalOutputTokens = 0; + let tokens: TokenUsage = { + input: 0, + inputCacheWrites: 0, + inputCacheReads: 0, + output: 0, + }; let interactions = 0; const apiKey = process.env.ANTHROPIC_API_KEY; @@ -218,19 +275,31 @@ export const toolAgent = async ( ); interactions++; - const response = await client.messages.create({ + + // Create request parameters + const requestParams: Anthropic.MessageCreateParams = { model: config.model, max_tokens: config.maxTokens, temperature: config.temperature, - messages, - system: systemPrompt, - tools: tools.map((t) => ({ - name: t.name, - description: t.description, - input_schema: t.parameters as Anthropic.Tool.InputSchema, - })), + messages: addCacheControlToMessages(messages), + system: [ + { + type: 'text', + text: systemPrompt, + cache_control: { type: 'ephemeral' }, + }, + ], + tools: addCacheControlToTools( + tools.map((t) => ({ + name: t.name, + description: t.description, + input_schema: t.parameters as Anthropic.Tool.InputSchema, + })), + ), tool_choice: { type: 'auto' }, - }); + }; + + const response = await client.messages.create(requestParams); if (!response.content.length) { // Instead of treating empty response as completion, remind the agent @@ -247,14 +316,15 @@ export const toolAgent = async ( continue; } - totalInputTokens += response.usage.input_tokens; - totalOutputTokens += response.usage.output_tokens; - logger.verbose( - ` Token usage: ${response.usage.input_tokens} input, ${response.usage.output_tokens} output`, - ); + // Track both regular and cached token usage + const tokenPerMessage = getTokenUsage(response); + tokens = addTokenUsage(tokens, tokenPerMessage); const { content, toolCalls } = processResponse(response); - messages.push({ role: 'assistant', content }); + messages.push({ + role: 'assistant', + content, + }); // Log the assistant's message const assistantMessage = content @@ -265,12 +335,17 @@ 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)}`), + ); + const { sequenceCompleted, completionResult, respawn } = await executeTools( toolCalls, tools, messages, - logger, - workingDirectory, + context, ); if (respawn) { @@ -289,14 +364,13 @@ export const toolAgent = async ( result: completionResult ?? 'Sequence explicitly completed with an empty result', - tokens: { - input: totalInputTokens, - output: totalOutputTokens, - }, + tokens, interactions, }; - logger.verbose( - `Agent completed with ${result.tokens.input} input tokens, ${result.tokens.output} output tokens in ${result.interactions} interactions`, + // Use the appropriate log level based on tokenUsage flag + const logLevel = tokenUsage ? 'info' : tokenLevel; + logger[logLevel]( + chalk.blueBright(`[Token Usage/Agent] ${getTokenString(tokens)}`), ); return result; } @@ -305,14 +379,13 @@ export const toolAgent = async ( logger.warn('Maximum iterations reached'); const result = { result: 'Maximum sub-agent iterations reach without successful completion', - tokens: { - input: totalInputTokens, - output: totalOutputTokens, - }, + tokens, interactions, }; - logger.verbose( - `Agent completed with ${result.tokens.input} input tokens, ${result.tokens.output} output tokens in ${result.interactions} interactions`, + // Use the appropriate log level based on tokenUsage flag + const logLevel = tokenUsage ? 'info' : tokenLevel; + logger[logLevel]( + chalk.blueBright(`[Token Usage/Agent] ${getTokenString(tokens)}`), ); return result; }; diff --git a/packages/agent/src/core/toolContext.ts b/packages/agent/src/core/toolContext.ts deleted file mode 100644 index 18b0577..0000000 --- a/packages/agent/src/core/toolContext.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Logger } from '../utils/logger.js'; - -import { ToolContext } from './types.js'; - -export function createToolContext( - logger: Logger, - workingDirectory?: string, -): ToolContext { - return { - logger, - workingDirectory, - }; -} - -export function loggerToToolContext( - logger: Logger, - workingDirectory?: string, -): ToolContext { - return createToolContext(logger, workingDirectory); -} diff --git a/packages/agent/src/core/types.ts b/packages/agent/src/core/types.ts index c44ba50..a976ab6 100644 --- a/packages/agent/src/core/types.ts +++ b/packages/agent/src/core/types.ts @@ -2,9 +2,14 @@ import { JsonSchema7Type } from 'zod-to-json-schema'; import { Logger } from '../utils/logger.js'; +export type TokenLevel = 'debug' | 'verbose' | 'info' | 'warn' | 'error'; + export type ToolContext = { logger: Logger; - workingDirectory?: string; + workingDirectory: string; + headless: boolean; + tokenLevel: TokenLevel; + tokenUsage?: boolean; }; export type Tool, TReturn = any> = { @@ -37,11 +42,6 @@ export type ToolUseContent = { export type AssistantMessage = { role: 'assistant'; content: (TextContent | ToolUseContent)[]; - tokenUsage?: { - promptTokens: number; - completionTokens: number; - totalTokens: number; - }; }; export type ToolResultContent = { diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index b898f07..bb716a4 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -24,7 +24,6 @@ export * from './tools/interaction/subAgent.js'; export * from './tools/interaction/userPrompt.js'; // Core -export * from './core/toolContext.js'; export * from './core/executeToolCall.js'; export * from './core/types.js'; export * from './core/toolAgent.js'; diff --git a/packages/agent/src/tools/browser/browseMessage.ts b/packages/agent/src/tools/browser/browseMessage.ts index ca0379e..53b8aa9 100644 --- a/packages/agent/src/tools/browser/browseMessage.ts +++ b/packages/agent/src/tools/browser/browseMessage.ts @@ -9,12 +9,26 @@ import { browserSessions, type BrowserAction, SelectorType } from './types.js'; // Schema for browser action const browserActionSchema = z .object({ - type: z.enum(['goto', 'click', 'type', 'wait', 'content', 'close']), - url: z.string().url().optional(), - selector: z.string().optional(), - selectorType: z.nativeEnum(SelectorType).optional(), - text: z.string().optional(), - options: z.object({}).optional(), + actionType: z.enum(['goto', 'click', 'type', 'wait', 'content', 'close']), + url: z + .string() + .url() + .optional() + .describe('URL to navigate to if "goto" actionType'), + selector: z + .string() + .optional() + .describe('Selector to click if "click" actionType'), + selectorType: z + .nativeEnum(SelectorType) + .optional() + .describe('Type of selector if "click" actionType'), + text: z + .string() + .optional() + .describe( + 'Text to type if "type" actionType, for other actionType, this is ignored', + ), }) .describe('Browser action to perform'); @@ -57,7 +71,7 @@ export const browseMessageTool: Tool = { returns: zodToJsonSchema(returnSchema), execute: async ({ instanceId, action }, { logger }): Promise => { - logger.verbose(`Executing browser action: ${action.type}`); + logger.verbose(`Executing browser action: ${action.actionType}`); try { const session = browserSessions.get(instanceId); @@ -67,7 +81,7 @@ export const browseMessageTool: Tool = { const { page } = session; - switch (action.type) { + switch (action.actionType) { case 'goto': { if (!action.url) { throw new Error('URL required for goto action'); @@ -136,7 +150,7 @@ export const browseMessageTool: Tool = { default: { throw new Error( - `Unsupported action type: ${(action as BrowserAction).type}`, + `Unsupported action type: ${(action as BrowserAction).actionType}`, ); } } @@ -150,7 +164,9 @@ export const browseMessageTool: Tool = { }, logParameters: ({ action, description }, { logger }) => { - logger.info(`Performing browser action: ${action.type}, ${description}`); + logger.info( + `Performing browser action: ${action.actionType}, ${description}`, + ); }, logReturns: (output, { logger }) => { diff --git a/packages/agent/src/tools/browser/browseStart.ts b/packages/agent/src/tools/browser/browseStart.ts index 94b7113..566cc59 100644 --- a/packages/agent/src/tools/browser/browseStart.ts +++ b/packages/agent/src/tools/browser/browseStart.ts @@ -10,10 +10,7 @@ import { browserSessions } from './types.js'; const parameterSchema = z.object({ url: z.string().url().optional().describe('Initial URL to navigate to'), - headless: z - .boolean() - .optional() - .describe('Run browser in headless mode (default: true)'), + timeout: z .number() .optional() @@ -41,8 +38,8 @@ export const browseStartTool: Tool = { returns: zodToJsonSchema(returnSchema), execute: async ( - { url, headless = true, timeout = 30000 }, - { logger }, + { url, timeout = 30000 }, + { logger, headless = true }, ): Promise => { logger.verbose(`Starting browser session${url ? ` at ${url}` : ''}`); diff --git a/packages/agent/src/tools/browser/types.ts b/packages/agent/src/tools/browser/types.ts index 22ab62c..b57470f 100644 --- a/packages/agent/src/tools/browser/types.ts +++ b/packages/agent/src/tools/browser/types.ts @@ -58,14 +58,14 @@ export const browserSessions: Map = new Map(); // Browser action types export type BrowserAction = - | { type: 'goto'; url: string } - | { type: 'click'; selector: string; selectorType?: SelectorType } + | { actionType: 'goto'; url: string } + | { actionType: 'click'; selector: string; selectorType?: SelectorType } | { - type: 'type'; + actionType: 'type'; selector: string; text: string; selectorType?: SelectorType; } - | { type: 'wait'; selector: string; selectorType?: SelectorType } - | { type: 'content' } - | { type: 'close' }; + | { actionType: 'wait'; selector: string; selectorType?: SelectorType } + | { actionType: 'content' } + | { actionType: 'close' }; diff --git a/packages/agent/src/tools/interaction/subAgent.ts b/packages/agent/src/tools/interaction/subAgent.ts index 191ae78..c2a830b 100644 --- a/packages/agent/src/tools/interaction/subAgent.ts +++ b/packages/agent/src/tools/interaction/subAgent.ts @@ -67,7 +67,7 @@ export const subAgentTool: Tool = { 'Creates a sub-agent that has access to all tools to solve a specific task', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), - execute: async (params, { logger }) => { + execute: async (params, context) => { // Validate parameters const { description, goal, projectContext, fileContext } = parameterSchema.parse(params); @@ -100,7 +100,11 @@ export const subAgentTool: Tool = { ...subAgentConfig, }; - const result = await toolAgent(prompt, tools, logger, config); + const result = await toolAgent(prompt, tools, config, { + ...context, + workingDirectory: + fileContext?.workingDirectory ?? context.workingDirectory, + }); return result.result; // Return the result string directly }, logParameters: (input, { logger }) => { diff --git a/packages/agent/src/tools/io/readFile.test.ts b/packages/agent/src/tools/io/readFile.test.ts index 9d8c597..2f89eec 100644 --- a/packages/agent/src/tools/io/readFile.test.ts +++ b/packages/agent/src/tools/io/readFile.test.ts @@ -10,7 +10,7 @@ describe('readFile', () => { it('should read a file', async () => { const { content } = await readFileTool.execute( { path: 'package.json', description: 'test' }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(content).toContain('mycoder'); }); @@ -19,7 +19,7 @@ describe('readFile', () => { try { await readFileTool.execute( { path: 'nonexistent.txt', description: 'test' }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(true).toBe(false); // Should not reach here } catch (error: any) { diff --git a/packages/agent/src/tools/io/updateFile.test.ts b/packages/agent/src/tools/io/updateFile.test.ts index 34bca74..3791ddf 100644 --- a/packages/agent/src/tools/io/updateFile.test.ts +++ b/packages/agent/src/tools/io/updateFile.test.ts @@ -23,7 +23,7 @@ describe('updateFile', () => { afterEach(async () => { await shellExecuteTool.execute( { command: `rm -rf "${testDir}"`, description: 'test' }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); }); @@ -41,7 +41,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); // Verify return value @@ -51,7 +51,7 @@ describe('updateFile', () => { // Verify content const readResult = await readFileTool.execute( { path: testPath, description: 'test' }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(readResult.content).toBe(testContent); }); @@ -72,7 +72,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); // Append content @@ -85,7 +85,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); // Verify return value @@ -95,7 +95,7 @@ describe('updateFile', () => { // Verify content const readResult = await readFileTool.execute( { path: testPath, description: 'test' }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(readResult.content).toBe(expectedContent); }); @@ -117,7 +117,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); // Update specific text @@ -131,7 +131,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); // Verify return value @@ -141,7 +141,7 @@ describe('updateFile', () => { // Verify content const readResult = await readFileTool.execute( { path: testPath, description: 'test' }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(readResult.content).toBe(expectedContent); }); @@ -162,7 +162,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); // Attempt update that should fail @@ -177,7 +177,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ), ).rejects.toThrow('Found 2 occurrences of oldStr, expected exactly 1'); }); @@ -196,7 +196,7 @@ describe('updateFile', () => { }, description: 'test', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); // Verify return value @@ -206,7 +206,7 @@ describe('updateFile', () => { // Verify content const readResult = await readFileTool.execute( { path: nestedPath, description: 'test' }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(readResult.content).toBe(testContent); }); diff --git a/packages/agent/src/tools/system/respawn.test.ts b/packages/agent/src/tools/system/respawn.test.ts index 9599441..2d396e6 100644 --- a/packages/agent/src/tools/system/respawn.test.ts +++ b/packages/agent/src/tools/system/respawn.test.ts @@ -15,7 +15,12 @@ describe('respawnTool', () => { it('should execute and return confirmation message', async () => { const result = await respawnTool.execute( { respawnContext: 'new context' }, - { logger: mockLogger }, + { + logger: mockLogger, + headless: true, + workingDirectory: '.', + tokenLevel: 'debug', + }, ); 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 f81dfdc..f3eee7b 100644 --- a/packages/agent/src/tools/system/shellExecute.test.ts +++ b/packages/agent/src/tools/system/shellExecute.test.ts @@ -10,7 +10,7 @@ describe('shellExecute', () => { it('should execute shell commands', async () => { const { stdout } = await shellExecuteTool.execute( { command: "echo 'test'", description: 'test' }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(stdout).toContain('test'); }); @@ -18,7 +18,7 @@ describe('shellExecute', () => { it('should handle command errors', async () => { const { error } = await shellExecuteTool.execute( { command: 'nonexistentcommand', description: 'test' }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(error).toContain('Command failed:'); }); diff --git a/packages/agent/src/tools/system/shellMessage.test.ts b/packages/agent/src/tools/system/shellMessage.test.ts index 1388908..dee3ab2 100644 --- a/packages/agent/src/tools/system/shellMessage.test.ts +++ b/packages/agent/src/tools/system/shellMessage.test.ts @@ -40,7 +40,7 @@ describe('shellMessageTool', () => { description: 'Test interactive process', timeout: 50, // Force async mode for interactive process }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); testInstanceId = getInstanceId(startResult); @@ -52,7 +52,7 @@ describe('shellMessageTool', () => { stdin: 'hello world', description: 'Test interaction', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(result.stdout).toBe('hello world'); @@ -66,7 +66,7 @@ describe('shellMessageTool', () => { instanceId: 'nonexistent-id', description: 'Test invalid process', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(result.error).toBeDefined(); @@ -81,7 +81,7 @@ describe('shellMessageTool', () => { description: 'Test completion', timeout: 0, // Force async mode }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); const instanceId = getInstanceId(startResult); @@ -94,7 +94,7 @@ describe('shellMessageTool', () => { instanceId, description: 'Check completion', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(result.completed).toBe(true); @@ -110,7 +110,7 @@ describe('shellMessageTool', () => { description: 'Test SIGTERM handling', timeout: 0, // Force async mode }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); const instanceId = getInstanceId(startResult); @@ -121,7 +121,7 @@ describe('shellMessageTool', () => { signal: NodeSignals.SIGTERM, description: 'Send SIGTERM', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(result.signaled).toBe(true); @@ -132,7 +132,7 @@ describe('shellMessageTool', () => { instanceId, description: 'Check on status', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(result2.completed).toBe(true); @@ -147,7 +147,7 @@ describe('shellMessageTool', () => { description: 'Test signal handling on terminated process', timeout: 0, // Force async mode }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); const instanceId = getInstanceId(startResult); @@ -159,7 +159,7 @@ describe('shellMessageTool', () => { signal: NodeSignals.SIGTERM, description: 'Send signal to terminated process', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(result.signaled).toBe(true); @@ -174,7 +174,7 @@ describe('shellMessageTool', () => { description: 'Test signal flag verification', timeout: 0, // Force async mode }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); const instanceId = getInstanceId(startResult); @@ -186,7 +186,7 @@ describe('shellMessageTool', () => { signal: NodeSignals.SIGTERM, description: 'Send SIGTERM', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); await sleep(50); @@ -197,7 +197,7 @@ describe('shellMessageTool', () => { instanceId, description: 'Check signal state', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(checkResult.signaled).toBe(true); diff --git a/packages/agent/src/tools/system/shellStart.test.ts b/packages/agent/src/tools/system/shellStart.test.ts index f86370f..ce0b5f0 100644 --- a/packages/agent/src/tools/system/shellStart.test.ts +++ b/packages/agent/src/tools/system/shellStart.test.ts @@ -26,7 +26,7 @@ describe('shellStartTool', () => { description: 'Test process', timeout: 500, // Generous timeout to ensure sync mode }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(result.mode).toBe('sync'); @@ -44,7 +44,7 @@ describe('shellStartTool', () => { description: 'Slow command test', timeout: 50, // Short timeout to force async mode }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(result.mode).toBe('async'); @@ -60,7 +60,7 @@ describe('shellStartTool', () => { command: 'nonexistentcommand', description: 'Invalid command test', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(result.mode).toBe('sync'); @@ -78,7 +78,7 @@ describe('shellStartTool', () => { description: 'Sync completion test', timeout: 500, }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); // Even sync results should be in processStates @@ -96,7 +96,7 @@ describe('shellStartTool', () => { description: 'Async completion test', timeout: 50, }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); if (asyncResult.mode === 'async') { @@ -111,7 +111,7 @@ describe('shellStartTool', () => { description: 'Pipe test', timeout: 50, // Force async for interactive command }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(result.mode).toBe('async'); @@ -144,7 +144,7 @@ describe('shellStartTool', () => { command: 'sleep 1', description: 'Default timeout test', }, - { logger }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, ); expect(result.mode).toBe('sync'); diff --git a/packages/agent/src/tools/system/sleep.test.ts b/packages/agent/src/tools/system/sleep.test.ts index b870105..f151f9c 100644 --- a/packages/agent/src/tools/system/sleep.test.ts +++ b/packages/agent/src/tools/system/sleep.test.ts @@ -12,7 +12,10 @@ describe('sleep tool', () => { }); it('should sleep for the specified duration', async () => { - const sleepPromise = sleepTool.execute({ seconds: 2 }, { logger }); + const sleepPromise = sleepTool.execute( + { seconds: 2 }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + ); await vi.advanceTimersByTimeAsync(2000); const result = await sleepPromise; @@ -22,13 +25,19 @@ describe('sleep tool', () => { it('should reject negative sleep duration', async () => { await expect( - sleepTool.execute({ seconds: -1 }, { logger }), + sleepTool.execute( + { seconds: -1 }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + ), ).rejects.toThrow(); }); it('should reject sleep duration over 1 hour', async () => { await expect( - sleepTool.execute({ seconds: 3601 }, { logger }), + sleepTool.execute( + { seconds: 3601 }, + { logger, headless: true, workingDirectory: '.', tokenLevel: 'debug' }, + ), ).rejects.toThrow(); }); }); diff --git a/packages/agent/src/utils/mockLogger.ts b/packages/agent/src/utils/mockLogger.ts index e7bda6f..4a95525 100644 --- a/packages/agent/src/utils/mockLogger.ts +++ b/packages/agent/src/utils/mockLogger.ts @@ -11,3 +11,6 @@ export class MockLogger extends Logger { warn(..._messages: any[]): void {} error(..._messages: any[]): void {} } + +// Export an instance of MockLogger for tests +export const mockLogger = new MockLogger(); diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 6399391..8a8b851 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,17 @@ # mycoder +## 0.2.2 + +### Patch Changes + +- Replaced `--tokenLog` with `--tokenUsage` boolean flag that outputs token usage at info log level when enabled + +## 0.2.1 + +### Patch Changes + +- Added `--tokenLog` option to output token usage at specified log level (defaults to debug), helping to monitor token caching effectiveness + ## 0.2.0 ### Minor Changes diff --git a/packages/cli/README.md b/packages/cli/README.md index 3786826..c1c1a1d 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -53,6 +53,7 @@ mycoder --promptFile=your-prompt.txt - `-i, --interactive`: Run in interactive mode, asking for prompts - `-f, --file`: Read prompt from a specified file - `--log`: Set log level (info, verbose, warn, error) +- `--tokenUsage`: Output token usage at info log level - `-h, --help`: Show help - `-V, --version`: Show version diff --git a/packages/cli/package.json b/packages/cli/package.json index f09f9ea..22878c9 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.2.0", + "version": "0.1.4", "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 d535955..9e8712b 100644 --- a/packages/cli/src/commands/$default.ts +++ b/packages/cli/src/commands/$default.ts @@ -7,6 +7,7 @@ import { Logger, getTools, getAnthropicApiKeyError, + TokenLevel, } from 'mycoder-agent'; import { SharedOptions } from '../options.js'; @@ -32,6 +33,9 @@ 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`, ); @@ -116,7 +120,14 @@ export const command: CommandModule = { ].join('\n'); const tools = getTools(); - const result = await toolAgent(prompt, tools, logger); + + const result = await toolAgent(prompt, tools, undefined, { + logger, + headless: true, + workingDirectory: '.', + tokenLevel, + tokenUsage: argv.tokenUsage, + }); const output = typeof result.result === 'string' ? result.result diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 073c682..5757401 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -14,6 +14,8 @@ import { checkForUpdates } from './utils/versionCheck.js'; import type { PackageJson } from 'type-fest'; +// Add global declaration for our patched toolAgent + sourceMapSupport.install(); const nameToLogIndex = (logLevelName: string) => { diff --git a/packages/cli/src/options.ts b/packages/cli/src/options.ts index 8bcb194..bbe9a3c 100644 --- a/packages/cli/src/options.ts +++ b/packages/cli/src/options.ts @@ -4,6 +4,7 @@ export type SharedOptions = { readonly log: LogLevel; readonly interactive: boolean; readonly file?: string; + readonly tokenUsage?: boolean; }; export const sharedOptions = { @@ -25,4 +26,9 @@ export const sharedOptions = { alias: 'f', description: 'Read prompt from a file', } as const, + tokenUsage: { + type: 'boolean', + description: 'Output token usage at info log level', + default: false, + } as const, };