AI-native agent-based modeling framework.
Simulate. Test. Analyze. Narrate.
pip install agentstanA model is pure JSON — environment, agents, and behaviors are all data. No code in the spec means it can be stored, diffed, generated by an LLM, validated, and executed by any engine that implements the spec.
from agentstan import Simulation
spec = {
"seed": 42, # same spec + same seed = same results, every time
"environment": {"type": "grid_2d", "dimensions": {"width": 40, "height": 40, "topology": "torus"}},
"agent_types": {
"rabbit": {
"initial_count": 80,
"initial_state": {"energy": 25, "perception_radius": 5},
"behavior": {
"rules": [
# Graze when hungry
{"when": {"<": ["$energy", 20]},
"do": [{"type": "modify_state", "attribute": "energy", "delta": 2}]},
# Flee the nearest wolf
{"when": {">": [{"count": {"type": "wolf"}}, 0]},
"do": [{"type": "move_away", "from": {"nearest": {"type": "wolf"}}}]},
# Otherwise wander
{"when": {"==": [{"count": {"type": "wolf"}}, 0]},
"do": [{"type": "move", "direction": [{"choice": [-1, 0, 1]}, {"choice": [-1, 0, 1]}]}]},
# Reproduce when well-fed
{"when": {">": ["$energy", 30]}, "prob": 0.08,
"do": [{"type": "reproduce", "energy_cost": 15}]},
# Metabolism and starvation
{"do": [{"type": "modify_state", "attribute": "energy", "delta": -0.8}]},
{"when": {"<=": ["$energy", 0]}, "do": [{"type": "die", "cause": "starvation"}]},
]
}
}
}
}
sim = Simulation(spec)
results = sim.run(200)
print(results["summary"]["final_counts"])See examples/predator_prey_rules.py for the full two-species model, and
agentstan/core/rules.py for the complete expression/action reference.
Each rule is {"when": <condition>, "prob": <chance>, "do": [<actions>]} —
every rule whose condition holds fires, in order. Expressions are single-key
dicts: comparisons (<, >=, == …), arithmetic (+, * …), logic
(and, or, not), randomness (random, choice, uniform), and
neighbor queries (count, nearest_distance). "$energy" reads the agent's
own state; selectors like {"nearest": {"type": "wolf"}} pick a target agent
for move_toward, move_away, and interact. All of an agent's rules are
evaluated first, then its actions apply — a rule reading $energy sees the
value from before this step's changes.
The kernel is domain-blind: interactions are generic effects
(kill_target, transfer, self_delta, target_delta), reproduction
costs any attribute you choose, and world laws are spec data too — e.g.
death at zero energy is a top-level "global_rules" entry, not engine code:
"global_rules": [
{"when": {"<=": ["$energy", 0]}, "do": [{"type": "die", "cause": "energy_depleted"}]}
]Models can declare world state and read other agents, so economies are
plain data too. "@name" reads a global, "&attr" reads the agent a rule
selected with target, where filters queries, and exchange is an
atomic trade — it happens in full or not at all, so no gold or goods are
ever created by a failed trade:
{
"environment": {"type": "none"},
"globals": {"gold_burned": 0},
"world_rules": [
{"when": {"==": [{"%": ["@step", 5]}, 0]},
"do": [{"type": "spawn", "agent_type": "player", "count": 2}]}
],
"observables": {
"gold_supply": {"sum": {"type": "player", "attr": "gold"}},
"potion_price": {"mean": {"type": "shop", "attr": "price"}}
},
"agent_types": {
"player": {"initial_count": 50, "initial_state": {"gold": 20},
"behavior": {"rules": [
{"prob": 0.6, "do": [{"type": "modify_state", "attribute": "gold", "delta": 5}]},
{"target": {"lowest": {"type": "shop", "by": "&price", "where": {">": ["&stock", 0]}}},
"when": {">=": ["$gold", "&price"]},
"do": [{"type": "interact", "interaction_type": "buy_potion",
"params": {"exchange": {"give": {"gold": "&price"}, "get": {"stock": 1}}}}]}
]}},
"shop": {"initial_count": 2, "initial_state": {"gold": 0, "stock": 30, "price": 10}}
}
}globals+modify_global: shared numbers (prices, treasuries, sinks)world_rules: run once per step before agents act — price updates, inflowsobservables: named expressions recorded inmetrics.historyevery stepsum/mean/total: world-wide aggregates, with optionalwhere- selectors
nearest,random,lowest,highest(the last two withby) spawn: create agents from a type'sinitial_statechoose: a rule runs exactly one weighted branch (loot tables, gacha)initial_statevalues may be expressions evaluated per agent, e.g."guild": {"choice": ["red", "blue"]},"skill": {"uniform": [0, 1]}
Specs are validated strictly: unknown actions, fields, interaction params,
agent types or globals are rejected with the path of the offending rule,
and a rule that fails while running raises instead of going quiet.
Simulation.check(spec) constructs a spec and smoke-runs a few steps.
The event log (results["events"]) records births, deaths, interactions
and global changes by default; "log_level": "detailed" adds every move
and state change, "minimal" keeps only births and deaths.
A pack is a single JSON file bundling models (runnable specs) and named scenarios (parameter variations) plus metadata. Packs are 100% data — they can be stored anywhere, shared, versioned, and run by anyone with the library:
from agentstan.pack import load
pack = load("goblin-economy.pack.json")
print(pack.models, pack.scenarios) # ['base'] ['gold-rush', 'crash']
results = pack.run("gold-rush") # resolves overrides, seeds, runs
pack.validate(deep=True) # full engine validation of every entryA scenario is just dot-path overrides on a model:
"scenarios": {
"gold-rush": {"model": "base", "steps": 500, "seed": 7,
"overrides": {"agent_types.miner.initial_count": 40}}
}The format is versioned (schema_version), so packs you export today keep
loading tomorrow.
For local power users, an agent type may instead define behavior_code — a
Python function as a string (see examples/predator_prey.py). This is not
recommended for anything that stores or transmits specs: it is not
sandbox-safe and not portable. Prefer rules.
from agentstan import Simulation, StagedScheduler, DataCollector
sim = Simulation(spec, scheduler=StagedScheduler(["prey", "predator"]))
collector = DataCollector(
model_metrics={"avg_energy": lambda s: sum(a["energy"] for a in s.agent_manager.get_living_agents()) / max(s.agent_manager.get_total_count(), 1)},
)
sim.add_collector(collector)
results = sim.run(200)
time_series = collector.get_model_data()from agentstan.experiment import batch_run, sweep, summarize
# Run 50 times to get statistical confidence (run i uses seed + i, so the
# batch is reproducible; defaults to the spec's seed)
results = batch_run(spec, n_runs=50, steps=200, seed=1000)
print(summarize(results)["metrics"]) # mean / std / p5 / median / p95 per metric
# Sweep a parameter
results = sweep(spec, param="agent_types.wolf.initial_count", values=range(5, 50, 5), n_runs=10)
for val, runs in results.items():
avg = sum(r["summary"]["final_counts"].get("wolf", 0) for r in runs) / len(runs)
print(f"wolves={val}: avg final = {avg:.1f}")from agentstan.analysis import analyze_population, analyze_events
pop_report = analyze_population(results)
# {'agent_types': {'rabbit': {'stability': 'oscillating', 'period': 34, ...}}}
event_report = analyze_events(results)
# {'deaths': {'by_cause': {'starvation': 42, 'predation': 18}, ...}}# pip install agentstan[ai]
from agentstan.ai import generate, interpret, validate
# Generate a model from natural language
spec = generate("simulate wolves hunting rabbits in a forest")
# Run it
sim = Simulation(spec)
results = sim.run(200)
# AI explains what happened
explanation = interpret(results)
# "Rabbits peaked at step 34 then crashed due to overgrazing..."
# Validate the model matches the description
issues = validate(spec, "wolves should hunt rabbits")Every AI helper (generate, interpret, validate, Steerer, LLM
agents) takes client= — any OpenAI-compatible client — or base_url=,
so OpenAI, Anthropic's OpenAI-compatible endpoint, or a local server all
work. Defaults come from AGENTSTAN_MODEL, AGENTSTAN_BASE_URL and
OPENAI_API_KEY.
agentstan run model.json # a spec or a .pack.json
agentstan run economy.pack.json gold-rush # a pack scenario
agentstan validate economy.pack.json # construct + smoke-run everything
agentstan batch model.json --runs 50 --vary globals.tax=0.05,0.1,0.2
agentstan generate "a F2P economy with a gold sink" -o economy.pack.json # needs agentstan[ai]batch prints the distribution (mean, p5, p95) of every observable and
agent count for each parameter combination; runs execute in parallel
processes and are reproducible from the spec's seed.
agentstan/
core/ # Simulation engine, agents, environments, schedulers
experiment/ # Batch runs, parameter sweeps
analysis/ # Population dynamics, event analysis
ai/ # LLM-powered generation, interpretation, validation
BSD 3-Clause