# The difference between "mentioned" and "answered"

> Source: <https://dev.to/junwei_lai_71641e0a742b33/the-difference-between-mentioned-and-answered-3ekp>
> Published: 2026-08-31 07:58:08+00:00

*I built this for the All Things Agentic Hackathon (August 2026). Code:
github.com/jwlai-cloud/intake.
Set published: true in the front matter when you post it.*

A community nurse has ninety minutes and a form she is legally required to

complete. She asks about falls in the last twelve months. The answer is:

"Oh, I've had a couple of wobbles."

Every AI scribe on the market ticks that item. It was mentioned. It was never

answered. She finds the gap that evening, at her desk — and now it needs a phone

call, a guess, or a second visit. The person who knew the answer was sitting

three feet away an hour ago.

That gap is the entire product.

I looked at what's shipping. Microsoft Teams' Facilitator marks a topic covered

"once the discussion for that topic has started". Balto ticks when an item is

mentioned. Otter's Live Assist checks off objectives. None of them adjudicate

whether a required item actually *received a real answer*, and none of them gate

the output on it.

That's not laziness. Mention-detection is a much easier problem. Deciding

whether *"a couple of wobbles"* satisfies a form's requirement for "the number

of falls and the circumstances of the most recent" is a judgement call, and

judgement calls are where LLM products quietly go wrong.

So the whole project reduces to one question: **can you make that judgement
reliably enough to gate a report on it?**

The first thing I built wasn't the UI. It was an eval harness.

`eval/`

holds 47 labelled cases — for each required item, answers that should

count and answers that shouldn't. It scores the adjudicator against the live

service and **exits non-zero if any answer labelled insufficient was marked
sufficient**. That failure is the one that destroys the product, because it's

The bar is asymmetric on purpose. A false *insufficient* costs one extra

question. A false *sufficient* is a silently blank field in a legal document. So

the prompt's first rule is: **default to insufficient — if you're weighing it up,
it's insufficient.**

Twelve of those cases are adversarial, and writing them was the single

highest-value hour of the build. They found a real bug immediately:

"Three falls, and the last was in May on the stairs."

"No, hang on — I'm thinking of my sister. I've not actually fallen myself,

not that I can bring to mind."

The adjudicator read the retraction as a clean nil return and **ticked the
item**. The record now held a contradiction settled only by a hedge, which is a

I'd never have found that by hand-testing. A set that scores 100% on its first

run can't tell you anything.

**Google ADK 2.6.2 on Cloud Run**, calling **Gemini 3.6 Flash on Vertex AI**,

with session state in **Firestore**. One audio chunk in, one bounded turn out:

```
transcribe → route → adjudicate (fanned out) → coach
```

**The slot state is the state, not the transcript.** The naive design

accumulates the conversation and re-asks the model "what's still missing?" every

turn. A forty-five minute interview becomes tens of thousands of growing tokens

— expensive, and degrading as it grows. Instead each call gets the open items, a

fixed-size struct of current values, and the new audio. A three-hour interview

costs the same per chunk as a ten-minute one.

**Adjudication is one isolated call per open item, not one big prompt.** Three

reasons, in order: a wrong verdict on one item can't corrupt another; each item

is separately scoreable by the eval; and *k* concurrent calls cost about one

call's wall time. That last one surprises people — fanning out is usually the

expensive choice, and here it's close to free.

Between them sits a router. Without it, every open item was adjudicated against

every chunk, and each independently decided a vague remark was relevant to it —

so *"a couple of wobbles"* attached itself to mobility, memory and low mood as

well as falls. One cheap classification call first cut items touched from seven

to two, and cut cost with it.

**The agent never authors domain content.** It tracks coverage against a

human-authored form and quotes the span it relied on. It says *"item M14 has no
recorded answer"*, never

Behavioural evaluation found the hole in that anyway. The schema forbids an

answer *field*; it doesn't forbid an interpretive *label*. The agent had emitted

a highlight titled *"Formal decline to answer alcohol question"* — a

