DialogWright

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:

KindAnswersIn 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 criteria in intents.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.

An intent's criteria, sent to the model as written: apps/secretary/intents.yaml (excerpt)
  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:

apps/secretary/app.yaml (excerpt)
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.

The settings

Where the decision model is reached is a setting in the app's .env, never in the app's files:

SettingWhat 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