docs / env-kernel / api reference

API reference

Import path: fg_env_kernel. Everything below is re-exported from fg_env_kernel/__init__.py (the import firewall) unless noted otherwise.

Loading worlds (pipeline/loader.py)

load_world(
    template: dict | WorldTemplate,
    *, seed: int = 0, decision_fn=None, on_event=None,
) -> tuple[WorldState, SimulationEngine]
# Build state + engine from a template (raw dict or WorldTemplate).

build_world_state(schema: dict) -> WorldState
# Canonical JSON→state builder — the single source of truth.

load_world_parts(
    schema: dict, rules: dict, viz: dict | None = None,
    *, seed: int = 0, decision_fn=None, on_event=None,
) -> tuple[WorldState, SimulationEngine]
# Three-document form (schema + rules + viz merged).

WorldTemplate

Pydantic BaseModel with extra="allow" — the canonical JSON contract.

Group Fields
Metadata name, description, rules (markdown brief → agent perception), contract_version="1.0"
Schema layer entity_types, resource_types, relation_types, visibility_rules, spatial={"type":"none"}, temporal={"phases":[]}, tables
Rules layer actions, triggers, derived_rules, termination_conditions, domain_modules
Instance layer entities, resource_holdings, resource_unallocated, initial_relations, factions, adjacency
Optional subsystems location_definitions, goals, skill_definitions, initial_skills, recipes, connectors, runtime_parameters, cognitive_config, social_config, crowd_config, viz

Note: a physics block is read by the loader from the schema dict but is not an explicit WorldTemplate field — it flows through via extra="allow".

Supporting Pydantic specs: PropertySpec, EntityTypeSpec, ResourceTypeSpec, PreconditionSpec, EffectConditionSpec, EffectSpec, ActionSpec, EntitySpec, TerminationSpec, DomainModuleSpec.

ActionSpec — the action-authoring contract

name, description, actor_type, target_type,
preconditions,
resolution_archetype="deterministic", resolution_params,
effects_on_success, effects_on_failure, effects_on_partial,
parameters,
requires_action, requires_action_success=True,
cooldown_rounds=0, sequence_rounds=0,
interruptible=True, message_action=False
# `duration` is accepted for continuous mode via extra="allow"

Effect specs name an operation from the built-in EffectOperation verb set (action.py; custom verbs register via @effect): set, add, subtract, multiply, transfer_resource, set_relation, modify_relation, move_to, kill, emit_event, apply_status, remove_status, spawn_entity, despawn_entity, give_item, take_item, drop_item, pickup_item, award_xp, craft_item, social verbs (post_content, share_content, react_content, follow_entity, unfollow_entity), and game-token/board/deck verbs (grant_token, consume_token, advance_track, place_on_board, reset_board, draw_from_deck).

When a resolution returns partial=True, the action's effects_on_partial list is applied, and effects with scale_by_magnitude=True multiply numeric values by success_degree.

SimulationEngine (runtime/engine.py)

SimulationEngine(
    state: WorldState,
    decision_fn=None,        # fn(entity_id, perception, valid_actions) -> ActionInstance | None
    outcome_fn=None,         # fn(entity_id, action_name, success, narrative, details) -> None
    narrative_fn=None,
    max_rounds: int = 100,
    seed: int | None = None, # unseeded → minted from entropy, recorded on engine.seed
    on_round_start=None, on_round_end=None,
    on_event=None,           # fn(event_dict) -> None — real-time streaming
    termination_conditions: list[TerminationCondition] | None = None,
    invariant_checker=None,
    world_event_engine=None,
    continuous_time=None,    # ContinuousTemporalModel → continuous mode
    parallel_decisions: int = 0,  # 0 = sequential; N = max concurrent LLM calls
)

engine.run() -> WorldState   # dispatches _run_discrete() or _run_continuous()
engine.pause()
engine.resume() -> WorldState  # resumes from the in-flight schedule/round
engine.is_paused() -> bool
engine.stop()

Determinism: one seeded random.Random(self.seed) is shared into property dynamics, world events, and domain-module reseed(rng) hooks so all draws stay reproducible.

The decision_fn contract

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

valid_actions is a list of action-name strings (WorldState.get_valid_actions, optionally narrowed by domain modules). Return ActionInstance(action_name=..., actor_id=..., target_id=..., reasoning=...) (from fg_env_kernel.action) or None to pass. Optional outcome_fn and narrative_fn callbacks receive resolution results and narration.

Resolution (resolution.py)

@dataclass
class ResolutionResult:
    success: bool; magnitude: float = 1.0; narrative: str = ""; details: dict = {}
    partial: bool = False          # graduated outcome between success and failure
    success_degree: float = 1.0    # 0.0 (total failure) … 1.0 (total success)

class ResolutionArchetype(ABC):
    def resolve(self, actor_properties, target_properties, params,
                action_params, rng=None) -> ResolutionResult

get_resolution(name: str) -> ResolutionArchetype   # raises ValueError on unknown
RESOLUTION_REGISTRY: dict[str, ResolutionArchetype]

Registry keys (13 built-ins): deterministic, skill_check, contest, deterministic_math, voting, evidence_chain, order_book, cpmm, market_impact, auction_sealed_first, auction_sealed_second, auction_english, auction_dutch.

Physics (physics.py)

