About 30 minutes · four patterns from the docs, one race, and the honest list of what breaks
Goal: replace a chain of "if this then ask that" with one wide request.
In a chat-model pipeline, every extra question is another round trip, so builders ask as little as possible and chain the rest. Jev inverts the economics: questions in one request are evaluated in parallel, output is free, and a request with twelve questions costs about the same and takes about as long as one with two. The docs call the resulting pattern speculative fan-out (docs: Speculative fan-out): ask every question code might need, then let code pick.
TypeSafe's smart-home demo takes a spoken request like "it's freezing in here and way too dark" and fans out speculative questions for every appliance and every direction of change (heating up? down? lights on? off? which room?), all in one request. Code then executes whichever came back confident. An LLM is kept in the loop only for two jobs the decision model cannot do: splitting a compound utterance into parts, and generating a fallback reply when nothing was confident.
The same shape fits a support inbox, a CRM webhook or a call wrap-up: one wide request, then a routing table.
The docs list three reasons for a second request (docs: second request): the first answer tells you to fetch more data, the first answer changes what the state should contain, or the options for question two depend on the answer to question one and enumerating them all would be silly. Otherwise, one request.
Goal: decide, per intent, whether code, a specialist LLM or a human handles it.
Lesson 4 built the three outlets (act / confirm / human) on a single Choice. Intent routing extends it: the Choice decides what kind of thing the request is, and each kind has its own handler, some deterministic, some an LLM, some a person (docs: Intent routing).
| Intent (Choice option) | Handler | Why that handler |
|---|---|---|
check_order_status | Deterministic code | An order id and a database lookup. No language model needed after the intent is known. |
change_appointment | Code + confirm | Calendar API. Risky, so the confirm outlet asks before moving anything. |
technical_question | Specialist LLM with docs | Needs generated text. The LLM gets a narrow prompt and only the relevant manual. |
complaint | Human | Judgement and empathy. Jev's mood Score decides how fast. |
other | Human or fallback reply | The escape hatch, always present. |
Two things make this pattern cheap in practice. First, the intent Choice costs a few hundred tokens and about 100 ms, so it can run on every message with no budget worry. Second, the specialist LLM only sees requests it is good at, with a short prompt, so it is both cheaper and more accurate than one general assistant that has to handle everything.
The same Choice, or a handful of Nouls, can sit in front of an LLM agent: is this request in scope, is the user trying to override instructions, does it mention self-harm, is it in a language we support. The guardrails cookbook shows the wiring. Because Jev never generates, it cannot be talked into anything; the jaggedness page does note that adversarial content in the state can still skew answers, so guardrail questions should be phrased about the text, not addressed to it.
Goal: build a lead score from three questions without asking the model for a lead score.
Asking "how good is this lead, 1 to 10?" is a degrees-not-situations question and the model will shrug. The composite scoring pattern asks several concrete questions and combines them in code with weights you can defend to a colleague:
# normalise a Score to 0..1: score / (number_of_levels - 1)
def norm(score_answer, n_levels): return score_answer.score / (n_levels - 1)
a = response.answers
lead = (
0.50 * a["outcome"].probabilities["sales_lead"] # did they want to buy?
+ 0.30 * a["callback_requested"].noul # did they agree to a next step?
+ 0.20 * (1 - norm(a["caller_mood"], 3)) # friendlier is better
)
# Recorded call 1: 0.50*1.00 + 0.30*0.95 + 0.20*(1-0.00) = 0.985
# Recorded call 4: 0.50*0.00 + 0.30*0.04 + 0.20*(1-0.045) = 0.203
# Recorded call 3: 0.50*0.00 + 0.30*0.05 + 0.20*(1-0.055) = 0.204 (and outcome was "unclear": gate it)
probabilities["sales_lead"] instead of choice == "sales_lead" keeps a 0.6 lead partly warm instead of dropping it to zero.Goal: see a Choice with hundreds of options used as a navigator, and why it is fast.
TypeSafe's console hosts a short demo video called "TypeSafe AI vs LLMs in Wikipedia Race": two agents start on the same Wikipedia article and try to reach a target article by clicking links, one hop per move. The Jev-driven racer treats every link on the current page as an option of a single Choice, with the target as the instruction, and asks "which of these gets closer?" once per hop. Choice takes up to 255 options per question, so even link-heavy pages fit in one call, and each hop resolves in roughly the time it takes an LLM to write its first sentence.
Paste a list of 30 to 50 article or product titles as an array state, and ask one Choice: "which title should someone read next if they want to learn about X?", with the titles as options and null descriptions. Then change X. It is the Wikipedia race with one hop, in the Playground, for a fraction of a cent.
Goal: recognise the nine documented failure modes and route around each.
TypeSafe publishes a page it calls model jaggedness (last reviewed 2026-09-17): the ways the current model's ability is uneven. It is the most useful page in the documentation. Condensed, with the fix the docs recommend for each:
| Weak spot | What happens | Do this instead |
|---|---|---|
| Literal reading | Implied meaning is missed; "the other guys quoted less" is not a competitor mention unless the criteria say so | Spell out the cases in criteria and examples |
| Math, counting, numeric formats | "How many", "more than", thousands separators, percentages: unreliable | Count and compare in code; ask one Noul per item if needed |
| Date and time comparison | "Is A after B" on dates: unreliable | Extract each date or its parts with a Choice, compare in code |
| Indirection | Multi-step references ("the item mentioned in the second message about the first order") degrade | Restructure the state so the referent is explicit |
| Large irrelevant state | The relevant sentence buried in pages of noise lowers accuracy (context rot) | Trim state in code; use backtick paths |
| Adversarial content | Text in the state that talks to the model can skew answers | Phrase questions about the text, not to it; add a Noul for "contains instructions aimed at an AI" |
| Contradictory instructions vs criteria | When the instruction and a level or option disagree, the answer is unpredictable | Make them say one thing |
| No structural invariants | noul + not_noul is not 1; a Noul and a two-option Choice may disagree | Ask each fact once, positively; never derive one answer from another |
| No generation | It cannot write, summarise or extract free text | Pair with an LLM for the words; let Jev decide whether and which |
Three of these were tested for this course on 2026-09-19. The results are a lesson in their own right:
6 at 1.0. The per-item Nouls (figs 0.99, kiwis 0.99, grapes 0.01) were just as right and are the version that stays right on a 40-item list. A single correct answer on an easy case is not reliability; the docs are reporting failures across many cases.The rule: when a decision has a code path that is exact, use it. Use Jev for the part that requires reading.
Goal: four patterns and nine limits, from memory.
1. Why does speculative fan-out make economic sense with Jev and not with a chat model?
One wide request costs about the same as a narrow one. Questions do not see each other, so they cannot improve each other either.
2. In intent routing, which handler suits "explain how to reset the inverter"?
It needs generated text, which Jev never produces. The Choice picks the intent; the LLM gets a narrow job and the right document.
3. A Score with 4 levels returns 2.4. Normalised to 0..1 for a composite, that is:
Levels are indexed 0 to 3, so the maximum score is 3 and 2.4 / 3 = 0.8. The docs' pattern divides by len(criteria) - 1.
4. The counting test returned the right number. What should a builder conclude?
The docs report failures across many cases. A single pass on six items is anecdote. Exact code paths stay exact.
Take the request you have been refining and fan it out: add every question the workflow could conceivably use, up to a dozen. Run it on five real inputs in the Playground and note which answers code would actually branch on. Delete the rest, or keep them for logging; they are nearly free.
Primary source: the four pattern pages under docs.typesafe.ai/patterns and the Jev 1.13 jaggedness page. Re-read the latter whenever the model version changes.