Jev Self-Driving Sim

September 18, 2026 · View on GitHub

A 2D, top-down autonomous car that runs in the browser and uses TypeSafe's Jev (a "System One" decision model) as its driving classifier. Every ~200 ms the car turns what its sensors see into a JSON state, sends it to Jev with four typed questions, and executes the answers:

questiontypewhat Jev decides
lane_actionchoicekeep_lane / change_left / change_right
speed_actionchoicestop / slow_down / hold / speed_up
hazardscore0 (clear) … 3 (collision likely within seconds)
pedestrian_yieldnoulprobability that ego must stop for a pedestrian

All four go in a single API call (speculative fan-out). Code then applies confidence-gated routing: a low-confidence lane change is ignored, a strong pedestrian_yield overrides speed, a severe hazard forces at least slow_down, and a stop is softened to slow_down when nothing is within 1.5× the stopping distance (so far-away pedestrians or obstacles cause a gentle slowdown, not a halt). Speed steps are scaled by the time since the previous answer, so a fast Jev does not brake harder than a slow one. A small "reflex" in code (emergency brake, blind-spot abort) exists only for imminent impacts and can be switched off in the UI.

Jev vs LLM, side by side

With an OpenRouter key the page offers a Compare mode: two tracks on the same course, the left car driven by Jev and the right one by an LLM (DeepSeek V4.1 Flash by default) that receives the very same state and questions and must answer in the same JSON shape. The course (public/js/course.js) is generated once from the seed as a list of spawn events with absolute road positions; each event fires when that track's car passes its mark, so both cars meet the same cones, barriers, parked cars, traffic and pedestrians in the same places, each at its own pace. Obstacles you click are placed on both tracks at the same distance ahead. A live table shows, per driver: distance, average speed, decisions, latency, input and output tokens, cost so far, cost per decision, per km and projected cost per hour of driving. Pricing: Jev $42 per billion input tokens ($0.042/M, output free, as advertised on typesafe.ai); the LLM price is fetched from OpenRouter. Both can be overridden in .env. Because Jev answers faster it also decides more often; the "Decision interval" slider fixes the same cadence for both brains when you want cost per hour to reflect price per decision only.

Preset courses

The Course selector offers, besides the seeded random traffic, static-obstacle-only courses (no traffic, no pedestrians) that force the models to steer: Slalom (cone pairs, open lane shifts one step every 32 m), Chicane (barriers, one step every 65 m), Parked cars, Gauntlet (mixed, spacing tightening from 50 m to 24 m), Single-lane squeeze (double lane changes, hard), Random obstacles and Empty road. Presets are defined in public/js/presets.js as repeating patterns with absolute positions, identical on both tracks. COURSE=slalom node scripts/headless.js 120 1 runs one headlessly.

Run it

npm install
cp .env.example .env      # TYPESAFE_API_KEY from https://console.typesafe.ai/settings/keys
                          # optional OPENROUTER_API_KEY for the comparison mode
npm start                 # http://localhost:3000

Without a key the app still runs, with a clearly labelled rule-based fallback brain, so you can test the world before wiring Jev in. The key never reaches the browser: server.js proxies /api/decide to the TypeSafe API with the official SDK (the API also rejects browser origins, so a proxy is required anyway).

Using the simulator

  • Click the road to add objects at runtime: cone, barrier, parked car, traffic car, truck, pedestrian, or remove (keys 1–7 select the tool).
  • Auto traffic keeps spawning slow cars ahead, faster cars from behind, trucks, pedestrians that cross the street, and static obstacles. Density is adjustable.
  • Restart (R) resets the world. Enter a seed to replay the same scenario.
  • The right panel shows every Jev answer with its probabilities, confidence, latency, the gating notes, the exact state JSON sent, and the questions.
  • Space pauses.

Layout

server.js            static hosting, /api/decide (Jev via @typesafe-ai/sdk), /api/llm-decide (OpenRouter)
public/js/brain.js   the four questions, fetch to /api/decide, local fallback, gating
public/js/sensors.js perception → state JSON, swept-path reflex, ray casting
public/js/car.js     ego vehicle: lane-centering + speed controller, bicycle model
public/js/course.js  deterministic spawn schedule shared by every track
public/js/presets.js static-obstacle preset courses (slalom, chicane, ...)
public/js/world.js   road, NPC cars, pedestrians, collisions
public/js/render.js  canvas drawing
public/js/main.js    game loop, decision loop, UI wiring
scripts/headless.js  runs the sim in Node with the fallback brain (no browser)

node scripts/headless.js 150 42 500 simulates 150 s with seed 42 and a 500 ms decision interval and prints a summary; set TRACE=1 to dump the last states. With the server running, BRAIN=jev node scripts/headless.js 60 7 drives the same simulation with real Jev answers and BRAIN=llm with the LLM (TRACE=1 TRACE_ACTION=slow_down lists those decisions); the summary includes tokens and cost.

Tuning

Constants live in public/js/config.js (speeds, sensor range, decision interval, confidence thresholds). Question wording lives in public/js/brain.js; the state schema Jev sees is built in public/js/sensors.js.