Choosing the Right Granularity for an Agent's Tools — Bundle or Split?
Should you split an agent's tools finely or bundle them coarsely? A single decision rule, an intentionally asymmetric two-tier design for destructive actions, and real numbers from running six apps.
When I give an agent a new tool, the thing I agonize over most is not the feature itself but how large a slice to carve it into. Do I split things finely, like readFile and writeFile, or do I fold a whole chunk of work into one updateConfig? It looks like a trivial design choice. In practice it turned out to be a surprisingly deep one — it quietly decides whether the agent behaves intelligently or not.
I have been building apps on my own since 2014. Today I run six of them across iOS and Android (around 50 million downloads in total) while handing the upkeep of four technical blogs to agents. At that scale there is almost no time left to approve each individual action by hand. That is exactly why "which tools, at what granularity" silently governs how stable the whole operation is. In this article I want to walk through the granularity rule I arrived at the long way round, and the two-tier structure I use for destructive actions, with real code and real numbers.
Tools that are too fine — or too coarse — leave the agent lost
My first mistake was turning API endpoints directly into tools. When I tried to semi-automate optimizing my AdMob mediation settings through Claude in Chrome, I copied the UI's units of operation straight into tools: getAdUnit, listMediationGroups, getEcpmFloor, setEcpmFloor, enableNetwork, and so on.
The results were underwhelming. To achieve a single goal — "align the eCPM floor across every network for this ad unit" — the agent would call five or six tools, each time re-reading the previous return value to assemble the next step. Misread the shape of one return value along the way, and every step after it drifted quietly off course. What I was watching was not a clever agent but an agent straining to follow a detailed instruction sheet one line at a time.
Reacting against that, I once built a single all-in-one optimizeMediation tool. Now the agent only had to call once — but from the outside no one could see what was happening inside it, and there was no room at all to step in and say "actually, don't make that change right now." A tool that is too coarse makes the agent look smart while stripping the human of any control point.
After bouncing between those extremes, something finally clicked. Granularity is not a question of "how many pieces," but of "where the decision happens."
The rule: does it resolve in one decision?
The rule I use now is a single one. Does this tool call resolve within one of the agent's decisions?
By "decision" I mean the unit in which the agent reasons about what to do next. Reading a value with getEcpmFloor and then writing it back with setEcpmFloor is, in a human's head, one decision: "adjust the floor." Yet when the tools are split, the agent is forced to cleave that single decision in two at the tool boundary. Into the seam between the halves slips a misread, an interruption, or some unrelated detour.
So I started mapping the unit a human feels as "doing one thing" onto one tool. If a read and the write that depends on it always happen together, I fold them into a single tool. Conversely, the places where the agent genuinely wants to branch its judgment — like "which ad unit do I target?" — I deliberately keep as separate tools.
It sounds simple stated plainly, but having this rule kept my design from wobbling. Whenever I hesitate over "should I split this?", I just re-ask: "Is this one decision for the agent, or two?" The answer falls out.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦The single rule that decides tool granularity: does the call resolve in one decision?
✦Three failures caused by tools that are too fine, and the control you lose with tools that are too coarse
✦A two-tier design that deliberately lowers granularity for destructive actions, with TypeScript code and production numbers
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Let me lay out why overly fine tools are dangerous, as the three failures I actually walked into.
The first is that the round trips bloat the context. Every tool call piles its input and output onto the conversation. Doing reads and writes as separate tools across five mediation groups is ten round trips on its own, and the context balloons fast. In my operation, the same task consumed roughly 40% fewer tokens after I bundled the tools than before.
The second is that the agent gets "lost" mid-procedure. It means to read with getEcpmFloor and then call setEcpmFloor, but slips a listMediationGroups for a different group in between, and which value it meant to write back where goes fuzzy. What a human holds as one continuous task, the agent is reassembling from memory at every tool boundary.
The third is that partially broken states tend to linger. Split the work of aligning floors across three networks into three writes, and when the second one fails you are left with one changed and two stale — a half-finished state. Fold it into one tool handled transactionally, and you get all-or-nothing.
The code below is the overly fine tool definition I wrote first.
// Before: UI operations copied straight into tools (too fine)const tools = [ { name: "get_ecpm_floor", description: "Get the current eCPM floor for the specified ad unit", parameters: { type: "object", properties: { adUnitId: { type: "string" } }, required: ["adUnitId"], }, }, { name: "set_ecpm_floor", description: "Set the eCPM floor on the specified ad unit", parameters: { type: "object", properties: { adUnitId: { type: "string" }, network: { type: "string" }, floor: { type: "number" }, }, required: ["adUnitId", "network", "floor"], }, },];
With this design, to serve the single intent of "align the floors" the agent calls get_ecpm_floor, interprets the return, and repeats set_ecpm_floor once per network. The intent is one; the calls are split many ways.
What tools-too-coarse take away: the room to stop
So should you bundle everything? No. Push granularity too coarse and you erase the room for a human to intervene.
When I built the all-purpose optimizeMediation tool, the agent did indeed do everything in one call. But inside it, operations of completely different character — "lower a floor," "enable a new network," "disable an old group" — were executed all at once. Fine-tuning a floor can be redone any number of times; disabling a group hits revenue directly. Cram those into the same single call and the human loses the perfectly ordinary judgment of "approve only the floor tweak, hold the disable."
This is where reversibility enters. Operations you can undo and operations you cannot must not be handled at the same granularity. The only things safe to bundle coarsely are "operations of uniform character that can be redone freely." An operation whose results linger should be split off as its own tool — even if procedurally it sits right next to the others — to preserve a human decision point.
So granularity design runs on two axes at once: "bundle by intent" and "split by reversibility." Next I will show the middle shape that satisfies both.
The middle I settled on: one intent, one tool
What I finally settled on was bundling reads and writes by intent, while always showing the human "what it intends to do" through the return value.
// After: bundle by intent, disclose the plan in the return valueconst tools = [ { name: "align_ecpm_floors", description: "Align the eCPM floors of all networks on the given ad unit to a target. " + "Reads, diffs, and writes are executed as a single intent.", parameters: { type: "object", properties: { adUnitId: { type: "string" }, targetFloor: { type: "number" }, // dryRun=true returns the plan only and writes nothing dryRun: { type: "boolean", default: true }, }, required: ["adUnitId", "targetFloor"], }, },];async function alignEcpmFloors(args: { adUnitId: string; targetFloor: number; dryRun: boolean;}) { const current = await mediation.getFloors(args.adUnitId); // compute the diff within the same single decision const plan = current .filter((f) => Math.abs(f.floor - args.targetFloor) > 0.01) .map((f) => ({ network: f.network, from: f.floor, to: args.targetFloor })); if (args.dryRun || plan.length === 0) { // write nothing; return what it intends to do, structured return { applied: false, plan }; } await mediation.applyFloors(args.adUnitId, plan); return { applied: true, plan };}
To the agent, this tool resolves within one decision: "align the floors." The read and the diff are hidden inside, so it is one round trip and the context does not bloat. At the same time, because dryRun defaults to true, the first call always returns only a "plan." The agent presents that plan to the human, and applies it for real with dryRun: false only after approval.
The point is that bundling did not cost us the control point. The granularity got coarser, yet inserting the single dryRun step kept "the room to stop" intact. This is the middle I reached after swinging between both extremes.
Lower granularity on purpose — only for destructive actions
Bundling by intent is the baseline, but I keep exactly one exception. Only the destructive operations whose results linger do I deliberately split fine.
Disabling a mediation group, changing a billing plan, deleting production data — these "costly to undo" operations I carve into independent tools, even when procedurally they sit right next to others. And I always give those tools a confirmation step.
// Destructive ops: lower granularity on purpose, require a confirm tokenconst tools = [ { name: "disable_mediation_group", description: "Disable a mediation group (destructive, affects revenue). " + "Call first without confirm, present the returned impact summary to the user, " + "and after approval call again with the confirmToken.", parameters: { type: "object", properties: { groupId: { type: "string" }, confirmToken: { type: "string" }, }, required: ["groupId"], }, },];async function disableMediationGroup(args: { groupId: string; confirmToken?: string;}) { const impact = await mediation.estimateImpact(args.groupId); const token = makeConfirmToken(args.groupId, impact.revenueShare); if (args.confirmToken !== token) { // token mismatch = not yet approved return { applied: false, impact, // includes e.g. revenue share over the last 30 days confirmToken: token, message: "Review the impact of this disable and re-run with the same token after approval", }; } await mediation.disableGroup(args.groupId); return { applied: true };}
Here I am deliberately breaking the rule of bundling by intent. The call for confirmation and the call for execution are split into two on purpose. For a reversible operation, coarsening the granularity to cut round trips is correct; for an irreversible one, the correct move is the opposite — add one round trip if that is what it takes to physically interpose a human approval.
I suspect this asymmetry is not unrelated to both my grandfathers having been temple carpenters. Shaping wood can be adjusted endlessly, but a beam once cut off does not come back. So the discipline of "measure as often as you like, cut only once" is in their hands. Tool design for an agent is the same: adjust freely, cut carefully, and let granularity follow that.
The return value is half of granularity design
People tend to think granularity is about inputs (arguments), but I consider the shape of the return value to be the other half.
The agent reads the return value to make its next decision. So if the return is just "succeeded," the agent cannot tell what it can do next and ends up adding another call just to re-read the state. There goes the round trip you saved by bundling inputs — undone by a poor return.
What I aim for is to include, no more and no less, the information the agent needs for its next judgment in the return value. That is why align_ecpm_floors returns plan and disable_mediation_group returns impact. From the return alone the agent can assemble "what to show the user next and what to ask."
The opposite — stuffing internal IDs and raw debug data into the return — is also forbidden. The agent pulls all of it into context, and the noise dulls its judgment. The return value should carry only what the next move needs, in a structured form. Like the input, I trim it against the same yardstick: one decision.
The numbers, and what to do next
After folding this granularity rework into the mediation operation across six apps, several measurable things changed. For the same task of "align the floors on every ad unit," tool calls dropped from an average of eleven to three, and token consumption fell by roughly 40% by feel. The accidents where the agent lost its place mid-procedure and left a half-finished state — two or three a month — went essentially to zero once dryRun became the default. With destructive actions behind a confirm token, I no longer discover an "I didn't want that disabled" after the fact.
Bigger than the numbers was how much calmer the agent's behavior looked. Back when I handed it fine-grained tools, it always seemed to be checking something one line at a time. After bundling by intent, it executes one decision in one call and stops only when it needs to. That a single change in tool granularity makes the same model look so different was, honestly, a surprise.
If you are designing the tools you will hand your own agent right now, start by reviewing your current tool list through one question: "Is this one decision for the agent?" Where two or more decisions are packed into one tool, split them; where one decision is cleaved across several tools, bundle them. And for the operations you cannot undo, break that rule on purpose and interpose a confirmation step. That single pass of review is probably the highest-leverage improvement you can make.
Thank you for reading to the end. I hope it helps the design work of others who, like me, are running a lot on their own.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.