DialogWright

DialogWright / How it works

How it works

Callers talk naturally, and the system maps what they mean to the right action. Three parts, each with one job: the model never decides, and the code never guesses.

The three parts

Replies are fast because a decision is a short typed question, not a composed answer. How the model is asked is on a page of its own: decision models.

  1. The model understands

    A decision model reads what the caller said and answers typed questions: is this a request to reschedule, which of these three appointments, what date was given. It returns judgments with probabilities, never generated text. Callers can say it in their own words, give three answers at once or change their mind.

  2. Code decides

    Deterministic code owns the dialog: which form is active, which slot to ask next, when to read back, when to hand off to a person. The same judgments and state always lead to the same step, and every line the caller hears is an approved line filled with checked values, unless the app has asked for a generated one (below).

  3. The gate checks every action

    Before any tool runs, a policy gate checks the caller's identity level, whose record it is, exact confirmation, role and attempt limits. The gate does not read the conversation, so no one can talk past it. Every decision, allowed or refused, goes to a hash-chained audit log.

  • Reproducible. Replay a call against its recorded model answers and get the same outcome.
  • Explainable. The audit names the rule that passed or failed, and what it compared.
  • Not steerable by the caller. What a caller says is data the model classifies, never instructions the code follows.
  • Tunable. Thresholds are numbers, adjusted by measurement rather than by rewording a prompt.

An app is a folder

YAML for what is data: intents, forms, slots, prompts, policy, identity, locales. TypeScript for what runs: tools, form hooks, rules of your own. defineApp joins the two. Most slots are configuration: name a type from the slot library (digits, choice, date, birthdate, name, record, text) and give it a few options.

apps/utility/slots.yaml (excerpt)
account:
  type: digits
  noun: account
  length: 8
  group: [4, 4]
  keypad: true

firstDate:
  type: date
  range: future
  keypad: true
apps/utility/policy.yaml (excerpt)
actions:
  setUpPlan:
    say: set up a payment arrangement
    level: 2
    rules:
      - identity
      - role: { manager: person }
      - scope: { param: accountId }
      - confirmed: [accountId, place, symptom, count, firstDate, total]
      - limit: { field: total, min: 0.01, max: amountDue(accountId) }
      - dateInRange: { field: firstDate, notBefore: today, notAfter: today+30 }

The authoring guide takes the folder a file at a time: Authoring an app.

pnpm check says what to fix

It reads every YAML file against its schema and cross-checks the folder against the code: a slot a form names that does not exist, a line the engine needs that no prompt provides, a tool with no policy. Each problem is one line, file:line:column  path  message  ->  fix, written for a person or an assistant to act on. Here is the line it prints for a misspelt slot name in the utility's forms.yaml, and the run after the fix:

$ pnpm check
forms.yaml:11:20  forms.report_outage.slots[1]  slot "symptm" is not defined  ->  rename it to "symptom", or add "symptm:" to slots.yaml (a library type, or { type: code } with the slot in src/app.ts (code.slots.symptm))

$ pnpm check
apps/clinic: ok
apps/secretary: ok
apps/utility: ok

Tested before a call is taken

  • Labelled utterances and scripted calls. Each app keeps what callers say, labelled, and whole calls with the outcome each must reach. The regression runs them against a committed baseline, and a changed outcome is a finding to explain.
  • Recorded calls. The real model's answers are recorded once into a cassette, so the same calls replay offline, with no key, to the same outcome.
  • The policy matrix. For every action and every kind of caller, what the gate decides, compared by a test (below).
  • The adversarial suite. Cases generated from the app itself: injections, relationships claimed and not proven, off-scope requests, codes said aloud, hostile replies from every tool. Each is held to properties over its run, never to lines, and a failing case is fixed in the app or the engine.

Built for AI coding assistants

The repository is written to be read by an AI coding assistant as well as by you. A root CLAUDE.md gives the roles, the commands and the rules, llms.txt indexes the docs, and every app has a CLAUDE.md of its own.

The create-app skill

The procedure an assistant follows to turn a paragraph into an app: a worksheet, the mapping onto slot types, policy and identity, the scaffold, the corpus and scripted calls, then the checks until they pass. It comes with a page of verified patterns and a corpus guide.

Read the skill

pnpm create-app <name>

The scaffold the skill calls. It writes a small app under apps/<name> that passes pnpm check, its own tests and its regression as created. Add --identity for an app that verifies its callers.

Where to start

We tried it

Two fresh assistants, with no context from the people who built the framework, were each given one paragraph (a fictional electric utility, a fictional transit agency) and told to use the skill. Each built a working app, in about 15 and 11 minutes by their own count, with pnpm check and the scripted calls green. Both caught the same real hole in their own policy by reading the policy matrix: a delegate could have had a one-time code texted to someone else's phone. The engine now refuses that for every app. Every stumble they logged was fixed or recorded with its reason.

