Five tool-calling patterns that separate hobby AI agents from production ones A developer outlines five tool-calling patterns that distinguish hobby AI agents from production-ready systems, including hard tool call budgets, deduplication, error handling, and safety checks. The patterns address common failures like infinite loops, repeated calls, and confabulated responses, with code examples using the Anthropic SDK. Almost every "build an AI agent" tutorial ends the same way: the model calls a tool, the tool returns data, the model uses the data to respond. It works in the demo. What the tutorial doesn't show: what happens when the tool times out. Or when the model calls the same tool three times in a row. Or when the model calls a destructive tool without the user intending it. Or when a tool returns an error and the model confabulates a response anyway. These aren't edge cases — they're the normal operating conditions of a production agent. Here are five patterns I use on every agent I ship to handle them. By default, most agent frameworks will let the model call tools indefinitely until it decides to stop and respond. This is fine in demos. In production, it means a single misbehaving agent can loop through dozens of API calls and rack up costs before anyone notices. The fix is a hard tool call budget per turn. python import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic ; async function runAgentWithBudget messages: Anthropic.MessageParam , tools: Anthropic.Tool , maxToolCalls = 5 : Promise<{ content: string; toolCallCount: number; hitBudget: boolean } { let toolCallCount = 0; let currentMessages = ...messages ; while true { const response = await client.messages.create { model: "claude-sonnet-4-5", max tokens: 2048, tools, messages: currentMessages, } ; // Model is done calling tools if response.stop reason === "end turn" { const text = response.content .filter b : b is Anthropic.TextBlock = b.type === "text" .map b = b.text .join "" ; return { content: text, toolCallCount, hitBudget: false }; } // Model wants to use tools if response.stop reason === "tool use" { const toolUseBlocks = response.content.filter b : b is Anthropic.ToolUseBlock = b.type === "tool use" ; toolCallCount += toolUseBlocks.length; // Budget exceeded — stop and tell the model if toolCallCount maxToolCalls { const budgetMessage: Anthropic.MessageParam = { role: "user", content: { type: "tool result", tool use id: toolUseBlocks 0 .id, content: "Tool call budget exceeded. Please respond with what you know so far.", is error: true, } , }; // One final completion without tools const finalResponse = await client.messages.create { model: "claude-sonnet-4-5", max tokens: 1024, messages: ...currentMessages, { role: "assistant", content: response.content }, budgetMessage , } ; const text = finalResponse.content .filter b : b is Anthropic.TextBlock = b.type === "text" .map b = b.text .join "" ; return { content: text, toolCallCount, hitBudget: true }; } // Execute the tools and continue const toolResults = await Promise.all toolUseBlocks.map async block = { type: "tool result" as const, tool use id: block.id, content: await executeToolSafely block.name, block.input , } ; currentMessages = ...currentMessages, { role: "assistant", content: response.content }, { role: "user", content: toolResults }, ; } } } The maxToolCalls = 5 default is conservative. Adjust based on what your agent actually does. For a simple lookup agent, 3 is plenty. For a research agent doing multi-step synthesis, 10-15 might be appropriate. The point is to have a limit at all. A common agent failure mode: the model calls the same tool with the same arguments multiple times in one turn or across turns . This is wasteful at best and dangerous at worst — imagine calling send email twice with the same content. class ToolCallDeduplicator { private seen = new Map