docs / env-kernel / getting startedgithub.com/Fareground/env-kernel ↗

Getting started

Install

pip install fg-env-kernel

The distribution name is fg-env-kernel; the import package is fg_env_kernel (deliberately distinct — downstream projects depend on both names, so they are not renamed). Requires Python ≥ 3.11. The only runtime dependency is pydantic>=2.0.

For development (linting and the dev toolchain), install the dev extra instead:

pip install "fg-env-kernel[dev]"

The CLI

A console script fg-env-kernel is installed (also runnable as python -m fg_env_kernel). Subcommands cover the whole authoring pipeline:

Command What it does
compile Validate, lint, and optionally smoke-test a JSON template
lint Lint a template without building it
contract / capabilities / versions Dump the kernel's JSON Schema contract and live capabilities
primitives List registered primitives (effects, resolutions, terminations, …)
new-primitive / scaffold-env Scaffold a new primitive file or environment directory
pack / unpack / env-info Work with .simworld env-package archives
replay Produce a deterministic step-by-step trace of a run

First simulation — one line

A world definition is a plain dict (or WorldTemplate, or a str/Path to a JSON file). simulate() loads it, runs it to completion with a built-in agent, and hands back the finished World:

from fg_env_kernel import simulate

template = {
    "name": "duel",
    "entity_types": [
        {"name": "warrior", "role": "agent",
         "properties": [{"name": "stamina", "type": "float", "default": 50.0,
                         "min_value": 0, "max_value": 100}]},
    ],
    "entities": [
        {"id": "w1", "name": "Alice", "entity_type": "warrior", "properties": {}},
        {"id": "w2", "name": "Bob",   "entity_type": "warrior", "properties": {}},
    ],
    "actions": [
        {"name": "rest", "actor_type": "warrior",
         "resolution_archetype": "deterministic",
         "effects_on_success": [
             {"target": "actor", "operation": "add", "field": "stamina", "value": 15},
         ]},
    ],
    "temporal": {"phases": [{"name": "action"}], "max_rounds": 3},
}

world = simulate(template, seed=42)

print(world)              # <World 'duel': round 3/3, finished, terminated_by=None>
print(world.summary())    # name, rounds vs budget, what terminated it, final event
print(world.state.get_entity("w1").get("stamina"))   # 95.0

No agent supplied means the built-in random_policy drives every agent entity: each turn it picks a uniformly random valid action (auto-picking a valid target for targeted actions, skipping actions that need parameters it can't invent). It is deterministic given seed — every entity gets its own RNG stream derived from (seed, entity_id), so replays are exact and never touch global random state. simulate() also accepts agent= (your own decision_fn), max_rounds= (overrides the template's temporal.max_rounds), on_event= (real-time event callback), and registry= (custom-primitive scoping). Passing a file path works the same way — simulate("worlds/duel.json", seed=42).

Templates are validated before any world is built: a garbage or typo'd template raises TemplateError listing every lint error (instead of silently "running" an empty simulation), warnings are logged, and strict=True raises on warnings too. Both simulate() and Kernel.load() apply the same gate.

The usage ladder is simulate()Kernel/Worldload_world: graduate one rung whenever you need more control.

Bring your own agent — the Kernel facade

To hold configuration across runs and plug in a real brain, build the World yourself. Hand the same template to a Kernel, attach an agent, run:

from fg_env_kernel import ActionInstance, Kernel

def decision_fn(entity_id, perception, valid_actions):
    if "rest" in valid_actions:
        return ActionInstance(action_name="rest", actor_id=entity_id)
    return None

kernel = Kernel(seed=42)
world = kernel.load(template, decision_fn=decision_fn)
world.run()                        # or: while not world.finished: world.step()

print(world.current_round)                     # 3 — the loader honors temporal.max_rounds
print(world.state.get_entity("w1").get("stamina"))  # 95.0
print(world.terminated_by)                     # None — the round budget ran out

