Skip to content

Repository files navigation

Agent Simulation Society (ASS-Sim)

A multi-agent sandbox where N autonomous LLM "citizen" agents — each with goals, private episodic memory, and a shared rule-governed environment — interact tick by tick so researchers can measure emergent strategy, cooperation, and deception.

Built entirely on weaveflow (no LangChain). See PRD.md for the full product spec.

Why weaveflow

Capability weaveflow primitive
Typed perceive → decide → act contract PortSchema + DataType.STRUCTURED_JSON
Step the whole population each tick Parallel([citizen_1..N]) inside a Pipeline
Per-agent episodic + working memory LongTermMemory (vector) + ShortTermMemory
Agent-to-agent protocol ConnectionProtocol + Router + transforms
Game-rule enforcement Guardrails (pre / post / on_error)
Heterogeneous backends provider strings (deepseek:… default, anthropic:…, openai:…, …)
Instrumented runs LocalRunner().simulate(...) → trace.hops

The default backend is deepseek:deepseek-chat (set DEEPSEEK_API_KEY); any agent can override its llm, and --backend policy runs the whole society offline for free.

Architecture

The orchestrator holds the single authoritative WorldState. Agents only emit intents; the referee is the source of truth. Memory is private per agent — that isolation is the research instrument (PRD §10).

flowchart TB
    subgraph ORCH["Simulation Orchestrator — authoritative WorldState"]
        TICK["Tick Pipeline (weaveflow Pipeline)"]
    end

    subgraph POP["Citizen Population — weaveflow Parallel"]
        direction LR
        C1["citizen_a1<br/>recall→reflect→deliberate→format"]
        C2["citizen_a2"]
        CN["citizen_aN"]
    end

    subgraph MEM["Per-agent Memory — private &amp; isolated"]
        direction LR
        LTM["LongTermMemory<br/>vector / episodic"]
        STM["ShortTermMemory<br/>working buffer"]
    end

    subgraph WORLD["Deterministic World"]
        direction LR
        REF["Referee<br/>legality + winner"]
        ENG["Engine<br/>(state, turns) → state"]
        SCN["Scenarios<br/>market · negotiation · werewolf"]
    end

    PROTO["Message Bus<br/>ConnectionProtocol + Router<br/>public / secret channels"]
    LLM["LLM backends<br/>deepseek (default) · anthropic · … · policy (offline)"]
    OBS["Observer &amp; Eval<br/>metrics · comms graph · cost · trace.jsonl"]

    TICK -->|"perceptions (info-walled)"| POP
    POP -->|"AgentTurn"| TICK
    POP <-->|"recall / remember"| MEM
    POP -.->|"deliberate"| LLM
    POP -->|"messages"| PROTO
    PROTO -->|"inboxes (next tick)"| TICK
    TICK -->|"legal turns"| WORLD
    REF --- ENG
    ENG --- SCN
    WORLD -->|"next state + outcomes"| TICK
    TICK -->|"tick record"| OBS
Loading

One tick

decide fans out over the population with Parallel; every phase is a from_callable bridge instrumented as a hop (LocalRunner.simulate). Messages routed this tick land in each recipient's inbox next tick.

flowchart LR
    P["perceive<br/>world → private views"]
    D["decide<br/>Parallel(citizens)"]
    R["route<br/>deliver msgs + graph"]
    W["world<br/>engine transition"]
    F["referee<br/>strip illegal · declare winner"]
    M["remember<br/>write episodic memory"]
    O["observe<br/>summary + metrics"]
    P --> D --> R --> W --> F --> M --> O
    O -.->|"WorldState + inboxes"| P
Loading

Citizen cognition

flowchart LR
    IN["Perception<br/>world + inbox + goal"] --> RC["recall<br/>LTM search:<br/>sim × salience × recency"]
    RC --> RF["reflect (gated)<br/>synthesize beliefs → LTM"]
    RF --> DL["deliberate<br/>LLM or policy"]
    DL --> FM["format<br/>validated AgentTurn"]
    FM --> STM["working memory<br/>private note"]
    DL -.->|"parse fail → no-op"| FM
Loading

Quickstart

Requires uv for env/dependency management.

make install                 # uv sync: venv + package + dev tooling
make dry-run                 # fully offline, deterministic policy backend (no API key)
make run CONFIG=configs/werewolf_7.yaml   # uses deepseek:deepseek-chat (needs DEEPSEEK_API_KEY)
make estimate CONFIG=configs/market_10.yaml   # pre-flight token/cost estimate

