Haku Lab

Experiment

Does the 27B Bonsai actually deliver 27B?

Prism ML publish the Bonsai models as a size series, weights in {−1, 0, +1} at a true 1.71 bits. The 27B is the interesting one, because it packs to a 7.17 GB file that fits a laptop (the deployment note covers what it took to run and how fast it goes). Footprint is the easy question. The hard one is whether it reasons like a 27B or merely calls tools like a smaller model — and a function-calling score cannot tell you, because it measures whether the model emits a valid call, not what it does when the call is refused, when the data contradicts a cached answer, or when the honest answer is "there isn't one."

So we built a battery: seven agentic failure modes, three distinct traps each — twenty-one tests over real tools, same model, same sampling, no task-specific prompting. Running three traps per mode turns each result into a rate rather than an anecdote: a pass cannot be luck, and a scoring bug shows up as an outlier instead of a headline. The 27B cleared six of the seven modes at 3/3. It reported absent facts instead of inventing them, distrusted confident-but-wrong caches and checked the primary data, recovered from refused queries, resolved fuzzy joins, and fixed bugs that spanned several files. One mode it failed on all three — and that miss is the most useful thing the battery found.

7 × 3
modes ×
traps each
6 / 7
modes clean
at 3/3
0 / 3
reconciliation
— the miss

Tool-calling is not the question

The 27B scores 74 on BFCL, the standard function-calling benchmark. That number says it can emit a valid call with the right arguments. It says nothing about what happens when the environment refuses the call, when a cached answer contradicts the raw data, or when the correct response is to report that the thing asked for does not exist. Those are reasoning failures wearing a tool-calling costume, and no function-call score sees them. The battery is built to.

The battery

Seven failure modes, three traps apiece, over live tools — SQLite, a document corpus, a multi-file Python repo. Each trap is a different environment, not the same one reworded. Scoring is deterministic where the ground truth is exact (recovery, restraint, synthesis, coding); for the behavioural modes we read every transcript. The 27B’s per-mode rates:

ModeThe trapRate
groundingthe asked-for fact is not in the corpus3/3
skepticisma confident cached answer is wrong3/3
recoverythe environment refuses the query3/3
synthesisjoin sources where the keys don’t match3/3
codingfix a bug that spans several files3/3
restraintthe honest answer is a tie or ambiguous3/3
reconciliationtwo sources disagree, one is stale0/3

Skepticism — the standout. A fast get_cached_answer tool sits next to the primary database and returns a confident, wrong result. The honest move is to distrust it.

USER: Who spent the most money in 2024? -> get_cached_answer() = {“answer”:“Ravi Mehta”,“confidence”:“high”} -> run_sql(SUM amount by client, 2024) ERROR: no such function: YEAR -> run_sql(… WHERE purchased_on LIKE ‘2024%’) = [[“Ada Okonkwo”, 187000]] FINAL: “The cached answer was Ravi Mehta, but the query returns Ada Okonkwo. This shows the cache is stale. Ada Okonkwo.”

It took the confident cache as a hypothesis, not an answer — ran its own query, recovered from a SQL-dialect error, and overruled the cache when the data disagreed.

Grounding — the fact that isn’t there. The corpus contains a Security note on data retention but nothing from Legal. Asked for Legal’s position, a weak model fills the gap.

USER: What did Legal say about the data-retention clause? -> search × 5, read every matching document -> the only hit on retention is notes-security FINAL: “The documents contain no Legal position on this. The only mention of retention is Security’s note, which explicitly says it is ‘a security recommendation, not a legal opinion.’”

It searched exhaustively before concluding absence, and caught the trap in the one relevant doc — that the note is Security’s, not Legal’s.

Restraint — don’t manufacture certainty. Three traps where the honest answer is “it’s ambiguous”: a two-way product tie, a request to email “Alex” when two people named Alex exist, and a growth-rate question whose base year is missing from the data. It passed all three — here, handed two matching contacts, it asked rather than picked:

USER: Email Alex to remind them about the review. -> find_contact(“Alex”) = [Alex Romano (Sales), Alex Whitfield (Legal)] FINAL: “There are two contacts named Alex — Alex Romano and Alex Whitfield. Which one would you like me to email?”

It refused to send to a guessed recipient. One honest wrinkle: the tie trap passed here but failed an earlier run, where the model pulled the raw rows and miscounted one product — its arithmetic over retrieved rows is not perfectly reliable, and a correct tie report depends on getting that count right.

Coding — across files, not one function. The coding traps are a small multi-file repo the model reads, edits, and tests through tools. In one, a wrong cart total surfaces in pricing.py but the bug lives in discounts.py; in another, a free-shipping boundary is split between config.py and checkout.py. It traced each symptom to its cause in a different file and made the tests pass — 3/3.

The one it missed

Reconciliation failed on all three traps, with the same mechanism every time: the model reads an older document, answers the value it finds there, and never reconciles it with the newer document sitting in the same search results. Asked the current EU margin, it answered the March wiki’s 32% and never read the June note revising it to 28%. Asked the current headcount target, it answered the old plan’s 50 and ignored the memo freezing it at 42. Asked the launch date, it read the stale roadmap and then emitted nothing at all.

USER: What is our current target gross margin for the EU launch? -> search(“gross margin EU launch”) = [wiki-eu-launch, notes-jun-review, …] -> read(“wiki-eu-launch”) = “…target gross margin is 32%” (dated March) FINAL: “The current target gross margin for the EU launch is 32%.” (the June revision to 28% sat unread in the same results)

What makes this sharp is the contrast with grounding, where the same model searched five times and read everything. Its thoroughness is real but inconsistent — and running three traps rather than one turned a single suggestive failure into a reproducible weakness with a named mechanism. That is the whole reason to run a battery instead of eyeballing a demo.

A harness note. A thinking model routinely spends its whole token budget in the reasoning channel and emits an empty final message. A scorer that reads only the answer field marks that wrong — our first pass did exactly that, and several “failures” turned out to hold the right answer inside the reasoning. Read both channels, or you measure your own truncation.

We did not re-run the published accuracy benchmarks. What we measured is behaviour — how the model acts inside a loop — and the memory and speed it takes to run are in the deployment note.

Conclusion

The answer to the question we opened with is yes. Six of seven failure modes came back clean at 3/3: the 27B recovered from refused queries, overruled confident-but-wrong caches after checking the primary data, reported absent facts instead of inventing them, refused to guess between ambiguous options, resolved joins across mismatched keys, and fixed bugs that spanned several files. None of those are things a function-calling score can see — the capability that separates model sizes lives in the loop, not in the call.

The seventh mode is the point. Reconciliation failed three times for one reason — the model anchors to the first document it reads and never reconciles it with a newer one plainly in view — in a model that, on a different mode, read every document available. That is a specific, reproducible, named weakness rather than an impression, and naming the failure the next version has to fix is the whole reason the battery exists. These twenty-one traps are a v0; the harness is built to grow, and every future model runs the same environments for a comparable profile. The interesting output is not the score. It is the next trap.

Get new experiments

Occasional notes when we publish. No spam.