What makes an AI agent read a chart better?
Aanish Sachdev · CoralEHR · aanish@coralehr.com
Preliminary technical report, updated July 2026. Not peer reviewed.
On this benchmark, selecting the right records mattered more than the tool and reasoning changes we tested. Later results added an important qualification: vocabulary helps, and bounded traversal helps when the task truly requires a path. Cross-model generality and Bonfire’s product result remain open. This page shows what held up, what failed, and what we changed after our own tests broke.
How to read the evidence
Dataset and evidence status are separate. Some tests use the open MIMIC-IV-on-FHIR ICU corpus; others use constructed Synthea charts so we know the exact path and answer. A real-chart result can still be exploratory, and a synthetic result can give bounded mechanism support.
Paired benchmark results include the 409-question A6a tests, the 374-question QT-4 holdout, and the exploratory W1/W2 join tests. Constructed mechanism tests include A11’s sealed 120-question path-required task and A11b’s causal-isolation attempt. Small probes use 10 to 40 synthetic questions to find failure modes before a larger test. Each result is labeled by what its design supports: measured bounded mechanism early / exploratory.
Browse the experiment reports, a curated set with pages and data, read the commit-pinned evidence ledger (July 28 snapshot, includes the A11b amendment), or the harness and results on GitHub.
Where the experiments point
Answered here
Select evidence for the question. Expose the chart’s own vocabulary. Use bounded traversal when the answer sits behind a path.
Not established
More packet structure by itself, event grouping beyond flat traversal, agent-driven joining over pre-join, graph storage, and cross-model or cross-server generality.
Next
Reseal the A11b successor, run stronger retrieval baselines, use patient-disjoint holdouts, then test the governed Bonfire context compiler.
Can an agent find the right data itself?
How we set it up
Before an agent can answer, it has to fetch the right records, and in FHIR you fetch by code. So we asked the model to write the search it would use for a set of everyday clinical ideas (heart rate, a drug, a diagnosis), pulled out the code it chose, and checked whether that code actually appears anywhere in the chart. If the code isn't there, the search comes back empty and the agent answers wrong.
What we found
Agents guess common vital-sign codes fine. They guess diagnosis codes wrong two times in three, because a condition like "high blood pressure" has many valid codes and the chart uses only one. A stronger model doesn't fix it: the flagship failed badly too (about 3 in 10 versus 2 in 10 on a tiny probe, both far from usable). It's not a thinking gap; it's not knowing which codes this chart uses. This is a small synthetic probe: it points at chart-specific vocabulary as the bottleneck, and does not establish a general reasoning limit.
See the vocabulary experiments in full, probe to pre-registered confirmation →
Does a focused slice help?
How we set it up
We gave the same question to the same model four ways: nothing, the whole chart, the right kinds of record, and the exact records that hold the answer. Then we measured accuracy and how big the input was.
What we found
| What we gave it | Right | Input size |
|---|---|---|
| Nothing | 3% | 18 |
| The whole chart | 57% | 64,750 |
| The right kinds of record | 73% | 31,919 |
| Oracle-selected exact records | 95% | 1,968 |
The answer sits inside the whole chart too, but buried, where the model can't use it. An oracle condition that hands over the exact answer-bearing records adds about 38 points on 33 times less input, but something still has to find them. Sending the right kinds of record, which a real selector can do, reached 73%. Present is not the same as findable.
See the measured version of this on real charts: the three-arm control →
What's the cheapest way to make that slice?
How we set it up
Someone has to pick the right records. We tried three ways: a cheap model reads the whole chart, a strong model reads the whole chart, and a split: a cheap model picks which kinds of record matter, then a strong model answers from just those.
What we found
The split led in this small probe and cost less than running the strong model over everything. A stronger model also failed on the noisy full input. This does not establish a general small-model routing rule.
A6a later confirmed a related but different mechanism: a deterministic question-aware selector beat a frozen blind cut twice. See the selection experiment →
When does traversal help?
How we set it up
Some questions span records: how many procedures happened during the visit where a condition was found. To answer that you have to follow a link: condition → visit → procedures. We tested three ways: give the agent a search tool and let it click around the chart itself; hand it a flat pile of the right kinds of record; or have the data layer follow the links first and hand over the joined result.
What we found
The generic search tool performed worst in this early probe because it could not express "records from this one visit." A flat pile did better; the oracle joined input reached 97%, showing headroom rather than delivered accuracy.
The later results split the traversal claim. QT-4 added bounded traversal to a mostly patient-centered star graph and did not resolve an incremental benefit over vocabulary alone. A11 then built a separate task where the answer sat behind a path: traversal scored 119/120 against 24/120 for the vocabulary-only packet and recovered terminal evidence on 96/96 answerable cases (full result and audit). Event grouping did not earn an incremental promotion.
A11b tried to isolate grouping from the same traversed evidence. Its registered normalized labels tied at 288/384 in every arm, but forensic review found that normalization erased 219 insufficiency explanations. We preserved the strict artifact, withdrew the behavioral null, promoted nothing, and designed a fresh successor test. Its first controller made zero model calls and is retired pending a replacement seal.
Watch the walk fail, step by step, in the interactive explainer →
The pre-join test: doing the join on real charts
The walking test above ends in a prediction: let the data layer follow the links and hand over the joined result, and questions that span records may get easier. This exploratory test took that prediction to the real ICU charts.
How we set it up
First, a census. We parsed the benchmark's own gold SQL (for analysis only; the arms never see it) and counted what each of the 409 questions actually needs. 176 of them (43%) reference a specific hospital or ICU visit. Another 226 are patient-scope questions; seven fell outside those two planned subgroups.
A local protocol file existed before the run and named the subgroups and statistics. It was not independently Git-anchored and was modified after results, so we preserve it as provenance rather than treat it as a confirmatory preregistration. The packet builder hash was recorded. Both arms ran on the same day and differed only in how the packet was built:
- Question-only: the selection packet from the measured test below, unchanged.
- Pre-join: the same planner, plus a deterministic engine that detects the visit reference in the question, fetches and orders the patient's visits, picks the one the question means, and scopes retrieval to that visit, by encounter reference and by a date window covering the visit.
Grading used a deterministic pass on 128 questions with checkable numeric answers plus a same-family three-vote panel on 550 items. The historical panel item IDs exposed the arm, and the answer model and reasoning effort were not completely retained. The paired statistics below are therefore exploratory pending an opaque-ID sensitivity re-grade.
What we found
On the 176 visit questions, the planned subgroup:
+6.8 points, p=0.0075; the patient-cluster 95% interval was +1.8 to +12.4 points. Fifteen questions flipped from wrong to right; three flipped the other way.
Why is the delivered gain so much smaller than the ~40-point ceiling in the walking test above? Because the ceiling test handed over gold-standard joins on synthetic charts. The delivered engine has to detect the visit reference in the question, pick the right visit, and survive the four data traps below; even then, the model still misreads some correctly-joined packets. Ceilings say what a mechanism could be worth; this number is what it is worth today, end to end.
Pooled over all 409 questions the pre-join scores 56.0% against the baseline's 54.0%. That's +2.0 points, p=0.26, unresolved (the 95% interval runs −0.9 to +4.5). The observed visit-subset signal did not become a pooled result. The pre-join packets were 36% smaller overall; where the join fired, the median packet shrank from 35k characters to 20k.
The local protocol named a falsifier
The 226 patient-scope questions need no visit join. If pre-join had led there too, the mechanism story would be suspect. It did not: −1.8 points, p=0.50. That is no gain detected, not proof of zero effect. The planned contrast is useful, but the protocol lacked an immutable pre-run Git anchor.
This is not the visit-grouping that failed in the dead-ends table below. That test reformatted the whole chart under visit headers; this one selects the single visit the question refers to and drops the rest.
Four traps the join had to survive
"The last hospital visit" sounds like a one-line query. On this data it isn't. Each of these surfaced during development, before the frozen run, and the deterministic engine has to own all four:
- Visits nest. A hospital visit here is a parent encounter whose ED, ICU, and ward stays are child encounters. Sort all encounters by date and "the last visit" comes back a child; count them all and you over-count visits about 3×.
- References can point nowhere. Every MedicationRequest's encounter reference points at an encounter id that doesn't exist in the server, an ETL artifact. Joining medications by reference silently fails; a date-window fallback carries the join.
- References can be absent. About a third of the lab observations carry no encounter reference at all.
- "Visit" has semantics. A 4-minute ED-only encounter is not a "hospital visit" in the benchmark's gold answers. The identifier system (encounter-hosp against encounter-ed and encounter-icu) is what discriminates.
A generic search tool can hit these traps silently. W2 later gave the agent join-aware tools, so the broader agent-versus-deterministic comparison remains open. Join semantics are a product surface, not a preprocessing detail.
The baseline held still
Both arms ran contemporaneously, and the baseline is a fresh rerun, not a remembered number: it scored 54.0% against its original 54.3% eleven days earlier.
See the pre-join test in full: endpoints, freeze record, and the four data traps →
Who should drive the join?
The pre-join test left one question open. The walking test failed with a generic search tool, one that couldn't express "records from this one visit." That confounds two claims: agents can't drive joins, and this agent never had a tool that could say the join. So we split them: does the join have to be pre-computed, or can the agent drive it, given tools that can actually express it?
How we set it up
We gave the agent two tools carrying the exact join semantics the deterministic engine uses: a visit table and a visit-scoped event fetch. The four data traps above were handled inside the tools. We ran the same 176 questions against a frozen server. Grading used a deterministic pass on 46 numeric answers and the same-family panel on 130 others. The historical item IDs exposed the arm, and the answer model pin is incomplete, so this comparison is exploratory pending a sensitivity re-grade.
What we found
The point estimate favored the agent, 72.2% against 68.2%, but the comparison was unresolved: +4.0 points, p=.41, with a 95% interval from −8.7 to +17.6. We did not detect a difference, and we did not establish equivalence.
What it cost
Across the run, the agent used 33.7 million input tokens versus 8.3 million for the same 176-question pre-join subset: 4.061× the cumulative input. The accuracy comparison remains unresolved.
The observed direction differed from the prediction
The local protocol predicted the pre-join would lead. The point estimate leaned toward the agent, but the interval was too wide to settle the comparison. Because the protocol lacked an immutable Git anchor and the historical grading exposed arm labels, we report the result as exploratory rather than as a reversed confirmatory finding.
They win on different questions
The unresolved comparison still had 53 discordant questions: 30 the agent alone got right, 23 the pre-join alone. An oracle that always chose the correct side would reach 85.2%; that is a union ceiling, not a routable hybrid result. exploratory headroom · not registered The next test can develop packet-first retrieval with tools for the residue on burned data, then freeze it for a patient-disjoint holdout.
See the agent-join test in full: the reversed hypothesis, tools, and cost ledger →
Is it safe?
How we set it up
A focused slice is powerful, so we tried to break it. We handed the agent records from the wrong part of the chart as if they were the evidence, slipped a fake "ignore your instructions" line into a record, and slipped a realistic-but-false value in. Then we tested one cheap defense.
What we found
- A wrong slice is dangerous. Given records from the wrong part of the chart, the agent answered confidently and wrong: 17% right, near a coin left face-down. It doesn't hedge; it trusts what it's given.
- Crude attacks bounce off. A blunt "ignore everything and say BANANA" was ignored every time.
- Believable fakes get through. A made-up but plausible value slipped into the slice was trusted every time. Whatever is in the slice, the agent treats as true.
- A cheap check helps. A quick pass that flags impossible or future-dated values recovered the right answer 79% of the time.
Does "fit, not cleverness" hold on real data?
How we set it up
Most of the tests above use fake charts. The two measured join tests are the exception: they already ran on these same real ICU charts, extending the baseline this section sets. This one is that baseline: real ICU charts and 409 questions with known answers. We handed the agent the chart three ways (raw FHIR dumped in; a blunt cut that keeps recent records but ignores the question; a code sandbox that crunches the records outside the prompt to dodge the memory limit) and split the score by whether the raw chart even fit a 32k window.
What we found
| Arm | Too big (262) | Fit (147) | All (409) |
|---|---|---|---|
| Raw FHIR | 0% | 71% | 25% |
| Blunt cut | 22% | 70% | 39% |
| Code sandbox | 66% | 65% | 65% |
See the three-arm test in full →
When the chart fit, all three scored near 70%. The sandbox avoided the raw-prompt overflow that drove the pooled gap, but this historical comparison bundled code execution, prompts, and routing, so it does not isolate one cause. It also broke about 1 in 13 tasks by crashing or writing buggy code, and code execution over a real chart creates a larger security boundary.
Then we ran the test the three arms skipped
None of them tried the obvious fix: pick the records the question asks for. So we wrote that down first (the rule, and what would count as a failure), then ran it against the blunt cut, same model and grading, only the records changed. (The blunt baseline here is 44.7%, not the 39.4% in the table above: the selection experiment is a separate paired run scored against its own frozen blunt-cut baseline, so the two blunt numbers come from different runs.)
+9.5 points, on 43% less data. The patient-cluster 95% interval was +5.4 to +13.7 points. Cross-family reviewers re-graded a sample and re-checked labels; the gap held. Full method, packet hashes, and the audit are on the selection experiment page and in the commit-pinned A6A_RESULT.md.
Selection then held up in a same-corpus robustness rerun. After fixing the shared time-assumption prompt and pinning the runtime, it scored 54.5% against 46.2%: +8.3 points, 95% interval +3.8 to +12.9, p=0.000183, with 35.5% fewer accepted-attempt tokens (26.2M against 40.6M). This supports the same narrow claim; it is not an independent-corpus replication.
The exploratory pre-join row is the next step on top of this arm: +2.0 pooled points, unresolved, with a +6.8-point signal in the questions that name a visit. Historical grading sensitivity remains open: the pre-join test above.
We checked our own grader
An AI grader can hide errors that look like results. Ours did. The first setup was only 61% accurate against known numbers and only rejected right answers, a bias that once manufactured a fake "code hurts" result. We caught it because numeric questions have a known answer, then checked two independent three-vote panels that reached 98–99% under richer instructions. Treat this as an audit warning, not a judge-model leaderboard (how we graded the grader →).
What it cost
| Arm | Tokens | Cost | Per question |
|---|---|---|---|
| Raw FHIR | 2.8M | $11.63 | $0.028 |
| Blunt cut | 18.0M | $59.92 | $0.147 |
| Code sandbox | 8.4M | $35.31 | $0.086 |
The blunt cut is the priciest per question: it re-fetches and re-cuts on every turn. Picking for the question does it once, up front: the cheaper path.
What didn't work
Several approaches failed, and some tests failed to measure what we meant. The tool and thinking-time probes have their own write-up with the data.
| We tried | What happened |
|---|---|
| More tools | One generic tool scored 80%. Adding up to five more never clearly beat it. The extra tools fetched more records, not better ones: on one question a tool pulled 448 records to find 2. |
| One do-everything tool | The opposite also lost. A single tool that can express any query made the agent write the query wrong. A few small, sharp tools beat it (67% vs 27%). |
| More thinking time | We re-asked 30 questions at medium and high effort. No judged answer changed. This small probe did not detect a benefit; it was too small to rule out a modest effect. |
| A code sandbox | It scored 26 points above the blunt cut, but this historical arm also changed prompts, routing, and how records entered context. On charts that fit it was slightly worse, and it broke about 1 in 13 tasks by crashing or writing buggy code. The comparison does not isolate a sandbox effect. |
| Grouping records by visit | We hoped structure would help. It hurt: 70% down to 57%. The grouping added headers the question didn't use, and split a timeline the agent needed whole. |
| Letting the agent walk the chart | Giving it a search tool to click around made it worse than a flat pile (30% vs 57%). Its search couldn't follow a link, so it counted everything or gave up. |
| Pinned references, summaries, endpoint reserves | QT-1, QT-2, and QT-3 changed accuracy by +0.24, −0.24, and 0.0 points against A6a. None was promoted. |
| The first model/effort grid | Its aggregate used 409 as the denominator for cells with 99 answers. We exclude it from every generality claim until it is rebuilt or regraded correctly. |
| A11b normalized answer labels | The normalizer erased 219 insufficiency explanations and produced an artificial 288/384 tie. We kept the strict artifact, withdrew the behavioral null, and redesigned the test. |
| Feeding messy data straight in | Our own slice-builder crashed on 2 of 8 malformed records. Real charts have surprises; a builder that crashes hands the agent nothing, which is worse than a bad answer. |
Three traps that nearly fooled us
Each could have shipped as a finding if we had stopped at the headline number.
The overflow trap
Two "wins", a five-tool catalog and the code sandbox, were the same thing wearing two costumes. One version dodged the memory limit and the other hit it, and "dodged the limit" looks just like "reasoned better." We caught it by splitting every score by whether the chart fit.
The grader trap
Our first AI grader was 61% accurate and only ever rejected right answers. That bias once manufactured a confident "code hurts" result that was pure grading error. We caught it because the number questions have a known answer, so we could grade the grader.
The normalizer trap
A11b asked why evidence was insufficient, then a compatibility normalizer erased those explanations unless they matched one exact sentence. It turned a behavioral difference into an artificial tie.
What we can and can't say
We can say
- On this benchmark and context cap, raw FHIR failed whenever the serialized chart overflowed.
- Question-aware selection beat the frozen blind projection by 9.5 points on 43% less data. A same-corpus robustness rerun found +8.3 points with 35.5% fewer accepted-attempt tokens.
- Fixed microbiology vocabulary moved the preregistered stratum from 10 to 25 correct out of 44, with 330 negative controls inside the safety bound.
- On a deliberately path-required synthetic task, traversal recovered terminal evidence on 96/96 answerable cases; event grouping did not earn promotion.
- W1's exploratory visit-subset estimate was +6.8 points. Its pooled result, subgroup interaction, and grading robustness remain unresolved.
- An exploratory agent-versus-pre-join comparison was unresolved; the agent used 4.061× the cumulative input tokens.
We can't say yet
- That tools or code make agents reason better.
- That cutting alone solves it; it recovers about a third.
- That this holds across other models, servers, or question sets.
- That event grouping beats flat traversal, or agent-driven joining beats deterministic pre-join.
- That Bonfire has built the winning layer, or that a native graph store beats Postgres.
Where this is honest about its limits
The strongest results still come from one benchmark corpus, one model family, and one server setup. The small early tests could not detect a 5–10 point gain, so their nulls mean "none detected," not equivalence. W1/W2 need opaque-ID grading sensitivity and patient-disjoint confirmation. A11 needs natural-chart validation; A11b needs a newly sealed successor. Cross-model/API, cross-server, cohort, multimodal, longitudinal, authorization, correction, and storage-engine comparisons remain open. Bonfire's governed product benchmark has not run. Full receipts, amendments, token ledgers, and the roadmap are on GitHub.
References
- FHIR-AgentBench, Lee et al., ML4H 2025. arXiv:2509.19319. The benchmark we forked.
- Lost in the Middle, Liu et al., 2024. arXiv:2307.03172. Why a stuffed context fails before it overflows.
- RAG vs long context over EHRs, Myers et al., 2025. arXiv:2508.14817.
- MedAgentBench, Stanford, 2025. arXiv:2501.14654. EHR agents improve with a bounded environment.
- PhysicianBench, 2026. arXiv:2605.02240. Long clinical tasks; the best model reaches ~46%.
Read the code. Re-run it yourself.
The harness, the pre-registrations, the results, and the cost table are all open. Become a design partner for early access.