L', 'BTC-USD'). Must be uppercase.",
},
timeframe: {
type: "string",
enum: ["1m", "5m", "1h", "1d"],
description: "Data aggregation window.",
},
},
required: ["symbol", "timeframe"],
},
},
{
name: "convert_currency",
description: "Convert a numeric value between supported fiat currencies. Supports USD, EUR, GBP, JPY.",
input_schema: {
type: "object",
properties: {
amount: { type: "number", description: "Positive numeric value to convert." },
source: { type: "string", description: "Three-letter ISO currency code." },
target: { type: "string", description: "Three-letter ISO currency code." },
},
required: ["amount", "source", "target"],
},
},
];
**Architecture decision:** Tools are defined as a static registry with strict `input_schema` objects. This forces the model to route requests deterministically. The `enum` constraint on `timeframe` and explicit type declarations prevent hallucinated parameters.
### Step 2: Implement the Agent Loop
The loop must handle three phases: inference, tool execution, and state reconciliation. It requires explicit termination conditions and iteration limits.
```typescript
async function runAgentLoop(
userPrompt: string,
toolRegistry: ToolRegistry,
maxIterations: number = 10
): Promise<string> {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userPrompt },
];
for (let i = 0; i < maxIterations; i++) {
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
tools: TOOLS,
messages,
});
const stopReason = response.stop_reason;
if (stopReason === "end_turn") {
const textContent = response.content.find(
(block) => block.type === "text"
);
return textContent?.text ?? "No response generated.";
}
if (stopReason === "tool_use") {
const toolBlocks = response.content.filter(
(block): block is Anthropic.ToolUseBlock => block.type === "tool_use"
);
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of toolBlocks) {
const executor = toolRegistry[block.name];
if (!executor) {
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: `Error: Tool '${block.name}' not registered.`,
is_error: true,
});
continue;
}
try {
const output = await executor(block.input);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: output,
});
} catch (err) {
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: `Execution failed: ${err instanceof Error ? err.message : "Unknown error"}`,
is_error: true,
});
}
}
messages.push(
{ role: "assistant", content: response.content },
{ role: "user", content: toolResults }
);
}
}
return "Agent exceeded maximum iteration limit. Task incomplete.";
}
Architecture decision: The loop uses a for counter instead of while(true) to guarantee termination. Tool execution is isolated in a try/catch block to prevent unhandled exceptions from breaking the conversation state. Results are appended as tool_result blocks, maintaining the strict message schema required by the API. Parallel tool calls are naturally supported because toolBlocks is an array.
Step 3: Wire the Registry and Execute
const registry: ToolRegistry = {
fetch_market_snapshot: async ({ symbol, timeframe }) => {
// Simulated external API call
return JSON.stringify({
symbol,
timeframe,
price: (Math.random() * 200).toFixed(2),
volume: Math.floor(Math.random() * 1000000),
timestamp: new Date().toISOString(),
});
},
convert_currency: async ({ amount, source, target }) => {
const rates: Record<string, Record<string, number>> = {
USD: { EUR: 0.92, GBP: 0.79, JPY: 149.5 },
EUR: { USD: 1.09, GBP: 0.86, JPY: 162.4 },
GBP: { USD: 1.27, EUR: 1.16, JPY: 189.2 },
JPY: { USD: 0.0067, EUR: 0.0062, GBP: 0.0053 },
};
const rate = rates[source]?.[target];
if (!rate) throw new Error(`Unsupported conversion: ${source} to ${target}`);
return JSON.stringify({
original: amount,
converted: +(amount * rate).toFixed(4),
rate,
});
},
};
// Execution
runAgentLoop("Convert 500 USD to EUR and fetch the 1h snapshot for BTC-USD.", registry)
.then(console.log)
.catch(console.error);
Architecture decision: The registry pattern decouples tool logic from the loop controller. This enables hot-swapping implementations, mocking for tests, and clear separation of concerns. External calls are wrapped in async functions to prevent blocking the event loop during API latency.
Pitfall Guide
Explanation: Vague descriptions like "get data" or "convert things" cause the model to misroute requests or invent parameters.
Fix: Include explicit use-case triggers, parameter constraints, and output format expectations in the description field. Use enum for bounded inputs.
2. Missing Loop Termination Guards
Explanation: Using while(true) without iteration caps or stop_reason checks leads to infinite loops when the model fails to reach end_turn.
Fix: Always implement a maximum iteration counter and explicitly check response.stop_reason. Return a graceful fallback message when the limit is reached.
3. Context Window Bloat from Tool Outputs
Explanation: Appending raw tool responses without size limits quickly exhausts the context window, increasing latency and cost.
Fix: Truncate or summarize tool outputs before appending. Implement a maxOutputLength parameter in your registry and strip unnecessary metadata.
Explanation: Running blocking I/O inside the loop stalls the event loop and degrades throughput under concurrent load.
Fix: Ensure all registry functions are async. Use Promise.all if you need to batch independent tool calls, though the SDK handles parallel tool routing natively.
5. Over-Delegating Business Logic to the Model
Explanation: Asking the model to perform calculations, string parsing, or conditional branching inside tool descriptions causes hallucination and inconsistent results.
Fix: Keep tools deterministic. The model should only route requests and format final responses. All computation belongs in the registry functions.
6. Ignoring Rate Limits and Token Budgets
Explanation: Unbounded agent loops can trigger API rate limits or exceed monthly token allocations without warning.
Fix: Implement token counting per turn, add exponential backoff on 429 responses, and set hard budget caps in your orchestration layer.
7. Inconsistent Error Propagation
Explanation: Swallowing tool errors or returning plain strings without is_error: true causes the model to retry failed tools indefinitely.
Fix: Always return structured tool_result blocks with is_error: true when execution fails. Include actionable error messages so the model can adjust its next step.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single-step data retrieval | Direct tool call (no loop) | Eliminates unnecessary inference turns | Low (1x API call) |
| Multi-step conditional workflow | Structured agent loop with max iterations | Handles branching logic safely | Medium (2-4x API calls) |
| High-throughput batch processing | Pre-compiled tool sequences + parallel execution | Bypasses model routing overhead | Low (deterministic routing) |
| Customer-facing assistant | Agent loop + context pruning + budget caps | Prevents runaway costs and hallucinations | Medium-High (controlled) |
| Internal data pipeline | Static orchestration (LangGraph/Airflow) | Removes model dependency for reliability | Low (infrastructure only) |
Configuration Template
// agent.config.ts
import Anthropic from "@anthropic-ai/sdk";
export const AGENT_CONFIG = {
model: "claude-sonnet-4-20250514",
maxTokens: 1024,
maxIterations: 10,
toolOutputLimit: 2000,
temperature: 0.2,
systemPrompt: `You are a precise routing agent. Use tools only when explicitly required.
Do not invent data. Return structured results. Stop when the task is complete.`,
};
export const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
maxRetries: 3,
timeout: 30000,
});
export type ToolExecutor = (args: Record<string, unknown>) => Promise<string>;
export type ToolRegistry = Record<string, ToolExecutor>;
Quick Start Guide
- Install dependencies: Run
npm install @anthropic-ai-sdk dotenv and create a .env file with ANTHROPIC_API_KEY=sk-ant-...
- Define your schema: Create a
TOOLS array with strict input_schema objects. Include enum constraints and explicit descriptions.
- Build the registry: Implement async functions for each tool. Wrap external calls in
try/catch and return JSON strings.
- Initialize the loop: Import the
runAgentLoop function, pass your prompt and registry, and set maxIterations to 10.
- Validate execution: Run 3-5 test prompts covering edge cases. Verify
stop_reason transitions, check token usage in the API dashboard, and confirm tool outputs match expectations.