Anthropic's Tool Use Primitives for Building Agents Directly
Build a bounded tool-using agent on Anthropic's Messages API without a full multi-agent framework.
You own the loop: call the model, execute tool_use blocks, append tool_result messages, repeat until stop.
Summary
Define JSON-schema tools, send them on messages.create, detect stop_reason == "tool_use", run local handlers, and continue the conversation until the model returns an end turn (or you hit max steps).
Recipe
- Install the Anthropic official SDK and set
ANTHROPIC_API_KEY. - Declare tools as name + description +
input_schema(JSON Schema). - Map tool names to pure Python callables with validation.
- Write a loop:
messages.create→ if tool use, execute → append assistant + tool results → continue. - Cap iterations (
max_steps) and wall-clock time; never loop unbounded. - Surface tool errors as structured
tool_resultcontent the model can repair. - Log every tool name, latency, and success flag with a redacted args policy.
- Add host-level policy (allowlists, auth) before side-effecting tools.
Working Example
import os
from typing import Any
import anthropic # pip install anthropic
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
TOOLS = [
{
"name": "get_weather",
"description": "Get a demo weather string for a city.",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
},
"required": ["city"],
},
}
]
def get_weather(city: str) -> str:
demo = {"paris": "sunny, 22C", "tokyo": "cloudy, 19C"}
return demo.get(city.lower(), f"No demo data for {city}")
DISPATCH = {"get_weather": lambda **kw: get_weather(**kw)}
def run_agent(user_text: str, max_steps: int = 5) -> str:
messages: list[dict[str, Any]] = [
{"role": "user", "content": user_text},
]
for step in range(max_steps):
response = client.messages.create(
model="claude-sonnet-4-5", # verify model id at build
max_tokens=1024,
tools=TOOLS,
messages=messages,
)
# Append the assistant turn (includes text and/or tool_use blocks)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
# Collect final text blocks
parts = [
b.text for b in response.content if getattr(b, "type", None) == "text"
]
return "\n".join(parts) or "(no text)"
tool_results = []
for block in response.content:
if getattr(block, "type", None) != "tool_use":
continue
fn = DISPATCH.get(block.name)
try:
if fn is None:
raise ValueError(f"Unknown tool: {block.name}")
output = fn(**block.input)
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(output),
}
)
except Exception as exc: # production: narrower types + metrics
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": f"ERROR: {exc}",
"is_error": True,
}
)
messages.append({"role": "user", "content": tool_results})
return "Stopped: max_steps exceeded"
if __name__ == "__main__":
print(run_agent("What is the weather in Paris?"))- The Messages API returns content blocks; tool calls are
tool_useblocks withid,name, andinput. - You must echo tool results with matching
tool_use_id. stop_reasontells you whether to continue the loop or finish.
Deep Dive
Why this belongs next to OpenAI Agents SDK content
OpenAI Agents SDK hides the loop behind Runner.
Anthropic's public agent posture for many teams is still: excellent tool primitives, explicit loop ownership.
Comparing them teaches when a high-level agent SDK is worth it versus a 40-line while-loop you fully control.
| Concern | Anthropic Messages tool loop | OpenAI Agents SDK |
|---|---|---|
| Loop ownership | You | Runner |
| Multi-agent handoffs | You design | First-class handoffs |
| Guardrails | You / host | Built-in tripwires |
| MCP | Separate integration choices | Native MCP helpers |
| Portability | High if tools stay pure | Provider-aligned |
Parallel tool calls
Models may emit multiple tool_use blocks in one assistant turn.
Execute them carefully:
- Prefer parallel reads when independent.
- Serialize writes that share state.
- Always return one
tool_resultpertool_use_id.
Schema quality is agent quality
Bad tool schemas produce bad agents faster than bad prose instructions.
- Required fields for anything you will execute on
- Enums for closed sets
- Descriptions that state side effects and units
- Small tools over kitchen-sink admin tools
See the tool-use section of this site for schema and description craft.
Stop conditions you must enforce yourself
| Condition | Mechanism |
|---|---|
| Model finished | stop_reason not tool_use |
| Step budget | max_steps |
| Token / cost budget | track usage fields if present |
| Wall clock | overall timeout around the loop |
| User cancel | cooperative cancellation flag |
Without these, tool loops become runaway spend.
Structured final answers
You can:
- Ask for JSON in the final text and validate with Pydantic
- Use provider structured-output features when available for your model (verify at build)
- Add a final
submit_answertool that is the only allowed terminal action
Terminal tools make "done" an explicit tool call instead of prose.
Safety differences from a full agent SDK
You must implement:
- Input filtering / injection defenses
- Output validation before showing users
- Tool allowlists per session role
- Approval gates for irreversible tools
None of that is optional just because the loop is short.
Gotchas
- Forgetting to append the assistant
tool_usemessage before results. The API requires a consistent transcript. - Mismatched
tool_use_ids. Results will fail or confuse the model. - Swallowing tool exceptions without
is_error. The model cannot repair what it never sees. - Unbounded loops. Always cap steps and time.
- Passing huge tool outputs back raw. Truncate or summarize untrusted blobs.
- Putting secrets in tool schemas or logs. Redact args in production logging.
- Assuming Anthropic "agents" require a separate product. Many production agents are Messages + tools + your loop.
Alternatives
| Approach | When | Trade-off |
|---|---|---|
| Messages tool loop (this page) | Full control, few tools, strong compliance needs | You own multi-agent and guardrails |
| OpenAI Agents SDK | Want handoffs, guardrails, MCP quickly on OpenAI | Provider coupling |
| Pydantic AI | Typed Python tools + validated outputs | Another framework dependency |
| LangGraph | Durable graphs and HITL resume | Heavier orchestration model |
| Google ADK | Gemini/Vertex multi-agent gravity | Different ecosystem |
FAQs
Is this an "agent" if there is no agent class?
Yes, if the system repeatedly perceives, chooses tools, acts, and observes under stop conditions. Classes are packaging, not the definition.
How do I do multi-agent handoffs on Anthropic?
Implement them explicitly: either switch system prompts/tools mid-loop, or call specialist functions that run nested model calls. There is no single required handoff primitive.
Can I stream tool-using responses?
Yes with streaming Messages APIs. You still must assemble tool_use blocks and execute tools before continuing. Verify streaming event shapes at build.
Should I prefer a terminal `submit_answer` tool?
For automation pipelines, often yes. It makes completion machine-detectable and keeps free-text chats optional.
How does this compare to OpenAI function calling loops?
Conceptually identical: model proposes tools, host executes, results return. Block formats and SDK helpers differ; the control theory does not.
Where should MCP plug in?
As another tool source: list MCP tools, translate to Anthropic tool schemas, dispatch calls to the MCP client. The Messages loop stays yours.
What model id should I hardcode?
None permanently. Inject model ids from config and verify at build against current Anthropic docs.
How do I test the loop without spending tokens?
Unit test the dispatcher and stop logic with recorded fixtures. Integration-test one golden path against a real model in CI on a schedule if cost allows.
When should I abandon the thin loop for OpenAI Agents SDK or LangGraph?
When handoffs, guardrails, MCP lifecycle, or durable graph resume cost more to maintain than a dependency. See the decision cheatsheet in this section.
Do I need both Anthropic and OpenAI agent stacks?
Only if product strategy requires both model families with deep agent features. Many teams pick one primary native stack and keep tools portable.
Related
- What Native Provider Agent Primitives Give You Over a Framework - native vs framework framing
- OpenAI Agents SDK Basics - high-level Runner contrast
- When Native SDK Primitives Are Enough (and When They Aren't) - exit criteria
- Tool Use & Function Calling Basics - schema and calling fundamentals
- Designing a ReAct Loop from Scratch in FastAPI or Express - custom runtime patterns
- OpenAI Agents SDK & Native Primitives Best Practices - shared practices
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.