UNPKG

hataraku

Version:

An autonomous coding agent for building AI-powered development tools. The name "Hataraku" (åƒć) means "to work" in Japanese.

496 lines (495 loc) • 23.3 kB
import { generateText, generateObject, streamText } from 'ai'; import { Thread } from './thread/thread'; import { v4 as uuid } from 'uuid'; import { log } from '../utils/colors'; const DEFAULT_MAX_STEPS = 25; const DEFAULT_MAX_RETRIES = 4; /** * Agent class that can execute tasks using a language model. * The agent can use tools, maintain conversation history, and generate structured responses. */ export class Agent { /** The name of the agent */ name; /** A description of what the agent does */ description; /** Promise that resolves to the language model used by the agent */ modelPromise; /** Set of tools the agent can use to perform tasks */ tools; /** Settings to customize model API calls */ callSettings; /** System instructions that define the agent's behavior and capabilities */ role; /** Optional task history manager to track agent interactions */ taskHistory; /** Whether to enable verbose logging of agent operations */ verbose; /** Whether to enable prompt caching */ enableCaching; constructor(config) { if (!config.name || config.name.trim() === '') { throw new Error('Agent name cannot be empty'); } this.name = config.name; this.description = config.description; this.modelPromise = config.model ? (config.model instanceof Promise ? config.model : Promise.resolve(config.model)) : undefined; this.tools = config.tools || {}; this.callSettings = config.callSettings || {}; this.role = config.role; this.taskHistory = config.taskHistory; this.verbose = config.verbose || false; this.enableCaching = config.enableCaching ?? true; // Enable caching by default } /** * Gets the system prompt content for the agent * @returns The system prompt content */ getSystemPrompt() { return ` ROLE: ${this.role} DESCRIPTION given to the user: ${this.description} `; } /** * Detects the provider from the model name * @param model The language model * @returns The provider name or undefined if not detected */ detectProvider(model) { const modelName = model.constructor.name.toLowerCase(); if (modelName.includes('anthropic')) { return 'anthropic'; } else if (modelName.includes('openai')) { return 'openai'; } else if (modelName.includes('bedrock')) { return 'bedrock'; } else if (modelName.includes('vertex')) { return 'vertex'; } else if (modelName.includes('openrouter')) { return 'openrouter'; } return undefined; } async task(task, input) { const model = input?.model || (this.modelPromise ? await this.modelPromise : undefined); if (!model) { throw new Error('No model provided'); } const thread = input?.thread || new Thread(); const maxSteps = this.callSettings.maxSteps || DEFAULT_MAX_STEPS; const maxRetries = this.callSettings.maxRetries || DEFAULT_MAX_RETRIES; // Add system message if the thread doesn't have one if (!thread.hasSystemMessage()) { thread.addSystemMessage(this.getSystemPrompt()); } // Detect provider and add cache control point to system message const provider = this.detectProvider(model); if (provider && this.enableCaching) { thread.addCacheControlPointToSystemMessage(provider); } thread.addMessage('user', task); // Use verbose mode if specified in input or agent config const isVerbose = input?.verbose !== undefined ? input.verbose : this.verbose; if (isVerbose) { log.system('\nšŸ” Task details:'); log.system(`Task: ${task}`); log.system(`Max steps: ${maxSteps}, Max retries: ${maxRetries}`); log.system(`Stream: ${input?.stream ? 'enabled' : 'disabled'}`); log.system('Thread history:'); for (const msg of thread.getMessages()) { log.system(`- ${msg.role}: ${msg.content.substring(0, 50)}${msg.content.length > 50 ? '...' : ''}`); } } // Create history entry const taskId = uuid(); const historyEntry = { taskId, timestamp: Date.now(), task, tokensIn: 0, tokensOut: 0, cacheWrites: 0, cacheReads: 0, totalCost: 0, model: model.constructor.name, messages: thread.getMessages() .filter(msg => msg.role === 'user' || msg.role === 'assistant') .map(msg => ({ role: msg.role, content: msg.content })), debug: { requests: [{ timestamp: Date.now(), systemPrompt: this.getSystemPrompt(), messages: thread.getFormattedMessages() .filter(msg => msg.role === 'user' || msg.role === 'assistant') .map(msg => ({ role: msg.role, content: Array.isArray(msg.content) ? msg.content.map(part => 'text' in part ? part.text : '').join('') : msg.content })) }], responses: [], toolUsage: [] } }; if (input?.stream) { if (isVerbose) { log.system('\nšŸ”„ Starting streaming text generation...'); } try { const { textStream } = streamText({ model, messages: thread.getFormattedMessages(), maxSteps, maxRetries, tools: this.tools, ...this.callSettings, onChunk: (event) => { if (isVerbose) { const chunk = event.chunk; if (chunk.type === 'text-delta' || chunk.type === 'reasoning') { log.system(chunk.textDelta); } else { log.system(JSON.stringify(chunk)); } } }, onStepFinish: (step) => { if (isVerbose) { log.system(`\nšŸ“ Step finished:`); if (step.reasoning) { log.system(`<thinking> ${step.reasoning?.substring(0, 100)}${step.reasoning?.length > 100 ? '...' : ''}</thinking>`); } log.system(`<response> ${step.text}</response>`); // Log tool calls if any if (step.toolCalls && step.toolCalls.length > 0) { log.system(`\nšŸ”§ Tool calls in this step: ${step.toolCalls.length}`); for (let i = 0; i < step.toolCalls.length; i++) { const toolCall = step.toolCalls[i]; log.system(`Tool call #${i + 1}: ${JSON.stringify(toolCall).substring(0, 150)}...`); } } } }, onError: (error) => { if (isVerbose) { log.system(`\nāŒ Error streaming text: ${error instanceof Error ? error.message : String(error)}`); } console.error('Error streaming text', error); if (this.taskHistory && historyEntry.debug) { historyEntry.debug.responses.push({ timestamp: Date.now(), content: error instanceof Error ? error.message : String(error), usage: { tokensIn: 0, tokensOut: 0, // cost: 0 } }); // Add to tool usage for error tracking historyEntry.debug.toolUsage.push({ timestamp: Date.now(), tool: 'stream', params: {}, result: error instanceof Error ? error.message : String(error), error: true }); this.taskHistory.saveTask(historyEntry).catch(console.error); } }, onFinish: (result) => { if (isVerbose) { log.system(`\nāœ… Streaming completed: ${result.text.substring(0, 50)}${result.text.length > 50 ? '...' : ''}`); log.system(`Tokens: In=${result.usage?.promptTokens || 0}, Out=${result.usage?.completionTokens || 0}`); } thread.addMessage('assistant', result.text, { usage: { tokensIn: result.usage?.promptTokens || 0, tokensOut: result.usage?.completionTokens || 0, // providerMetadata: result.providerMetadata } }); if (this.taskHistory && historyEntry.debug) { historyEntry.debug.responses.push({ timestamp: Date.now(), content: result.text, usage: { tokensIn: result.usage?.promptTokens || 0, tokensOut: result.usage?.completionTokens || 0 } }); historyEntry.tokensIn += result.usage?.promptTokens || 0; historyEntry.tokensOut += result.usage?.completionTokens || 0; this.taskHistory.saveTask(historyEntry).catch(console.error); } // Add cache control point to the last message if (provider && this.enableCaching) { thread.addCacheControlPointToLastMessage(provider); } } }); return textStream; } catch (error) { console.error('Error streaming text', error); throw error; } } try { let result; if (Object.keys(this.tools).length > 0 && input?.schema) { if (isVerbose) { log.system('\nšŸ”„ Starting text generation with tools and schema...'); log.system(JSON.stringify(this.tools)); } result = await generateText({ model, messages: thread.getFormattedMessages(), maxSteps, maxRetries, onStepFinish: (step) => { if (isVerbose) { log.system(`\nšŸ“ Step finished:`); log.system(`Reasoning: ${step.reasoning?.substring(0, 100)}${step.reasoning && step.reasoning.length > 100 ? '...' : ''}`); log.system(`Text: ${step.text.substring(0, 100)}${step.text.length > 100 ? '...' : ''}`); // Log tool calls if any if (step.toolCalls && step.toolCalls.length > 0) { log.system(`\nšŸ”§ Tool calls in this step: ${step.toolCalls.length}`); for (let i = 0; i < step.toolCalls.length; i++) { const toolCall = step.toolCalls[i]; log.system(`Tool call #${i + 1}: ${JSON.stringify(toolCall).substring(0, 150)}...`); } } } }, tools: this.tools, ...this.callSettings, }); if (isVerbose) { log.system(`\nāœ… Text generation completed: ${result.text.substring(0, 50)}${result.text.length > 50 ? '...' : ''}`); log.system(`Tokens: In=${result.usage?.promptTokens || 0}, Out=${result.usage?.completionTokens || 0}`); } thread.addMessage('assistant', result.text, { usage: { tokensIn: result.usage?.promptTokens || 0, tokensOut: result.usage?.completionTokens || 0, // providerMetadata: result.providerMetadata } }); // Add cache control point to the assistant message if (provider && this.enableCaching) { thread.addCacheControlPointToLastMessage(provider); } thread.addMessage('user', 'Based on the last response, please create a response that matches the schema provided. It must be valid JSON and match the schema exactly.'); if (this.taskHistory && historyEntry.debug) { historyEntry.debug.responses.push({ timestamp: Date.now(), content: result.text, usage: { tokensIn: result.usage?.promptTokens || 0, tokensOut: result.usage?.completionTokens || 0, // providerMetadata: result.providerMetadata } }); historyEntry.tokensIn += result.usage?.promptTokens || 0; historyEntry.tokensOut += result.usage?.completionTokens || 0; } if (isVerbose) { log.system('\nšŸ”„ Starting object generation with schema...'); } const { object } = await generateObject({ model, messages: thread.getFormattedMessages(), maxRetries, maxSteps, schema: input.schema, ...this.callSettings, }); if (isVerbose) { log.system(`\nāœ… Object generation completed: ${JSON.stringify(object).substring(0, 50)}${JSON.stringify(object).length > 50 ? '...' : ''}`); } thread.addMessage('assistant', JSON.stringify(object), { usage: { tokensIn: result.usage?.promptTokens || 0, tokensOut: result.usage?.completionTokens || 0, // providerMetadata: result.providerMetadata } }); // Add cache control point to the last message if (provider && this.enableCaching) { thread.addCacheControlPointToLastMessage(provider); } if (this.taskHistory && historyEntry.debug) { historyEntry.debug.responses.push({ timestamp: Date.now(), content: JSON.stringify(object), usage: { tokensIn: result.usage?.promptTokens || 0, tokensOut: result.usage?.completionTokens || 0, providerMetadata: result.providerMetadata } }); this.taskHistory.saveTask(historyEntry).catch(console.error); } return object; } if (input?.schema) { if (isVerbose) { log.system('\nšŸ”„ Starting object generation with schema...'); } const result = await generateObject({ model, messages: thread.getFormattedMessages(), maxRetries, maxSteps, schema: input.schema, ...this.callSettings, }); if (isVerbose) { log.system(`\nāœ… Object generation completed: ${JSON.stringify(result.object).substring(0, 50)}${JSON.stringify(result.object).length > 50 ? '...' : ''}`); log.system(`Tokens: In=${result.usage?.promptTokens || 0}, Out=${result.usage?.completionTokens || 0}`); } thread.addMessage('assistant', JSON.stringify(result.object), { usage: { tokensIn: result.usage?.promptTokens || 0, tokensOut: result.usage?.completionTokens || 0, // providerMetadata: result.providerMetadata } }); // Add cache control point to the last message if (provider && this.enableCaching) { thread.addCacheControlPointToLastMessage(provider); } if (this.taskHistory && historyEntry.debug) { historyEntry.debug.responses.push({ timestamp: Date.now(), content: JSON.stringify(result.object), usage: { tokensIn: result.usage?.promptTokens || 0, tokensOut: result.usage?.completionTokens || 0, providerMetadata: result.providerMetadata } }); historyEntry.tokensIn += result.usage?.promptTokens || 0; historyEntry.tokensOut += result.usage?.completionTokens || 0; this.taskHistory.saveTask(historyEntry).catch(console.error); } return result.object; } if (isVerbose) { log.system('\nšŸ”„ Starting text generation...'); } result = await generateText({ model, messages: thread.getFormattedMessages(), maxSteps, maxRetries, onStepFinish: (step) => { if (isVerbose) { log.system(`\nšŸ“ Step finished:`); log.system(`Reasoning: ${step.reasoning?.substring(0, 100)}${step.reasoning && step.reasoning.length > 100 ? '...' : ''}`); log.system(`Text: ${step.text.substring(0, 100)}${step.text.length > 100 ? '...' : ''}`); // Log tool calls if any if (step.toolCalls && step.toolCalls.length > 0) { log.system(`\nšŸ”§ Tool calls in this step: ${step.toolCalls.length}`); for (let i = 0; i < step.toolCalls.length; i++) { const toolCall = step.toolCalls[i]; log.system(`Tool call #${i + 1}: ${JSON.stringify(toolCall).substring(0, 150)}...`); } } } }, tools: this.tools, ...this.callSettings, }); if (isVerbose) { log.system(`\nāœ… Text generation completed: ${result.text.substring(0, 50)}${result.text.length > 50 ? '...' : ''}`); log.system(`Tokens: In=${result.usage?.promptTokens || 0}, Out=${result.usage?.completionTokens || 0}`); } thread.addMessage('assistant', result.text, { usage: { tokensIn: result.usage?.promptTokens || 0, tokensOut: result.usage?.completionTokens || 0, // providerMetadata: result.providerMetadata } }); // Add cache control point to the last message if (provider && this.enableCaching) { thread.addCacheControlPointToLastMessage(provider); } if (this.taskHistory && historyEntry.debug) { historyEntry.debug.responses.push({ timestamp: Date.now(), content: result.text, usage: { tokensIn: result.usage?.promptTokens || 0, tokensOut: result.usage?.completionTokens || 0, providerMetadata: result.providerMetadata } }); historyEntry.tokensIn += result.usage?.promptTokens || 0; historyEntry.tokensOut += result.usage?.completionTokens || 0; this.taskHistory.saveTask(historyEntry).catch(console.error); } return result.text; } catch (error) { if (isVerbose) { log.system(`\nāŒ Error in task execution: ${error instanceof Error ? error.message : String(error)}`); } if (this.taskHistory && historyEntry.debug) { historyEntry.debug.responses.push({ timestamp: Date.now(), content: error instanceof Error ? error.message : String(error), usage: { tokensIn: 0, tokensOut: 0, providerMetadata: {} } }); // Add to tool usage for error tracking historyEntry.debug.toolUsage.push({ timestamp: Date.now(), tool: 'task', params: {}, result: error instanceof Error ? error.message : String(error), error: true }); this.taskHistory.saveTask(historyEntry).catch(console.error); } throw error; } } } /** * Creates a new Agent instance with the provided configuration. * * @param config - The configuration options for the agent * @returns A new Agent instance * * @example * ```typescript * const agent = createAgent({ * name: 'MyAssistant', * description: 'A helpful assistant', * role: 'You are a helpful assistant that answers questions accurately.', * model: new OpenAI({ apiKey: process.env.OPENAI_API_KEY }), * verbose: true * }); * ``` */ export function createAgent(config) { return new Agent(config); } //# sourceMappingURL=agent.js.map