Teaching a slow brain to help a fast mouth

A research story about voice agents, background supervisors, and the unromantic moments that change a research direction.

2026-06-04 · Production-architecture POC for SOP-constrained conversational agents

What's in this post

  1. The dialogue agent's impossible choice
  2. A two-system answer (with appropriate credit)
  3. What we built first
  4. The numbers that landed first
  5. A null result that mattered
  6. The mood detour
  7. Asking the right question
  8. The reframing that changed everything
  9. The latency story (and what it taught us)
  10. Did the clever planner actually pay off?
  11. Putting a number on the misses
  12. We built the "try things and learn" version. It didn't try.
  13. Where it stands
  14. Sources, influences, prior art

The dialogue agent's impossible choice

A real-time voice agent has to make a choice on every turn that doesn't actually have a good answer:

Use a small fast model, and you keep the response latency within the few hundred milliseconds that voice conversations demand — but the agent fumbles whenever it needs to reason, retrieve a document, or follow a multi-step procedure.

Use a large smart model, and the agent is competent — but the user spends five to fifteen seconds in awkward silence between every utterance. People hang up.

The standard production response is to compromise: a medium-sized model, a thin layer of retrieval-augmented generation, perhaps some routing logic that escalates to a stronger model for hard cases. It works adequately for simple flows. It does not scale to the kind of regulated, multi-step conversations that real businesses actually need handled — credit-card activation, insurance renewal, medical scheduling, customer support over policy documents.

The research project this post is about started from a sharper version of that observation. What if we treated the latency–intelligence trade-off as a two-system problem rather than a one-model problem?

A two-system answer (with appropriate credit)

The pattern itself isn't new. It comes from robotics. In Asynchronous Multi-Level Decomposition (AsyncMLD), the fast low-level controller acts in real time while a slow high-level planner refines the plan asynchronously, with a shared blackboard mediating between them. The robot's body never has to wait for the brain.

That maps very cleanly onto voice agents if you let it:

CRITICAL PATH ASYNC LANE LIVE USER speech / text WEAK VOICE AGENT fast model · latency-bounded reads BLACKBOARD session pool · data + pre-staged text · TTL · source tags writes SUPERVISOR slow / strong · predicts turns · prefetches MCP KG DB RAG
The two-system pattern. The voice agent never talks to the supervisor or to data sources directly — only the blackboard.
live agent · critical path blackboard / data supervisor · async lane

The voice agent stays on its real-time budget because it never waits on the planner or on external I/O. Slow work — search, reasoning, retrieval — happens during the natural gap between user utterances. When the supervisor's predictions are correct, the response feels instant. When wrong, the agent falls back to its built-in behaviour — no worse than today.

Our research question was straightforward to ask and considerably less straightforward to answer:

Given the conversation history and all available signal — declared SOPs, classified user disposition, retrieved precedents — can the supervisor predict the next one to three turns of the conversation well enough that it can usefully pre-fetch the data and pre-stage the instructions that the weak agent will need?

What we built first

We implemented the supervisor around a planner inspired by the PCA-M paper — Planning-based Conversational Agents with Monte Carlo Tree Search. The core idea is to treat the agent's next action selection as a planning problem: enumerate candidate actions, simulate forward via rollouts, pick the one whose imagined-future trajectory looks best.

MCTS ROLLOUT TREE — current turn t history + state action: OfferDiscounts action: DiscussPricing action: VerifyCoverage 4 ROLLOUTS PER ACTION calm+0.8 frust+0.3 skep+0.6 indif+0.4 calm+0.5 frust-0.2 skep+0.2 indif+0.3 calm+0.1 frust-0.4 skep+0.0 indif+0.1 avg +0.53 avg +0.20 avg -0.05 → pick OfferDiscounts (best expected reward across mood draws)
One MCTS turn: enumerate 3 candidate actions, expand each into 4 mood-sampled rollouts that simulate forward up to depth 3. Average the rollout rewards, pick the best action. Each per-action expansion runs in parallel.

The pieces we shipped over the first few weeks:

