diff --git a/docs/tools/agent-tools.md b/docs/tools/agent-tools.md new file mode 100644 index 0000000..8b28e9b --- /dev/null +++ b/docs/tools/agent-tools.md @@ -0,0 +1,130 @@ +# Agent Tools + +The agent tools provide ways to create and interact with sub-agents. There are two approaches available: + +1. The original `subAgent` tool (synchronous, blocking) +2. The new `agentStart` and `agentMessage` tools (asynchronous, non-blocking) + +## subAgent Tool + +The `subAgent` tool creates a sub-agent that runs synchronously until completion. The parent agent waits for the sub-agent to complete before continuing. + +```typescript +subAgent({ + description: "A brief description of the sub-agent's purpose", + goal: "The main objective that the sub-agent needs to achieve", + projectContext: "Context about the problem or environment", + workingDirectory: "/path/to/working/directory", // optional + relevantFilesDirectories: "src/**/*.ts", // optional +}); +``` + +## agentStart and agentMessage Tools + +The `agentStart` and `agentMessage` tools provide an asynchronous approach to working with sub-agents. This allows the parent agent to: + +- Start multiple sub-agents in parallel +- Monitor sub-agent progress +- Provide guidance to sub-agents +- Terminate sub-agents if needed + +### agentStart + +The `agentStart` tool creates a sub-agent and immediately returns an instance ID. The sub-agent runs asynchronously in the background. + +```typescript +const { instanceId } = agentStart({ + description: "A brief description of the sub-agent's purpose", + goal: "The main objective that the sub-agent needs to achieve", + projectContext: "Context about the problem or environment", + workingDirectory: "/path/to/working/directory", // optional + relevantFilesDirectories: "src/**/*.ts", // optional + enableUserPrompt: false, // optional, default: false +}); +``` + +### agentMessage + +The `agentMessage` tool allows interaction with a running sub-agent. It can be used to check the agent's progress, provide guidance, or terminate the agent. + +```typescript +// Check agent progress +const { output, completed } = agentMessage({ + instanceId: "agent-instance-id", + description: "Checking agent progress", +}); + +// Provide guidance (note: guidance implementation is limited in the current version) +agentMessage({ + instanceId: "agent-instance-id", + guidance: "Focus on the task at hand and avoid unnecessary exploration", + description: "Providing guidance to the agent", +}); + +// Terminate the agent +agentMessage({ + instanceId: "agent-instance-id", + terminate: true, + description: "Terminating the agent", +}); +``` + +## Example: Using agentStart and agentMessage to run multiple sub-agents in parallel + +```typescript +// Start multiple sub-agents +const agent1 = agentStart({ + description: "Agent 1", + goal: "Implement feature A", + projectContext: "Project X", +}); + +const agent2 = agentStart({ + description: "Agent 2", + goal: "Implement feature B", + projectContext: "Project X", +}); + +// Check progress of both agents +let agent1Completed = false; +let agent2Completed = false; + +while (!agent1Completed || !agent2Completed) { + if (!agent1Completed) { + const result1 = agentMessage({ + instanceId: agent1.instanceId, + description: "Checking Agent 1 progress", + }); + agent1Completed = result1.completed; + + if (agent1Completed) { + console.log("Agent 1 completed with result:", result1.output); + } + } + + if (!agent2Completed) { + const result2 = agentMessage({ + instanceId: agent2.instanceId, + description: "Checking Agent 2 progress", + }); + agent2Completed = result2.completed; + + if (agent2Completed) { + console.log("Agent 2 completed with result:", result2.output); + } + } + + // Wait before checking again + if (!agent1Completed || !agent2Completed) { + sleep({ seconds: 5 }); + } +} +``` + +## Choosing Between Approaches + +- Use `subAgent` for simpler tasks where blocking execution is acceptable +- Use `agentStart` and `agentMessage` for: + - Parallel execution of multiple sub-agents + - Tasks where you need to monitor progress + - Situations where you may need to provide guidance or terminate early \ No newline at end of file diff --git a/packages/agent/src/tools/getTools.ts b/packages/agent/src/tools/getTools.ts index 43d67cb..39033d8 100644 --- a/packages/agent/src/tools/getTools.ts +++ b/packages/agent/src/tools/getTools.ts @@ -3,6 +3,8 @@ import { Tool } from '../core/types.js'; // Import tools import { browseMessageTool } from './browser/browseMessage.js'; import { browseStartTool } from './browser/browseStart.js'; +import { agentMessageTool } from './interaction/agentMessage.js'; +import { agentStartTool } from './interaction/agentStart.js'; import { subAgentTool } from './interaction/subAgent.js'; import { userPromptTool } from './interaction/userPrompt.js'; import { fetchTool } from './io/fetch.js'; @@ -26,6 +28,8 @@ export function getTools(options?: GetToolsOptions): Tool[] { const tools: Tool[] = [ textEditorTool as unknown as Tool, subAgentTool as unknown as Tool, + agentStartTool as unknown as Tool, + agentMessageTool as unknown as Tool, sequenceCompleteTool as unknown as Tool, fetchTool as unknown as Tool, shellStartTool as unknown as Tool, diff --git a/packages/agent/src/tools/interaction/__tests__/agentTools.test.ts b/packages/agent/src/tools/interaction/__tests__/agentTools.test.ts new file mode 100644 index 0000000..c1d378f --- /dev/null +++ b/packages/agent/src/tools/interaction/__tests__/agentTools.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { agentMessageTool } from '../agentMessage.js'; +import { agentStartTool, agentStates } from '../agentStart.js'; + +// Mock the toolAgent function +vi.mock('../../../core/toolAgent/toolAgentCore.js', () => ({ + toolAgent: vi.fn().mockResolvedValue({ + result: 'Mock agent result', + interactions: 1, + }), +})); + +// Mock context +const mockContext = { + logger: { + info: vi.fn(), + verbose: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + warn: vi.fn(), + }, + tokenTracker: { + tokenUsage: { + add: vi.fn(), + }, + }, + workingDirectory: '/test', +}; + +describe('Agent Tools', () => { + describe('agentStartTool', () => { + it('should start an agent and return an instance ID', async () => { + const result = await agentStartTool.execute( + { + description: 'Test agent', + goal: 'Test the agent tools', + projectContext: 'Testing environment', + }, + mockContext, + ); + + expect(result).toHaveProperty('instanceId'); + expect(result).toHaveProperty('status'); + expect(result.status).toBe('Agent started successfully'); + + // Verify the agent state was created + expect(agentStates.has(result.instanceId)).toBe(true); + + const state = agentStates.get(result.instanceId); + expect(state).toHaveProperty('goal', 'Test the agent tools'); + expect(state).toHaveProperty('prompt'); + expect(state).toHaveProperty('completed', false); + expect(state).toHaveProperty('aborted', false); + }); + }); + + describe('agentMessageTool', () => { + it('should retrieve agent state', async () => { + // First start an agent + const startResult = await agentStartTool.execute( + { + description: 'Test agent for message', + goal: 'Test the agent message tool', + projectContext: 'Testing environment', + }, + mockContext, + ); + + // Then get its state + const messageResult = await agentMessageTool.execute( + { + instanceId: startResult.instanceId, + description: 'Checking agent status', + }, + mockContext, + ); + + expect(messageResult).toHaveProperty('output'); + expect(messageResult).toHaveProperty('completed', false); + }); + + it('should handle non-existent agent IDs', async () => { + const result = await agentMessageTool.execute( + { + instanceId: 'non-existent-id', + description: 'Checking non-existent agent', + }, + mockContext, + ); + + expect(result).toHaveProperty('error'); + expect(result.error).toContain('No sub-agent found with ID'); + }); + + it('should terminate an agent when requested', async () => { + // First start an agent + const startResult = await agentStartTool.execute( + { + description: 'Test agent for termination', + goal: 'Test agent termination', + projectContext: 'Testing environment', + }, + mockContext, + ); + + // Then terminate it + const messageResult = await agentMessageTool.execute( + { + instanceId: startResult.instanceId, + terminate: true, + description: 'Terminating agent', + }, + mockContext, + ); + + expect(messageResult).toHaveProperty('terminated', true); + expect(messageResult).toHaveProperty('completed', true); + + // Verify the agent state was updated + const state = agentStates.get(startResult.instanceId); + expect(state).toHaveProperty('aborted', true); + expect(state).toHaveProperty('completed', true); + }); + }); +}); \ No newline at end of file diff --git a/packages/agent/src/tools/interaction/agentMessage.ts b/packages/agent/src/tools/interaction/agentMessage.ts new file mode 100644 index 0000000..305659c --- /dev/null +++ b/packages/agent/src/tools/interaction/agentMessage.ts @@ -0,0 +1,132 @@ +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; + +import { Tool } from '../../core/types.js'; +import { agentStates } from './agentStart.js'; + +const parameterSchema = z.object({ + instanceId: z.string().describe('The ID returned by agentStart'), + guidance: z + .string() + .optional() + .describe('Optional guidance or instructions to send to the sub-agent'), + terminate: z + .boolean() + .optional() + .describe('Whether to terminate the sub-agent (default: false)'), + description: z + .string() + .describe('The reason for this agent interaction (max 80 chars)'), +}); + +const returnSchema = z.object({ + output: z.string().describe('The current output from the sub-agent'), + completed: z.boolean().describe('Whether the sub-agent has completed its task'), + error: z.string().optional().describe('Error message if the sub-agent encountered an error'), + terminated: z.boolean().optional().describe('Whether the sub-agent was terminated by this message'), +}); + +type Parameters = z.infer; +type ReturnType = z.infer; + +export const agentMessageTool: Tool = { + name: 'agentMessage', + description: + 'Interacts with a running sub-agent, getting its current state and optionally providing guidance', + logPrefix: '🤖', + parameters: parameterSchema, + parametersJsonSchema: zodToJsonSchema(parameterSchema), + returns: returnSchema, + returnsJsonSchema: zodToJsonSchema(returnSchema), + + execute: async ( + { instanceId, guidance, terminate }, + { logger }, + ): Promise => { + logger.verbose( + `Interacting with sub-agent ${instanceId}${guidance ? ' with guidance' : ''}${terminate ? ' with termination request' : ''}`, + ); + + try { + const agentState = agentStates.get(instanceId); + if (!agentState) { + throw new Error(`No sub-agent found with ID ${instanceId}`); + } + + // Check if the agent was already terminated + if (agentState.aborted) { + return { + output: agentState.output || 'Sub-agent was previously terminated', + completed: true, + terminated: true, + }; + } + + // Terminate the agent if requested + if (terminate) { + agentState.aborted = true; + agentState.completed = true; + + return { + output: agentState.output || 'Sub-agent terminated before completion', + completed: true, + terminated: true, + }; + } + + // Add guidance to the agent state for future implementation + // In a more advanced implementation, this could inject the guidance + // into the agent's execution context + if (guidance) { + logger.info(`Guidance provided to sub-agent ${instanceId}: ${guidance}`); + // This is a placeholder for future implementation + // In a real implementation, we would need to interrupt the agent's + // execution and inject this guidance + } + + // Get the current output + const output = agentState.result?.result || agentState.output || 'No output yet'; + + return { + output, + completed: agentState.completed, + ...(agentState.error && { error: agentState.error }), + }; + } catch (error) { + if (error instanceof Error) { + logger.verbose(`Sub-agent interaction failed: ${error.message}`); + + return { + output: '', + completed: false, + error: error.message, + }; + } + + const errorMessage = String(error); + logger.error(`Unknown error during sub-agent interaction: ${errorMessage}`); + return { + output: '', + completed: false, + error: `Unknown error occurred: ${errorMessage}`, + }; + } + }, + + logParameters: (input, { logger }) => { + logger.info( + `Interacting with sub-agent ${input.instanceId}, ${input.description}${input.terminate ? ' (terminating)' : ''}`, + ); + }, + logReturns: (output, { logger }) => { + if (output.error) { + logger.error(`Sub-agent interaction error: ${output.error}`); + } else if (output.terminated) { + logger.info('Sub-agent was terminated'); + } else if (output.completed) { + logger.info('Sub-agent has completed its task'); + } else { + logger.info('Sub-agent is still running'); + } + }, +}; \ No newline at end of file diff --git a/packages/agent/src/tools/interaction/agentStart.ts b/packages/agent/src/tools/interaction/agentStart.ts new file mode 100644 index 0000000..75acfc3 --- /dev/null +++ b/packages/agent/src/tools/interaction/agentStart.ts @@ -0,0 +1,168 @@ +import { v4 as uuidv4 } from 'uuid'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; + +import { + getDefaultSystemPrompt, + getModel, +} from '../../core/toolAgent/config.js'; +import { toolAgent } from '../../core/toolAgent/toolAgentCore.js'; +import { Tool, ToolAgentResult, ToolContext } from '../../core/types.js'; +import { getTools } from '../getTools.js'; + +// Define AgentState type +type AgentState = { + goal: string; + prompt: string; + output: string; + completed: boolean; + error?: string; + result?: ToolAgentResult; + context: ToolContext; + workingDirectory: string; + tools: Tool[]; + aborted: boolean; +}; + +// Global map to store agent state +export const agentStates: Map = new Map(); + +const parameterSchema = z.object({ + description: z + .string() + .describe("A brief description of the sub-agent's purpose (max 80 chars)"), + goal: z + .string() + .describe('The main objective that the sub-agent needs to achieve'), + projectContext: z + .string() + .describe('Context about the problem or environment'), + workingDirectory: z + .string() + .optional() + .describe('The directory where the sub-agent should operate'), + relevantFilesDirectories: z + .string() + .optional() + .describe('A list of files, which may include ** or * wildcard characters'), + enableUserPrompt: z + .boolean() + .optional() + .describe('Whether to allow the sub-agent to use the userPrompt tool (default: false)'), +}); + +const returnSchema = z.object({ + instanceId: z.string().describe('The ID of the started agent process'), + status: z.string().describe('The initial status of the agent'), +}); + +type Parameters = z.infer; +type ReturnType = z.infer; + +// Sub-agent specific configuration +const subAgentConfig = { + maxIterations: 50, + model: getModel('anthropic', 'claude-3-7-sonnet-20250219'), + maxTokens: 4096, + temperature: 0.7, + getSystemPrompt: (context: ToolContext) => { + return [ + getDefaultSystemPrompt(context), + 'You are a focused AI sub-agent handling a specific task.', + 'You have access to the same tools as the main agent but should focus only on your assigned task.', + 'When complete, call the sequenceComplete tool with your results.', + 'Follow any specific conventions or requirements provided in the task context.', + 'Ask the main agent for clarification if critical information is missing.', + ].join('\n'); + }, +}; + +export const agentStartTool: Tool = { + name: 'agentStart', + description: + 'Starts a sub-agent and returns an instance ID immediately for later interaction', + logPrefix: '🤖', + parameters: parameterSchema, + parametersJsonSchema: zodToJsonSchema(parameterSchema), + returns: returnSchema, + returnsJsonSchema: zodToJsonSchema(returnSchema), + execute: async (params, context) => { + // Validate parameters + const { + description, + goal, + projectContext, + workingDirectory, + relevantFilesDirectories, + enableUserPrompt = false, + } = parameterSchema.parse(params); + + // Construct a well-structured prompt + const prompt = [ + `Description: ${description}`, + `Goal: ${goal}`, + `Project Context: ${projectContext}`, + workingDirectory ? `Working Directory: ${workingDirectory}` : '', + relevantFilesDirectories + ? `Relevant Files:\n ${relevantFilesDirectories}` + : '', + ] + .filter(Boolean) + .join('\n'); + + const tools = getTools({ enableUserPrompt }); + + // Create an instance ID + const instanceId = uuidv4(); + + // Store the agent state + const agentState: AgentState = { + goal, + prompt, + output: '', + completed: false, + context: { ...context }, + workingDirectory: workingDirectory ?? context.workingDirectory, + tools, + aborted: false, + }; + + agentStates.set(instanceId, agentState); + + // Start the agent in a separate promise that we don't await + Promise.resolve().then(async () => { + try { + const result = await toolAgent(prompt, tools, subAgentConfig, { + ...context, + workingDirectory: workingDirectory ?? context.workingDirectory, + }); + + // Update agent state with the result + const state = agentStates.get(instanceId); + if (state && !state.aborted) { + state.completed = true; + state.result = result; + state.output = result.result; + } + } catch (error) { + // Update agent state with the error + const state = agentStates.get(instanceId); + if (state && !state.aborted) { + state.completed = true; + state.error = error instanceof Error ? error.message : String(error); + } + } + }); + + return { + instanceId, + status: 'Agent started successfully', + }; + }, + logParameters: (input, { logger }) => { + logger.info(`Starting sub-agent for task "${input.description}"`); + }, + logReturns: (output, { logger }) => { + logger.info(`Sub-agent started with instance ID: ${output.instanceId}`); + }, +}; \ No newline at end of file