env-kernel
A deterministic simulation kernel for agent environments — LLMs are brains, code is physics.
Overview
env-kernel is a continuous-time simulation kernel for agent environments: you describe a world as declarative data, and the kernel compiles it into an executable, deterministic, multi-agent simulation — with RK4 coupled-ODE integration for continuous dynamics and grounded, verifiable outcomes. It is pure Python with a single runtime dependency (pydantic) and no coupling to any game, domain, or LLM provider.
env-kernel is stewarded by Fareground and is one of six open-source building blocks alongside agent-id, agent-messaging, agent-knowledge, agent-memory, and agent-framework.
Agents make discrete, turn-based decisions; the kernel is the deterministic rule engine that resolves those decisions and evolves the world around them. It knows nothing about chess or markets or elections — those are just configurations. New mechanics plug in through registries and decorators, never by editing the engine.
Between agent turns the world does not have to sit still: an event-driven clock and a coupled-ODE physics integrator can evolve numeric state continuously, so action durations and reaction speed become part of the strategy.
Install
Note: the distribution name is
fg-env-kerneland the import package isfg_env_kernel. These are unchanged — downstream projects depend on them, and renaming them would break those imports.
The package is not on PyPI — install from GitHub:
pip install "fg-env-kernel @ git+https://github.com/Fareground/env-kernel.git"
Importing the package never scans the filesystem. Drop-in primitive discovery (kernel_primitives/*.py) is opt-in: call fg_env_kernel.discover() explicitly, or set the KERNEL_PRIMITIVES_DIR environment variable — an explicitly configured directory is honored at import time.
Quickstart
One line — the built-in seeded random agent plays every turn:
from fg_env_kernel import simulate
world = simulate("path/to/template.json") # or a template dict
print(world.summary())
simulate(template, *, agent=None, seed=None, max_rounds=None, on_event=None, registry=None) loads the template (dict, WorldTemplate, or path to a JSON file), runs to completion, and returns the finished World. With no agent, a deterministic random-valid-action policy (random_policy) drives every turn — same seed, same run. Pass your own decision_fn as agent to plug in an LLM.
Bring your own agent
A world is a plain dict; an agent is a plain function. This is a complete, runnable program:
from fg_env_kernel import ActionInstance, Kernel
template = {
"name": "Race to 10",
"description": "Two runners sprint; first to distance 10 wins.",
"entity_types": [
{"name": "runner", "role": "agent", "properties": [
{"name": "distance", "type": "float", "default": 0}
]}
],
"entities": [
{"id": "alice", "entity_type": "runner", "name": "Alice"},
{"id": "bob", "entity_type": "runner", "name": "Bob"},
],
"actions": [
{"name": "sprint", "description": "Run forward.", "actor_type": "runner",
"effects_on_success": [
{"operation": "add", "target": "actor", "field": "distance",
"value": "$random(1, 3)"}
]}
],
"termination_conditions": [
{"name": "finish_line", "check_type": "expr",
"params": {"expr": "$state.entities.alice.distance >= 10 || "
"$state.entities.bob.distance >= 10"}}
],
"temporal": {"max_rounds": 20},
}
def decision_fn(entity_id, perception, valid_actions):
"""Called once per agent turn. Swap in an LLM call here."""
if "sprint" not in valid_actions:
return None
return ActionInstance(action_name="sprint", actor_id=entity_id)
world = Kernel(seed=42).load(template, decision_fn=decision_fn)
world.run() # or: while not world.finished: world.step()
print(world.terminated_by) # "finish_line"
print(world.current_round) # 5
print(world.events[-1].narrative) # "Simulation ended after 5 rounds."
Same seed, same template, same decision_fn → same run, every time. More in examples/ — including tic-tac-toe built from a domain module.
The agent contract
decision_fn(entity_id, perception, valid_actions) -> ActionInstance | None is the only interface between your agent (LLM or otherwise) and the kernel:
entity_id— id of the agent whose turn it is.perception— a plain dict of what this agent can see, visibility-filtered. Always present:self(own id/name/properties),visible_entities,visible_relations,visible_resources,round,phase,location,faction. Present when the world provides them:world_brief(the template's name/description/rules markdown),incoming_messages,your_recent_actions,domain_data(board layout, hand contents, market state, ...), and more (roles, polls, time context, trade history).valid_actions— names of the actions whose preconditions currently pass. Return anActionInstancewhoseaction_nameis one of these (withactor_id=entity_idand anyparametersthe action declares), orNoneto skip the turn.
The engine is fully decoupled from the LLM — the same world runs with real agents, cheap heuristics, or a deterministic test stub.
Kernel(seed=..., registry=...) holds run configuration; Kernel.load(template, decision_fn=..., on_event=..., seed=..., max_rounds=...) accepts a template dict, WorldTemplate, or path to a JSON file, and returns a World with run(), step(), finished, terminated_by, current_round, events, state, seed, and a readable summary(). An on_event callback streams each event as it is emitted.
The ladder: simulate() for one-shot runs → Kernel/World for stepwise control → load_world for the raw engine.
Going lower level
The facade is a thin wrapper over load_world(template, *, seed=0, decision_fn=None, on_event=None, registry=None), which returns the raw (WorldState, SimulationEngine) pair — use it when you need direct engine or state access. Custom primitives register through the decorator surface (@effect, @resolution, @phase, @termination_decorator, @module) shown below.
The full template shape is documented in docs/template_schema.md; the machine-readable contract (including the live list of every registered effect operation, resolution archetype, termination check, and domain module) is docs/kernel_contract.json.
Continuous time and physics
A physics block on the world definition declares numeric variables and their rates of change. A dt-aware 4th-order Runge–Kutta integrator evolves them between turns — predator/prey, epidemics (SIR), price discovery. Variables can read entity aggregates and write values back onto the world. The result is deterministic and serializable.
"physics": {
"params": {"alpha": 1.1, "beta": 0.4, "delta": 0.1, "gamma": 0.4},
"variables": [
{"name": "prey", "value": 10, "rate": "alpha*prey - beta*prey*pred", "min": 0},
{"name": "pred", "value": 5, "rate": "delta*prey*pred - gamma*pred", "min": 0}
]
}
Extending the engine
Register custom verbs, resolution archetypes, phases, and terminations with decorators — the engine looks everything up by string name through the registry. The module-level decorators register process-wide:
from fg_env_kernel import effect, EffectContext
@effect("grant_gold")
def grant_gold(ctx: EffectContext, spec: dict) -> None:
gold = ctx.actor.properties.get("gold", 0)
ctx.actor.properties["gold"] = gold + spec.get("value", 1)
For per-kernel isolation, fork the registry and register on the fork. A fork sees every built-in primitive (nothing is copied — unknown names fall back to the parent), but its own registrations are invisible to the global registry and to other forks:
from fg_env_kernel import Kernel, registry
mine = registry.fork()
@mine.effect("grant_gold")
def grant_gold(ctx, spec): ...
kernel = Kernel(seed=42, registry=mine) # worlds resolve against `mine` only
Every namespace has an instance decorator (mine.effect, mine.precondition, mine.resolution, mine.phase, mine.termination, mine.module, mine.target_selector). Validation/reporting surfaces (lint_template, export_kernel_contract) read the global registry.
Concepts
- Determinism — given a template, a seed, and a
decision_fn, a run is fully reproducible. State is serializable end to end, so runs can be replayed step by step. - Declarative worlds — entities, properties, resources, relations, actions, effects, and terminations are all data. A safe expression grammar (
$actor.gold >= 100 && $count(player, alive) > 1) powers guards, effects, and terminations without per-game Python. - Turn-based agents, continuous world — agents decide in discrete turns; an event-driven clock and the physics integrator evolve the world between those turns.
- Registry extension points — custom verbs, resolution archetypes, phases, terminations, and domain modules register by name, keeping the engine core untouched.
Project Structure
src/fg_env_kernel/
runtime/ the tick loop (discrete + continuous)
physics.py coupled-dynamics ODE integrator
state.py the world state graph
action.py … actions, effects, resolution archetypes
predicates.py the expression language
domain/ optional game-genre modules (markets, boards, …)
pipeline/ compile · lint · smoke · replay · package
See the CHANGELOG for what's new.
Contributing
See CONTRIBUTING.md for dev setup, running the test suite, and lint/format tooling.
Licensed under the Apache License 2.0.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fg_env_kernel-0.2.0.tar.gz.
File metadata
- Download URL: fg_env_kernel-0.2.0.tar.gz
- Upload date:
- Size: 361.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
200228f998d0d61e53eb5bfe93b0e8e6a1821a092b6ec14f1dea96d0eab5d717
|
|
| MD5 |
643f0f16ea9a2ac2fbdc0d0092eec2ee
|
|
| BLAKE2b-256 |
9d178c5ce3b16cd4f3933176ba465386843dfaceb16adbe9f89ff694adeba43c
|
Provenance
The following attestation bundles were made for fg_env_kernel-0.2.0.tar.gz:
Publisher:
release.yml on Fareground/env-kernel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fg_env_kernel-0.2.0.tar.gz -
Subject digest:
200228f998d0d61e53eb5bfe93b0e8e6a1821a092b6ec14f1dea96d0eab5d717 - Sigstore transparency entry: 2414957593
- Sigstore integration time:
-
Permalink:
Fareground/env-kernel@c58e9b0ed161a86127278f4070826a9cf35a773d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Fareground
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c58e9b0ed161a86127278f4070826a9cf35a773d -
Trigger Event:
push
-
Statement type:
File details
Details for the file fg_env_kernel-0.2.0-py3-none-any.whl.
File metadata
- Download URL: fg_env_kernel-0.2.0-py3-none-any.whl
- Upload date:
- Size: 339.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
38acbbd339e8101408601c4e498341e40ba2067d427988ce73ca91c2f990ef83
|
|
| MD5 |
035d8b58b0adf2cdac068e53c7174c1e
|
|
| BLAKE2b-256 |
be407c72718e39e6fd1b931fdec5136c65093b82530eb38c47dcff41fd883e41
|
Provenance
The following attestation bundles were made for fg_env_kernel-0.2.0-py3-none-any.whl:
Publisher:
release.yml on Fareground/env-kernel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fg_env_kernel-0.2.0-py3-none-any.whl -
Subject digest:
38acbbd339e8101408601c4e498341e40ba2067d427988ce73ca91c2f990ef83 - Sigstore transparency entry: 2414957595
- Sigstore integration time:
-
Permalink:
Fareground/env-kernel@c58e9b0ed161a86127278f4070826a9cf35a773d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Fareground
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c58e9b0ed161a86127278f4070826a9cf35a773d -
Trigger Event:
push
-
Statement type: