Docs /Guides

Agent guide

How a coding agent sets up, drives and verifies SimEngine: the loop, the contract, scenarios, sessions and exit codes.

Use this guide when a coding agent (Claude Code, Codex, Cursor, Copilot, Gemini CLI or any agent with a shell) builds, runs or changes a SimEngine project. It is written for agents first and humans second.

Last updated: 2026-10-29. Engine state: pre-release, M1 foundations done, M2 in progress.

Read this first

  • SimEngine is a local C++20 / Vulkan 1.3 game engine driven from the command line. There is no hosted API, no API key, no account and no MCP server. Every tool is a local executable that prints one JSON document per command.
  • The repository is not public yet. It opens with the first distributable milestone (MIT OR Apache-2.0). Until then, this guide documents the interface so you can evaluate it; you can only run it with access to a checkout.
  • You cannot ship a game with SimEngine today. What works: seeded headless scenarios of point entities, offscreen PBR capture to PNG, a Windows scene playground and offline cooking of a strict static glTF/GLB subset. What does not exist yet: supported physics or collisions, a game loop, input for gameplay, audio, textured materials, shadows, terrain or orbital propagation. Do not promise or attempt these.
  • The runtime never calls an LLM. You drive development; the engine never waits for you.

How you work with SimEngine

The loop the engine is optimized for. Every step is a command you run and a document you parse.

  1. Discover: simengine_host --capabilities prints the scenario schema, units, frames, the components and commands of exactly the compiled-in modules, limits, the determinism scope, every exit and error code, and an example scenario valid for this build. Read it instead of guessing from documentation.
  2. Author: edit versioned text. A scenario (simengine.scenario, version 1) declares a seed, a tick schedule, entities, scheduled commands and expectations. Every position states its value, unit and frame.
  3. Run headless: simengine_host --run scenario.json --out RUN_DIR loads, steps and checks expectations, then writes the result and artifacts.
  4. See: simengine_capture --scene scene.sescene --out-dir CAPTURE_DIR renders offscreen through Vulkan and writes a PNG plus a manifest. A black, empty or background-only image is a failure (exit 12), never a pass.
  5. Prove: a change is done only when its expectations pass, the final_state_sha256 changed as intended and the capture shows what you meant. Compiling, surviving one tick or printing a success boolean is not acceptance.

Setup

From a SimEngine checkout, read AGENTS.md first, then use the maintained, rerunnable setup scripts that humans and CI also use:

  • Linux: ./tools/setup.sh --install-missing detects the environment, installs only missing recorded prerequisites (Ubuntu/Debian), configures and builds the linux-release preset, runs the test suite and the host smoke check.
  • Windows 11: install Visual Studio 2022 (C++ x64), CMake 3.28+, Ninja and Python 3.12+, then run tools/setup.ps1. No Vulkan SDK is needed: the pinned Khronos headers are fetched and the loader is opened at run time.

Expect zero failures among the selected CTest tests; report skips separately. The final smoke check prints one JSON summary naming the Vulkan device, or a structured BLOCKED outcome on a machine without a Vulkan 1.3 driver. Report failures verbatim with the exit code. Do not work around a failure silently.

Implementation contract

  1. Start from capabilities. Run --capabilities before writing a scenario. Use only the commands, components and modules it lists; a disabled module’s commands do not exist and are rejected.
  2. Keep your project outside the engine checkout. Never edit engine sources, tests or examples/ to get a pass.
  3. Be explicit. Positions are { "value": [x, y, z], "unit": "m", "frame": "world" }. Frames are right-handed, +y up. Durations are nanoseconds ("unit": "ns").
  4. Validate before you run. --load FILE validates all-or-nothing and prints the initial state; simengine_capture --check-scene FILE validates a scene without a GPU.
  5. Use new or empty output directories. Existing content is never overwritten.
  6. Classify outcomes by exit code, then by error.code. Do not parse stderr prose. Diagnostics on stderr are JSON Lines (simengine.diagnostic, version 1).
  7. Do not retry blindly. A failed expectation (exit 12) is a result, not a flake: read which expectation failed and why. Timeouts (exit 11) mean your --max-wall-ms bound was reached.
  8. Missing GPU is BLOCKED, not PASS. Exit codes 3 to 5 mean no usable Vulkan device. Report BLOCKED_ENVIRONMENT; never fake a capture.
  9. Report evidence. Finish with the commands run, exit codes, final_state_sha256 values, PNG paths and what SimEngine could not do.

Scenario format (simengine.scenario v1)

This is the scenario of the first_scene example, abridged:

