Skip to content

Drive-Thru Ordering Walkthrough

A drive-thru ordering system where a fallible voice parser proposes what the customer said, the platform keeps those proposals behind candidate. relations, and the POS only receives accepted orders. The system enforces that absurd or low-confidence parses such as “18,000 waters” stay in review instead of becoming trusted facts or downstream actions.

This walkthrough is the cleanest demonstration of the candidate -> acceptance -> intent pattern:

voice.parse_result
-> candidate.*
-> accepted_order_*
-> intent.*

Taco Bell’s AI drive-thru went viral after it confidently submitted a customer’s joke order for 18,000 waters straight to the POS. The failure was not that the parser made a mistake — every sensor makes mistakes. The failure was that a mistake propagated directly into an action with no gate between transcription and execution. This example shows you the gate.

It covers the full JacqOS pipeline with a focus on fallible sensor boundaries:

  1. Observations arrive as JSON events (order.started, voice.parse_result, customer.confirmation, crew.review, pos.result)
  2. Mappers extract both trusted structural atoms and requires_acceptance semantic atoms from the same observation
  3. Rules derive candidate.*, current parse state, review gates, accepted order facts, and order status
  4. Invariants enforce bounded acceptance and prevent successful POS submission without accepted order facts
  5. Intents derive outbound POS submission only from accepted order state
  6. Fixtures prove the happy path, correction-turn contradiction, impossible-order, and disagreement flows
jacqos-drive-thru-ordering/
jacqos.toml
ontology/
schema.dh # relation declarations
rules.dh # candidate acceptance, review gates, invariants
intents.dh # POS submission intent derivation
mappings/
inbound.rhai # mapper contract + observation mapping
fixtures/
happy-path.jsonl
happy-path.expected.json
contradiction-path.jsonl
contradiction-path.expected.json
impossible-order-path.jsonl
impossible-order-path.expected.json
disagreement-path.jsonl
disagreement-path.expected.json
prompts/
ordering-system.md # prompt bundle for package export
generated/
... # verification, graph, and export artifacts

jacqos.toml declares the app identity, the POS capability binding, and the Studio metadata:

app_id = "jacqos-drive-thru-ordering"
app_version = "0.1.0"
[paths]
ontology = ["ontology/*.dh"]
mappings = ["mappings/*.rhai"]
prompts = ["prompts/*.md"]
fixtures = ["fixtures/*.jsonl"]
[capabilities]
http_clients = ["pos_api"]
models = []
timers = false
blob_store = true
[capabilities.intents]
"intent.submit_pos_order" = { capability = "http.fetch", resource = "pos_api" }

This example does something important on purpose: JacqOS is not calling a live speech parser here. The fallible sensor output arrives as voice.parse_result observations, which means the walkthrough starts at the observation step while still demonstrating the same acceptance boundary. If you later decide to call a parser through an effect runtime, the ontology and mapper boundary can stay the same.

The schema separates structural state, candidate evidence, accepted facts, review state, and effects:

relation order_started(order_id: text, lane_id: text)
relation voice_parse_turn(order_id: text, parse_seq: int, confidence: float)
relation candidate.requested_item(order_id: text, item: text, parse_seq: int)
relation candidate.quantity(order_id: text, quantity: int, parse_seq: int)
relation candidate.modifier(order_id: text, modifier: text, parse_seq: int)
relation customer_confirmed(order_id: text, parse_seq: int)
relation crew_rejected(order_id: text, parse_seq: int, reason: text)
relation accepted_order_item(order_id: text, item: text)
relation accepted_quantity(order_id: text, quantity: int)
relation accepted_modifier(order_id: text, modifier: text)
relation order_requires_confirmation(order_id: text)
relation order_requires_review(order_id: text)
relation order_status(order_id: text, status: text)
relation intent.submit_pos_order(order_id: text, item: text, quantity: int, modifier: text)

The key point is that candidate.* and accepted_* are separate on purpose. The voice parser can propose combo meal x2, but until confirmation arrives, that is not the system’s accepted order.

This example demonstrates mapper-level partial trust directly. The mapper contract marks only parse.* as requires_relay into candidate.*:

fn mapper_contract() {
#{
requires_relay: [
#{
observation_class: "voice.parse_result",
predicate_prefixes: ["parse."],
relay_namespace: "candidate",
}
],
}
}

Then map_observation() extracts both ordinary and fallible atoms from the same voice-parse event:

if obs.kind == "voice.parse_result" {
let atoms = [
atom("order.id", body.order_id),
atom("turn.seq", body.seq),
atom("turn.confidence", body.confidence),
atom("parse.item", body.item),
atom("parse.quantity", body.quantity),
];
if body.contains("modifier") {
atoms.push(atom("parse.modifier", body.modifier));
}
return atoms;
}

That split is the whole design:

  • order.id and turn.seq are trusted structure
  • parse.item, parse.quantity, and parse.modifier are fallible interpretation

Step 4: Derive Candidates, Review Gates, And Accepted Facts

Section titled “Step 4: Derive Candidates, Review Gates, And Accepted Facts”

The first rules lift the parse into candidate evidence:

rule assert candidate.requested_item(order, item, seq) :-
atom(obs, "order.id", order),
atom(obs, "parse.item", item),
atom(obs, "turn.seq", seq).
rule assert candidate.quantity(order, quantity, seq) :-
atom(obs, "order.id", order),
atom(obs, "parse.quantity", quantity),
atom(obs, "turn.seq", seq).

