Tool-Call Combinators¶
Compose structured tool calls without crossing the computation channel boundary.
The problem¶
Bash beats structured tools on cost because it packs multi-step operations into single calls. A Python fix script reads a file, applies five edits, and writes it back — one tool call. Structured tools make five file_edit calls — five round-trips, five context re-sends.
The overhead is structural: each tool call is a turn boundary. The Harness re-sends the full conversation context on every turn. Five calls with 50K context = 250K tokens re-sent. One call = 50K tokens.
The pattern¶
Combinators compose tool calls at the Harness level — no new computation channel, no executable specification, no bash. The Harness executes the composition deterministically.
First-order combinators¶
gather — collect named results from parallel calls:
{
"combinator": "gather",
"calls": {
"source": {"tool": "file_read", "args": {"path": "src/tokenizer.py"}},
"tests": {"tool": "file_read", "args": {"path": "tests/test_tokenizer.py"}},
"status": {"tool": "run_tests", "args": {}}
}
}
Returns {"source": "...", "tests": "...", "status": "..."} in one round-trip. The agent’s preparation phase (read source, read tests, check status) becomes one turn instead of three.
for_each — apply a template over items:
{
"combinator": "for_each",
"items": {"tool": "file_glob", "args": {"pattern": "src/**/*.py"}},
"template": {"tool": "file_search", "args": {"pattern": "getattr", "path": "{item}"}}
}
Search for getattr in every Python file. One turn. The Harness fills the {item} holes.
pipe — output of A feeds into B:
{
"combinator": "pipe",
"stages": [
{"tool": "file_glob", "args": {"pattern": "src/**/*.py"}},
{"tool": "file_read_batch", "args": {"paths": "{result}"}}
]
}
Find all Python files, then read them all. One turn.
Second-order combinators¶
try_and_check — do A, then verify with B:
{
"combinator": "try_and_check",
"do": {"tool": "file_edit_batch", "args": {"edits": [...]}},
"check": {"tool": "run_tests", "args": {}}
}
Edit and verify in one turn. The edit-test cycle compressed from 2 turns to 1.
scope — run in isolation with rollback:
{
"combinator": "scope",
"do": {
"combinator": "try_and_check",
"do": {"tool": "file_edit_batch", "args": {"edits": [...]}},
"check": {"tool": "run_tests", "args": {}}
},
"on_failure": "rollback"
}
The Harness snapshots, runs the inner sequence, reverts on failure. Safe exploratory editing without the computation channel.
annotate — attach intent and observations:
{
"combinator": "annotate",
"intent": "Fix the string quoting bug",
"hypothesis": "The tokenizer includes quote chars in the value",
"do": {"combinator": "try_and_check", "do": {...}, "check": {...}}
}
The AIDR pattern: every operation carries metadata about why it’s being done. The intent travels with the operation through the Harness.
The grade analysis¶
Every combinator stays in the specified band:
Combinator |
Monad operation |
Grade of composite |
|---|---|---|
gather |
Product in Kleisli |
Join of components |
for_each |
Traverse |
Join of generator + template |
pipe |
Kleisli composition |
Join of stages |
try_and_check |
Bind with validation |
Join of do + check |
scope |
Bracket / local |
Join of inner + rollback |
annotate |
Writer layer |
Same as inner (metadata is data, not code) |
The composite’s grade is the join of its components’ grades. Combinators over data-channel tools stay in the data channel. The Harness is transparent at every step.
What this would buy¶
The pre-ratchet A condition used 28 turns and 16 calls. With combinators:
Current: list, list, list, run_tests, read, read, read, read, edit, edit, edit, edit, edit, run_tests, write
= 15 calls, 28 turns
With combinators: gather({files: glob, tests: run_tests}),
try_and_check(do: edit_batch, check: run_tests),
write
= 3 calls, ~8 turns
Implementation status¶
Theoretical. The combinator design is in drafts/tool-call-combinators.md. No MCP server implementation exists yet. The experiment showed that file_edit_batch (a simple first-order combinator) wasn’t used by the agent — suggesting that combinators need the Strategy Instruction pattern (pattern 2) to be effective. The agent needs to know when to batch, not just that it can.
The open question¶
Do agents naturally use combinators, or do they need the Strategy Instruction to change their approach? Our experiment suggests the latter: file_edit_batch was available but ignored until the principle instruction changed the agent’s cognitive pattern. Combinators may need to be paired with strategy to be effective.