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:
pip install "fg-env-kernel[dev]" # pytest, ruff, build
python -m pytest # 28 test files, ~353 tests
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 in three lines
A world definition is a plain dict (or WorldTemplate). Load it, attach an agent brain,
run:
from fg_env_kernel import load_world
state, engine = load_world(my_world_definition, seed=42, decision_fn=my_agent_brain)
engine.run()
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 (from fg_env_kernel.action) 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 test 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:
from fg_env_kernel.state import WorldState
from fg_env_kernel.engine import SimulationEngine
from fg_env_kernel.entity import EntityType, Entity
from fg_env_kernel.resource import ResourceType
from fg_env_kernel.action import (
ActionDefinition, ActionInstance, 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, clamped at 100 later)
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.