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.
| 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.
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 & 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 & 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
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
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
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 estimateAnything 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.
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.
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.
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.
# 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.
Two-tier, private per agent (memory/):
- LongTermMemory — vector/episodic autobiography under
namespace=citizen:<id>. Recall ranks bysimilarity × 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.
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) |
- 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 estimateprojects tokens and USD per backend;--budgethard-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, aconcurrencycap, and--backend policyfor zero-cost iteration.
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
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).
| 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 |
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/.
make ci # no-langchain check + ruff + mypy strict + pytest
make test # tests onlyApache-2.0.