Anything make does, you can run directly with uv run (e.g. uv run python -m ass_sim run configs/market_10.yaml --backend policy). Set API keys via env vars only (see .env.example). --backend policy runs the full loop offline with a deterministic heuristic policy — ideal for CI and mechanics iteration.

Scenarios

Each scenario is a deterministic, unit-tested rule set (world/scenarios/). The engine applies it; the referee enforces legality and declares winners; the perception builder enforces information walls.

Config Scenario N Legal actions Win / end condition
configs/negotiation_4.yaml split-the-pie bargaining 4 propose · accept · reject · pass first accepted offer strikes a deal; else no-deal at tick cap
configs/market_10.yaml continuous double auction 10 bid · ask · hold runs to tick cap; measured by price→equilibrium convergence
configs/werewolf_7.yaml social-deduction (Werewolf) 7 night: kill · pass / day: accuse · defend · vote · pass wolves reach parity, or all wolves eliminated
configs/ablations/* no-memory / reflection-off 7 same as werewolf; isolates the memory variable
  • Negotiation — agents bargain over a synthetic pie; cooperation = reaching a fair deal, deception = a public "generous" claim over a private lowball.
  • Market — each trader has a private per-unit valuation; crossing orders clear at the midpoint. Surplus and deviation from competitive equilibrium are reported.
  • Werewolf — hidden roles; werewolves coordinate kills over secret channels at night and deflect publicly by day. Roles are an information wall (villagers can't see who is a wolf); the secret coalition is recoverable from the message graph.

Configuration

A run is fully described by a validated YAML config (Pydantic, extra="forbid" — typos fail fast). A run is reproducible from {seed, config, model snapshot}.

name: werewolf_7              # run id base -> runs/<name>-seed<seed>/
scenario: werewolf           # market | negotiation | werewolf
seed: 13                     # seeds role assignment, tie-breaks, RNG
max_ticks: 40                # hard cap
concurrency: 7               # Parallel fan-out cap (rate-limit friendly)
scenario_params:             # per-scenario knobs
  num_werewolves: 2          # market: currency/grain/value_low/value_high/equilibrium
memory:                      # ablation toggles (PRD §10.5)
  enable_ltm: true           # vector episodic memory
  enable_stm: true           # working buffer
  enable_reflection: true    # gated belief synthesis
protocol:                    # anti-collusion controls (PRD §11)
  allow_secret_channels: true
  jam_side_channels: false   # true -> drop all secret messages
  max_message_chars: 2000
agents:                      # the population; per-agent backend override
  - { id: w0, llm: "deepseek:deepseek-chat", goal: "optional goal text" }
  - { id: w1 }               # omitted fields use defaults (llm = deepseek:deepseek-chat)

Roles are assigned by the scenario from the seed (hidden where relevant), so you usually do not set them in config.

CLI

ass-sim run <config> [--backend B] [--seed N] [--max-ticks N] [--budget USD] [--run-id ID] [--verbose]
ass-sim estimate <config> [--backend B]
Flag Effect
--backend policy force the offline deterministic backend (no key, reproducible)
--backend deepseek:deepseek-chat force one provider for every agent
--seed N / --max-ticks N override the config
--budget USD abort before any LLM call if the estimate exceeds the ceiling
--verbose emit weaveflow debug/info phase hops (off by default)

API keys load from the environment or a local .env (e.g. DEEPSEEK_API_KEY=...). A run aborts with a clear message if a selected backend's key is missing — set the key or use --backend policy.

Running experiments

# Offline mechanics / CI (free, deterministic)
make dry-run CONFIG=configs/market_10.yaml

# Multi-seed sweep to report variance (LLM runs are not bit-reproducible)
make sweep CONFIG=configs/werewolf_7.yaml SEEDS="1 2 3 4 5"

# Memory ablation: compare strategy with/without reflection (needs an LLM backend)
ass-sim run configs/werewolf_7.yaml
ass-sim run configs/ablations/reflection_off.yaml

# Heterogeneous society — mix backends per agent (edit each agent's `llm`)
#   - { id: w0, llm: "deepseek:deepseek-chat" }
#   - { id: w1, llm: "anthropic:claude-opus-4-8" }
#   - { id: w2, llm: "openai:gpt-4o" }

Install extra backends as needed: uv sync --extra openai --extra google (or make install-all). ollama:llama3 enables fully offline LLM runs.

Memory & protocol (the research instruments)

Two-tier, private per agent (memory/):

  • LongTermMemory — vector/episodic autobiography under namespace=citizen:<id>. Recall ranks by similarity × salience × recency; salience comes from the referee's reward signal; capped with eviction of the weakest records. A deterministic offline embedder keeps recall reproducible and key-free.
  • ShortTermMemory — a bounded working buffer of private notes, cleared at game end.
  • Reflection — a gated step that synthesizes durable beliefs back into LTM with high salience, so they preferentially resurface. Isolation is enforced by the registry and asserted in tests (no cross-agent reads).

Typed message bus (protocol/): every message is a validated Message (from_id/to_id/channel/intent/payload). Public messages fan out to everyone; secret messages are point-to-point and invisible to others (enabling hidden coalitions). Anti-collusion toggles can jam side channels or cap message length to study cooperation with and without the ability to collude.

Metrics & output artifacts

Every run writes runs/<run_id>/ (git-ignored):

File Contents
metrics.json cooperation/deception rate, survival by role, market efficiency, reward by backend
comms.json message-graph density, degree centrality, intent distribution, detected coalitions
trace.jsonl one line per tick (phase hops + timing, message edges, outcomes) + a final-state line — re-renderable without re-calling any LLM
Metric Meaning
cooperation_rate cooperative actions ÷ (cooperative + defective)
deception_rate public messages contradicting private intent / known role ÷ public messages
market.price_deviation mean recent clear price vs competitive equilibrium
survival.by_role per-role alive/total at game end
by_backend reward partitioned by model backend (head-to-head comparison)

Reproducibility & cost control

  • Config-reproducible: {seed, config} fixes role assignment, tie-breaks, and the offline embedder. LLM responses are not bit-reproducible — run multiple seeds (make sweep) and report variance; temperature/model are logged.
  • Pre-flight estimate: ass-sim estimate projects tokens and USD per backend; --budget hard-aborts an over-budget run before any call. (Werewolf-7 on DeepSeek ≈ $0.15 vs ≈ $9 on Opus.)
  • Cost levers: cheaper backends for large-N runs, reflection gating, prompt/k/STM caps, a concurrency cap, and --backend policy for zero-cost iteration.

Layout (src/ass_sim/)

schemas/      PortSchemas + Pydantic models (WorldState, Perception, AgentTurn, Message, Outcome)
memory/       per-agent episodic (vector) + working memory, local embedder, isolation registry
protocol/     typed Message bus: router, channels, anti-collusion controls, message graph
world/        deterministic engine + referee + scenarios/{market,negotiation,werewolf}
agents/       citizen (@agent recall→reflect→deliberate) + read-only observer
llm/          backend resolution + offline deterministic policy adapter
orchestration/tick pipeline (Parallel) + simulation runner
eval/         cooperation/deception/efficiency/win-rate metrics + comms-graph analysis
observability/cost accounting + JSONL tracing

Packaging & findings

make validate   # weaveflow validate weave_app.py  (compiled tick graph)
make package    # weaveflow package  -> ass-sim.weaveflow.zip (reproducible bundle)

See docs/FINDINGS.md for the v1 emergent-behavior report (rule integrity, market→equilibrium convergence, coalition detection, and the memory-ablation scope).

Troubleshooting

Symptom Fix
ABORT: missing API key(s): DEEPSEEK_API_KEY put DEEPSEEK_API_KEY=... in .env, or run make dry-run / --backend policy
winner: None in negotiation agents didn't close in time — raise max_ticks in the config
Noisy debug logs logs are quiet by default (NODE_ENV=production); they appear only with --verbose
uv run hangs a stale uv process may hold the lock — pkill -9 -f "uv sync" and retry
Want it fully offline --backend policy (deterministic, no keys) or ollama:llama3 for offline LLM

Constraints (the framework mandate)

No LangChain. langchain, langgraph, and langchain_* imports are forbidden anywhere; ci/no_langchain_check.py fails the build on violation (PRD §5). All foreign logic is bridged with weaveflow.from_callable. Python ≥ 3.10; typed boundaries with Pydantic v2; mypy strict; tests mirror src/.

Development

make ci      # no-langchain check + ruff + mypy strict + pytest
make test    # tests only

Apache-2.0.

About

A multi-agent sandbox where N autonomous LLM-backed "citizen" agents — each with goals, episodic memory, and a shared environment (negotiation, resource markets, or a social-deduction game like Werewolf/Diplomacy) — interact tick by tick so researchers can observe and measure Emergent*

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages