Appearance
Tool Calling Is Easy. The Agent Loop Is the Product.
There's a moment in every tool-calling tutorial where the story shifts from code to philosophy. The model never runs anything. It reads your tool descriptions and hands back a structured note: which function it wants, with which arguments. Your code executes. That division, model decides, code acts, is the whole foundation. MCP, agent SDKs, multi-agent home servers, all of it is layers on top of that note-passing loop.
The AWS-authored dev.to post "How AI Actually Calls an API?" walks this progression on Amazon Bedrock with Claude: one tool, then two, then a loop, then a standard. The Strands Agents SDK and Tencent's Octop show what happens when that loop becomes a product. This piece traces the same path, and flags where each layer saves you time and where it costs you control.
The model doesn't call anything
When I first heard "the model calls a tool," I pictured the model reaching out and running code by itself. That is not what happens. The model is still just reading a prompt and producing text. What it produces is a structured request that says "I'd like to call this tool, with these inputs." It hands you a note, your code reads it and runs the actual function, then hands the result back.
Four steps, every single time:
- You send the question plus descriptions of the tools the model is allowed to use.
- The model decides whether it can answer alone or needs a tool. If it needs one, it replies with a structured request containing a
toolUseId. - Your code runs the real function, the one that actually hits the API.
- You send the result back as a
toolResult. The model writes the final answer grounded in data it could never have known on its own.
In the Bedrock Converse API, describing a tool to the model is three parts: a name, a plain-English description, and a JSON input schema. That schema and description are the only contract the model sees. The real function behind it is normal code, no AI in it. In the demo it hits Open-Meteo, a free weather API that needs no key at all.
The model is the decision-maker. Your code is the hands. Forgetting that second half is the root of most "why isn't my tool being called" confusion. The model saying it wants get_weather is a request, not an execution. If you don't write the step that runs the function, nothing runs.
One tool is a straight line. Two tools are a loop
With a single weather tool, the flow is deterministic. Send, get the request, run it, send the result back, get the answer. You can hardcode it top to bottom, and the demo does exactly that. One tool, one round trip.
Then comes a second tool, a datetime one. Ask "Do I need an umbrella in Toronto? And what's today's date?" and the model returns two toolUse blocks in one response: get_weather with {"city": "Toronto"}, then get_current_datetime with {}. Your code runs both and sends both results back. One sentence, two different needs, right tool for each.
Here's the shift. With one tool you know there will be exactly one round trip. With two, you don't know which tool the model picks, how many requests come back, or whether it will ask for another tool after seeing the first result. The straight line becomes a while loop:
python
while True:
response = bedrock.converse(
modelId=MODEL, messages=messages,
toolConfig={"tools": [WEATHER_TOOL, DATETIME_TOOL]}
)
assistant_message = response["output"]["message"]
messages.append(assistant_message)
if response["stopReason"] != "tool_use":
break
for block in assistant_message["content"]:
if "toolUse" in block:
request = block["toolUse"]
result = TOOLS[request["name"]](**request["input"])
messages.append({
"role": "user",
"content": [{
"toolResult": {
"toolUseId": request["toolUseId"],
"content": [{"json": result}]
}
}]
})That while loop is the seed of every agent framework. The moment the number of steps is decided by the model during the run, you've built an agent, just a small one. And that's where most people realize they're re-implementing the same loop for every project.
Inject the static facts, tool the live ones
The demo has an exchange worth stealing. Ask the model for today's date with only a weather tool available, and it refuses cleanly. It says it doesn't have access to the current date. No hallucination, just a bounded model being honest.
The obvious fix is a second tool. But there's a cheaper path for facts like this. Anthropic publishes the system prompt used in Claude's web and mobile apps, and it includes the current date, injected as plain text at the start of every conversation. No tool runs at all. That's how assistants answer "what day is it?" instantly.
You can do the same in a script. Paste f"Today's date is {datetime.now():%A, %d %B %Y}." into the system prompt and the model answers the date question with zero tool calls. One line, no schema, no round trip.
The clean rule: cheap and static facts get injected, live and changing facts get a tool. You can't inject the weather, because you'd have to know it in advance, which defeats the point. You can always inject the date. Getting this backwards costs you latency and tokens, since every tool round trip is a full model call plus an API call. Also remember the schema: the weather tool takes city and nothing else, so this model can't answer questions about next week. No tool for that, no pretend.
Tool descriptions are prompts
The comments on the dev.to post carry the lesson that matters most past two or three tools. The line that got the most agreement: your tool description is a prompt, so treat it like one.
I found this the hard way. I had a tool named search_documents that the model kept calling when it should have called get_user_profile. Both descriptions opened with "Retrieves information about". The model was routing on description semantics, not function names. It doesn't care what you named the function. It reads the description, and two descriptions that open with the same generic verb look identical.
Rewriting both descriptions to lead with the specific entity and the action direction fixed it. get_user_profile became "Look up a user's name, email, and role by user ID." search_documents became "Search text inside the uploaded PDF corpus by content match." Misrouting dropped to near zero.
When the model picks the wrong tool, the first thing to audit is not the model. It's your descriptions.
Quick Take: tool descriptions and input schemas are the only thing the model reads to decide if, when, and how it calls your tools. Write them like prompts, because that's what they are.
The hardcoding problem and why MCP exists
Two tools work fine with hand-written glue. Fifty tools across five apps, all changing over time, is a maintenance nightmare. Schema updates, description drift, reshuffled arguments. Everyone building AI apps was writing the same integration code, over and over, for the same tools.
MCP, the Model Context Protocol, is the industry answer. Think of it as USB-C for AI tools. Before USB-C, every device had its own cable and connector. MCP is one standard plug for connecting models to tools and data. A tool lives behind an MCP server that describes itself: here are the tools I offer, what each does, what inputs I need. Your app is the client. It asks "what have you got?" and the server answers. Tools get discovered at runtime instead of being hardwired into your codebase.
The mental model that matters: tool calling is how one model uses one tool, and MCP is how any model discovers and uses tools without custom glue. If someone builds an MCP server for GitHub, your database, or Slack, you don't write the integration. You point your app at the server and the tools show up. Building one is its own topic, but the standard is what makes the next layer viable.
The SDK layer: Strands productizes the loop
Once you've written that while loop three times, you stop. That's the gap the Strands Agents SDK fills. It's an open-source Python and TypeScript SDK that runs the agent loop inside your process, with no hosted control plane. The pitch is precise: choose Strands when you would otherwise write your own agent loop.
Python 3.10+ and pip install strands-agents strands-agents-tools get you a working agent. The README's opener is four lines:
python
from strands import Agent
from strands_tools import calculator
agent = Agent(tools=[calculator])
agent("What is the square root of 1764")What the SDK adds over your hand-rolled loop is the stuff that loop grows into. Lifecycle controls: turn limits, token budgets, cancellation, stop reasons. That last one matters more than it looks. Your naive while loop checks stopReason once. The SDK treats it as a first-class control surface. Tools and structured output come with schema validation, MCP is built in, so you skip the USB-C adapter work, and memory, sessions, model portability, streaming, guardrails, tracing, and evals are all in the box.
Model portability is the sleeper feature. The SDK defaults to Amazon Bedrock with Claude Sonnet, so you need AWS credentials and model access enabled to start, but it also supports Anthropic, OpenAI, Gemini, Ollama, and custom providers. Swap backends and your code stays the same. For anyone who has migrated a hand-rolled loop between providers, that alone justifies the dependency.
Two SDKs, Python and TypeScript, with the same loop semantics. Python needs 3.10+, which most existing environments already run. TypeScript needs Node.js 22+, the current LTS track, and ships Zod-typed tools and structured output. If your team is split across languages, the docs and governance live in the same monorepo.
Key numbers: 4 steps in a tool-calling round trip, per the Bedrock demo. 2 tools in the demo before the straight line becomes a
whileloop. 14 sub-features across 10 feature areas in the Strands SDK, per its README. 6 IM channels in Octop, plus web dashboard, CLI, cron, and ACP.
The platform layer: Octop is a home server for agents
SDKs assume you're building a product. Octop assumes you want an assistant, today, for a household or small team. It's an open-source, self-hosted platform that runs one process serving a web dashboard, CLI, IM channels (Feishu, DingTalk, QQ, Discord, WeCom, and more), and cron automation. Everything lives under ~/.octop/ with SQLite by default, PostgreSQL optional. Fully self-hosted, single-process startup.
The architecture is a set of focused runtimes composed into one process, with every surface routed through a single in-process HarnessProcessor. No external queue, no message broker, and the whole state rebuilds from the control-plane database on boot:
The multi-agent part is the interesting bit. One admin, multiple users, each with their own agents, workspaces, providers, and channels. Agents switch between expert profiles per task, and there are 16 MBTI persona templates plus custom system prompts, so giving each agent a character is configuration rather than prompt engineering. An expert library is scanned at boot, and a knowledge base does RAG over your documents.
Security is where self-hosted multiplies. JWT multi-user isolation, tool approval, shell command guardrails, and PII redaction. A terminal AI that can execute commands needs guardrails before it gets chat access, and Octop ships them rather than hoping you add them later. Low-level install requires no pre-installed Python: the installer uses uv to provision a 3.12 virtualenv under ~/.octop/, so it never touches system Python. Hardware needs are a multi-core CPU and a few GB of RAM, meaning a spare mini-PC runs it fine, no GPU box required.
The ACP integration is the piece I'd highlight. Inbound, external tools use your agent via octop acp, a stdio server for Zed, OpenCode, and similar editors. Outbound, Octop delegates to coding agents like Claude Code, Codex, and CodeBuddy, with permission gates in between. So Octop is both host and client to other agents. Tencent's AuK space headlines with "agents running on zero," which is the same ambition from the hosted side of the fence.
Choosing your layer
The three options aren't competitors. They're escalation paths.
| Layer | You handle | You get | When it fits |
|---|---|---|---|
| Hand-rolled loop | Tool schemas, execution code, message history, the while loop | Full control, zero dependencies, complete understanding | Learning the mechanics, 1-3 tools, one-off scripts |
| Agent SDK (Strands) | Agent definition, config, business logic | Loop lifecycle, MCP, guardrails, tracing, evals, provider portability | Product development with a team, production workloads |
| Self-hosted platform (Octop) | Install, user onboarding, agent config | Multi-user auth, IM channels, cron, browser and terminal access, RAG, memory | A household or small team assistant, no engineering time to spare |
If you're a cloud architect learning AI from first principles, a hand-rolled loop on Bedrock is the right classroom. If you're shipping a feature, Strands is the floor. If you want an assistant that your family or team can use over WeCom or Discord tomorrow, Octop.
Common pitfalls
Tool descriptions with the same generic opening
Two tools starting with "Retrieves information about" look identical to the model. Lead with the entity and the action direction. "Look up a user's profile by user ID", not "Retrieves information about users".
No execution loop behind the toolUse block
The model returns a toolUse block and waits. If your code doesn't read it, run the function, and send back a toolResult, the conversation just stops mid-task. The classic symptom is an assistant that answers "I need to check the weather" and then goes silent.
Hardcoding a single round trip
The moment you have two tools, you can't know how many calls the model will make. It might need three tools in sequence, or it might come back for more after seeing the first result. If your flow doesn't loop while stopReason is tool_use, the agent stalls.
Using a tool for a fact you could inject
Today's date in the system prompt costs one line and zero round trips. Weather needs a tool because it's live. Getting this backwards adds latency and token cost to every conversation, for no benefit.
Running the loop without budgets or guardrails
Turn limits, token budgets, and cancellation exist for a reason. A misdirected chain of tool calls burns tokens, and a shell-capable agent without command guardrails is a liability. The SDK's lifecycle controls and Octop's tool approval and shell rules aren't feature noise. They're the difference between a demo and a deployable assistant.
One thing to remember: the layers stack. Tool calling teaches you the loop. The SDK manages the loop. The platform hides the loop. Your job is knowing which layer you're living in, because reaching for a heavier layer than you need costs you control, and staying on a hand-rolled loop past three tools costs you time.
The bottom line
If you're learning agentic engineering from