DialogWright / Decision models
Decision models
DialogWright understands callers through a decision model: a model that answers typed questions with probabilities and writes no text. Everything else, what happens next and what is said, is code and the lines you approved.
What a decision model is
A generative LLM writes text. To act on it, an agent prompts it, reads what it wrote and parses a choice out of the words, and the words differ from one call to the next. A decision model is asked a question with its answers defined in advance, and returns the answer as a type, with a probability:
| Kind | Answers | In the shape it comes back |
|---|---|---|
| Noul | Yes or no: the probability that a condition holds | {"type":"noul","noul":0.97} |
| Choice | Which one of a defined set, with a probability for each option | {"type":"choice","choice":"sam","probabilities":{"sam":1,"riley":0}} |
| Score | How much, along ordered levels the question describes | {"type":"score","score":0,"probabilities":{"low":1,"normal":0,"high":0}} |
The answers above are an illustration, abridged from one turn of the front desk's recording. A noul near 0.5 is a coin toss, yes and no about equally likely, not a medium amount of anything. And a typed answer guarantees the shape, not the truth: how well a model reads an app's callers is measured on that app's own recorded calls.
How DialogWright asks one
Each turn, the engine sends the caller's words, the state of the call and every question the moment needs, in one request. The model answers them together, and none sees another's answer. The questions come from three places:
- The intents. One choice over everything a caller can ask for, each option described by its
criteriainintents.yaml. - The slots. Each slot type asks its own: whether a value was given, which part of the words it is, whether it is complete, which option was meant.
- The engine's own checks. Whether the caller is talking to the line, whether the words could be made out, whether they finished, whether they want a person, how frustrated or urgent they are, whether they switched language, and a screen for an attempt to steer the agent.
On a turn of the front desk, that is about 20 questions. A second request is made only in a narrow case: a
question for the knowledge base asked in the middle of a task, whose topic is asked once the turn has been
read as one. A generated line's checks are asked after it is worded. Each request has a budget,
JEV_TIMEOUT_MS (1500 ms by default), with one retry inside it; a turn the model never answers
gets the slow-turn line, and is handed to a person if it fails again.
urgent:
criteria: Says their call or their matter is urgent, an emergency, or cannot wait for a call back, and wants to reach someone at the studio now. Said at any point in the call it is urgent's, even in the same breath as a correction or another detail of a message, as in wait, it's for Sam, and it's urgent
label: reach the studio's main line
kind: form
priority: { correctsForm: true, threshold: URGENT_SURE }
Thresholds
Deterministic code reads the answers against named thresholds: how sure the model must be to take an intent, to fill a slot, to treat a turn as addressed to the line. The engine has defaults, and an app sets its own where its callers need it. An intent the model is unsure of is confirmed with the caller by default. The front desk lowers the bar for an urgent call, because missing an urgent caller costs more than putting through one who did not need it:
thresholds:
URGENT_SURE: 0.4
A threshold is moved by measurement across an app's recording, never on one call: the regression takes
--threshold NAME=value, and a sweep compares values across the recorded calls.
Known gaps
A known gap is a line where the real model, in a recorded run, reads a borderline case differently from what the caller meant. Almost all are short: a single word, a fragment, a bare name. Each is tagged in its app's corpus with the reason and the outcome the model is known to produce, and none of the current ones leads to a wrong action: each ends in a re-ask, a reasonable different question, or a person. The usual fix is the question's words or a rule of context in code, not a different model.
Jev, the default
The default decision model is Jev, TypeSafe's System One model (jev-1.13). It is reached
through TypeSafe, the default, or through OpenRouter or the Vercel AI Gateway, which serve the same model.
The engine's thresholds were measured on Jev, and every example app is recorded against it.
The perceiver is pluggable. Any model that speaks the same System One API (a state, typed questions, typed answers) can take Jev's place. Its probabilities are not calibrated like Jev's, so a run on one says so at start, its answers are recorded into a cassette of their own, and the app's thresholds are checked against that recording before they are trusted.
Speed and cost, measured
A decision model writes no text, only a few probabilities, so a turn pays almost only for reading it. From the maintainer's recordings on 2026-10-14:
- Speed. A turn's batch of about 20 questions was answered in about 90 to 150 ms at the median (p50), and about 150 to 250 ms at p95.
- Cost. A recorded run of 309 requests to Jev, the front desk's recording, cost about $0.046: about $0.00015 a turn.
- Microsoft-Decision-1's public price has no charge for output.
Measuring a model on your own cases
Which model reads an app's callers best is a measurement on the app's own words. Every adapter records into
the same cassette format, so an app's labelled utterances and scripted calls are a benchmark: record them
with another model (regress --client record) and compare its run with Jev's.
The decision model also judges generated wording: asked whether a generated summary, heard in its sentence,
is faithful to what the caller said. pnpm render:judge-eval measures a judge on labelled cases,
each a caller's message and a summary marked faithful or unfaithful by a person. On 2026-10-14 the
maintainer compared two decision models on the same 180 cases, both through OpenRouter:
| Judge | Unfaithful summaries caught | Faithful summaries refused | p50 | p95 |
|---|---|---|---|---|
Jev (typesafe/jev-1.13) |
56 of 64 | 15 of 116 | 198 ms | 299 ms |
Microsoft-Decision-1 (microsoft/microsoft-decision-1) |
48 of 64 | 3 of 116 | 262 ms | 370 ms |
Both fit the 500 ms a call gives the checks. Microsoft-Decision-1 refused far fewer faithful summaries, so callers would hear a generated summary more often; Jev caught more unfaithful ones, and every everyday slip. Neither follows who "you" is in a summary spoken to the caller. Jev stayed the judge, as the stricter and the faster. A refused summary is never lost: the caller hears their exact words instead.
Measuring the judge (guide 16.13) Choosing models by measurement (guide 17.11) The measurement
The settings
Where the decision model is reached is a setting in the app's .env, never in the app's files:
| Setting | What it does |
|---|---|
JEV_PROVIDER |
typesafe (the default, with TYPESAFE_API_KEY), openrouter (OPENROUTER_API_KEY), vercel (AI_GATEWAY_API_KEY) or custom, a compatible endpoint |
JEV_MODEL |
The model to ask, as microsoft/microsoft-decision-1 on OpenRouter; required for custom |
JEV_BASE_URL |
The endpoint's base, the part before /v1/systemone; required for custom, with JEV_API_KEY if it needs one |
Keys and cassettes (guide 2) Every option (guide 14.12) The roles in the design