Adopt the Agent Framework or Write the Loop Yourself? Count the Defaults You'd Have to Override
Framework feature lists don't settle this choice. What settles it is how many of the framework's hidden defaults your agent would have to fight, and a short audit on one real task tells you before you commit.
Most build-vs-buy arguments about agent frameworks compare feature lists: this one has memory, that one has tracing, the other has a graph editor. That comparison misses what actually decides the outcome. Every framework ships a set of defaults for how the loop behaves, and the real cost of adopting one is the number of those defaults your agent needs to be different.
If the defaults suit your task, the framework saves you weeks. If you end up overriding most of them, you are writing your own loop anyway, only now it runs inside someone else's abstractions, and you debug it through their stack traces.
The loop is smaller than it looks
A plain agent loop fits on a page. Here is its shape:
messages = [system_prompt, user_request]
for step in range(MAX_STEPS):
reply = model.call(messages, tools=TOOLS)
messages.append(reply)
if not reply.tool_calls:
return reply.text
for call in reply.tool_calls:
result = run_tool(call) # your error handling lives here
messages.append(tool_result(call, result))
raise StepBudgetExceeded(messages)
A framework wraps this loop and decides a set of things on your behalf. Those decisions are what you are buying. Each one is either a gift or a fight, depending on your task.
🔍 The six defaults worth auditing
Take one real task your agent must handle end to end, and check what the framework does for each of these by default:
- Retry policy. When a tool raises, does the framework retry it silently? For how many attempts, and does it tell the model? A silent retry is fine for a read. On a write it can mean a duplicate side effect.
- Context management. When the conversation grows, does the framework truncate, summarise, or fail? Which messages does it drop first? A summariser that runs in the background can quietly remove the one constraint your agent had to obey.
- Stop condition. What ends the loop: no tool calls, a special "final answer" tool, a step cap? And what does the caller get back when the cap is reached, an exception or a partial answer that looks complete?
- Injected prompt text. Does the framework prepend its own instructions to your system prompt, or reformat your tool descriptions? You can't tune a prompt you can't fully see.
- Tool error shape. When a tool fails, what does the model see: the raw exception, a generic "tool failed", or nothing at all? The model's next move depends on this text.
- State and persistence. Where does run state live between steps, and can you resume a run that crashed halfway through?
For each default, score it:
- ✅ Keep: the default is what you would have written.
- ⚙️ Configure: it's wrong, but there is a documented option to change it.
- ❌ Fight: it's wrong, and changing it means subclassing internals, monkey-patching, or wrapping the framework's calls in your own.
A worked audit
Suppose you're building an intake agent for a small bookkeeping team. It reads receipts that arrive by email, extracts line items, checks them against a ledger, and posts entries. Posting is a write that must never happen twice. Runs are long because a single email can carry a dozen receipts. And the accountants want to see why an entry was posted, not only that it was.
Run the six checks against a hypothetical framework:
| Default | Framework behaviour | Your need | Score |
|---|---|---|---|
| Retry policy | Retries any failed tool up to three times, silently | Never retry post_entry without an idempotency key |
❌ |
| Context management | Summarises older turns when near the limit | Keep every receipt's extraction verbatim until posted | ⚙️ |
| Stop condition | Ends when the model stops calling tools | Stop only when every receipt is posted or flagged | ❌ |
| Injected prompt | Adds a short hidden preamble | Acceptable if you can print it | ⚙️ |
| Tool error shape | Passes the exception message to the model | Same | ✅ |
| Persistence | In-memory only | Resume after a crash mid-batch | ❌ |
Three fights out of six. For this task, the framework is a liability. You would spend your time working around its retry wrapper and its stop condition, and those two defaults sit on exactly the path where a mistake costs real money.
Now run the same framework against a different task: a research assistant that searches, reads pages, and writes a summary. All its tools are reads, runs are short, and a crash just means you run it again. Retry is ✅, the stop condition is ✅, persistence doesn't matter. Here the framework is plainly the right call.
The framework is the same in both audits. The answer flips because the tasks need different things.
The decision rule
After the audit, count the scores:
- No fights, a few configures: adopt the framework. Its tracing, integrations, and maintained tool adapters are worth more than the control you give up.
- One fight, on a low-stakes path: adopt it and isolate the fight behind a thin wrapper you own and test.
- Two or more fights, or any fight on a write path: write the loop yourself. You can still borrow pieces, such as a model client, a tracing library, or tool schema helpers, without letting a framework own the control flow.
⚠️ The fights on write paths weigh heaviest because that's where hidden behaviour does irreversible damage. A framework default that silently retries, summarises away a constraint, or declares the run finished early is harmless on a read and expensive on a write.
What the audit costs
The audit takes an afternoon: read the framework's loop source (not just its docs), then run your one real task with verbose logging turned on and look at exactly which messages reach the model. The docs describe the defaults the maintainers meant to ship. The logged messages show the defaults that actually run.
Two habits make the audit hold up over time:
- Pin the version and re-audit on upgrade. Defaults change between releases, and a changelog line about "improved context handling" can mean your summariser behaves differently.
- Keep the score table in the repo. When someone asks in six months why you wrote your own loop, or why you didn't, the answer is sitting next to the code.
The takeaway
Don't ask whether a framework is good. Ask how many of its decisions your agent would need made differently, and where those decisions sit. A framework whose defaults match your task is the fastest route to shipping. A framework you have to fight on your write path gets harder to live with as the agent grows, because every workaround has to be kept up. Count the fights before you commit.