DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Building Production AI Agents: Agentic Loops, Tool Execution, Error Recovery, and Observability

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 })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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'}`,
        }
      }
    })
  )
}
Enter fullscreen mode Exit fullscreen mode

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.'
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
    },
  })
}
Enter fullscreen mode Exit fullscreen mode

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.'
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)