DEV Community

Teruo Kunihiro
Teruo Kunihiro

Posted on

Jev HighwayEnv: 60 Seconds Without a Crash

“I'll send Jev the cars' positions and speeds and have it choose whether to change lanes or slow down. How hard can that be?”

That was the idea. Instead, the car crashed almost immediately. When I added braking, it stopped and stayed there. When I added acceleration, it crashed again.

This is the story of getting it to drive for 60 seconds—roughly 1.20 km—without a collision, on a three-lane highway with traffic approaching from behind.

Here's the final run:

The result: a full 60-second run

The car completed the run at an average speed of 20.08 m/s (about 72.3 km/h), without stopping to run down the clock.

Metric Measured result Notes
Model / environment jev-1.13.0 / 3 lanes, 30 surrounding vehicles Seed 42, traffic density 2.0
Distance / duration 1,203.72 m / 60.074 seconds of wall time 60 simulated seconds; no collisions or stops; minimum speed 23.6 km/h
Average / final speed 72.3 km/h / 79.6 km/h Accelerated and decelerated in discrete target-speed steps
Response latency (p50 / p95) 311 ms / 482 ms Observation-to-action latency: approximately 320 ms / 497 ms
Decision application rate About 2.73 per second 165 requests sent, 164 decisions applied
Target-speed consistency 105 out of 105 matched The target shown to the model matched the target applied in the logs

What is Jev?

Jev is an AI model specialized for decisions rather than free-form text generation. Its developer, TypeSafe AI, calls it a System One model: a model that evaluates a state and returns typed answers and probabilities that software can use directly. See TypeSafe's explanation of System One models.

The name draws on the fast, intuitive System 1 described in Daniel Kahneman's Thinking, Fast and Slow. TypeSafe discusses that connection in its introduction to Jev.

Jev is trained using RLCD—Reinforcement Learning for Calibrated Decisions. Your code can use its results to determine what an agent does next, without generating a conversational response for every decision.

Send a state and questions

To call Jev, you send a state containing the context and questions about that state.

For example, to assess whether a support request is urgent:

{
  "model": "jev-latest",
  "state": "I've been trying to set up my account for four days, but it keeps failing. It's affecting my sales. Please help ASAP.",
  "questions": {
    "is_urgent": {
      "type": "noul",
      "instructions": "The message conveys urgency or time-sensitivity"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

An illustrative answer, with the rest of the response omitted:

{
  "is_urgent": {
    "type": "noul",
    "noul": 0.999
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, the model assigns a 99.9% probability to the message being urgent. Your application can use that value to prioritize the ticket. This illustrates the response format; it is not a measured result for the translated input above.

Three question types

Type Use case Returned values
Choice Choose from a predefined set of options The selected option, a probability for each option, and confidence
Score Evaluate against ordered levels, such as low, medium, and high A continuous score, the distribution across levels, and confidence
Noul Ask a yes-or-no question The probability of yes

Evaluate multiple questions in one request

You can ask several questions about the same state in a single request.

According to TypeSafe, the questions are evaluated in parallel, so adding questions barely changes response time; the extra questions add token cost. See the documentation on evaluating multiple questions.

In this driving experiment, I used that to ask several questions together: “Will I collide if I keep going?”, “What if I slow down?”, and “What if I change lanes to the left?” The code compares those evaluations and selects the action.

That's how I used Jev: send a state, receive several evaluations, and use the results in code.

Switching from Choice to Noul

The first implementation used Choice to have the model pick an action directly. Eventually, I switched to Noul, asking “Will this cause a collision?” separately for each action.

An illustrative set of risk predictions:

{
  "risk_IDLE": 0.7,      // Predicted collision risk if we keep our current targets
  "risk_SLOWER": 0.2,    // Predicted collision risk if we slow down
  "risk_LANE_LEFT": 0.4  // Predicted collision risk if we change lanes to the left
}
Enter fullscreen mode Exit fullscreen mode

This gave me a useful division of responsibilities: the model estimates risk, while the code handles action selection and thresholds. For example, the code allows acceleration when both the current course and the faster option have predicted risks at or below 0.3.

What comparing Jev with Luna taught me about latency

I also wanted to see what would happen with another model, so I connected gpt-5.6-luna through Codex, with reasoning effort set to low.

Integration Response latency Decisions applied, including the initial decision Result
Jev via the TypeSafe SDK 311 ms median 164 Completed 60 seconds, covering 1,204 m
Luna via a persistent Codex App Server 14.28 seconds for the first response 1 Crashed after 7.4 simulated seconds

Both runs waited for an initial decision before starting the simulation. In the Luna run, the second response had not arrived by the time the car crashed.

In an environment that keeps changing, there has to be time to observe the next situation and revise the decision. In this setup, Jev's roughly 300 ms responses let the controller keep making those corrections throughout the run.

This is a comparison of the integrations I tested, not a controlled benchmark of the models alone. The traffic settings and seed matched, but the response deadlines differed—1.5 seconds for Jev and 30 seconds for Luna—which also affected the prediction horizons sent to each model. Codex added its own instructions and conversation context. I did not test Luna through a direct model API. The table also compares Jev's median with Luna's first response, so it should not be read as a speedup ratio.

The bugs and design problems I ran into

I thought the main job would be plugging in Jev. Once I started running it, there was a surprising amount to fix in the surrounding code.

1. The minimum speed left no way out

  • Problem: The available target speeds were 20, 25, and 30 m/s. At 20 m/s, the car had no slower option.
  • Fix: I expanded the target speeds to 0–30 m/s in 5 m/s increments, allowing it to slow down in steps.

2. Stopping counted as success

  • Problem: Once slowing down was allowed, the car could stop completely, wait out the timer, and be marked as having completed the run.
  • Fix: I added a STALLED failure condition: staying below 1 m/s for 10 consecutive seconds ends the run. I also introduced vehicles approaching from behind at initial speeds of 32–34 m/s, so the controller had to account for rear traffic as well.

The rear vehicles retained their normal braking behavior. Adding them alone did not prevent the car from stopping; the STALLED rule closed that loophole.

3. Local processing added latency

  • Problem: Responses were delayed, and the car drove into traffic before it could react.
  • Fix: Profiling showed that rendering every frame and generating unused Gym observations were significant costs. I separated physics updates at 30 Hz from rendering at up to 10 fps, reducing the local work that delayed API tasks.

The unused Gym observation generation was identified during profiling, but removing it remained a possible future optimization.

4. Crashing 1.4 seconds after the start

  • Problem: In an early run, the initial API response took roughly 1.1 seconds. The simulated car kept driving straight while it waited and crashed soon after the start.
  • Fix: I froze the simulation at its initial state until the first Jev response arrived. Once driving began, simulation time continued to advance while later requests were in flight.

5. Making vague questions concrete

  • Problem: A vague question such as “Will this cause a collision?” leaves the time horizon open to interpretation.
  • Fix: I explicitly included the prediction horizon, target speed, and estimated action delay. For example: “Keep the current targets for 1.135 seconds, then apply the action. Will a collision occur within 2.268 seconds of this observation?”

Here is an excerpt from an actual request, showing the left-lane-change question. Surrounding vehicle observations and other fields are omitted:

{
  "state": {
    "ego": {
      "lane": 2,
      "speed_mps": 20.0,
      "target_lane": 2,
      "target_speed_mps": 20.0
    },
    "control": {
      "min_target_speed_mps": 20.0,
      "speed_tracking_gain_per_s": 1.6667,
      "acceleration_formula": "gain_per_s * (target_speed_mps - speed_mps)"
    },
    "actions": {
      "LANE_LEFT": {
        "target_lane": 1,
        "initial_acceleration_command_mps2": -0.0,
        "target_speed_mps": 20.0
      }
    },
    "timing": {
      "action_delay_s": 1.135,
      "horizon_s": 2.268
    }
  },
  "questions": {
    "risk_LANE_LEFT": {
      "type": "noul",
      "instructions": "Will ego collide with any observed vehicle within `timing.horizon_s` seconds from this observation if it executes `actions.LANE_LEFT`? Continue its current targets for `timing.action_delay_s` seconds, then apply this action once and hold its targets. A lane change is gradual: include contact while crossing the current and destination lanes, and check front, rear, and crossing traffic. IDLE keeps the current targets. There is no emergency brake or automatic collision avoidance. Observed acceleration is a past estimate, not guaranteed future motion. Evaluate only observed traffic."
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The 2.268-second horizon starts at the observation, not when the action begins. This request also predates the expanded speed range, which is why the minimum target speed is still 20 m/s.

6. The biggest bug: the executed action differed from the evaluated action

Even after adding acceleration, one run ended in a collision after 9.7 seconds. I suspected the model was failing to account for the car's increasing speed. The logs showed a bug in my code instead.

At observation time:
  Car speed: 21.27 m/s
  Ask Jev to evaluate FASTER with a target speed of 25 m/s.

At application time, roughly 0.3 seconds later:
  The car's speed has changed.
  MDPVehicle.act() recalculates FASTER from the current speed.
  The applied target becomes 30 m/s.
Enter fullscreen mode Exit fullscreen mode

The model evaluated an action targeting 25 m/s. The code executed one targeting 30 m/s.

I changed the implementation to preserve the target speed shown to the model at observation time and apply that exact value when the decision arrived.

# Apply the exact target speed shown to the model at observation time.
ego.speed_index = list(ego.target_speeds).index(target_speed)
ego.target_speed = target_speed
env.step(ACTION_IDS["IDLE"])  # Advance while maintaining the target.
Enter fullscreen mode Exit fullscreen mode

What I learned

The code connecting the model to the simulation had a major effect on the outcome. Debugging the model's answers alone would not have uncovered the target-speed mismatch.

Three questions kept coming up:

  • When was the state sent to the model observed?
  • How far will the environment advance before the response arrives?
  • Does the action executed by the code mean exactly what the model evaluated?

Those details need to line up before I can meaningfully evaluate the model's decisions. Having AI estimate risks while code manages control, action constraints, and validation seems useful beyond driving simulations, including business systems with complex branching logic.

For this experiment, the first milestone was modest but concrete: 60 seconds of driving, no crashes, no stopping, and every applied speed target matching the one the model had been asked to evaluate.

Originally published in Japanese on Zenn.

Top comments (0)