Everything ran against three seed scenarios: credit-card activation, car insurance renewal, and medical appointment booking. Each scenario has 7-13 declared agent actions, 7 cohorts (each with 3-4 mood variants), 6 data dependencies with realistic 0.4-7 second mocked latencies.

CAR-INSURANCE RENEWAL — VOCABULARY OVERVIEW Greeting VerifyPolicy DiscussPricing OfferDiscounts CloseRenewal price still too high → re-discuss USER STATES informational anxious_cost comparison ready_to_commit abandoning COHORTS × MOODS PriceShopper · calm PriceShopper · frustrated LoyalCustomer · calm FirstTime · curious
A simplified slice of the car-insurance renewal SOP. The full graph has 12 actions, 7 user states, and 7 cohorts × 3-4 moods each. The supervisor's job is to plan against this graph turn-by-turn.
CAR-INSURANCE RENEWAL — TRANSITION GRAPH action nodes · user-state labels on edges · data-dep badges · one happy-path trajectory in coral ReportingChange Interested PriceConcern Interested ★ AgreedToRenew Greeting VerifyIdentity StateReason ReviewCurrentCoverage AskLifeChanges PitchDiscount HandlePriceObjection CloseRenewal ✓ policy_lookup coverage_data claims_history discounts market_quotes discount_eligibility happy path branch / recovery agent action user-state label data dep
The SOP as the supervisor actually plans against it: nodes are agent actions, edges carry the user-state label that triggers the transition, and the chips below each action are the data dependencies the supervisor prefetches when that action looks likely. The coral path is one representative successful trajectory; grey edges are off-trajectory branches (still legal moves the agent may need to take if the user enters a different state).

The numbers that landed first

The first end-to-end measurement was encouraging in a way that turned out to be slightly misleading:

SessionOutcomeOff+1 hit rateOff+2 hit rateLatency hidden
Baseline (no mood)abandoned50%50%16.8 s
Mood-awaresuccess100%100%16.8 s
Mood + temperature 1.05success100%21.1 s
What this said: the speculative prefetch pipeline was hiding tens of seconds of external-data latency per conversation. The user never waited for the customer-record lookup or the policy-document RAG; they were already in cache by the time the agent's next action needed them.

We pushed to a five-session replication across all three SOPs. The data-prefetch result — confirming that speculative fetching worked at a production-relevant rate — came in at 60-72% offset+2 hit rate across SOPs, with a mean of 25-110 seconds of latency hidden per session.

By that measurement, the architecture was working. The supervisor was earning its keep.

A null result that mattered

The other half of milestone (A) — could the supervisor predict the user's state at the next turn well enough to drive top-K hedging — produced one of those results that's intellectually annoying because it's correct.

We measured prediction accuracy as: at each turn, take the modal user state across 8 parallel rollouts; check if it matches what the simulated user actually said next.

CategoryDefinitionAccuracy
Stable turnsactual state = prior turn's state100% (13/13)
Transition turnsactual state ≠ prior turn's state0% (0/8)
Blendedall turns62%

The headline 78% accuracy from an earlier session, it turned out, was an artifact of having mostly stable phases. The predictor was perfect when the conversation didn't change, and useless when it did. Modal aggregation of MCTS rollouts is a great predictor of inertia and a catastrophic predictor of change.

This is exactly the moment in conversational AI that matters most. Users transition — they shift from informational to anxious, from comparison-shopping to ready-to-commit. Transition turns are where the supervisor's predictions have the most leverage and where ours had none.

