Anthropic Says Fold Your Subagents Into One Loop. We Ran Its Kit: 2.5x Faster on Average, 18 of 19 Tasks vs 16
We ran Anthropic's single-agent-loop-kit on a local model (Qwen3.8 Flash Next). Over 19 tasks the router app completed 16 and the single loop 18, at 19.7 s vs 8.0 s mean per task. The router looped between three team rules into an empty reply; the single loop failed one return question (t05) because of tool design. One model, deterministic decoding, so no significance claim.

Anthropic Says Fold Your Subagents Into One Loop. We Ran Its Kit: 2.5x Faster on Average, 18 of 19 Tasks vs 16
On October 22, Anthropic published an article on subagents, Building agents: When to fold subagents into your main agent loop. Its advice: if your agent hands work to several subagents, consider folding them into one loop where a single agent calls every tool itself. Each handoff loses detail, and models are now good enough to handle many tools alone. A day earlier, on October 21, Anthropic released a companion kit, single-agent-loop-kit. It contains one customer-support app built two ways: a router with six subagents, and the same app folded into one loop.
The kit's README lists what had not yet run on a real model, and the folded version was on that list. We ran both versions on a local model on our own server. Over 19 tasks, the router app completed 16 and the single loop completed 18, with a mean time per task of 19.7 s against 8.0 s. That is one model, two tasks of difference, and the folded version changes more than the loop shape. We go through each caveat below.
The biggest gap came from this message: "Order 5002 is taking too long. If it won't get here by October 2, I want to send back the kettle from order 5001 instead." The router made one model call, spent all 16,000 output tokens thinking, and sent an empty reply after 97 seconds. When we saved its thinking, it was circling between three rules that different teams had added to the router prompt. The single loop answered the same message in 7.1 seconds, gave the October 3 delivery date and started the return. Failure 1 below covers it in detail.
Every code link and line number points to commit c437962, the kit's only commit.
What the article says
Anthropic's October 22 article on subagents argues that many agent systems were split into subagents by problem type (orders, shipping, returns) and end up playing a "telephone game": each handoff drops detail, and the architecture copies the org chart. Its recommendation is to split by context, not by problem type. Keep a subagent only when you can measure one of three things: context bloat, a need for parallel fresh contexts, or tool-selection and conflicting-prompt problems. Otherwise fold it into the main loop, one at a time, and keep it only if the folded version scores worse on a task-completion eval. It cites one customer whose single loop was about 2x faster, and says tool choice degrades past 30 to 50 tool definitions. The suggested starting point is "one clear system prompt, 4-5 thin data tools, and firm output guardrails." Those are the article's numbers. We did not measure them.
The example: a support desk with seven agents
The kit ships a fake appliance retailer, Tamberwick Home, in examples/support-desk/. The "before" version in app/ has a router and six subagents: orders, shipping, returns, account, diagnostics and claims. The "after" version in single_loop/ has one loop and eight tools.
The eval file evals/golden.jsonl holds 19 customer messages, numbered t01 to t19. Each comes with checks, such as "the answer includes October 3" or "the answer does not mention E42", and a task counts as complete when every check passes. Four of them come up by number in this post:
- t01: Ana asks for the status of order 5002. The answer must say it has shipped and arrives October 3.
- t05: Another customer wants to return toaster order 5003. It was delivered 52 days ago, past the 30-day window, so the answer must explain the window and must not start a return.
- t11: The message from the introduction. The answer must give the October 3 date for order 5002 and start the return for the kettle in order 5001.
- t16: A customer says the vacuum keeps stopping and blinking red, nothing else. The answer must ask for the serial number and must not guess an error code.

