Skip to main content
Fareground

env-kernel

A deterministic simulation kernel for agent environments — LLMs are brains, code is physics.

CI Python Dependencies Engine


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-kernel and the import package is fg_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 an ActionInstance whose action_name is one of these (with actor_id=entity_id and any parameters the action declares), or None to 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.


Stewarded by Fareground.
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

fg_env_kernel-0.2.0.tar.gz (361.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fg_env_kernel-0.2.0-py3-none-any.whl (339.9 kB view details)

Uploaded Python 3

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

Hashes for fg_env_kernel-0.2.0.tar.gz
Algorithm Hash digest
SHA256 200228f998d0d61e53eb5bfe93b0e8e6a1821a092b6ec14f1dea96d0eab5d717
MD5 643f0f16ea9a2ac2fbdc0d0092eec2ee
BLAKE2b-256 9d178c5ce3b16cd4f3933176ba465386843dfaceb16adbe9f89ff694adeba43c

See more details on using hashes here.

Provenance

The following attestation bundles were made for fg_env_kernel-0.2.0.tar.gz:

Publisher: release.yml on Fareground/env-kernel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for fg_env_kernel-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 38acbbd339e8101408601c4e498341e40ba2067d427988ce73ca91c2f990ef83
MD5 035d8b58b0adf2cdac068e53c7174c1e
BLAKE2b-256 be407c72718e39e6fd1b931fdec5136c65093b82530eb38c47dcff41fd883e41

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page