Kernel holds run configuration (seed, primitive registry); World wraps the (WorldState, SimulationEngine) pair and adds no behavior of its own — read world.state, world.events, world.current_round, world.finished, world.terminated_by, world.seed, and drive it with world.step() / world.run(). The loader honors the template's temporal.max_rounds; kernel.load(..., max_rounds=N) overrides it. Power users can keep calling load_world directly — the facade delegates to it. In the repo, examples/00_simulate.py is the one-liner and examples/quickstart.py drives the full examples/tic_tac_toe/ template this same way.

decision_fn is the entire agent interface:

decision_fn(entity_id: str, perception: dict, valid_actions: list) -> ActionInstance | None

valid_actions is a list of action-name strings (the actions currently legal for that entity). Return an ActionInstance (importable from the package root) to act, or None to pass. The kernel is fully decoupled from any LLM — the same world runs identically with a real model, a heuristic, or a deterministic scripted stub.

A complete programmatic example

Worlds can also be built in Python directly against the state API. Two warriors, an attack action resolved by a strength contest, and a rest action:

# WorldState, SimulationEngine, ActionInstance are exported from the package
# root; the schema-building classes live in their submodules.
from fg_env_kernel import ActionInstance, SimulationEngine, WorldState
from fg_env_kernel.entity import EntityType, Entity
from fg_env_kernel.resource import ResourceType
from fg_env_kernel.action import (
    ActionDefinition, Precondition, Effect, Operator, EffectOperation,
)
from fg_env_kernel.types import PropertySchema, PropertyType

state = WorldState()

# 1. Types: what kinds of things exist, and what properties they carry
state.register_entity_type(EntityType(
    name="warrior", role="agent",
    properties=[
        PropertySchema(name="health", type=PropertyType.FLOAT, default=100.0,
                       min_value=0, max_value=100),
        PropertySchema(name="strength", type=PropertyType.INT, default=10),
        PropertySchema(name="stamina", type=PropertyType.FLOAT, default=50.0,
                       min_value=0, max_value=100),
    ],
))
state.register_resource_type(ResourceType(name="gold", conservation=True, discrete=True))
state.resources["gold"].holdings = {"w1": 50, "w2": 50}

# 2. Instances
state.spawn_entity(Entity(id="w1", name="Alice", entity_type="warrior",
                          properties={"health": 100.0, "strength": 15, "stamina": 50.0}))
state.spawn_entity(Entity(id="w2", name="Bob", entity_type="warrior",
                          properties={"health": 100.0, "strength": 10, "stamina": 50.0}))

# 3. Rules: an action = precondition + resolution archetype + effects
state.register_action(ActionDefinition(
    name="attack", description="Strike an opponent (opposed strength check)",
    actor_type="warrior", target_type="warrior",
    preconditions=[Precondition(subject="actor", operator=Operator.GTE,
                                field="stamina", value=10)],
    resolution_archetype="contest",
    resolution_params={"attacker_property": "strength", "defender_property": "strength"},
    effects_on_success=[
        Effect(target="target", operation=EffectOperation.SUBTRACT, field="health", value=20),
        Effect(target="actor",  operation=EffectOperation.SUBTRACT, field="stamina", value=10),
    ],
    effects_on_failure=[
        Effect(target="actor", operation=EffectOperation.SUBTRACT, field="stamina", value=5),
    ],
))
state.register_action(ActionDefinition(
    name="rest", description="Recover stamina",
    actor_type="warrior", resolution_archetype="deterministic",
    effects_on_success=[
        Effect(target="actor", operation=EffectOperation.ADD, field="stamina", value=15),
    ],
))

# 4. An agent brain (here: a stub that always rests)
def always_rest(entity_id, perception, valid_actions):
    if "rest" in valid_actions:
        return ActionInstance(action_name="rest", actor_id=entity_id,
                              reasoning="I need to rest.")
    return None

engine = SimulationEngine(state=state, decision_fn=always_rest, max_rounds=3, seed=42)
result = engine.run()
# result.get_entity("w1").get("stamina") == 95.0   (50 + 15 * 3)

What just happened

Where to go next