A simulated espresso bar where every barista decision is made by Jev, TypeSafe's System One model. Type an order in plain English ("a cap but with almond milk"), and watch Jev grind, tamp, pull shots, steam milk, and serve. You can work the bar too, or break the equipment and see how Jev copes.
Live demo: https://jevspresso.dev
demo.1.mp4
An LLM agent usually acts by writing tool calls, so the app has to check every action after the fact. Jevspresso does it the other way around: an XState v6 machine models what is physically possible at the bar, and Jev only chooses among the events the machine accepts right now. Jev never writes an action. It picks one from a closed set, and the bar takes it only if it is still possible.
The repo is an example of using XState and Jev together, through @xstate/jev in packages/jev.
pnpm install
cp .env.template .env # set TYPESAFE_API_KEY, or a gateway key
pnpm dev # http://localhost:3000Without a key, the page asks for one. pnpm test runs without a key, against a mock Jev.
The same pattern as the bar, on a lamp:
import { createActor, setup, types } from 'xstate';
import { z } from 'zod';
import { createJevLogic } from '@xstate/jev';
const lamp = setup({
actors: {
jev: createJevLogic({
events: 'lamp.*',
instructions: 'Do what the latest request in `requests` asks, if the lamp is not like that already.',
noop: 'leave the lamp as it is',
client, // your server-side TypeSafe call
}),
},
}).createMachine({
schemas: {
context: types<{ requests: string[] }>(),
events: {
'user.request': types<{ text: string }>(),
// What Jev may do: runtime schemas, described.
'lamp.turnOn': z.object({}).describe('switch the lamp on'),
'lamp.turnOff': z.object({}).describe('switch the lamp off'),
},
},
context: { requests: [] },
// Jev decides on the lamp whenever it changes.
invoke: { src: 'jev' },
// A request is just context: Jev sees it change, and acts on it.
on: {
'user.request': ({ context, event }) => ({ context: { requests: [...context.requests, event.text] } }),
},
initial: 'off',
states: {
off: { on: { 'lamp.turnOn': { target: 'on' } } },
on: { on: { 'lamp.turnOff': { target: 'off' } } },
},
});
const actor = createActor(lamp).start();
actor.send({ type: 'user.request', text: 'it is getting dark in here' }); // → on
actor.send({ type: 'user.request', text: 'time to sleep' }); // → offclient sends Jev's request to the TypeSafe SDK's systemOne() on your server, so the API key stays there. src/lib/jev.ts is the bar's client.
Try it live at /light (src/machines/light.ts): the lamp there is a switch and a bulb, as a real one is, and you can break the bulb. Ask for light with the bulb broken and the switch on, and Jev replaces the bulb. It is never told about bulbs: it sees whether the room is lit, and replacing the bulb is the move that makes it so.
- The bar (
src/machines/espressoBar.ts) is one parallel machine: order intake, the grinder, espresso machine and steam wand, one portafilter, one milk pitcher, and each barista's hands. It models physics, not recipes. Mistakes are possible; recipes are data the cups are compared against. - The barista is a
createJevLogicagent invoked at the top of the bar. Whenever the bar changes, it offers Jev everybarista.*event thatcan()accepts, each described in the machine's own words plus what it would change (computed with the puretransition()). Jev picks one, and the agent sends it if the bar still accepts it. - Orders are parsed by one Jev call that answers a set of closed questions about the text (which drinks, how many, which milk). Unclear orders go to a router agent that asks the customer to confirm.
/lightis the smallest example: a lamp with your requests in its context, and Jev invoked at its top./evalruns scenarios (rushes, breakdowns, sabotage) against real Jev and reports how many orders were served correctly and what it cost.
Jev (1.14) runs the bar by default. Pick another model in the header, or with ?model=: every decision (reading orders, routing replies, the barista's moves) goes to it.
One gateway key is enough to switch between models: OpenRouter carries all of them, and Vercel AI Gateway all but Kev and Clef. A model's own key works too, and is tried first.
| Model | ?model= |
Own key | OpenRouter | Vercel AI Gateway |
|---|---|---|---|---|
| Jev 1.14 (TypeSafe) | jev |
TYPESAFE_API_KEY |
✓ | ✓ |
| Kev 4B (Jared Palmer) | kev |
KEV_BASE_URL (self-hosted); optional KEV_API_KEY |
✓ | |
| Clef, Clef Flash, Clef Omni (Cloudflare) | clef, clef-flash, clef-omni |
CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN |
✓ | |
| GPT-6 Luna Decisions (OpenAI) | openai |
✓ | ✓ | |
| Decision-1 (Microsoft) | microsoft |
✓ | ✓ | |
| Decider V1.2 (Perplexity) | perplexity |
✓ | ✓ | |
| d1 (Liquid AI) | liquid |
✓ | ✓ |
Set them in .env (copy .env.template): OPENROUTER_API_KEY or AI_GATEWAY_API_KEY for a gateway. Or give a key on the page (a gateway's, TypeSafe's, or a Cloudflare account and token): it stays in your browser and goes along with the requests it can serve.
Play a day (or open ?day=<seed>) to give the bar twelve customers and a few mishaps (parts breaking, ingredients running out), each at a fixed time in bar time. The same seed is the same day, whichever model runs it, so ?day=42&model=clef and ?day=42 can be compared.
The customers know what they meant: when the bar asks "did you mean…?", they answer yes or no, and say their order again if it was not taken (twice at most). The day's clock stands still while the bar asks. At the end, a scorecard reports orders read right, asks, drinks served and how long people waited, and the model's requests and latency, with a button to play the same day with another model.
@xstate/jev: options,createJevLogic, caching, loop detection, the client- XState docs
- TypeSafe docs and JavaScript SDK