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/World → load_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
- Types before instances:
EntityTypedeclares the property schemas (with defaults and min/max clamps);Entityinstances are spawned against them. Resources are typed too (conservation=Truemeans the total pool is conserved). - Actions are data: preconditions gate validity, a resolution archetype (here
contest) decides success and magnitude, and separate effect lists apply on success/failure. The engine — not the agent — enforces all of it. - The seed makes it reproducible: rerunning with
seed=42gives byte-identical results. Leave the seed off and the engine mints one from entropy and records it onengine.seedso any run can still be replayed exactly.