Overview
econagents runs LLM agents in economic experiments. The game server remains the source of truth; econagents connects simulated players to that server, projects server events into each player's local state, asks the player's role for an action, and sends that action back through the configured protocol.
Architecture
The runtime is organized around explicit boundaries:
Domain types describe stable concepts such as
Event,Action,PhaseId, andAgentContext.Ports define interfaces for protocol codecs, transports, prompt renderers, response parsers, and state projectors.
Adapters implement those ports for concrete systems such as IBEX JSON envelopes, WebSockets, Jinja prompt files, and
EventFieldstate mapping.Runtime services compose those pieces.
Agentruns one simulated player,PhaseEnginecontrols turn-based and continuous phases, andGameRunnersupervises a set of agents.
The default protocol adapter is IbexMessageCodec. It expects messages in
this envelope:
{"meta": {"type": "phase-started"}, "payload": {"phase": "decision"}}
meta.type becomes the internal event type and payload becomes event
data. Outbound actions are encoded with the same codec before they are sent
through the transport.
Runtime Flow
For each simulated player, an Agent performs this sequence:
Receive a raw message from the transport.
Decode it into an
Eventwith the configuredMessageCodec.Project the event into the player's
GameState.If the event changes phase, ask
PhaseEnginewhether to act once or run a continuous action loop.Ask the
Rolefor an action when the role participates in that phase.Encode the action and send it through the transport.
Roles
A Role defines what a player does. It specifies:
role: numeric role idname: prompt/template role namellm: provider used for decisionstask_phasesortask_phases_excluded: phases where the role actsoptional response schemas for structured model output
Example:
from typing import Literal
from pydantic import BaseModel
from econagents import Role
from econagents.adapters.llm import ChatOpenAI
class Choice(BaseModel):
meta: dict
payload: dict[str, Literal["COOPERATE", "DEFECT"]]
class Prisoner(Role):
role = 1
name = "Prisoner"
llm = ChatOpenAI(model_name="gpt-5.4-mini")
task_phases = ["decision"]
default_response_schema = Choice
Agent supplies Jinja prompt rendering and JsonResponseParser by
default. You can inject another prompt renderer or response parser on the role
when a game needs a different decision pipeline.
Game State
Each runtime owns a GameState with three sections:
meta: game id, phase, player number, and administrative contextprivate_information: state visible to the current playerpublic_information: state visible to all players
Fields declared with EventField are updated by EventFieldStateProjector
when incoming event data contains the matching key.
from pydantic import Field
from econagents import EventField, GameState, MetaInformation, PrivateInformation, PublicInformation
class Meta(MetaInformation):
phase: str | int = EventField(default=0)
class PrivateInfo(PrivateInformation):
total_score: int = EventField(default=0)
class PublicInfo(PublicInformation):
history: list[dict] = EventField(default_factory=list)
class MyState(GameState):
meta: Meta = Field(default_factory=Meta)
private_information: PrivateInfo = Field(default_factory=PrivateInfo)
public_information: PublicInfo = Field(default_factory=PublicInfo)
Running A Game
Code-driven experiments build agents explicitly and pass them to
GameRunner:
from pathlib import Path
from econagents import GameRunner, TurnBasedGameRunnerConfig
from examples.prisoner.agents import create_prisoner_agents
config = TurnBasedGameRunnerConfig(
game_id=1,
hostname="localhost",
port=8765,
path="",
prompts_dir=Path("prompts"),
max_game_duration=300,
)
agents = create_prisoner_agents(config, recovery_codes)
runner = GameRunner(config=config, agents=agents)
await runner.run_game()
YAML-driven experiments use runtime settings to create the same runtime
objects from configuration:
runtime:
mode: turn_based
runner:
type: TurnBasedGameRunner
hostname: localhost
port: 8765
path: ""
phase_transition_event: phase-transition
phase_identifier_key: phase