// LOADING
// LOADING
// LOADING_ARTICLE
"Agent" is an overloaded word, but in practice it means one thing: an LLM in a loop that can take actions based on its own outputs. The model decides what to do, calls a tool, observes the result, and continues until it reaches a stopping condition. That loop is simple to describe and surprisingly difficult to build reliably.
This post covers the agent loop in detail, common architectural patterns, tool design, and the failure modes worth planning for before you ship anything to production. For grounding in the individual building blocks, Tool Use and Function Calling with LLMs and Prompt Engineering for Developers are useful prerequisites.
At its core, an agent is a while loop:
typescriptimport Anthropic from '@anthropic-ai/sdk'; const client = new Anthropic(); type Tool = { name: string; description: string; input_schema: object; execute: (input: Record<string, unknown>) => Promise<string>; }; async function runAgent( userMessage: string, tools: Tool[], maxIterations = 10, ): Promise<string> { const messages: Anthropic.MessageParam[] = [ { role: 'user', content: userMessage }, ]; const toolDefs = tools.map(({ name, description, input_schema }) => ({ name, description, input_schema, })); for (let i = 0; i < maxIterations; i++) { const response = await client.messages.create({ model: 'claude-opus-4-8', max_tokens: 4096, tools: toolDefs, messages, }); // Append assistant response to history messages.push({ role: 'assistant', content: response.content }); if (response.stop_reason === 'end_turn') { const text = response.content.find((b) => b.type === 'text'); return text ? text.text : ''; } if (response.stop_reason === 'tool_use') { const toolResults: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type !== 'tool_use') continue; 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 as Record<string, unknown>); toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: result, }); } messages.push({ role: 'user', content: toolResults }); } } throw new Error(`Agent exceeded max iterations (${maxIterations})`); }
Tools are the primary surface where agents interact with the real world. Their design determines whether the agent behaves reliably or erratically.
A tool named manageDatabase that can read, write, and delete is harder for the model to use correctly than three focused tools. Narrow tools have smaller input schemas, clearer descriptions, and more predictable behavior. The model's tool-selection accuracy improves when tools are distinct.
The description is the model's only documentation. Include what the tool does, what its inputs mean, and—critically—what it does not do. Avoid vague descriptions like "does database stuff."
typescriptconst searchPostsTool: Tool = { name: 'search_posts', description: 'Full-text search across published blog posts. Returns up to 5 matching posts ' + 'with slug, title, and excerpt. Does NOT search drafts or comments.', input_schema: { type: 'object', properties: { query: { type: 'string', description: 'Search terms' }, }, required: ['query'], }, execute: async ({ query }) => { const results = await searchPosts(String(query)); return JSON.stringify(results); }, };
Models can produce malformed tool inputs, especially under ambiguous prompts. Validate with Zod or similar before executing. Return a structured error string rather than throwing—the agent can often self-correct when given an error message as a tool result.
The default pattern: the model reasons in natural language before each tool call. Claude does this naturally when given a system prompt that encourages thinking through the problem. It is transparent and debuggable—you can read the reasoning in the message history.
For multi-step tasks with many dependencies, ask the model to produce a complete plan first, then execute steps in order. This reduces mid-task course corrections and produces more coherent results.
typescript// Two-phase approach const plan = await client.messages.create({ model: 'claude-opus-4-8', max_tokens: 1024, messages: [{ role: 'user', content: `Plan the steps needed to accomplish: ${task}\nList each step on a new line. Do not execute yet.`, }], }); // Then feed the plan as context into the execution loop
For complex tasks, a supervisor agent can delegate to specialist subagents. The supervisor maintains the high-level plan; subagents each have a narrow tool set. This pattern is explored in the Background Jobs and Queues in Node.js context as well—long-running agent tasks belong in a queue with proper retry handling, not in a synchronous request.
Always set a maxIterations cap. Add logic to detect when the agent is calling the same tool with identical inputs repeatedly.
Return structured error objects from tools. Include enough context for the model to decide whether to retry, try an alternative, or surface the error to the user.
Models occasionally call tools that do not exist or pass extra fields. Validate block.name against your tool registry and reject unknown calls gracefully.
Each loop iteration is an API call. Log token usage per iteration. Set a total-token budget in addition to the iteration cap. For cost optimization techniques, Cost and Latency Optimization for LLM Apps covers model selection and caching strategies that apply directly.
Not every task needs the most capable model. A rough heuristic:
claude-opus-4-8): complex reasoning, multi-step planning, tasks requiring judgment.claude-sonnet-4-6): balanced performance and cost for most production agents.claude-haiku-4-5): high-volume, low-complexity tool calls, classification, extraction.claude-fable-5): the most capable model available when maximum reasoning depth is needed.For tasks with multiple subagents, using a cheaper model for leaf-level tool calls and a capable model only for planning can cut costs significantly without degrading quality.
For interactive agents, stream the response as the model produces it rather than waiting for the full output. This is especially valuable for the reasoning steps, which let the user see that the agent is working. Streaming LLM Responses in a Next.js App covers the mechanics.
Agents add latency, cost, and failure surface. A single well-structured prompt often handles tasks that appear to need an agent. Use an agent when:
If the task is deterministic and the steps are fixed, a simple sequential script with LLM calls at specific points is simpler, cheaper, and easier to test.
For practical examples of AI features in a portfolio context, see the projects section and the interview-prep tools. Both use constrained LLM calls rather than full agents—a deliberate choice based on the predictability requirement.
The agent loop is conceptually simple, but production reliability requires deliberate decisions at every layer: tool design, error handling, iteration caps, and model selection. Start with the simplest loop that solves your problem, add telemetry early, and expand complexity only when the simpler approach is demonstrably insufficient.