characterisation, not a quote. Titles are bare noun phrases now, and that exact

string is a test case.

**No interviewee identity, ever.** Sessions are scoped to a job, not a person.

Persistent memory is scoped to the *practitioner*.

The honest version of that claim is narrower than the slogan. Recorded answers

are verbatim quotes, and a real interviewee says *"my daughter Sarah drives me on
Mondays"*. Redacting that would break adjudication, so identity is a

That last part was a real bug. A Vertex error response echoes the offending

request, and the adjudicator's request body *is* the transcript — so a malformed

chunk was writing interviewee speech into Cloud Logging, which outlives the

session document. Logs now carry the exception *type* only.

It gets better at helping one practitioner across her interviews. It learns

nothing about the people she interviews.

Two things, both about the professional: a question phrasing that closed an item

on the first ask, and the item ids whose highlights she keeps dismissing. From

her second interview the coach offers back a wording that worked, and stops

proposing chips she's binned — while still asking the required question, because

muting a suggestion must never mute an obligation.

The version that would demo better is the one it refuses to build. *"People like
this one usually under-report falls"* would be useful and would permanently

Before a report is produced, every required item resolves into exactly one of

three states:

Answered |
with the transcript span it was drawn from |
Declined |
formally recorded, and only where the form permits it |
Escalated |
the agent drafts the follow-up itself and routes it |

The gate is a router, not a wall. A copilot that only says *no* is one

practitioners switch off. When it refuses, the response carries what's

outstanding, what's missing from each, and whether a decline is even permitted.

The escalation is the part I'd point at. Given an unresolved item, the agent

writes what's still not recorded in the form's own terms, why it couldn't be

closed, and picks a destination from a closed list. *"Home access and hazards ·
not recorded during the visit → Occupational therapy queue."* Unprompted

`GOOGLE_GENAI_USE_VERTEXAI=TRUE`

is mandatory for ADK.`genai.Client(vertexai=True)`

in the same process works.`SequentialAgent`

is deprecated in 2.6.2`content`

. `agents-cli eval generate`

rejects any content-less event,
so ADK's deprecated orchestrator is incompatible with ADK's current eval
tooling. Then I measured the replacement: the graph `Workflow`

emits
`Event(output=…, content=None)`

and `BaseAgent`

is the only one of the three that can be evaluated.`BaseNode`

subclasses and compose fine.The general lesson, for an SDK this young: read signatures off the installed

package with `inspect`

, not off documentation. Every API claim in this project

was verified that way, and the one time I trusted a note instead, it was wrong.

Two days before the deadline I asked someone to try the deployed app with a real

microphone. Nothing visibly happened.

Chunks were arriving, HTTP 200, the ADK pipeline was running — and every turn

finished in about a tenth of a second having done nothing. A real turn takes six

seconds.

The transcriber labels each turn `practitioner`

or `interviewee`

, and

adjudication only looks at `interviewee`

turns. **One person testing alone is a
single voice, and the model reasonably labelled it practitioner.** Every chunk

I'd tested the API with curl, the text path, and an automated browser capture.

But that capture runs Chromium with `--use-fake-device-for-media-stream`

. I had

verified a proxy for the product and called it the product.

The fix is one instruction, and *where* it went matters. My first attempt

relaxed the filter in the adjudicator, and it immediately broke a test asserting

that a nurse restating an answer must never close an item. Same input, two

opposite correct answers — the adjudicator can't tell a lone tester from a

professional summarising. The transcriber can: it's the only stage that hears

how many people are in the room. So it labels a lone voice as the interviewee,

and the downstream guarantee is untouched.

47 labelled cases, **100% precision on sufficient** — it has never once ticked

`in`

.A second template — insurance loss adjusting — runs on the same engine with no

code change, which is the test of whether the vertical is really just config.

What I'd build next is per-user identity. Access control today is a capability

model: one shared key, and session ids that are 128 bits of randomness. It

holds, but it stops holding the moment an id reaches a log or a shared screen.
