PROTRAILBLAZER
A game director does not need to generate anything. Twice a second the game sends its own telemetry as JSON state to a System One model and asks typed questions: how tense is the player, how competent, how stale is the action, and which of 14 designer-authored beats should come next. The answers return as scores and probability distributions in well under a second, and the director samples the next beat from those probabilities so no two runs pace the same way.
The director asks Jev every 500 ms and samples a beat whenever the last one has finished.
The bar spans one 500 ms tick. The red edge is when the next request goes out.
Four questions go out in each request: three score dials and one choice over 14 beats. Start a run to watch them update.
{}{}Every tick the game packs its own numbers into a JSON state: health and how it moved in the last ten seconds, damage taken, kills, how many enemies are alive and how close they are, ammo, pickups left lying on the floor, how varied the player's movement has been, and the last six beats. It sends that state with four questions in one request. The model never sees the question ids (tension, next_beat); those keys exist for the code that reads the answers.
every 500 ms: // never blocks the frame loop
send({ state: telemetry(), model: "jev-latest", questions })
on answer(a): // lands 70 to 500 ms later
error = target_tension(run_time) - a.tension.score
speed = a.competence.score > 2.5 ? 1.15 : 1.0
if hp < 20 and a.tension.score > 3.2 and not rescue_running:
run(sample({ medkit_drop, rescue })) // code override, ignores the slot
else if beat_running:
return // dials still update the chart
else if a.next_beat.confidence < 0.35:
log("flat distribution, holding")
else:
p = a.next_beat.probabilities
if a.stagnation.score > 1.5:
p = without(p, last_2_beats) // renormalized
run(sample(p)) // weighted random, not argmax
"Is the player struggling?" is a yes or no. It flips back and forth as a fight swings, and code that branches on it slams between easy and hard. A score question returns a position on a ladder the designer wrote, five levels from "cruising, nothing threatens them" to "about to die", and the score is the probability-weighted mean of those levels. A 2.6 means a real fight leaning toward overwhelmed. That number can be compared against a target curve, plotted, and subtracted, which is what a pacing model needs. Competence and staleness come back the same way, from the same call that picks the beat.
If the director always ran the single most likely beat, the same state would produce the same encounter every time, and players learn that pattern within two runs. Sampling from the probabilities keeps the designer's preferences (a swarm at 0.40 runs about four times in ten) while letting a lights out at 0.10 happen now and then. The confidence floor guards the other end. A flat distribution is the model saying several beats fit about equally, and rolling dice on noise is not pacing, so the director holds for a tick and asks again.
Three rules sit between the answer and the spawn. A player under 20 HP who is close to panic always gets a medkit or a rescue, whatever the distribution says. A stale run cannot repeat either of its last two beats. A dominant player gets enemies 15 percent faster. These are the guarantees a designer has to be able to promise and test, so they live in code, not in instructions a model weighs. The model supplies judgment, the code supplies the rules, and the code has the final word.
The director asks twice a second, so one tick is 500 ms. TypeSafe reports 70 to 500 ms end to end, so even the slowest answer lands before the next request goes out, and at most one request is ever in flight. The game never waits on it. The frame loop keeps drawing, and the answer is applied on whatever frame it lands. The inspector counts the frames drawn while each request was out. A beat that starts 300 ms late is invisible to a player; a frame that stalls for 300 ms is not.
Each call carries the full question set (the 14 beat descriptions are most of it) plus the telemetry, and only input is billed. The meter multiplies the measured call rate by the token count of each request and $0.042 per million input tokens. Two calls a second for an hour is 7,200 calls, which is why the numbers stay in cents rather than dollars, and why trimming the beat descriptions is the first optimization worth making.
About this demo: every answer here comes from a local mock, and nothing on this page calls Jev, which is in early access. Tension is a pressure estimate from damage taken, enemies alive, how close they are, and health, turned into logits over the five levels. Competence comes from kills per damage taken and movement variety. Staleness comes from repeated beats and long quiet stretches. The beat logits come from a rules table keyed by rounded tension and the pacing phase, with jitter so no distribution is ever one-hot. All of them go through a softmax. Confidence is shown as 1 minus normalized entropy, an approximation of TypeSafe's unpublished formula. The state also carries the pacing phase and target tension, because a model can only weigh what it is sent. Latency is drawn uniformly from 70 to 500 ms, and token counts are the JSON length divided by 4.