When a later parse arrives for the same order, the old candidates retract:

rule retract candidate.requested_item(order, item, old_seq) :-
atom(obs, "order.id", order),
atom(obs, "parse.item", item),
atom(obs, "turn.seq", old_seq),
voice_parse_turn(order, new_seq, _),
old_seq < new_seq.

That is how the contradiction path models a correction turn. The first parse remains visible as contradiction history, but it no longer drives current behavior.

Review gates sit between candidates and accepted facts:

rule order_requires_review(order) :-
current_parse_seq(order, seq),
candidate.quantity(order, quantity, seq),
quantity > 8.
rule order_requires_review(order) :-
current_parse_seq(order, seq),
voice_parse_turn(order, seq, confidence),
confidence < 0.7.

Accepted facts derive only after confirmation and bounds checks:

rule accepted_order_item(order, item) :-
current_parse_seq(order, seq),
candidate.requested_item(order, item, seq),
candidate.quantity(order, quantity, seq),
quantity > 0,
quantity <= 8,
voice_parse_turn(order, seq, confidence),
confidence >= 0.7,
customer_confirmed(order, seq).

This means the parser can say “18000 waters”, but the ontology still refuses to believe it.

Step 5: Derive POS Submission Only From Accepted Facts

Section titled “Step 5: Derive POS Submission Only From Accepted Facts”

The outbound effect is intentionally narrow:

rule intent.submit_pos_order(order, item, quantity, modifier) :-
accepted_order_item(order, item),
accepted_quantity(order, quantity),
accepted_modifier(order, modifier),
not pos_submission_succeeded(order),
not pos_submission_failed(order, _).

There is no path from raw parse candidates to intent.submit_pos_order. That is the safety boundary in one rule.

This example ships four fixtures:

The parser says water x1, the customer confirms, and the order is submitted successfully. Final status: submitted.

The first parse says burger x1. A correction turn replaces it with cola x2, no ice. The earlier candidates retract into contradiction history, and only the corrected order reaches POS.

The parser proposes water x18000 at low confidence. The system derives order_requires_review and never derives accepted order facts or a POS submission intent. The original viral failure recreated as a fixture: the same parser output that crashed Taco Bell’s headlines lands here in Waiting, with the bound check and the missing confirmation both named in the drill inspector.

The parser produces a plausible parse, but the crew rejects it after the customer clarifies the order. The candidate facts stay visible for audit, while the final order status becomes rejected. The crew rejection is itself an observation, so the entire transcript — what the parser said, what the crew overruled, why — is preserved in the timeline. There is no hidden state machine that quietly forgets the contested parse.

Run all fixtures and invariants on a clean database:

Terminal window
$ jacqos verify
Replaying fixtures...
happy-path.jsonl PASS (3 observations, 7 facts)
contradiction-path.jsonl PASS (4 observations, 9 facts, 3 contradictions)
impossible-order-path.jsonl PASS (1 observation, 4 facts)
disagreement-path.jsonl PASS (3 observations, 6 facts)
Checking invariants...
acceptance_within_quantity_bound PASS
pos_submission_requires_accepted PASS
All checks passed.

Every fixture replays from scratch deterministically — same observations, same evaluator, same facts every time. The contradiction count in contradiction-path.jsonl is expected: it reflects the candidates that were superseded by the correction turn, not an error.

Open the demo with jacqos studio and the bundled happy-path fixture loads automatically. Switch fixtures from the timeline picker to walk every scenario:

  • Normal order -> the Done tab shows a row like accepted_order: water x1 — confirmed by customer, submitted to POS. Drill in and the inspector takes you from POS submission back through accepted_order_item, the customer confirmation, and the original voice-parse observation.
  • 18,000 waters -> the Waiting tab shows a staged parse that never accepts. Drill in and the inspector surfaces the order_requires_review rule, the missing confirmation, and the bounds check that would fail if the parse were promoted. The POS submission never derives.
  • Correction turn -> a second parse arrives for the same order; the first staged candidates retract and the second takes their place. Activity animates the transition and the contradiction history stays inspectable.
  • Crew disagreement -> the Blocked tab shows the rejected order with the specific crew-rejection observation that gated it.

At no point does JacqOS need to trust the parser. The parser can be as bad as you like. Containment does not depend on its quality.

This is the fallible-sensor pattern in its purest form:

  • the parse is visible
  • the parse is queryable
  • the parse can drive confirmation or review
  • the parse cannot silently become trusted fact
  • trusted fact alone can drive external action

That is how you stop a funny drive-thru glitch from turning into an operational outage.

The drive-thru example is one shape of fallible-sensor containment. The same pattern fits:

  • Vision tagging pipelines — a model proposes a label; an acceptance rule requires either confidence above a threshold or a human review signal before the label becomes accepted.
  • OCR-driven document workflows — a model proposes extracted fields; an acceptance rule gates downstream filing on a reviewer signal or a cross-document consistency check.
  • LLM extractors over patient or customer notes — see Medical Intake for the same pattern applied to clinical extraction.
  • Sensor-fusion alarms — multiple noisy sensors propose state changes; an acceptance rule requires quorum across sensors before an alarm intent fires.

Any time you have a sensor whose output you cannot fully trust, the candidate-evidence pattern fits. To start building, scaffold a starter app with:

Terminal window
jacqos scaffold --pattern sensor my-sensor-app