json{
  "schema": "simengine.scenario",
  "schema_version": 1,
  "name": "first_scene",
  "seed": 20261027,
  "time": { "tick_duration": { "value": 16666667, "unit": "ns" }, "ticks": 120 },
  "frame": { "id": "world", "kind": "cartesian", "handedness": "right", "up_axis": "+y" },
  "entities": [
    { "id": "table", "position": { "value": [0, 0, -1], "unit": "m", "frame": "world" } },
    { "id": "ball",  "position": { "value": [2, 0.25, 1.5], "unit": "m", "frame": "world" } }
  ],
  "generators": [
    { "kind": "scatter_box", "id_prefix": "barrel_", "count": 3,
      "min": { "value": [1.5, 0, -3.5], "unit": "m", "frame": "world" },
      "max": { "value": [3.5, 0, -2], "unit": "m", "frame": "world" } }
  ],
  "commands": [
    { "at_tick": 60, "command": "simengine.entity.set_position", "version": 1,
      "args": { "entity": "ball", "position": { "value": [0.25, 1, -1], "unit": "m", "frame": "world" } } }
  ],
  "expect": [
    { "name": "ball_on_table", "entity": "ball", "quantity": "position",
      "value": { "value": [0.25, 1, -1], "unit": "m", "frame": "world" },
      "tolerance": { "value": 0, "unit": "m" } }
  ]
}

The full file is at /simengine/examples/first_scene/scenario.json. Without the optional point_mass module, motion is a sequence of scheduled teleports; with it (-DSIMENGINE_MODULES=point_mass), entities gain point_mass.body (velocity) and the scene point_mass.world (gravity), with the commands point_mass.apply_impulse and point_mass.set_velocity. Always confirm with --capabilities.

Run artifacts

simengine_host --run FILE --out DIR writes into DIR:

  • scenario.json: a byte copy of the input
  • initial_state.json and final_state.json (simengine.scene_state); the result carries the SHA-256 of the final state
  • trace.jsonl when --trace-every N is set
  • result.json: the same simengine.host.result document printed to stdout

Two runs of the same scenario on the same build and configuration give the same final_state_sha256. Cross-platform bitwise determinism is not promised.

Interactive control: --session

simengine_host --session [--max-wall-ms MS] reads JSON Lines requests on stdin and writes one simengine.session.response per request on stdout. Operations: load, step, inspect, command (with dry run and an expected state version), expectations, capabilities and quit. Use it when you need to step and inspect without re-running a whole scenario.

The first_scene example and its skills

examples/first_scene is the reference project. Its driver first_scene.py calls only the public tools, in separate processes, and verifies what they write:

  • python first_scene.py new DEST creates a project (and copies the three skills into DEST/.claude/skills)
  • python first_scene.py check --project P --build B validates engine compatibility, scene and scenario without a GPU
  • python first_scene.py run --project P --build B runs the full journey: load, run twice, capture, verify, manifest
  • python first_scene.py verify [MANIFEST] re-verifies a finished journey from its files

Expected outcome for an unchanged project: PASS, 3 of 3 expectations, three PNG captures and a manifest.json. The driver stops with incompatible_engine (exit 21) if the engine reports interface versions the skills were not written for.

Agent skills (load them; they are the procedures this guide summarizes):

Exit codes

simengine_host (also in the JSON exit_code; error.code names the failure):

Code Name Meaning
0 ok Success
1 internal_error Unexpected exception inside the host
2 usage_error Invalid command line
3 vulkan_loader_unavailable No Vulkan loader library on this machine
4 vulkan_driver_unavailable Loader present, no driver supporting Vulkan 1.3
5 no_vulkan_device Instance created, zero physical devices
6 no_suitable_device Devices exist, none meets the requirements
7 backend_initialization_failed Other instance or device creation failure
8 validation_errors Backend opened, the validation layer reported errors
9 runtime_self_check_failed Backend fine, core runtime self-check failed
10 scenario_invalid Validation failed; nothing ran
11 scenario_timeout The wall-time bound stopped the run or session
12 scenario_expectation_failed Run completed, an expectation failed
13 scenario_io_error Input unreadable or output directory unusable
14 simulation_fault A tick produced non-finite state (rolled back)
15 module_startup_failed Optional modules could not be initialized

Codes 3 to 5 all mean “no usable Vulkan device”. A process killed by a signal prints no document; your supervisor must classify it as a crash. Capture exit codes are in the CLI reference.

What not to do

  • Do not invent commands, flags, components or modules. If --capabilities does not list it, it does not exist in this build.
  • Do not edit engine code, tests or examples to make a check pass.
  • Do not treat “it compiled” or exit 0 from a partial step as done.
  • Do not fake or reuse a capture when the GPU is unavailable.
  • Do not add an LLM call, network call or API key to a game or to the runtime.
  • Do not claim physics, collisions, input, audio, textures or a game loop: they are not supported yet.