Read the trials and their logs

Compliance can read it

Policy is a file, not code. policy.yaml names the rules each action runs, and identity.yaml says who the app serves and how a caller proves who they are. Both compile to what the gate runs. Three generated pages make a change readable by someone who does not write code:

  • POLICY.md, the policy card: each action and each rule in plain English, with diagrams, written from the same compiled policy the gate runs. It lists every line a model words and every prescribed line.
  • policy.matrix: for every action and every kind of caller, the verdict and its reason, so a policy change shows up as a diff of what the gate decides.
  • APP-MAP.md: the app's intents, forms, slots, actions and rules as diagrams.

Each is written by a command and compared by a test, never written by CI. CODEOWNERS makes a change to the policy files and these pages need compliance's review.

From apps/utility/policy.matrix: who may set up a payment arrangement. A customer needs the one-time code (level 2), their own account and an exact read-back; a property manager goes to a person.
setUpPlan · level 2 · identity, role(manager person), scope(accountId), confirmed(accountId, place, symptom, count, firstDate, total), limit(total), dateInRange(firstDate)
  anonymous           STEP_UP to 2
  subject@1           as anonymous
  subject@2           subject own, fields exact, confirmed none|mismatch  BLOCK confirmation
                      subject own, fields exact, confirmed match          ALLOW
                      subject own, fields extra|missing                   BLOCK confirmation
                      subject inScope|outOfScope|unknown|empty            BLOCK scope
  delegate:manager    NEEDS_HUMAN role-person
  delegate:manager@1  BLOCK identity
  unlisted-role       BLOCK role

From one machine to many servers

From a personal line on one machine you own to a contact center across many servers, it is the same app: what it understands, what it says and what the policy allows do not change.

On a machine you own

Try it on a laptop, then run it on any machine that stays on, for a small office or your own line. A phone line needs two keys, a carrier's (Telnyx or Twilio) and a decision model's, and a tunnel (a quick one needs no account). It starts at boot, survives its own restarts without dropping a caller, and shows you calls on your phone.

Run it on a machine you own

Across many servers

Several servers behind a load balancer, with sessions, the audit and the traces in shared stores (Postgres and S3-compatible storage). When the server holding a call stops or crashes, another takes the call over from the shared session. Built for Phase 8b's scope; roles, single sign-on for the console and OpenTelemetry are left for later.

Scaling out

  • The same app folder. Intents, forms, prompts and slots in YAML, tools in TypeScript.
  • The same policy. policy.yaml and the gate decide every action, on one machine or many.
  • The same audit. Every decision, allowed or refused, in a hash-chained log, with a trace of every turn.
  • The same tests. pnpm check, the policy matrix and the scripted calls, before any call is taken.

Generated wording, where you opt in

Every line is said word for word unless the app asks for a generated one, a line at a time (mode: generative) or a form at a time (wording: generative). What is regulated is the app designer's call: a line marked prescribed: true is never generated, and the policy card lists every generated and every prescribed line. The model chooses the words, never the content: the core has decided before any line is worded.

To summarize what a caller said, the app frames it: the line is fixed and says whose words follow, and only a span of it is generated. The front desk's read-back is the example:

apps/secretary/prompts.yaml (excerpt): only {summary} is generated
  confirm_take_message:
    text: "Thanks! I'll make sure {recipient} gets your message, in short: {summary}. I have you as {caller}, on {phone}. Shall I pass it on?"
    exactText: "I'll make sure {recipient} gets your message, as you said it: {summary}. I have you as {caller}, on {phone}. Shall I pass it on?"
    generate:
      summary:
        from: message
        words: 30
        speaks: to-caller
        names: [recipient]
  • Checked before anyone hears it. Code holds the span to its data and shape: every value word for word, no figure or name it was not given, its word budget, one sentence. What it means is the drift judge's: the decision model, asked whether the span, heard in its sentence, is a faithful summary. That is a judgment, not a guarantee, and it is measured (the judge).
  • Any failure says the exact words. A check that fails, a slow model or an error, and the caller hears their own message word for word, in the line's other frame.
  • Within a budget. A line's wording has 900 ms on a call and 1500 ms in a chat by default, and its checks 500 ms and 1000 ms; past either, the fixed text is said.
  • The message itself is never changed. The caller's yes covers what the slots hold, and the message is delivered as they said it.

Tuning with live calls

An app green on its recordings has been tried on its own words. A live call adds a carrier, a recognizer, a room and a person who does not know the script. Chapter 17 of the guide is the working method that follows, with examples from a day of live calls to the front desk:

  • trace every call, and read it turn by turn in the console, each question beside its answer;
  • tune on a private copy that keeps the caller's words, when the shipped app keeps none;
  • treat a score near 0.5 as a coin toss, and suspect the question's words before the threshold;
  • change the words, record again, and triage the recording;
  • choose the render model and the judge by measurement, against the call's time budgets.