Skip to content

Latest commit

 

History

55 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AgentStan

AI-native agent-based modeling framework.

Simulate. Test. Analyze. Narrate.

pip install agentstan

Quick Start

A 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.

Rule language in one minute

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"}]}
]

Shared state — markets, prices, economies

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, inflows
  • observables: named expressions recorded in metrics.history every step
  • sum / mean / total: world-wide aggregates, with optional where
  • selectors nearest, random, lowest, highest (the last two with by)
  • spawn: create agents from a type's initial_state
  • choose: a rule runs exactly one weighted branch (loot tables, gacha)
  • initial_state values 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.

Packs — save, share, export your work

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 entry

A 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.

Python behaviors (escape hatch)

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.

The STAN Framework

Simulate — Run agent-based models

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()

Test — Batch runs and parameter sweeps

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}")

Analyze — Understand what happened

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}, ...}}

Narrate — AI explains the results

# 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.

CLI

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.

Architecture

agentstan/
  core/           # Simulation engine, agents, environments, schedulers
  experiment/     # Batch runs, parameter sweeps
  analysis/       # Population dynamics, event analysis
  ai/             # LLM-powered generation, interpretation, validation

License

BSD 3-Clause

About

Agent-Based Modeling Library for LLMs

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages