From 5f3bb0f3fac93d9dd2e155ac934a14d957a6fd19 Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Wed, 5 Mar 2025 08:01:06 -0500 Subject: [PATCH 1/3] refactor: Move toolAgent implementation to toolAgentCore.ts and update imports - Move main toolAgent implementation from index.ts to toolAgentCore.ts\n- Update index.ts to re-export from toolAgentCore.ts\n- Remove deprecated toolAgent.ts file\n- Update all direct imports to use the new structure\n- Update documentation in README.md\n\nCloses #92 --- packages/agent/src/core/toolAgent/README.md | 15 +- packages/agent/src/core/toolAgent/index.ts | 172 +++--------------- .../agent/src/core/toolAgent/toolAgentCore.ts | 150 +++++++++++++++ packages/agent/src/index.ts | 2 +- .../agent/src/tools/interaction/subAgent.ts | 6 +- 5 files changed, 190 insertions(+), 155 deletions(-) create mode 100644 packages/agent/src/core/toolAgent/toolAgentCore.ts diff --git a/packages/agent/src/core/toolAgent/README.md b/packages/agent/src/core/toolAgent/README.md index dbcc9a9..56147b4 100644 --- a/packages/agent/src/core/toolAgent/README.md +++ b/packages/agent/src/core/toolAgent/README.md @@ -1,10 +1,11 @@ # Tool Agent Module -This directory contains the refactored Tool Agent implementation, split into smaller, focused modules for improved maintainability and testability. +This directory contains the Tool Agent implementation, split into smaller, focused modules for improved maintainability and testability. ## Module Structure -- **index.ts**: Main entry point and orchestration of the tool agent functionality +- **index.ts**: Re-exports from toolAgentCore.ts and other modules +- **toolAgentCore.ts**: Main implementation of the tool agent functionality - **config.ts**: Configuration-related code and default settings - **messageUtils.ts**: Utilities for handling and formatting messages - **toolExecutor.ts**: Logic for executing tool calls @@ -14,10 +15,10 @@ This directory contains the refactored Tool Agent implementation, split into sma ## Usage ```typescript -import { toolAgent } from './toolAgent/index.js'; -import { Tool, ToolContext } from './toolAgent/types.js'; +import { toolAgent } from '../../core/toolAgent/index.js'; +import { Tool, ToolContext } from '../../core/types.js'; -// Use the toolAgent function as before +// Use the toolAgent function const result = await toolAgent(prompt, tools, config, context); ``` @@ -28,7 +29,3 @@ const result = await toolAgent(prompt, tools, config, context); - **Clearer responsibilities**: Each module has a single purpose - **Easier onboarding**: New developers can understand the system more quickly - **Simpler future extensions**: Modular design makes it easier to extend functionality - -## Migration - -The original `toolAgent.ts` file now re-exports from this directory for backward compatibility, but it will display a deprecation warning. New code should import directly from the toolAgent directory. diff --git a/packages/agent/src/core/toolAgent/index.ts b/packages/agent/src/core/toolAgent/index.ts index 1d5b3dd..8b5525f 100644 --- a/packages/agent/src/core/toolAgent/index.ts +++ b/packages/agent/src/core/toolAgent/index.ts @@ -1,152 +1,38 @@ -import { CoreMessage, ToolSet, generateText, tool as makeTool } from 'ai'; - -import { getAnthropicApiKeyError } from '../../utils/errors.js'; - -import { DEFAULT_CONFIG } from './config.js'; -import { - addCacheControlToMessages, - createCacheControlMessageFromSystemPrompt, - createToolCallParts, - formatToolCalls, -} from './messageUtils.js'; -import { logTokenUsage } from './tokenTracking.js'; -import { executeTools } from './toolExecutor.js'; -import { Tool, ToolAgentResult, ToolContext } from './types.js'; - /** - * Main tool agent function that orchestrates the conversation with the AI - * and handles tool execution + * Main entry point for the toolAgent module + * Re-exports all functionality from the modular structure */ -export const toolAgent = async ( - initialPrompt: string, - tools: Tool[], - config = DEFAULT_CONFIG, - context: ToolContext, -): Promise => { - const { logger, tokenTracker } = context; - - logger.verbose('Starting agent execution'); - logger.verbose('Initial prompt:', initialPrompt); - - let interactions = 0; - - const apiKey = process.env.ANTHROPIC_API_KEY; - if (!apiKey) throw new Error(getAnthropicApiKeyError()); - - const messages: CoreMessage[] = [ - { - role: 'user', - content: [{ type: 'text', text: initialPrompt }], - }, - ]; - - logger.debug('User message:', initialPrompt); - - // Get the system prompt once at the start - const systemPrompt = config.getSystemPrompt(context); - - for (let i = 0; i < config.maxIterations; i++) { - logger.verbose( - `Requesting completion ${i + 1} with ${messages.length} messages with ${ - JSON.stringify(messages).length - } bytes`, - ); - - interactions++; - - const toolSet: ToolSet = {}; - tools.forEach((tool) => { - toolSet[tool.name] = makeTool({ - description: tool.description, - parameters: tool.parameters, - }); - }); - - // Apply cache control to messages for token caching - const messagesWithCacheControl = [ - createCacheControlMessageFromSystemPrompt(systemPrompt), - ...addCacheControlToMessages(messages), - ]; - - const generateTextProps = { - model: config.model, - temperature: config.temperature, - messages: messagesWithCacheControl, - tools: toolSet, - }; - const { text, toolCalls } = await generateText(generateTextProps); - const localToolCalls = formatToolCalls(toolCalls); +// Export the main toolAgent function +export { toolAgent } from './toolAgentCore.js'; - if (!text.length) { - // Instead of treating empty response as completion, remind the agent - logger.verbose('Received empty response from agent, sending reminder'); - messages.push({ - role: 'user', - content: [ - { - type: 'text', - text: 'I notice you sent an empty response. If you are done with your tasks, please call the sequenceComplete tool with your results. If you are waiting for other tools to complete, you can use the sleep tool to wait before checking again.', - }, - ], - }); - continue; - } - - messages.push({ - role: 'assistant', - content: [{ type: 'text', text: text }], - }); - - if (text) { - logger.info(text); - } - - if (toolCalls.length > 0) { - const toolCallParts = createToolCallParts(toolCalls); - - messages.push({ - role: 'assistant', - content: toolCallParts, - }); - } - - const { sequenceCompleted, completionResult, respawn } = await executeTools( - localToolCalls, - tools, - messages, - context, - ); - - if (respawn) { - logger.info('Respawning agent with new context'); - // Reset messages to just the new context - messages.length = 0; - messages.push({ - role: 'user', - content: [{ type: 'text', text: respawn.context }], - }); - continue; - } - - if (sequenceCompleted) { - const result: ToolAgentResult = { - result: completionResult ?? 'Sequence explicitly completed', - interactions, - }; - logTokenUsage(tokenTracker); - return result; - } - } +// Re-export everything from the module +export * from './config.js'; +export * from './messageUtils.js'; +export * from './toolExecutor.js'; +export * from './tokenTracking.js'; +export * from './types.js'; - logger.warn('Maximum iterations reached'); - const result = { - result: 'Maximum sub-agent iterations reach without successful completion', - interactions, - }; +// Export default system prompt for convenience +export const getDefaultSystemPrompt = (context: any) => { + return `You are an AI agent that can use tools to accomplish tasks. - logTokenUsage(tokenTracker); - return result; +Current Context: +Directory: ${context.workingDirectory} +Files: +${context.directoryListing ?? 'No directory listing available'} +System: ${context.systemInfo ?? 'No system info available'} +DateTime: ${new Date().toString()}`; +}; +export const getDefaultSystemPrompt = (context: any) => { + return `You are an AI agent that can use tools to accomplish tasks. + +Current Context: +Directory: ${context.workingDirectory} +Files: +${context.directoryListing ?? 'No directory listing available'} +System: ${context.systemInfo ?? 'No system info available'} +DateTime: ${new Date().toString()}`; }; // Re-export everything from the module diff --git a/packages/agent/src/core/toolAgent/toolAgentCore.ts b/packages/agent/src/core/toolAgent/toolAgentCore.ts new file mode 100644 index 0000000..a472f07 --- /dev/null +++ b/packages/agent/src/core/toolAgent/toolAgentCore.ts @@ -0,0 +1,150 @@ +import { CoreMessage, ToolSet, generateText, tool as makeTool } from 'ai'; + +import { getAnthropicApiKeyError } from '../../utils/errors.js'; + +import { DEFAULT_CONFIG } from './config.js'; +import { + addCacheControlToMessages, + createCacheControlMessageFromSystemPrompt, + createToolCallParts, + formatToolCalls, +} from './messageUtils.js'; +import { logTokenUsage } from './tokenTracking.js'; +import { executeTools } from './toolExecutor.js'; +import { Tool, ToolAgentResult, ToolContext } from './types.js'; + +/** + * Main tool agent function that orchestrates the conversation with the AI + * and handles tool execution + */ +export const toolAgent = async ( + initialPrompt: string, + tools: Tool[], + config = DEFAULT_CONFIG, + context: ToolContext, +): Promise => { + const { logger, tokenTracker } = context; + + logger.verbose('Starting agent execution'); + logger.verbose('Initial prompt:', initialPrompt); + + let interactions = 0; + + const apiKey = process.env.ANTHROPIC_API_KEY; + if (!apiKey) throw new Error(getAnthropicApiKeyError()); + + const messages: CoreMessage[] = [ + { + role: 'user', + content: [{ type: 'text', text: initialPrompt }], + }, + ]; + + logger.debug('User message:', initialPrompt); + + // Get the system prompt once at the start + const systemPrompt = config.getSystemPrompt(context); + + for (let i = 0; i < config.maxIterations; i++) { + logger.verbose( + `Requesting completion ${i + 1} with ${messages.length} messages with ${ + JSON.stringify(messages).length + } bytes`, + ); + + interactions++; + + const toolSet: ToolSet = {}; + tools.forEach((tool) => { + toolSet[tool.name] = makeTool({ + description: tool.description, + parameters: tool.parameters, + }); + }); + + // Apply cache control to messages for token caching + const messagesWithCacheControl = [ + createCacheControlMessageFromSystemPrompt(systemPrompt), + ...addCacheControlToMessages(messages), + ]; + + const generateTextProps = { + model: config.model, + temperature: config.temperature, + messages: messagesWithCacheControl, + tools: toolSet, + }; + const { text, toolCalls } = await generateText(generateTextProps); + + const localToolCalls = formatToolCalls(toolCalls); + + if (!text.length) { + // Instead of treating empty response as completion, remind the agent + logger.verbose('Received empty response from agent, sending reminder'); + messages.push({ + role: 'user', + content: [ + { + type: 'text', + text: 'I notice you sent an empty response. If you are done with your tasks, please call the sequenceComplete tool with your results. If you are waiting for other tools to complete, you can use the sleep tool to wait before checking again.', + }, + ], + }); + continue; + } + + messages.push({ + role: 'assistant', + content: [{ type: 'text', text: text }], + }); + + if (text) { + logger.info(text); + } + + if (toolCalls.length > 0) { + const toolCallParts = createToolCallParts(toolCalls); + + messages.push({ + role: 'assistant', + content: toolCallParts, + }); + } + + const { sequenceCompleted, completionResult, respawn } = await executeTools( + localToolCalls, + tools, + messages, + context, + ); + + if (respawn) { + logger.info('Respawning agent with new context'); + // Reset messages to just the new context + messages.length = 0; + messages.push({ + role: 'user', + content: [{ type: 'text', text: respawn.context }], + }); + continue; + } + + if (sequenceCompleted) { + const result: ToolAgentResult = { + result: completionResult ?? 'Sequence explicitly completed', + interactions, + }; + logTokenUsage(tokenTracker); + return result; + } + } + + logger.warn('Maximum iterations reached'); + const result = { + result: 'Maximum sub-agent iterations reach without successful completion', + interactions, + }; + + logTokenUsage(tokenTracker); + return result; +}; diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 28ee7e0..a99d2fa 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -25,7 +25,7 @@ export * from './tools/interaction/userPrompt.js'; // Core export * from './core/executeToolCall.js'; export * from './core/types.js'; -export * from './core/toolAgent.js'; +export * from './core/toolAgent/index.js'; export * from './core/toolAgent/config.js'; // Utils diff --git a/packages/agent/src/tools/interaction/subAgent.ts b/packages/agent/src/tools/interaction/subAgent.ts index d175bdf..5431386 100644 --- a/packages/agent/src/tools/interaction/subAgent.ts +++ b/packages/agent/src/tools/interaction/subAgent.ts @@ -2,8 +2,10 @@ import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { getModel } from '../../core/toolAgent/config.js'; -import { getDefaultSystemPrompt } from '../../core/toolAgent/index.js'; -import { toolAgent } from '../../core/toolAgent.js'; +import { + getDefaultSystemPrompt, + toolAgent, +} from '../../core/toolAgent/index.js'; import { Tool, ToolContext } from '../../core/types.js'; import { getTools } from '../getTools.js'; From f302073457b73be5097a8ff0cc1a47f178b99631 Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Wed, 5 Mar 2025 08:13:12 -0500 Subject: [PATCH 2/3] Fix re-export issues in toolAgent module --- packages/agent/src/core/toolAgent.ts | 22 ------------------- packages/agent/src/core/toolAgent/index.ts | 19 +--------------- .../agent/src/core/toolAgent/toolExecutor.ts | 11 +++++++++- packages/agent/src/core/toolAgent/types.ts | 11 ++++++---- packages/agent/src/index.ts | 1 - .../agent/src/tools/interaction/subAgent.ts | 2 +- 6 files changed, 19 insertions(+), 47 deletions(-) delete mode 100644 packages/agent/src/core/toolAgent.ts diff --git a/packages/agent/src/core/toolAgent.ts b/packages/agent/src/core/toolAgent.ts deleted file mode 100644 index f99fccb..0000000 --- a/packages/agent/src/core/toolAgent.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @fileoverview - * This file has been refactored into a modular structure. - * Please import from the toolAgent directory instead. - */ - -import { toolAgent, ToolAgentResult } from './toolAgent/index.js'; -import { Tool, ToolContext } from './toolAgent/types.js'; - -// Re-export the main toolAgent function and types for backward compatibility -export { toolAgent, ToolAgentResult, Tool, ToolContext }; - -// Provide a deprecation warning when this file is imported directly -const deprecationWarning = () => { - console.warn( - 'Warning: Importing directly from toolAgent.ts is deprecated. ' + - 'Please import from the toolAgent directory instead.', - ); -}; - -// Call the deprecation warning when this module is loaded -deprecationWarning(); diff --git a/packages/agent/src/core/toolAgent/index.ts b/packages/agent/src/core/toolAgent/index.ts index 8b5525f..6292320 100644 --- a/packages/agent/src/core/toolAgent/index.ts +++ b/packages/agent/src/core/toolAgent/index.ts @@ -14,7 +14,7 @@ export * from './tokenTracking.js'; export * from './types.js'; // Export default system prompt for convenience -export const getDefaultSystemPrompt = (context: any) => { +export const getDefaultSystemPrompt = (context: Record) => { return `You are an AI agent that can use tools to accomplish tasks. Current Context: @@ -24,20 +24,3 @@ ${context.directoryListing ?? 'No directory listing available'} System: ${context.systemInfo ?? 'No system info available'} DateTime: ${new Date().toString()}`; }; -export const getDefaultSystemPrompt = (context: any) => { - return `You are an AI agent that can use tools to accomplish tasks. - -Current Context: -Directory: ${context.workingDirectory} -Files: -${context.directoryListing ?? 'No directory listing available'} -System: ${context.systemInfo ?? 'No system info available'} -DateTime: ${new Date().toString()}`; -}; - -// Re-export everything from the module -export * from './config.js'; -export * from './messageUtils.js'; -export * from './toolExecutor.js'; -export * from './tokenTracking.js'; -export * from './types.js'; diff --git a/packages/agent/src/core/toolAgent/toolExecutor.ts b/packages/agent/src/core/toolAgent/toolExecutor.ts index 52c0f64..14bf99f 100644 --- a/packages/agent/src/core/toolAgent/toolExecutor.ts +++ b/packages/agent/src/core/toolAgent/toolExecutor.ts @@ -5,6 +5,15 @@ import { TokenTracker } from '../tokens.js'; import { Tool, ToolCallResult, ToolContext, ToolUseContent } from './types.js'; +const safeParse = (value: string) => { + try { + return JSON.parse(value); + } catch (error) { + console.error('Error parsing JSON:', error, 'original value:', value); + return { error: value }; + } +}; + /** * Executes a list of tool calls and returns the results */ @@ -67,7 +76,7 @@ export async function executeTools( type: 'tool-result', toolCallId: call.id, toolName: call.name, - result: JSON.parse(toolResult), + result: safeParse(toolResult), } satisfies ToolResultPart; }), ); diff --git a/packages/agent/src/core/toolAgent/types.ts b/packages/agent/src/core/toolAgent/types.ts index a7c57b0..52c6b12 100644 --- a/packages/agent/src/core/toolAgent/types.ts +++ b/packages/agent/src/core/toolAgent/types.ts @@ -1,7 +1,10 @@ -// Re-export all types from the original types.ts file -export * from '../types.js'; +// Import types from the core types file +import { Tool, ToolContext } from '../types.js'; -// Only define new types specific to toolAgent here +// Export the imported types explicitly +export { Tool, ToolContext }; + +// Define types specific to toolAgent here export interface ToolAgentResult { result: string; interactions: number; @@ -10,7 +13,7 @@ export interface ToolAgentResult { export interface ToolCallResult { sequenceCompleted: boolean; completionResult?: string; - toolResults: any[]; + toolResults: unknown[]; respawn?: { context: string }; } diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index a99d2fa..e9bf527 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -26,7 +26,6 @@ export * from './tools/interaction/userPrompt.js'; export * from './core/executeToolCall.js'; export * from './core/types.js'; export * from './core/toolAgent/index.js'; -export * from './core/toolAgent/config.js'; // Utils export * from './tools/getTools.js'; diff --git a/packages/agent/src/tools/interaction/subAgent.ts b/packages/agent/src/tools/interaction/subAgent.ts index 5431386..41784a3 100644 --- a/packages/agent/src/tools/interaction/subAgent.ts +++ b/packages/agent/src/tools/interaction/subAgent.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; -import { getModel } from '../../core/toolAgent/config.js'; import { getDefaultSystemPrompt, + getModel, toolAgent, } from '../../core/toolAgent/index.js'; import { Tool, ToolContext } from '../../core/types.js'; From ece572adaf7d13eff7fe2eaf6e3f926851d2b355 Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Wed, 5 Mar 2025 08:22:34 -0500 Subject: [PATCH 3/3] fix build error --- packages/agent/src/core/toolAgent/toolExecutor.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/agent/src/core/toolAgent/toolExecutor.ts b/packages/agent/src/core/toolAgent/toolExecutor.ts index 14bf99f..3f05e1a 100644 --- a/packages/agent/src/core/toolAgent/toolExecutor.ts +++ b/packages/agent/src/core/toolAgent/toolExecutor.ts @@ -2,8 +2,9 @@ import { CoreMessage, CoreToolMessage, ToolResultPart } from 'ai'; import { executeToolCall } from '../executeToolCall.js'; import { TokenTracker } from '../tokens.js'; +import { ToolUseContent } from '../types.js'; -import { Tool, ToolCallResult, ToolContext, ToolUseContent } from './types.js'; +import { Tool, ToolCallResult, ToolContext } from './types.js'; const safeParse = (value: string) => { try {