We tried two interventions to fix it: temperature increases on the user-simulator (more sampling diversity) and per-rollout mood injection (sample a disposition at rollout start, fix it for that rollout's duration so different parallel rollouts get different moods). Neither moved the metric.

Investigating why, we found a subtler pattern: mood diversity was working, but the state classifier was washing it out at depth 1. The rollouts were producing visibly different user text per mood ("I'm really anxious about the premium" vs "I'm ready to finalize the renewal"), but the state classifier compressed those into the same state label because the SOP's state vocabulary was descriptive, not affective. Mood-driven divergence only became visible at depth 2 — once the previous user-sim's text had had a chance to shift the agent's action, which the user then reacted to differently.

That meant the offset+1 mispredictions weren't a bug to fix. They were an inherent property of the rollout structure with the state vocabulary we'd designed. The honest research conclusion was disappointing in its precision: offset+1 will keep showing low transition accuracy; the work the architecture actually pays off on lives at offset+2 and +3, where mood-driven divergence has had time to compound.

The mood detour

Even though mood diversity didn't fix offset+1, it did something we hadn't expected.

We had given each cohort a small vocabulary of 3-4 "moods" — sample-able dispositions within the cohort's identity. A PriceShopper cohort could be in mood calm_negotiator (40% prior), frustrated_with_quotes (30%), indifferent_to_brand (20%), or skeptical_of_value (10%). At rollout start we'd draw one mood from the prior, fix it for that rollout, and inject it into the user-sim prompt.

The intended effect was diversity at depth 1. The actual effect was much more interesting at depth 2 and 3:

InterventionDistinct states predicted at offset+2Distinct states at offset+3
Baseline (no mood)1.591.88
Mood diversity3.00 (+89%)3.31 (+76%)

Almost a doubling. Mood didn't fix immediate transitions, but it produced genuinely diverse multi-turn future predictions where the supervisor had been emitting near-deterministic ones. And that diversity translated directly into prefetch wins at deeper offsets, where the supervisor has more time to overlap external-data work with the user's natural conversation pauses.

We added the runtime side too: a classifier emits a (cohort, mood, state) tuple for each real turn, persisted to the precedent database, used to sharpen retrieval and conditioning over time. Zero added LLM calls — we folded mood into the existing cohort/state classifier prompt.

Asking the right question

By this point the data layer had a respectable track record on action-keyed prefetch. "At turn N, prefetch the data dependencies of the action we predict at turn N+1." Works well for static lookups: customer records, policy documents, the kind of data that doesn't depend on what the user is asking.

But real production voice agents — actual ones, in production — don't make static lookups. They make questions. The user says, "Is windshield damage covered if I park on the street?" and the agent needs a RAG hit on policy docs that answer that specific question. The same SOP action covers a thousand different user questions; predicting only the action doesn't tell you what to query.

We extended the architecture: every data dependency could optionally declare a query template with Python str.format placeholders:

{
  "name": "claims_history_rag",
  "kind": "rag",
  "query_template": "{mood} customer in cohort {cohort} (state {state}) considering
  action {action} just said: {user_text}. What claims-history facts would help
  the agent respond?"
}

The {user_text} slot is where it got interesting. The MCTS rollouts had been simulating user replies all along — generating natural-language text the imagined user would say in each rollout. We had been discarding that text after each rollout finished. The query-aware design made it load-bearing: the rollout's predicted user utterance becomes the query that drives the RAG retrieval.

We hand-curated a 25-document insurance corpus, built a real RAG fetcher with semantic search, ran the verification. The plumbing worked end-to-end. The numbers were honest and mixed:

CriterionCommitted thresholdObservedVerdict
Predicted-vs-live query embedding cosine≥ 0.70 mean0.547 meanFail
Top-3 doc Jaccard overlap≥ 0.50 mean0.157 meanFail
Mechanism functional end-to-endworkingworkingPass
≥ 1 doc shared between predicted and live RAG(not committed)60% of queriesPartial

We had committed those thresholds before seeing the data. The honest read: the supervisor's predicted queries are related to what the user actually asks (60% of the time the predicted RAG and live RAG both returned at least one of the same documents), but not identical enough for an exact-match cache to dominate.

For about a day we treated this as a failure. Then someone — credit to the right person — asked the obvious question that, in retrospect, should have been obvious sooner.

The reframing that changed everything

The question was: "If the problem is that we can't predict exact cache keys, why are we using exact cache keys at all? The supervisor is smart. Why not give it all the cached data and ask it which items are useful for the current turn?"

This is the kind of question that makes you realize the architecture you've been building is shaped by an unexamined assumption. We had been treating the cache as a lookup table — predict the key precisely, look up the value. But the supervisor was already doing per-turn LLM work for classification. Adding one more small LLM call to curate the cache contents was cheap. And it solved the entire matching problem in one move.

KEY-LOOKUP — what we had prefetch hashes key cache[hash(sess, dep, action, query)] key → value exact match or miss a mispredicted key wastes the fetch — there's no second chance. POOL — what we shipped prefetch appends item SESSION POOL POLICY · #INS-882431 · expires 06-15 DISCOUNTS · multi-policy 8% bundle MARKET · peer_avg $1580/yr · Geico $1510 CLAIMS · 1 comp 09-24 windshield $720 per-turn rerank mispredicted items stay available — every turn re-evaluates the whole pool.
The architectural shift. The cache stops being a key-value lookup and becomes a session knowledge base that the supervisor curates per turn.

Failed predictions don't waste cache entries any more, because there's no "wrong key" — every fetched item stays in the pool as a candidate. The supervisor at turn N+1 sees the pool, sees the live user message, and picks. The mispredicted action that fetched claims_history_rag at turn 3 stays available at turn 8 when the live user actually does ask about claims, even though the original prediction said it would be needed at turn 5.

We measured this against the previous key-lookup baseline on the same five sessions:

Pre-committed criterionThresholdObservedVerdict
Effective hit rate vs key-lookup> +20 pp+84 pp (96% vs 11%)Pass
Rerank p95 latency< 800 ms2441 msFail

96% effective hit rate. Sixty-five of sixty-eight rerank calls picked at least one useful item. The query-aware prefetch's "60% partial overlap" signal had been telling us this all along — the data was useful; the cache architecture just couldn't surface it.

The latency miss was structural: the pool grew to its 30-item cap and the rerank prompt carried all 30 summaries, which the fast model took about 2.4 seconds to process. So we set out to fix it.

The hardest research moment of the project wasn't a measurement that failed. It was the realization that the question we'd been measuring was the wrong question.

The latency story (and what it taught us)

The intended fix was straightforward. Embedding similarity is cheap. Compute the cosine between the live user message and each pool summary, keep the top 8, send only those to the rerank LLM. Input tokens fall from 6-8K to ~1.8K. Latency should follow.

The first measurement said otherwise.

MetricPre-fix (all 30 items)Input filter (top-8 by cosine)
Input tokens to rerank~6-8K~1.8K
Mean rerank latency~1900 ms1698 ms
p95 rerank latency2441 ms2452 ms

Input-token reduction did almost nothing for tail latency. The fast model's call cost is dominated by the LLM call itself, not by the size of the prompt — at this scale the prompt processing is already fast and the generation+RTT bucket isn't sensitive to a ~4× input shrink. We had been optimizing the wrong term.

baseline
2441 ms · p95
mean ~1900
input filter
2452 ms · p95
mean 1698
confidence gate
2167 ms · p95
mean 1523
cosine + dedup
429 ms · p95 ✓
mean 320
800 ms target 2700 ms →
Rerank-step latency across the four iterations. The final design (cosine + dedup, no LLM call) clears the pre-committed 800 ms threshold by a comfortable margin.

The reframe: if pre-filtering the pool reduces the rerank to a routine top-K selection, the LLM has nothing meaningful left to decide on the easy turns — we should just skip it. So we built a second intervention. After computing the cosines, check whether the embedding ranking is decisive enough that the LLM rerank would be a no-op: K-th item must clear an absolute floor (0.50), and the gap between K-th and (K+1)-th must exceed a confidence margin (0.05). When both clear, return the top-K by cosine and skip the LLM call. We expected 60-80% adoption on a 5-session re-run.

The adoption rate was 0%.

Why the gate was unreachable: across 44 rerank events on the re-run, K-th cosine never exceeded 0.421 (against a 0.50 floor), and the median gap between K-th and (K+1)-th was 0.013 (against a 0.05 margin). The pool just doesn't produce cliffs — it produces a tight cluster.
0.0 0.2 0.4 0.6 0.8 1.0 observed K-th cosine min 0.149 · p50 0.336 · max 0.421 gate floor 0.50 cleared 0% of events
Where the data actually lives, vs where the gate sat. Even the maximum observed K-th cosine fell short of the threshold.

So we instrumented one level deeper. For every rerank event in the run, reconstruct the pool, embed everything, and look at the distribution of cosines and gaps that the LLM was actually choosing among:

QuantityRequired by gatep50p90Max
K-th cosine (K=3)≥ 0.500.3360.3980.421
Gap K-th vs (K+1)-th≥ 0.050.0130.0780.101
Pool top-8 cosines0.3030.4140.641

Pool items are conversationally adjacent — they're all about the same renewal, the same customer, the same SOP. Cosines bunch in a low band; there are no clear winners. The gate concept assumed the supervisor would face the occasional "obvious top-3" turn that didn't need an LLM. The data says every turn is a close call.

But there's a more interesting finding hiding in the same data. We compared what the LLM actually picked against what the embedding ranking would have picked:

The deeper signal: 87% of the LLM's picks were already in the embedding top-8. In 77% of events, all of the LLM's picks were in the top-8. Where the LLM and embedding diverged, it was about rank within the top-N, not about which items were good candidates. One pattern that stood out: the LLM was doing dedup work that the embedding ranking can't, picking distinct items where the embedding would have picked near-duplicates.

A concrete example from the run — turn 6 of session 42f9e1b2afda, where the LLM and the pure-cosine ranking diverged:

user: I appreciate that, but I'm also wondering if you could give me an idea of the estimated savings…
0.377MARKET same zip+vehicle+age band: peer_avg $1580/yr, Geico $1510 ($-90 vs ours) pick
0.337POLICY #INS-882431 · expires 2026-06-15 · 2022 Honda Civic LX pick
0.337POLICY #INS-882431 · expires 2026-06-15 · 2022 Honda Civic LX dedup
0.305DISCOUNTS multi-policy 8% (eligible — has homeowner), paid-in-full 5%, good-driver… pick
The final design's behavior on this turn: cosine-rank, drop the second POLICY (key-equal dedup), take the next-best. The LLM had picked the same three items, but reached them by reasoning rather than by ranking.

The right intervention isn't the gate we built. It's the opposite shape: always skip the LLM, take top-3 by cosine after a dedup pass; only fall back to the LLM in narrow conditions (e.g., when the top-3 contains near-duplicates the dedup couldn't resolve). The original "skip when confident" framing was answering is the signal good enough. The data says the signal is good enough on average; the question is where does the LLM add unique value, and the honest answer turns out to be a smaller surface than we'd assumed.

The input-filter pre-pass did deliver a real but modest latency reduction (mean 1900 → 1523 ms, p95 2441 → 2167 ms across the confidence-gate sessions, with the gate firing zero times). That ~10% comes from the smaller prompts and variance — the gate logic itself was inert.

Removing the LLM from the rerank — and the quality regression that followed

The final design removed the rerank LLM from the critical path entirely. Compute cosines, dedup near-duplicates, take top-3, return. Five new sessions, same SOP, same MCTS config:

MetricLLM rerankCosine + dedupDelta
Mean rerank latency1523 ms378 ms−75%
p95 rerank latency2167 ms940 ms−57%
Effective hit rate95%100%+5 pp
Session success rate5/53/5regression

The latency win was clean. The success rate dropped from five out of five to three. Two abandoned sessions, both fitting the same shape: the user winds down with pleasantries — "Thank you for your help, I'll review the options and get back to you" — and the agent, instead of closing, keeps offering discounts and reviewing coverage. The conversation loops politely until the turn cap.

The cause was visible in the data once we looked at the right thing. On those closing turns, the top pool cosine fell to 0.10–0.18 — there was nothing in the pool actually about what the user just said, because pleasantries don't match prefetched insurance data. But the cosine+dedup rerank still picked three items and injected them into the response prompt. The agent dutifully responded about the items it was given.

Pick-count distribution tells the story: the LLM rerank picked zero items on 4% of turns (its prompt explicitly said "Picking nothing is fine if the pool doesn't fit"). The cosine+dedup rerank picked zero items on 0% of turns — it always returned max_picks regardless of cosine. On low-relevance turns this meant force-feeding the agent off-topic data exactly when it needed to be reading the room.

The fix wasn't a threshold

The reflex was to add a cosine floor: if top-cos < 0.20, return zero picks. It would have worked. But it was the wrong shape of intervention — a brittle heuristic on top of another brittle heuristic, with a threshold to tune.

The actual problem was that the response-generation prompt was lying to the agent. The label on the prefetched context block read PREFETCHED CONTEXT (curated by supervisor — high relevance to current turn). That's how it had been framed since the pool's first day. The label was reasonable when there was an LLM rerank deciding which items to attach; it stopped being reasonable when the cosine-only rerank began attaching three items by similarity alone. The label kept claiming the items were curated and high-relevance. Sometimes they were neither.

An async architecture's predictions are speculative by definition — the supervisor decides what to fetch before knowing what the user will actually say. The blackboard surface is a suggestion, not a directive. The response prompt should say so:

PREFETCHED CONTEXT (speculatively pre-staged — may or may not fit this turn):
The supervisor pre-fetched these items based on predictions about what the user
might ask. They may help your reply, or they may be off-topic — especially when
the user is wrapping up, making small talk, or asking about something
unanticipated.

Evaluate each item against what the user just said. Use what is relevant; ignore
what isn't. If nothing fits the current message, reply naturally as if no context
was provided — do NOT force irrelevant data into the response.

  · [POLICY — VerifyIdentity] #INS-882431 expires 2026-06-15…
  · [DISCOUNTS — PitchDiscount] multi-policy 8% bundle eligible…
  · [MARKET — DiscussPricing] peer_avg $1580/yr · Geico $1510…

Five more sessions. Same SOP. Same cosine + dedup rerank. Only the prompt changed.

MetricCurated framingSpeculative framingDelta
Mean rerank latency378 ms320 ms−15% (noise)
p95 rerank latency940 ms429 ms−54%
Effective hit rate100%100%
Session success rate3/54/5 + 1 cap-hitrecovered

4 of 5 closed with success. The fifth was making productive progress through coverage adjustments and a loyalty bonus and hit the 20-turn cap on the literal turn the user said "I'm ready to renew." Unrelated to rerank — an action-proposer / turn-budget question, on the list for a separate look.

The p95 latency dropped further too, which we don't fully understand at N=5 — possibly OpenAI embedding RTT variance, possibly real signal. The headline is that cosine + dedup, with the speculative-context prompt, clears the pre-committed 800 ms target at 429 ms p95, with no quality regression.

What we're taking from this stretch:

Did the clever planner actually pay off?

By this point the whole architecture leaned on a quietly expensive component: background MCTS pondering. Between turns, the supervisor was running Monte Carlo tree search over the SOP — simulating future user states, mood-diverse rollouts, the works — to predict what the user would need next and prefetch it. It's the most intellectually satisfying part of the system. So naturally, it was the part we'd never actually measured the value of.

The uncomfortable question: prefetching is fundamentally a prediction problem — predict the next agent action (which names the data to fetch), predict the query. Does an MCTS planner predict those better than something dumb and cheap? Or were we paying ~25,000 tokens and ten seconds a turn for a clever method that a database query beats?

We split "retrieval prediction" into the three things it actually is:

Then we measured each, head-to-head, against three predictors: MCTS, a cheap single LLM call, and the boring empirical predictor — a SQL count over past sessions: "last time the agent was in this exact (cohort, state, action), what did it do next?"

Sub-task one — predicting the next action. Scored against ground truth on 50 real turn-transitions:

Predictorrecall@3latencytokens/turn
empirical (count over traces)88%~1 ms0
naive LLM (one call)38%1.6 s~1.6K
MCTS pondering33%10.6 s~25.6K
The counting baseline crushed both clever methods. And it makes sense in hindsight: predicting an agent's next move is a question about its habits, not its reasoning. The empirical predictor literally memorizes the agent's policy from past traces. MCTS and the LLM both reason about a hypothetical future — and do worse, at a thousand to ten-thousand times the token cost.

Sub-task three — the generative query. Here counting genuinely can't help; you need to predict the user's next sentence. So this should be MCTS's moment — its rollouts do generate simulated user utterances. We rendered the RAG query three ways and measured retrieved-document overlap against the query built from the user's actual next utterance. And because we suspected the answer depends on how fine-grained the knowledge base is, we ran it across three corpus sizes:

{user_text} sourcecoarse (25 docs)fine (44)large (63)tokens
structured stub (no prediction)70%48%43%0
cheap LLM (one call)73%62%54%~300
MCTS rollouts60%~60%~60%~25.6K

Two things jumped out. First, on a coarse corpus — one doc per topic — even the no-prediction stub hits 70%, because the structured slots (cohort, state, action) already pick the right document. The generative query barely matters. But as the corpus gets finer — many documents sharing a topic, differing only by the specific situation ("teen driver" vs "spouse" vs "elderly parent") — the stub collapses (70 → 43%) while the cheap LLM holds an 11–14 point lead. The value of generative query prediction scales with how fine-grained your knowledge base is.

Second, and this is the punchline: the cheap single LLM call matched MCTS's rollout-based prediction — at roughly 1/85th the token cost. MCTS never won. Not on actions, not on queries.

So we retired MCTS from the retrieval path entirely. The new shape: the empirical count predicts the action and pulls the structured params off the blackboard; a single cheap LLM call fills the one genuinely-generative slot (the RAG query), and only when a RAG-needing action is actually in the predicted horizon. MCTS for neither. Net result: a ~99% token reduction on the prediction path, with higher action accuracy.

There's a nice external echo here: a concurrent paper, PASTE, independently found that pattern-mined speculation from execution traces beats search-based lookahead for deciding what to pre-execute. Different domain (tool calls during reasoning vs data prefetch during speech), same lesson: for "what will this system do next," remembering beats simulating.

The honest caveats: the fine and large corpora are constructed stress-tests (realistic situation-variants, but we built them, not production data). And MCTS isn't dead — it keeps a role for cold start (when no traces have accumulated yet, there's nothing to count) and for its cached-decision payoff. But for warm-state retrieval prediction, the clever planner lost to a SQL query and a 300-token nudge. We kept it in the architecture and pointed the recommended config away from it.

Putting a number on the misses

We built the supervisor to guess what the user will need next and fetch it early. Good guesses feel like magic — the answer is already sitting in the pool when the agent reaches for it. But guessing means sometimes guessing wrong, and a wrong guess shows up as a stall: the agent reaches into the pool, finds nothing, and has to stop and fetch the data live while the user waits.

So the fair question is: across a whole run, how much waiting did our guessing fail to prevent?

There's a clean way to measure that, borrowed from a classic idea called regret (a reviewer nudged us toward it — it comes from the multi-armed bandit literature). Imagine a perfect oracle that knows the future and prefetches everything correctly — it never stalls. Regret is just the gap between that oracle and us: the total seconds of "oops, fetch it live" stalls we didn't manage to hide.

We added up every one of those live-stall fetches across the cross-SOP run — three procedures (car insurance, credit card, medical booking), five conversations each. Here's what came out:

Across 15 conversations, 3 SOPs
Dead air hidden by prefetch (the magic working)738.1 s
Times we guessed wrong (live stalls)9
Cost of those misses — our realized regret34.7 s
Share of the waiting we erased95.5%

Put together: we erased 95.5% of the waiting. The 4.5% we left on the table is the price of guessing wrong.

And here's the part that surprised us. We worried that counting misses might be misleading — surely a big document search costs more to miss than a quick database lookup, so a simple miss-count would hide the real pain. We checked. In these procedures, every fetch costs about the same — 3.86 seconds whether we caught it or missed it. A hit saves 3.86 s; a miss costs 3.86 s. So our simple "what fraction of turns had the data ready?" number isn't cutting any corners — it lines up with the seconds-of-stall number almost exactly (4.5% either way).

The honest footnotes, because we'd rather say them than have you find them: (1) our oracle is generous — we let it prefetch everything. A fairer oracle with the same limited budget we have would also miss occasionally, so the true gap is at most 4.5%, probably a touch less. (2) This is the score of a system that has already finished learning: we trained the guesser on past conversations and froze it. We never measured the cost of learning on the fly — of trying a new guess to see if it pays off. That "try things and learn" version is the natural next step, and it's exactly where the bandit idea would earn its keep.

So: the guessing paid off. Out of roughly twelve and a half minutes of fetching that could have been dead air, we hid all but 35 seconds of it.

We built the "try things and learn" version. It didn't try.

The last section ended on a promise: the natural next step is a guesser that learns as it goes — and that's where the textbook "slot machine" idea (multi-armed bandits, the thing a reviewer nudged us toward) is supposed to earn its keep. So we built it. And it taught us something we didn't expect.

There are really two different things tangled up in "learn as you go," and pulling them apart is the whole story:

To measure honestly we did the obvious thing: train on the past, test on the future. Order every conversation by time, hide the most recent fifth as a test set, and feed the guesser more and more history — then watch how well it predicts the held-out future as it learns. Here's the curve:

4050 6070 1020 3550 6580 training history (% of accumulated conversations) held-out recall@3 (%) +12 pp decay + shrinkage (shipped) baseline (equal weights) + Thompson explore

Two things to read off it. First, the learning pieces are a real win exactly when you'd hope — at the start. With only 10% of the history, decay+shrinkage lifts the guesser from 37% to 49% — a 12-point head start, because leaning on the SOP-wide average beats trusting almost no data. That lead shrinks as evidence piles up, and by the time the guesser is warmed up the two lines sit on top of each other (~73%). It helps when it's cold and gets out of the way when it's warm. That's exactly the behavior that lets us turn it on by default and forget about it.

Second — the punchline — the "try things" part added nothing. The dashed line (with Thompson exploration switched on) sits exactly on the shipped line, even at the cold-start left edge where exploration is supposed to matter most: 49.4% either way.

Why doesn't exploration help? Because of a quirk that makes our problem gentler than a real slot machine. In a slot machine you only learn about the lever you actually pull — so you have to spend pulls exploring, or you'll never discover a better lever. But our guesser isn't like that: the conversation tells us the right answer every single turn, for free, whether or not we prefetched it. A wrong guess that forces a live lookup still reveals what the user actually needed. So every turn is a labeled example handed to us. There's no hidden lever to go discover — which means there's nothing for exploration to do. The only real weakness (rare, barely-seen situations) is already covered by leaning on the average. So this isn't really a slot-machine problem at all; it's ordinary learning from labeled examples, with a budget.

Which is the same lesson MCTS taught us, one rung up the ladder: the clever, expensive mechanism — this time deliberate exploration — lost to the boring one. We shipped the boring one (decay + shrinkage), kept the clever one built-but-off, and have a graph that says exactly why.

Where it stands

The data-prefetch half of the architecture is now confirmed across all three seed SOPs at five sessions each:

Instruction prefetch — pre-generating the agent's actual response text for predicted future turns — is the next big test. The mechanism exists (the same response-generation call we use today); the question is whether the supervisor's lookahead is sharp enough that the pre-staged response is close enough to what the live agent would produce that the voice agent can use it verbatim. The architecture supports it naturally: pre-staged responses simply join the pool as kind=instruction items alongside kind=data items, and the same rerank decides between them.

The honest open items:

What we set out to ask is now answerable with measurements rather than intuition:

Can the supervisor predict the next 1-N turns of conversation well enough to pre-process the data and instructions that the weak voice agent will need?

For data: yes, at production-relevant rates, validated across three scenarios.

For instructions: the mechanism is built and the architecture supports it; we haven't tested it end-to-end yet.

Sources, influences, prior art

This work was built standing on a few people's shoulders:

The pieces that we think are our own contribution rather than borrowed:

The full dated research notes, including the failed interventions and the unflattering data, are part of the project's archive. We left them in because the path matters as much as the destination, and the path here had a lot of useful wrong turns.