The gap between a prototype AI agent and a production one is mostly error handling, retry logic, and observability. A prototype works 80% of the time. A production agent handles the other 20% gracefully.
The Agentic Loop
An AI agent that can use tools runs in a loop:
async function runAgent(userMessage: string, tools: Tool[]): Promise<string> {
const messages: Message[] = [{ role: 'user', content: userMessage }]
while (true) {
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 4096,
tools,
messages,
})
// Add assistant response to history
messages.push({ role: 'assistant', content: response.content })
// If model is done, return the text
if (response.stop_reason === 'end_turn') {
const textBlock = response.content.find(b => b.type === 'text')
return textBlock?.text ?? ''
}
// If model wants to use tools, execute them
if (response.stop_reason === 'tool_use') {
const toolResults = await executeTools(response.content, tools)
messages.push({ role: 'user', content: toolResults })
}
}
}
Tool Execution with Error Handling
async function executeTools(
content: ContentBlock[],
tools: Tool[]
): Promise<ToolResultBlock[]> {
const toolUseBlocks = content.filter(b => b.type === 'tool_use')
return Promise.all(
toolUseBlocks.map(async (block) => {
try {
const tool = tools.find(t => t.name === block.name)
if (!tool) throw new Error(`Unknown tool: ${block.name}`)
const result = await tool.execute(block.input)
return {
type: 'tool_result' as const,
tool_use_id: block.id,
content: JSON.stringify(result),
}
} catch (error) {
// Return error to the model -- let it decide how to recover
return {
type: 'tool_result' as const,
tool_use_id: block.id,
is_error: true,
content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
}
}
})
)
}
Returning errors to the model (rather than throwing) lets Claude decide whether to retry, use a different approach, or report the failure.
Loop Guard
Prevent infinite loops with a step counter:
async function runAgent(userMessage: string, tools: Tool[], maxSteps = 10) {
let steps = 0
while (steps < maxSteps) {
steps++
// ... loop body
if (steps >= maxSteps) {
logger.warn({ steps, userMessage }, 'Agent hit max steps')
return 'I was unable to complete the task within the allowed steps.'
}
}
}
Observability: Tracing Tool Calls
async function runAgentWithTracing(userMessage: string, agentId: string) {
const traceId = crypto.randomUUID()
const steps: AgentStep[] = []
// ... run agent, recording each step
steps.push({
type: 'tool_call',
toolName: block.name,
input: block.input,
output: result,
durationMs: Date.now() - stepStart,
success: true,
})
// Save full trace to database
await db.agentTrace.create({
data: {
traceId,
agentId,
userMessage,
steps: JSON.stringify(steps),
totalSteps: steps.length,
durationMs: Date.now() - startTime,
success: true,
},
})
}
Token Budget Management
const TOKEN_BUDGET = 50000 // per agent run
let tokensUsed = 0
// After each API call
tokensUsed += response.usage.input_tokens + response.usage.output_tokens
if (tokensUsed > TOKEN_BUDGET * 0.8) {
logger.warn({ tokensUsed, budget: TOKEN_BUDGET }, 'Approaching token budget')
}
if (tokensUsed > TOKEN_BUDGET) {
return 'Token budget exceeded. Stopping to prevent excessive cost.'
}
Parallel Tool Execution
Claude can request multiple tools in one response. Execute them in parallel:
// Claude returns multiple tool_use blocks at once
// Run them concurrently
const results = await Promise.allSettled(
toolUseBlocks.map(block => executeOneTool(block))
)
// All results (success and failure) go back to Claude
The Workflow Automator MCP at whoffagents.com lets Claude trigger n8n and Make.com workflows as agent tool calls — connect your AI to any automation. $15/mo.
Top comments (0)