
Articles · Code · 17 min
How does tool calling work in an AI agent that edits files, runs tests, and calls APIs?
Tool calling is how an AI agent acts on a repo, a test runner, and an API: you declare named functions with a JSON schema, a timeout, and a permission, and the model picks a name and arguments. Your runtime executes that function, returns a short true result, and the loop continues until the job is done. The runtime owns the loop, not the model.
By Eric · Rome · Aug 28, 2026
I do not want a coding agent to talk about a failing test. I want a patch on a branch, the suite green, and a note on the ticket. That job is a sequence of tool calls: read the file, apply a patch, run the tests, then call the tracker API if the ticket needs a comment.
Without tools you have a chatbot. With an unbounded shell and thirty vague functions you have a wanderer. The difference is the contract you put on each function: a schema, a timeout, and a permission, so the model proposes and the runtime decides.
An AI agent is software given a job and a set of tools. It plans, calls those tools, reads what came back, and continues until the job is finished or it hits a limit you set. Tool calling is the part of that loop that actually touches the world.
Tool calling is a contract, not a prompt trick. You name the functions, write the schema, and set the timeout and the permission. The model proposes a call, the runtime runs it or refuses it, and the loop continues until the job is done or the budget is gone.
How tool calling works
The model never touches the filesystem or the network. It emits a name and arguments. Your runtime validates those arguments against the schema, checks the grant, starts a timer, executes, writes a short result back, and then decides whether to call the model again.
OpenAI, Anthropic, and the Model Context Protocol all expose the same idea: you declare tools, the model emits a call, your code runs it, and you feed the result back. The description of the tool is the prompt, so be exact. Vague descriptions produce vague calls.
That is the whole loop. People add planners, critics, and swarms, but the mechanism is still declare, choose, run, return, stop. If you cannot draw those five steps on one page, you have a chat with extra functions, not an agent.
| Step | Who owns it | What goes wrong if you invert it |
|---|---|---|
| Declare | You | The model invents tools that do not exist |
| Choose | The model, inside your list | You hard-code a path and build a script |
| Run | The runtime | The model pretends a write succeeded |
| Return | The runtime | The next turn reasons over marketing copy |
| Stop | The runtime | It chats until the invoice arrives |
Schema, timeout, and permission
Three fields sit on every tool I ship. Schema says what the call may look like, timeout says how long it may run, and permission says whether this agent may run it at all. Miss one of those and the model starts improvising in production.
Schema is the part most teams write. Timeout and permission are the parts they leave in a comment. A coding agent that can apply_patch with no timeout will hang on a lock, and one that can run_tests with no permission check will run them on production data because a prompt said staging was fair game.
The schema is a JSON object. The MCP spec (2024-2026) is blunt: tool arguments are always JSON objects, so `type: object` is required at the root. OpenAI’s Agents SDK tools guide (2026) does the same with a Zod object or a raw JSON schema.
I write descriptions that say what the tool does and when to use it, which is what OpenAI’s Agents SDK tools guide (2026) asks for: short, explicit descriptions, validated inputs, one responsibility per tool. `apply_patch(path, diff)` is the production tool: path constrained, diff bounded, and a `dry_run` flag if the write is expensive to reverse. I do not ship a weather demo in a coding runtime.
| Tool | Schema that matters | Timeout | Permission |
|---|---|---|---|
| read_file | path under the workspace, optional start and end line | 5 seconds | Read in the repo. Not .env. Not ssh keys. |
| apply_patch | path, unified diff, optional dry_run | 15 seconds | Write under src/ and tests/. Not .git. Not infra. |
| run_tests | optional file or name filter, no free-form shell | 120 seconds | The test runner. No network unless the suite needs it. |
| http_request | allowlisted host, method enum, bounded body | 10 seconds | Staging APIs. GET by default. POST only on named routes. |
Timeouts belong on the tool, not in the prompt. OpenAI’s Agents SDK tools guide (2026) lets you set `timeoutMs` on a function tool, and default timeout behavior returns a model-visible message such as `Tool 'slow_lookup' timed out after 2 seconds` so the agent can recover, or you can raise `ToolTimeoutError` and fail the run. MCP’s tools guidance (2024-2026) tells clients to implement timeouts, because the protocol will not save you if you forget.
I use error_as_result for reads and tests, so a hung suite comes back as a timeout the model can see rather than a silent stall. I use raise_exception for anything that spends money or mutates production. If that tool hangs, the run is over, and the model does not get a second try on a wire.
Permission is a grant, not a vibe. OpenAI’s Agents SDK (2026) has needsApproval for tools that should pause and isEnabled for tools that should be hidden on this turn, but isEnabled runs before the model produces arguments, so it cannot replace a check that depends on the path or the host. That check lives in execute or in a guardrail, and MCP servers must authorize their own protected operations, with annotations treated as untrusted unless the server is trusted.
I split grants the way I split Unix permissions: read is not write, write is not test, and test is not deploy. An agent that can read billing.ts should not also be able to POST to the payments API. If one worker would need both, I split the workers, which is the start of multi-agent orchestration, not a reason to dump thirty tools on one body.
- Schema: required fields, enums, path prefixes, body size. Free-form strings are how it wanders.
- Timeout: a number the runtime enforces. A sentence in the prompt is not a timeout.
- Permission: a grant the runtime can refuse. A warning in the description is not a grant.
- Idempotency: writes take a dry_run or a client key. Retries should not double-apply a patch or double-post a comment.
The runtime owns the loop
The model can propose the next tool. It cannot decide to retry a payment, ignore a spend cap, or keep going after the tests already passed. Those rules live in the runtime, and if they live in the prompt they are suggestions.
This is the line most demos skip. The model looks like it is in charge because it picks apply_patch instead of read_file, but that is choice inside a fence. The fence is the runtime: which tools exist this turn, whether the arguments validate, whether the grant allows the path, how long the call may run, whether a retry is legal, and whether the job is already done.
OpenAI’s Agents SDK (2026) is built on that split: an agent is a model with instructions and tools, and the SDK runs the loop. Anthropic's Building effective agents (December 2024) draws the same line in different words: a workflow is a predetermined path, and an agent is a model that directs its own tool use. Directing tool use is not owning the stop rule.
I put four things in the runtime and nowhere else: retries on transport failure, memory of what already ran so it does not apply the same patch twice, a budget in steps and spend, and a stop check that can see the artifact. The prompt may say stop when you are done. The runtime is the only component that can actually stop.
A dropped connection is a retry, and a failing assertion is not. If you retry a test because the model asked nicely you will wait out a real bug and call it flake, and if you retry a POST because the timeout fired after the server accepted the body you will double-comment the ticket. The runtime needs a rule per tool, not a global retry count.
Stop conditions are tests, not vibes. For a coding agent I use machine-checkable done: the named tests pass, the diff is confined to the allowed paths, and the PR body exists as a file. If a person has to read the trace to know whether it finished, a person will stay in the loop forever.
File edits, tests, and APIs in one job
A coding agent's job is one finish line that happens to need three kinds of action: read and patch the files, run the tests, and call an API if the ticket, the CI, or the preview needs a note. Keep that list short. Extra tools are how it shops.
Take a job I actually run: a failing tax-rounding test. Done means the test passes, the patch stays under src/billing and tests/, and the tracker comment contains the commit sha. That is three tools plus a read, not a general developer with git, bash, the browser, and the production database.
The first call is almost always `read_file` or a search. The runtime returns the failing assertion and the nearby source, not the whole repo. The model proposes `apply_patch`, the runtime applies it in a workspace, refuses paths outside the grant, returns the diff stats, runs `run_tests`, and if the suite passes the agent may call the tracker API with a bounded body before the runtime stops.
If the tests fail, the model sees the assertion and patches again, which is the loop doing its job. If the tests hang, the timeout fires, the model sees a timeout, and it can narrow the filter or stop. I do not let it open a general shell to debug a hung suite, because a general shell is how a coding agent becomes a sysadmin with your keys.
I prefer apply_patch over write_file because a patch is reviewable, while a whole-file write hides the change and invites the model to rewrite comments it did not understand. I prefer run_tests(file) over run_shell(command) because a test runner has a schema and a shell is an escape hatch. If I must expose a shell, it is allowlisted, it has needsApproval, and it cannot see production credentials.
API calls get the same treatment as file writes: host allowlist, method enum, bounded body, idempotency key. A coding agent that can comment on a ticket should not be able to close the ticket, reassign the epic, or hit the billing endpoint because the OpenAPI spec was pasted into the prompt. Paste the three routes it needs, and leave the rest out of the tool list.
- read_file and search_code to see the failure, not to ingest the company.
- apply_patch to change source, with path grants and a dry_run.
- run_tests to see whether the change worked, with a hard timeout.
- One HTTP tool for the ticket or CI note, allowlisted, idempotent.
What the OpenAI Agents SDK actually models
OpenAI’s Agents SDK tools guide (2026) is the public map I actually implement: tools let an agent fetch data, call APIs, execute code, or use a computer. The SDK then splits those tools by who executes them, and that split is the point, because execution is not the model's job.
Hosted tools run beside the model on OpenAI servers: web search, file search, code interpreter, image generation, tool search. Built-in execution tools are requested by the model and run in your process or a container: computer use, shell, apply_patch. Function tools wrap any local function with a JSON schema, and you can also attach MCP servers, treat a whole agent as a tool, or bind sandbox filesystem tools to a SandboxAgent.
For a coding agent I live in function tools and the local execution tools, and a function tool needs a description, a schema, and an execute function. Zod parameters enable strict mode, so bad arguments come back as a model-visible error instead of a surprise in your database. `timeoutMs` bounds each call, `needsApproval` pauses before a write, and `isEnabled` hides a tool for this run without deleting it from the codebase.
Agents as tools are how you keep a manager in control of the user-facing reply. summarizer.asTool() turns a specialist into a function with an input, and the manager never hands over the conversation. Use a real handoff when the next worker needs different grants, which is multi-agent orchestration, not one giant tool list.
MCP tools and MCP resources
The Model Context Protocol splits the world in two. Tools are functions the model may call. Resources are data the application may load as context, and mixing them is how teams accidentally let a model write to a file they only meant to display.
MCP launched in November 2024 as an open standard for connecting agents to tools and data, and the spec is the source of truth. Servers that support tools declare a tools capability and answer tools/list and tools/call. A tool has a name, a description, and an inputSchema, it may have an outputSchema, and clients that aggregate several servers should prefix names because uniqueness is per server.
Tools are model-controlled: the model discovers them and decides when to call, while the spec still tells implementations to keep a human able to deny invocations, to show which tools are exposed, and to confirm sensitive operations. That is permission in protocol language. Timeouts and audit logs sit on the client, because MCP will not invent them for you.
YouTube
Open originalJohn Welsh and Michael Cohen of Anthropic walked the same MCP tool-versus-resource split on 9 Oct 2025.
Resources are application-driven. A resource has a URI, a name, an optional mimeType, and an optional size, and clients list them with resources/list, read them with resources/read, and may subscribe to updates. A file at file:///project/src/main.rs is a resource, and the application decides whether that bytestring enters the model; the model does not get a write handle because it saw a URI.
| MCP tool | MCP resource | |
|---|---|---|
| Who controls it | The model | The application |
| Wire calls | tools/list, tools/call | resources/list, resources/read |
| Shape | Name plus inputSchema | URI plus mime type |
| Use it for | Actions: patch, test, POST | Context: files, schemas, docs |
| Danger if inverted | The model cannot act | The model acts with a read-only name |
Errors split the same way the loop does. Protocol errors are JSON-RPC for unknown tools and malformed requests, which the model is unlikely to fix, while tool execution errors come back as a result with isError true: bad date, missing record, API 422. Clients should feed those to the model so it can correct the arguments, and I return the same shape from my own function tools: a stack trace for me, a one-line cause for the next turn.
MCP also tells servers to validate inputs, enforce access control, rate-limit invocations, and sanitize outputs, which is the permission layer again, written as MUST. If your MCP filesystem server can write anywhere the process can write, you did not adopt a standard. You wrapped rm in JSON.
When the agent should write code to call tools
Direct tool calls put every definition and every intermediate result through the model, which is fine for five tools and a short file. It falls over when the agent must move a transcript from Drive into Salesforce, or filter a 10,000-row sheet. Then you want the model to write code, and the code to call the tools.
Direct tool calls consume context for each definition and result. Agents scale better by writing code to call tools instead.
Anthropic's Code execution with MCP (November 2025) is the page to read. The pattern is simple: present each MCP server as files on disk, let the agent list ./servers, read only the tool files it needs, and write TypeScript that imports those functions. Intermediate data stays in the execution environment, so the model sees the script and a short log, not the full transcript twice.
They give a measured example: a workflow that burned about 150,000 tokens when every tool definition and intermediate payload went through the model dropped to about 2,000 tokens with code execution, a 98.7% saving on that trace. Cloudflare published a similar pattern under the name Code Mode. I treat both as the same design: the model is good at writing glue, and glue should not be a chain of 80-kilobyte tool results.
Progressive disclosure is the first win, because the agent loads getDocument.ts when it needs Drive rather than the entire catalog, and context-efficient results are the second: filter the sheet in code, log five rows, throw the rest away. Control flow is cheaper as code than as a dozen model turns. Privacy holds because PII can flow from Sheet to Salesforce inside the sandbox while the model receives tokenized stand-ins.
This does not delete schema, timeout, or permission. Each generated call still hits a real tool with a real grant, and the sandbox needs its own timeout, memory cap, and network policy. Anthropic is explicit that code execution adds operational weight, so use it when the payload is large or the composition is a graph, and use direct calls when the job is read, patch, test, comment.
OpenAI's programmatic tool calling sits in the same family: the model writes JavaScript, eligible tools run with the same validation and approvals as a direct call, and allowedCallers decides whether the model, the program, or both may invoke a tool. I mark apply_patch as direct. I mark bulk fetches as programmatic when I have already seen the model copy a table from one call into the next by hand and lose a row.
What good tools look like
Good tools are boring: one job, a strict schema, a timeout, a grant, and an error the model can use. I start under ten. Each extra tool is a way to wander, and if I need more roles I split the agent instead of dumping the company API into one worker.
- One job per tool. Search is not send. Read is not patch. Test is not shell.
- Strict schemas. Required fields. Enums. Path prefixes. Bounded bodies.
- Idempotent writes, or a dry_run flag the runtime honors.
- Errors the model can use. not found, denied, timeout, invalid path. Not a stack trace.
- A timeout the runtime kills. Reads in seconds. Tests in minutes. Never unbounded.
- A permission the runtime can refuse even if the model is confident.
How many is a product question with a technical ceiling. I have shipped coding agents with four tools that close tickets, and I have watched prototypes with forty MCP servers stall before the first patch. Anthropic wrote that piece because the community hit it: the fix is a shorter list, deferred loading, or code execution, in that order.
Retrieval is one tool among others, not the whole of tool calling. An agent that only retrieves and replies is still close to a chatbot, while an agent that retrieves the failing test, patches the file, runs the suite, and comments on the ticket is using tools to finish work. RAG can sit behind search_code, and it cannot replace apply_patch.
How I ship the tool list
I do not ship a coding agent because the trace looks clever. I ship it when the artifact exists on a frozen set of jobs: patch applied, tests green, comment posted, grants respected. Tool calling is the mechanism, and eval is how I know the mechanism held.
Write the finish line first, then list the tools that finish line actually needs, then put schema, timeout, and permission on each one. Run the same jobs many times and log every call: name, arguments, result, latency, cost, error. If you only keep the final message, you will not see the bad search or the double POST.
How to put an AI agent in production is the longer sequence: done condition, tight tool set, runtime, eval, locked permissions, one desk. This guide is the tool-set chapter. A runtime with sloppy tools will still write to the wrong path, and a prompt with no timeout will still hang the suite.
I read failures in the tool log, not in the chat. The model will explain a missed file with confidence, while the log will show it never called read_file, or it called apply_patch on a path the grant refused, or run_tests timed out and it invented a pass. Pretty traces that miss the artifact are zeros, so I keep last week's agent as the baseline, and if the new list does not beat it, it does not ship.
Permissions stay split after it ships: draft is not send, and a branch is not production, so a coding agent that may open a pull request should not merge to main on the same grant. When research and deploy should never share a body, I split specialists and pass a packet: goal, evidence, next action, limits. That is orchestration, and it comes later, after one agent, one short list, and one runtime that can kill a runaway trace.
The model will get better at choosing tools, and it will not get better at being the runtime. Schema, timeout, permission, and the stop rule stay yours. That is the work, and that is also the product if you are honest about what you are selling.
Questions
The model selects a named function and arguments. The runtime validates the schema, checks the permission, enforces the timeout, and executes. The agent uses the short result to continue until the job is done.
No. The model proposes a name and arguments. The runtime executes. Hosted tools run on a provider. Function tools, shell, and apply_patch run in your process or a sandbox you control. The loop owner is the runtime.
As few as the job needs. Start under ten. A typical coding job is read, patch, test, and one allowlisted API. Split into specialists if you need more roles, rather than dumping thirty tools on one worker.
No. Retrieval can be one tool. Retrieval plus a reply is still close to chat. Retrieval, then patch, then tests, then a ticket comment is an agent finishing work.
Tools are model-controlled functions with an inputSchema, listed and called over tools/list and tools/call. Resources are application-driven context at a URI, listed and read over resources/list and resources/read. A file you display is a resource. A patch you apply is a tool.
No. Expose the three calls the finish line needs, with a schema, a timeout, and a grant. A catalog of endpoints is how a coding agent wanders into close, delete, and deploy.
Next

