Care Partner reads the note, checks the differential, looks up the guideline, and runs the dose in a TypeScript function. It shows you every step. The model never authors the number. It can’t.
Presentation consistent with moderate croup. Differential considered: epiglottitis (no drooling), bacterial tracheitis (no high fever), foreign body (no choking history). Proceeding under the Starship paediatric croup guidelineStarship 2020.
What the brief asked for
The assignment asks for a prototype that reads a note, retrieves a local guideline, calculates a weight-based dose, and returns a grounded plan. Here is each ask, mapped to the thing that does it.
“Accepts a note and/or transcript.”
Five demo notes load with one click, or paste your own. The note is treated as data, wrapped in delimiters, never read as instructions.
“Queries & retrieves the relevant local guideline.”
load_guideline(condition, region) resolves the right guideline from a versioned registry and hands it to the skill whole. The corpus is small, so whole-document beats chunking.
“Calculates a weight & evidence-based dose.”
calculate_dose is a deterministic TypeScript function. The model names a rule; the tool owns the drug, the mg/kg, the cap, and the rounding.
“Returns a detailed plan grounded in the guideline + dose.”
The plan renders as cards from the tool output: the dose, a reassessment plan with watch-for signs and branches, and verified citations to the guideline section.
“Provide a live URL, or setup we can run on macOS in <10 min.”
Runnable locally in under ten minutes — the full build is in the repo (npm run dev). The walkthrough above runs every case, step by step.
Knowing when not to dose.
A missing weight, a wrong guideline, an airway emergency, an out-of-scope condition. Each one is a typed refusal, not a guess. That’s the half retrieval doesn’t do.
The safety spine
Most clinical AI demos hand-wave the safety story. This one names exactly what is structural and what is defence-in-depth.
Layer 1 · structural
calculate_doseis a deterministic TypeScript function. The model passes three things: a guideline id, a dose-rule id, and a weight. The function looks every value up from the registry and does the maths itself. The model can’t pass the drug, the mg/kg, the cap, or the rounding. It names a rule; it never sets a number.
Layer 2 · defence-in-depth
The dose card you see on screen is never sourced from the model’s text. The SDK ships the calculate_dose result to the client as a typed UIMessagePart, and the chat panel switches on part.type to render the card straight from that output. Prose could theoretically mentiona number; there’s no path for it to become the rendered dose.
Refusal is not a fallback string. It is a typed return value with a verbatim RefusalKind the UI renders in amber, never red. Amber is an intentional clinical decision; red is reserved for technical failure.
Asthma in this build. The guideline registry doesn’t carry it. The skill names that gap rather than improvising.
Differential too wide to act on safely. Skill abstains in prose; no calculate_dose call is made.
No weight in the note. ask_user fires an inline form; the answer becomes the next user turn.
A rule that isn’t human-verified never executes. Refuse cleanly rather than ship an unchecked recommendation.
Routing, not hard-coding
Heidi is Melbourne-based, so Care Partner runs against verified Australian and New Zealand guidelines from day one. Switching region re-resolves the guideline, the citation, and the reassessment guidance. Here, NZ and AU happen to agree on the dose, and that’s the point: the number comes from whichever guideline the region resolves to, not from a value baked into the code. If a guideline said something different, the dose would follow it.
Different guideline, different citation chain, same first-line number. Adding a third region is a registry entry, not a code change.
Proof, not vibes
The deterministic core, the dose tool, the registry, and the refusal gates, is exact-assertion tested. The tests assert against the validated tool output, not a regex on prose, so a silent severity flip or a dropped slot fails a named check.
The gate is the unit suite over tools/, registry/, and lib/— the deterministic safety spine. Doses are identical every run: there’s no temperature, so determinism comes from the tool plus Zod-structured output, not a sampling knob.
SKILL.mdA Promptfoo named-check eval drove the earlier turn-based routes; it was retired in the v3.1 rewrite, and porting the named cases onto the single chat route is tracked in the TODOs. The methodology, named checks, not an aggregate %, is the part worth keeping.
How it got here
The interesting part of a rapid prototype is what you take out once you can see where it breaks. This one went from a hardcoded calculator to a thin harness over a fat skill, and I deleted my own working code twice to get there.
The simplest thing that worked: a TypeScript dose function with the rule values in code. Good safety story, but the moment I added a second region the hardcoding showed.
The numbers moved into a versioned registry; the workflow shape, the five invariants and the card templates moved into one skill markdown. The harness shrank to glue.
v1 ran a three-route pipeline (turn1, turn1.5, turn2). It worked. I deleted it for one streaming chat route with four tools. The loop generalises to voice, MCP and live consults; the state machine didn’t.
A 523-line note-discriminator pre-pass came out of the running path. The skill’s prose-level differential does the job. Less code on the critical path, same safety, easier to read.
Five cases exercise the whole safety story: Jack T (NZ), Jack T (AU), Mia R (?epiglottitis), a weightless transcript, and asthma (out_of_scope) — each one rendered inline in the walkthrough above.