hataraku
Version:
An autonomous coding agent for building AI-powered development tools. The name "Hataraku" (åć) means "to work" in Japanese.
501 lines (500 loc) ⢠23.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Agent = void 0;
exports.createAgent = createAgent;
const ai_1 = require("ai");
const thread_1 = require("./thread/thread");
const uuid_1 = require("uuid");
const colors_1 = require("../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.
*/
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_1.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) {
colors_1.log.system('\nš Task details:');
colors_1.log.system(`Task: ${task}`);
colors_1.log.system(`Max steps: ${maxSteps}, Max retries: ${maxRetries}`);
colors_1.log.system(`Stream: ${input?.stream ? 'enabled' : 'disabled'}`);
colors_1.log.system('Thread history:');
for (const msg of thread.getMessages()) {
colors_1.log.system(`- ${msg.role}: ${msg.content.substring(0, 50)}${msg.content.length > 50 ? '...' : ''}`);
}
}
// Create history entry
const taskId = (0, uuid_1.v4)();
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) {
colors_1.log.system('\nš Starting streaming text generation...');
}
try {
const { textStream } = (0, ai_1.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') {
colors_1.log.system(chunk.textDelta);
}
else {
colors_1.log.system(JSON.stringify(chunk));
}
}
},
onStepFinish: (step) => {
if (isVerbose) {
colors_1.log.system(`\nš Step finished:`);
if (step.reasoning) {
colors_1.log.system(`<thinking> ${step.reasoning?.substring(0, 100)}${step.reasoning?.length > 100 ? '...' : ''}</thinking>`);
}
colors_1.log.system(`<response> ${step.text}</response>`);
// Log tool calls if any
if (step.toolCalls && step.toolCalls.length > 0) {
colors_1.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];
colors_1.log.system(`Tool call #${i + 1}: ${JSON.stringify(toolCall).substring(0, 150)}...`);
}
}
}
},
onError: (error) => {
if (isVerbose) {
colors_1.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) {
colors_1.log.system(`\nā
Streaming completed: ${result.text.substring(0, 50)}${result.text.length > 50 ? '...' : ''}`);
colors_1.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) {
colors_1.log.system('\nš Starting text generation with tools and schema...');
colors_1.log.system(JSON.stringify(this.tools));
}
result = await (0, ai_1.generateText)({
model,
messages: thread.getFormattedMessages(),
maxSteps,
maxRetries,
onStepFinish: (step) => {
if (isVerbose) {
colors_1.log.system(`\nš Step finished:`);
colors_1.log.system(`Reasoning: ${step.reasoning?.substring(0, 100)}${step.reasoning && step.reasoning.length > 100 ? '...' : ''}`);
colors_1.log.system(`Text: ${step.text.substring(0, 100)}${step.text.length > 100 ? '...' : ''}`);
// Log tool calls if any
if (step.toolCalls && step.toolCalls.length > 0) {
colors_1.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];
colors_1.log.system(`Tool call #${i + 1}: ${JSON.stringify(toolCall).substring(0, 150)}...`);
}
}
}
},
tools: this.tools,
...this.callSettings,
});
if (isVerbose) {
colors_1.log.system(`\nā
Text generation completed: ${result.text.substring(0, 50)}${result.text.length > 50 ? '...' : ''}`);
colors_1.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) {
colors_1.log.system('\nš Starting object generation with schema...');
}
const { object } = await (0, ai_1.generateObject)({
model,
messages: thread.getFormattedMessages(),
maxRetries,
maxSteps,
schema: input.schema,
...this.callSettings,
});
if (isVerbose) {
colors_1.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) {
colors_1.log.system('\nš Starting object generation with schema...');
}
const result = await (0, ai_1.generateObject)({
model,
messages: thread.getFormattedMessages(),
maxRetries,
maxSteps,
schema: input.schema,
...this.callSettings,
});
if (isVerbose) {
colors_1.log.system(`\nā
Object generation completed: ${JSON.stringify(result.object).substring(0, 50)}${JSON.stringify(result.object).length > 50 ? '...' : ''}`);
colors_1.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) {
colors_1.log.system('\nš Starting text generation...');
}
result = await (0, ai_1.generateText)({
model,
messages: thread.getFormattedMessages(),
maxSteps,
maxRetries,
onStepFinish: (step) => {
if (isVerbose) {
colors_1.log.system(`\nš Step finished:`);
colors_1.log.system(`Reasoning: ${step.reasoning?.substring(0, 100)}${step.reasoning && step.reasoning.length > 100 ? '...' : ''}`);
colors_1.log.system(`Text: ${step.text.substring(0, 100)}${step.text.length > 100 ? '...' : ''}`);
// Log tool calls if any
if (step.toolCalls && step.toolCalls.length > 0) {
colors_1.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];
colors_1.log.system(`Tool call #${i + 1}: ${JSON.stringify(toolCall).substring(0, 150)}...`);
}
}
}
},
tools: this.tools,
...this.callSettings,
});
if (isVerbose) {
colors_1.log.system(`\nā
Text generation completed: ${result.text.substring(0, 50)}${result.text.length > 50 ? '...' : ''}`);
colors_1.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) {
colors_1.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;
}
}
}
exports.Agent = Agent;
/**
* 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
* });
* ```
*/
function createAgent(config) {
return new Agent(config);
}
//# sourceMappingURL=agent.js.map