class PhysicsModel:
    MAX_SUBSTEPS = 4096
    def __init__(self, variables=None, params=None, substeps=4, time=0.0)
    values: dict[str, float]        # property
    integrated: list[str]           # property — names of rate-driven variables
    def is_empty(self) -> bool
    def integrate(self, dt: float, state=None) -> list[dict]
    def tick(self, state, dt: float) -> list[dict]     # engine entry point
    def to_dict(self) / from_dict(cls, d)
    @classmethod
    def from_schema(cls, spec) -> PhysicsModel | None

@dataclass
class PhysicsVariable:
    name: str; value: float = 0.0; rate: str | None = None
    min: float | None = None; max: float | None = None
    source: EntitySource | None = None
    writeback: EntityWriteback | None = None
    # rate (integrated ODE) XOR source (algebraic read) — never both

@dataclass
class EntitySource:    entity_type: str; property: str; reduce: str = "sum"
@dataclass
class EntityWriteback: entity_type: str; property: str; mode: str = "broadcast"

class PhysicsExprError(ValueError): ...   # raised at build time for invalid rate exprs

Registry & extension decorators (registry.py, effect_context.py)

registry: KernelRegistry   # global singleton
# namespaces: effects, preconditions, resolutions, phases,
#             terminations, modules, target_selectors

@effect(name, *, aliases=None, replace=False)        # handler(ctx: EffectContext, spec: dict) -> dict | None
@precondition(name, *, aliases=None, replace=False)  # fn(...) -> bool
@resolution(name, *, aliases=None, replace=False)    # class or instance
@phase(name, *, aliases=None, replace=False)
@termination(name, ...)      # exported as `termination_decorator`
@module(name, ...)           # factory
@target_selector(name, ...)

KernelRegistry.scoped() gives context-managed temporary registration. Each namespace offers register / get / try_get / has / keys / unregister / __contains__ / __iter__ / __len__.

EffectContext — the argument bundle passed to @effect handlers (as handler(ctx, spec), where spec is the effect-spec dict). Fields: state, actor, target, params, result (the ResolutionResult that produced the effect), rng, emit (event-emit callable), changes. Methods: record(change) appends a state-change record for the round transcript; resolve(value) evaluates $-expressions. There are no property getters/setters — mutate entities directly (e.g. ctx.actor.set(...)) and log via record.

Termination (termination submodule)

termination.evaluate(state, condition, rng=None) -> bool
termination.check_all(state, conditions, rng=None) -> condition | None  # first that fires
termination.resolve_winner(state, condition, rng=None) -> dict  # {"winner_id", "winner_name"}
termination.register_winner_resolver(name, fn) -> None

Built-in check_types: expr, compound_and, compound_or, all_dead, resource_exhausted, round_limit, rounds_idle, property_threshold, all_goals_complete, event_triggered, last_one_standing, first_to_score, score_after_n_rounds, count_property, bankruptcy, faction_win, vote_threshold. Winner resolvers ship for last_one_standing, first_to_score, score_after_n_rounds, bankruptcy, faction_win, vote_threshold.

Expression language (predicates.py, effects.py)

evaluate_predicate(expr, state=..., rng=...) -> bool   # predicates.evaluate
resolve_expression_value(...)                          # predicates.resolve

The safe grammar powering guards, effects, and terminations — $actor.gold >= 100 && $count(player, alive) > 1. effects.py resolves $-expressions: $actor.x, $random(...), $count(...), $lookup against schema tables.

Pipeline & tooling (pipeline/)

compile_template(raw, *, seed=0, decision_fn=None, on_event=None,
                 skip_lint=False, smoke=False, smoke_rounds=20) -> CompileResult
lint_template(template) -> list[CompileIssue]
smoke_test(engine, *, rounds=20, seed=42,
           decisions="random" | "first" | "round_robin" | Callable) -> SmokeReport
replay(...) -> ReplayTrace              # deterministic step trace (list of ReplayStep)
export_kernel_contract() -> dict        # JSON Schema + live capabilities; CONTRACT_VERSION

Env packages (.simworld archives): EnvPackage, PACKAGE_EXTENSION, load_env_package, save_env_package, scaffold_env(directory, name), pack_env, unpack_env, validate_package. Result types: CompileIssue, CompileResult, SmokeReport, ReplayStep, ReplayTrace. pipeline/versioning.py handles contract-version upgrades.

Domain modules (domain/base.py)

class DomainModule(ABC):
    # lifecycle & action hooks
    tick(state, round); validate_action(...); filter_valid_actions(...)
    modify_resolution(...); post_resolution(...)
    get_perception_data(...); get_constraints(...)
    required_properties; custom_actions
    to_dict() / from_dict()

DomainModuleRegistry   # singleton: get_instance, register, create, list_available
DomainModuleManager    # lifecycle management

Full implementations in domain/markets.py: PredictionMarketModule, SecuritiesTradingModule. domain/stubs.py holds Economic/Political/Ecological/Health placeholders.

Module lifecycle & observability

Continuous time (continuous_time.py)

@dataclass
class ScheduledEvent:
    fire_time; priority; event_type  # "agent_turn" | "environment" | "custom"
    entity_id; data; id; cancelled
    # __lt__: time → priority → entity_id → id (deterministic total ordering)

class EventQueue:  # heap-based priority queue
    schedule; pop; peek; cancel; cancel_for_entity
    get_events_for_entity; is_empty; to_dict / from_dict

class ContinuousTemporalModel:
    # the clock: pops events, advances current_time, reschedules agent turns
    # by action duration, fires recurring environment ticks (environment_interval)
    MIN_ACTION_DURATION = 1e-9     # forces forward progress
    DEFAULT_MAX_EVENTS = 1_000_000 # termination backstop; also max_time

Errors