Here is a pattern that quietly kills agent projects. The agent handles simple requests by stringing a few tools together. Then a bulk request arrives, something like “re-tag these two hundred records,” which needs filtering, then combining, then aggregating. The agent cannot do it. The reflex is to write another tool. But the missing capabilities combine combinatorially, so hand-writing tools costs one unit of work per request, and you lose that race.
The wall is not the framework. The wall is the action model.
Tool-calling cannot compose
Standard tool-calling emits one call per round, or a few parallel ones, sees the result, then emits the next call. Five hundred records become five hundred round trips. That is slow, expensive, and destructive to the context window, and state often gets lost halfway through. You are forcing the model to simulate a for loop in generated tokens, which is the thing it is worst at. Adding more tools does not fix this. Each new tool just hard-codes one more special case.
Write the code instead
Do not have the model pick a tool. Have the model write code that calls the tools.
Expose the backend primitives as a set of functions inside a sandbox, rather than as tool schemas to choose from. The model’s action becomes write a short script, with loops, filters, conditionals, and one function feeding into the next. Then run the script once.
# "tag the 200 active candidates as reviewed"
cands = client.list_candidates(status="active", page_size=200)["items"]
done = [client.tag_candidate(c["id"], "reviewed") for c in cands]
print(f"handled {len(done)}")
One script, one execution. Only the aggregate result comes back, instead of two hundred intermediate results filling the context.
| Collecting tools | Code-action | |
|---|---|---|
| New request | you write a tool | nothing |
| Where composition lives | hard-coded in tools | model writes it at runtime |
| 500 records | 500 round-trips | one script |
| Your effort | scales with requests | scales with primitives |
For internal, read-mostly work the safety story stays simple. The sandbox holds only the current user’s token, and a gateway passes that identity through with rate-limiting and audit logging. The agent can write any code it likes, but it cannot step past the user’s own permissions.
You maintain a stable layer of primitives. The combinatorial explosion of combinations moves to where it belongs, which is the model’s runtime rather than your backlog. The same idea appears elsewhere as code execution with MCP, and as “code mode.” In a maturing agent, the highest-leverage move is not adding more tools. It is letting the model write code against the tools you already have.