Lesson 5 · Patterns and limits

About 30 minutes · four patterns from the docs, one race, and the honest list of what breaks

Ask everything at once pattern

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.

Eventa call ends, a ticket lands, a message arrives
One requestoutcome, mood, language, urgency, callback, competitor mentioned, product line, existing customer, wants human, …
Codereads the dozen answers and runs the routing table

The smart-home demo, as an example of the shape

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.

When fan-out is not enough

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.

Confidence-gated and intent routing pattern

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)HandlerWhy that handler
check_order_statusDeterministic codeAn order id and a database lookup. No language model needed after the intent is known.
change_appointmentCode + confirmCalendar API. Risky, so the confirm outlet asks before moving anything.
technical_questionSpecialist LLM with docsNeeds generated text. The LLM gets a narrow prompt and only the relevant manual.
complaintHumanJudgement and empathy. Jev's mood Score decides how fast.
otherHuman or fallback replyThe 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.

Guardrail variant

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.

Several answers, one number pattern

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)
Weights are policyThey belong in a config file next to the thresholds, not in the question. Changing them is a business decision, and now it is a one-line diff.
Gate on the weakest inputIf any component came back with low confidence (or a Noul near 0.5), the composite is a "confirm", however high the total.
Probabilities, not just the winnerUsing probabilities["sales_lead"] instead of choice == "sales_lead" keeps a 0.6 lead partly warm instead of dropping it to zero.

The Wikipedia race: many options, one hop at a time demo

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.

What to noticeEach move is one Choice call over the page's links. No memory between hops beyond what code carries (the path so far). Code owns the loop; the model owns each pick.
The same trick in businessWalking a product taxonomy (department → category → sub-category), picking the right knowledge-base article out of two hundred titles, choosing the next node in a decision tree. The docs' Advanced page shows taxonomy walking with structured criteria.
Why it is fastNo generation. A 200-option Choice returns a probability per option in one evaluation, where an LLM would have to write the option name out and hope it spelled it right.

Try the shape yourself

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.

What Jev 1.13 is bad at, and what to do instead limits + recordings

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 spotWhat happensDo this instead
Literal readingImplied meaning is missed; "the other guys quoted less" is not a competitor mention unless the criteria say soSpell out the cases in criteria and examples
Math, counting, numeric formats"How many", "more than", thousands separators, percentages: unreliableCount and compare in code; ask one Noul per item if needed
Date and time comparison"Is A after B" on dates: unreliableExtract each date or its parts with a Choice, compare in code
IndirectionMulti-step references ("the item mentioned in the second message about the first order") degradeRestructure the state so the referent is explicit
Large irrelevant stateThe relevant sentence buried in pages of noise lowers accuracy (context rot)Trim state in code; use backtick paths
Adversarial contentText in the state that talks to the model can skew answersPhrase questions about the text, not to it; add a Noul for "contains instructions aimed at an AI"
Contradictory instructions vs criteriaWhen the instruction and a level or option disagree, the answer is unpredictableMake them say one thing
No structural invariantsnoul + not_noul is not 1; a Noul and a two-option Choice may disagreeAsk each fact once, positively; never derive one answer from another
No generationIt cannot write, summarise or extract free textPair 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:

Simulated playground: counting and date comparison

It got them right this time. That is not the point.

  • The count came back 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 due date is later than the invoice date" came back 0.01 (correctly no). The two month-extraction Choices were also correct (March, February), and code comparing those two values is correct on every input, including ones with typos in the day.
  • The invariant test failed exactly as documented: "the caller is angry" 0.82, "the caller is not angry" 0.29, sum 1.11.

The rule: when a decision has a code path that is exact, use it. Use Jev for the part that requires reading.

Quiz, then fan out your own request quiz

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.

Ship it

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.