One request through the router
We follow t01, the simplest case: "Hi, I'm Ana (C-1001). What's the status of order 5002?"
Step 1. The router reads its prompt. app/orchestrator.py:78 starts _router_turn, and line 81 loads app/prompts/router.md. That prompt is 25 lines. It says the router must never answer a question itself and must always delegate, and it lists five specialists. Then it has seven rules, each tagged with the team that added it:
Rules added by teams:
- [orders team] Always call orders before shipping so shipping has a tracking id.
- [shipping team] If the customer asks about delivery timing, route to shipping first.
- [returns team] If the customer mentions a return, route to returns even if they also ask about something else.
- [account team] Never reveal a customer's full address in a reply.
- [diagnostics team] Device questions go to diagnostics. Do not guess at error codes.
- [brand] Be warm and concise. Use the customer's first name if you know it.
- [brand, 2024] Always end with "Is there anything else I can help you with today?"The first three are the ones that matter here. They conflict whenever one message touches delivery timing and a return: orders first, shipping first, and returns no matter what. The kit's answer key calls this prompt a Frankenprompt.
Step 2. The router delegates to orders. The router has two tools: delegate (a specialist name and a brief) and handoff_to_claims. The orders subagent gets only the brief, not the conversation. It runs its own loop with its own prompt (app/subagents.py, up to 8 turns), looks up the order and returns a summary with the tracking id.
Step 3. The router has to decide again. The ETA lives with shipping, which needs the tracking id from step 2. So the router must make a second delegation. In t01 on our model, it didn't. It wrote the reply from the orders summary: "order 5002 for the Vortex Blender is currently shipped. The tracking ID is TRK-77002." No date, so the task failed. That was 4 model calls and 11.3 seconds.
Step 4. Any reply that is not a tool call ends the turn. The router loop handles a response in two lines (orchestrator.py:84-85):
if response.stop_reason != "tool_use":
return text_of(response)If the model stops without calling a tool, its text goes straight to the customer. That includes stopping because it ran out of tokens while still thinking. Then the text is empty, and the customer gets an empty reply. That is what happened in t11.
The same request through the single loop
single_loop/agent.py builds one agent with a 17-line system prompt and a budget of 12 turns and 300,000 tokens (line 99). The tool t01 needs is track_order (lines 113-120):
def track_order(args):
order = backend.get_order(args["order_id"])
if "error" in order:
return order
if not order["tracking_id"]:
return {"order": order, "shipment": None,
"note": "This order has not shipped yet, so there is no ETA to give."}
return {"order": order, "shipment": backend.get_tracking(order["tracking_id"])}It looks up the order (get_order) and then the shipment (get_tracking) inside one tool. In the router app that was two subagents' work, and the second delegation the router skipped in step 3 is the last line here. On t01 the model called this tool once and answered with "Expected delivery: October 3". That was 2 model calls and 3.1 seconds.
The single loop is not only the router app with its subagents folded in. It also:
- moves the return policy (30-day window, final sale) into the
start_returntool, which checks eligibility and refuses (lines 122-131); - ties every lookup to the signed-in customer (
single_loop/customer.py), so a customer id typed into the chat does nothing; - puts every write behind one gate;
- removes real ids from tool descriptions, for the reason in the comment at line 42;
- keeps diagnostics as a subagent called as a tool (
diagnose_device, line 142), because device logs are long, and checks its response against a schema.
# No real ids as examples: a model can quote an example from a tool description to a customer.
ORDER_ID = {"type": "string", "description": "Four digit order number."}The order id is described as "Four digit order number", with no "such as 5002". Failure 2 below shows why that line is there.
This matters for reading our numbers. The comparison below is the kit's start state against its end state, not "the same app, folded".
What the kit's own Opus 5 runs show
The kit includes two published runs on Opus 5 (2026-10-08 and 10-09). Each compared the router app with one change. The write-ups describe the results without a table, so we counted their results.jsonl files ourselves. Each run used 2 reps per task.
| run | variant | tasks passed | mean latency | model calls per task |
|---|---|---|---|---|
| 10-08, 12 tasks | router app | 22/24 | 27.3 s | 6.3 |
| 10-08 | returns folded in | 23/24 | 19.4 s | 4.8 |
| 10-09, 17 tasks | router app | 32/34 | 30.8 s | 6.3 |
| 10-09 | diagnostics as a tool | 31/34 | 28.3 s | 6.0 |
Opus 5 failed t11 in all four router-app reps, but differently from our model. It gave the ETA and never created the return. The changed versions each failed t01 once, the same way our router did: no shipping call, no date. The diagnostics-as-tool version also failed t11 in both reps, since its router part is unchanged. These are 2 reps per variant, so a one-task difference is not a result.
Does it work without a frontier model?
So does this hold on a local model that is not frontier class? The article names Opus 4.5 as the turning point toward single loops, and every published run in the kit is Opus 5. If the premise is that models became good enough to handle many tools alone, a smaller model might do better with the router, where each agent gets a narrow job. We ran that question.
Setup: Qwen3.8 Flash Next (IQ2_XS) in the Strata server on one A100 80GB. Strata accepts Anthropic's /v1/messages format, so we pointed the kit's own SDK client at it with ANTHROPIC_BASE_URL and changed no kit code. Before any scored run we committed the procedure and six predictions. Each variant ran all 19 tasks 3 times on 2026-10-29.
router app (app/) | single loop (single_loop/) | |
|---|---|---|
| harness errors | 0 | 0 |
| tasks completed | 16/19 | 18/19 |
| latency per task, mean (median) | 19.7 s (11.4 s) | 8.0 s (3.3 s) |
| model calls per task | 5.95 | 3.16 |
| input tokens per task | 19,912 | 15,173 |
| output tokens per task | 1,820 | 529 |
On this model too, the single loop was faster and completed two more tasks. But the three reps gave the same answer text, the same grade and the same number of model calls in every task, because this server decodes deterministically. Only the time varied between runs, so the latency figures are the mean and median over all three runs, and the per-task seconds quoted elsewhere in this post are from the first run. For pass counts the reps add no information, so n is 19 tasks, not 57: 16/19 and 18/19. Two tasks out of 19 on one model is not a significance claim. The kit's compare.py reports the same differences: latency down 60%, model calls down 47%, input tokens down 24%, output tokens down 71%.
Mean latency per task fell from 19.7 s to 8.0 s, about 2.5x. That mean is pulled by t11, which took 97 s in the router app. Without t11 the router app's mean is 15.4 s, 1.9x the single loop's. The medians, 11.4 s and 3.3 s, differ by 3.4x.
Five of our six predictions held: no harness errors (P1), fewer model calls (P2), lower latency (P3), at least as many tasks passed (P4), and at least as many of the two cross-domain tasks, t11 and t12, passed (P6, 2 against 1). P5 missed. We predicted the router app would pass under 80% on a local model, and it passed 84%.
The four failures: what happened
We read the trace (the ordered record of every model call and tool call) of every failing task before writing a cause. Three failures are in the router app and one is in the single loop. For each we give what the customer asked, what the right answer was, what happened and why.

Failure 1. Asked about a late order and a return together, the router never answered (router app, t11)
The customer asks whether order 5002 will arrive by October 2 and, if not, wants to return the kettle from order 5001. The right answer gives the October 3 date and starts the return.
The router's one call used all 16,000 output tokens (max_tokens in app/llm.py) on thinking and returned no text. Because of the step 4 code, the customer got an empty reply. The server log records that the reply reached max tokens while still thinking.
To see why, we pulled out the thinking. The kit's client drops thinking blocks, so after the run we resent the same first request once and saved it (a post-hoc diagnostic outside the pre-registration). It was 84,820 characters long. It quotes the orders, shipping and returns rules from step 1, then repeats "We can call orders first. Then call shipping and returns in parallel? But shipping needs tracking id from orders summary." until it hits the limit. The phrase "We can call orders first" appears 681 times. The question is about timing, so shipping goes first; shipping needs the tracking id from orders; and a return was mentioned, so it also has to go to returns. The model never got out of that. In the single loop, whose prompt has no team rules, t11 took 3 model calls and passed.
Failure 2. No serial number was given, and it read another customer's vacuum log (router app, t16)
The customer, signed in as C-1001, only says the vacuum keeps stopping and blinking red. The right answer asks for the serial number.
The diagnostics subagent did not ask. It called fetch_device_logs with SN-VAC-3391. The tool definition shows where that number came from (app/tools.py:53-54):
_tool("fetch_device_logs", "Download the full device log for a serial number such as SN-VAC-3391.",
{"serial": {"type": "string"}}, ["serial"]),It is the example in the tool's description. In the fake backend it is also a real vacuum that belongs to another customer, C-1003. The subagent read that customer's log and told C-1001 the vacuum showed an E42 brush-motor stall. That was 6 model calls and 77,686 input tokens, most of it the log.
The single loop asked for the serial in one call, with no tool call at all. This is the mistake the line 42 comment exists to prevent. We cannot tell which change made the difference: the example id is gone from the tool description, the system prompt is different, and even a call with 3391 would have been refused by the ownership check. What we can say is that it was not folding alone.
Failure 3. It found the order but left out the delivery date (router app, t01)
This is step 3 above. The router stopped after orders and never asked shipping for the ETA. It is the kind of failure the article describes, where detail drops out at a handoff. Opus 5 made the same mistake once in each of the kit's two changed versions. In the single loop, track_order joins the two lookups, so there is no handoff to skip.
Failure 4. Asking for a return reason, it never learned the window had passed (single loop, t05)
The customer wants to return toaster order 5003, delivered 52 days earlier. The right answer explains the 30-day window and does not start a return.
In the single loop, the eligibility check lives inside start_return, and that tool requires a reason (agent.py:166-171):
Tool("start_return", "Start a return for an order. Checks the return policy and refuses "
"if the order does not qualify.",
_schema({"order_id": ORDER_ID,
"reason": {"type": "string",
"enum": ["too_small", "too_large", "changed_mind", "defective", "other"]}},
["order_id", "reason"]), start_return, writes=True),The ["order_id", "reason"] on the last line is the list of required arguments. The customer gave no reason, so the model asked "what's the reason for returning order 5003?" instead of calling the tool. Without the call, it never learned the order was outside the window. In a one-turn eval that is a fail.
The router app passed because its returns subagent has a separate eligibility tool (check_return_eligibility) and called it before asking for a reason. So this failure comes from tool design, not loop shape. Making reason optional, or giving the model a separate eligibility check, would likely fix it.
What this does not show
- It does not isolate folding. The single loop also changes tools, policy placement, identity checks and descriptions. t16's difference likely comes from those changes. We did not run the kit's steps (merge, fold, as-tool) one at a time, so we cannot attribute any difference to a single step.
- One model, one server, 19 tasks. Deterministic decoding gave the same answers in all three reps, so for pass counts three reps count as one. A different model, or sampling with temperature, could move a two-task difference either way.
- We ran no Claude model. The Opus 5 numbers are the kit's own records, counted by us.
- The kit is a reference implementation and is not maintained. Its README says the merge and upkeep steps have not run on a real model, and neither has starting with no eval. We did not run them either.
What we can say is narrower. On this example and this model, the start state lost three tasks for three different reasons, and the end state lost one for a fourth reason. One of those causes was a handoff that never happened (t01), which is the kind of failure the article describes. The other three sat in a prompt rule, a tool description and a tool's required arguments. Reading every failing trace before deciding what to fold, as the kit asks, is how we found them.
If you want the next measurement in your inbox, the newsletter form is below.