Open source
The Autonomous Enclave
github.com/juanmmm21/the-autonomous-enclave
A local society of LLM citizens who perceive, reason and trade under computational scarcity.
- Lines
- 6,995
- Files
- 104
- Test files
- 10
- Commits
- 50
- Started
- 5 Jul 2026
- Last commit
- 7 Jul 2026
- Python 58.1%
- TypeScript 39.9%
- JavaScript 1.3%
- CSS 0.5%
- 01
Every citizen is a local model with a personality, an inventory and a SimCoin balance.
- 02
Perceive → Think → Act on every tick, with schema-validated JSON before anything touches the world.
- 03
A real market: accepting an offer moves the inference quota before the payment settles.
- 04
A God Observer can devalue the currency, subsidise agents or cut their inference — and watch what emerges.
README · 9 min read
Commits per day
A fully local social and macroeconomic simulation ecosystem: LLM-driven digital citizens that perceive, reason and act under computational scarcity, while a "God Observer" watches over the colony through a real-time pixel-art web interface.
What it is and what problem it solves
The Autonomous Enclave is not a game with prewired decision trees: it's a laboratory for emergent synthetic cognition. Each citizen is an instance of an LLM (local, via Ollama) with its own personality, inventory and balance in SimCoin. On every tick of the global clock, the agent runs a closed Perceive → Think → Act loop: it checks its inbox and the market, invokes the model with its personality and perceived context, and translates the response (structured, schema-validated JSON) into movement, transfers, contracts or messages.
The underlying question the project explores is what social and economic dynamics emerge — cooperation, fraud, inequality, collapse — when agents with their own goals and limited resources have to coexist without a script. The "God Observer" can intervene directly on the economy (devalue the currency, subsidize agents, cut off their inference access) and watch how the system adapts or collapses.
Goal / skills demonstrated
- Orchestrating multiple stateful LLM agents: each citizen keeps its personality, memory and inventory across ticks, with a strict JSON output contract validated against a schema (
AgentAction) before any effect is applied to the world. - Interfaces designed before implementation: the
Protocols inprotocols.py(LLMBackend,JudgeBackend,MemoryStore,MessageBroker,TrustLedger,ResourceLedger) separate domain logic from concrete backends (Ollama, Redis, Qdrant), so any of them can be swapped without touching the rest of the system. - A real market, not just a payment rail: accepting a market offer resolves the specific offer by id and moves the underlying resource (inference quota) before the SimCoin payment, so an oversold offer fails cleanly instead of letting a buyer pay for nothing — observed live when a seeded citizen posted an inference-quota offer for more than it actually held.
- Financial precision: the entire economic ledger uses
Decimalin the backend and serializes asstringover the API, avoiding the binary rounding errors typical of usingfloatfor money. - End-to-end real-time architecture: an async
TickEngineruns in the background inside FastAPI's lifespan and broadcasts every tick to all connected WebSocket clients through a subscriber hub, consumed on the frontend by a Phaser map and React panels that resync without a page reload. - Evidence-grounded dispute resolution by a second LLM: the Judge Agent cites the real transaction history between the two parties (not just the contract terms) when arbitrating a breach, uses a higher-capacity reasoning model to decide who's at fault, applies the fine on the central bank, and docks the injured party's trust in the offender — verified live against a real
phi4judge with a crafted advance-payment dispute. - Real memory compression cycle: at the end of every simulated day, each agent's tick-by-tick reasoning is summarized, embedded and persisted in Qdrant, then retrieved as relevant past memories on future perceives — verified live against a running Ollama + Qdrant stack, not just unit-tested.
- Macro indicators derived from real state, not placeholders: the simulated energy price evolves every tick (scarcity drift plus a periodic oscillation) and feeds a real day-over-day inflation figure; transactions-per-minute is computed from the central bank's actual transaction ledger over a real-time window, not a hardcoded
0.0. - State survives a restart without slowing down the simulation:
CentralBankand the inference quota ledger stay in memory during execution (no DB round-trip on the hot per-tick path), but a Postgres checkpoint is taken every simulated day and on clean shutdown, then restored on the next boot — verified live by restarting the process twice and confirming each citizen came back with its real, persisted balance instead of its seed default. - A pixel-art map with reproducible, code-generated art: every ground tile, animated citizen spritesheet and decorative building — market, bank, judicial hall, vector-memory lab, signal tower, script workshop, derivatives exchange, housing — is drawn pixel-by-pixel by a build-time script (
scripts/generate_tileset.py, Python + Pillow) and committed as PNGs — no hand-authored or third-party art, and the whole tileset can be regenerated or restyled by editing one script. - A city you can actually navigate, not just watch: citizens path-find around building footprints instead of walking through them, keep wandering locally between backend ticks so the colony never looks frozen, and the camera supports drag-to-pan and cursor-centered zoom with a dynamically recomputed minimum zoom (tied to the real viewport size, not a hardcoded constant) so zooming out never exposes empty canvas outside the world.
- The invisible half of the economy, made visible:
TickEventstreams the open market board, unresolved contracts and recent Judge rulings every tick — not just agent snapshots — so the "Actividad económica" panel shows real offers, disputes and verdicts as they happen instead of leaving that state locked inside the backend. - Citizens created at runtime, not just at boot:
POST /api/v1/citizensregisters a new agent on the liveTickEngineand broadcasts an immediateTickEventsnapshot so it appears on the map at once, instead of waiting for the next scheduled tick.
How it works
- The Central Bank (
CentralBank) opens a SimCoin account for every agent registered on theTickEngine. - On every tick, the
TickEnginewalks through the living agents and, for each one, theAgentRuntimeruns:- Perceive: fetches its inbox (Redis), the open market offers, and its most relevant past-day memories (Qdrant).
- Think: invokes the local LLM (Ollama) with its personality and perceived context, requiring a JSON response validated against
AgentAction. - Act: applies the corresponding effect (move, send a message, post or accept a market offer — moving both the SimCoin and, for inference quota, the underlying resource — transfer SimCoin, sign a contract, file a dispute, sleep, or do nothing).
- The tick's passive cost (computational upkeep) is charged; if the balance reaches zero, the agent goes bankrupt.
- A contract left
PENDINGfor more than a simulated day is auto-escalated toDISPUTEDeven if neither party ever files one, so a wronged agent that simply goes quiet still reaches the Judge. EveryDISPUTEDcontract is resolved asynchronously by the Judge Agent, which reviews the real transaction history between the two parties, fines whoever is at fault, and docks the injured party's trust in the offender. - Every
ticks_per_dayticks, the sleep cycle runs: each agent's accumulated reasoning for the day is summarized and persisted as an embedding in Qdrant, and the intermediate log is cleared for the next day. - At the end of every tick, a
TickEvent(agent snapshots, macro indicators, the open market board, unresolved contracts and recent Judge rulings) is broadcast over WebSocket to the web interface, which updates the Phaser map and the telemetry panels in real time. - The "God Observer" can intervene at any moment from the Divine Intervention Console: devalue the currency, subsidize an agent, cut its inference quota (technological blackout), or shock the energy price (scarcity or abundance).
- Every simulated day (and on clean shutdown), the economic state is checkpointed to Postgres and restored on the next boot, so a restart doesn't reset the colony back to its seed defaults.
- New citizens aren't limited to the seeded population: the "Fundición de ciudadanos" panel calls
POST /api/v1/citizensto register one on the liveTickEnginewith a name and personality, which opens its bank/quota accounts and broadcasts an immediateTickEventso it shows up on the map right away.
Architecture
the-autonomous-enclave/
├── docs/vision.md # original project vision document
├── scripts/generate_tileset.py # build-time pixel-art generator (Python + Pillow) -> frontend/public/tileset/
├── docker-compose.yml # Redis (broker) + Qdrant (vector memory) + Postgres (checkpoints) for local dev
├── backend/
│ ├── pyproject.toml
│ ├── src/enclave/
│ │ ├── models.py # domain: AgentState, MarketOffer, Contract, Transaction, TickEvent...
│ │ ├── protocols.py # interfaces: LLMBackend, JudgeBackend, MemoryStore, MessageBroker, TrustLedger, ResourceLedger
│ │ ├── exceptions.py # domain errors (insufficient funds, breached contract...)
│ │ ├── config.py # Settings from environment variables
│ │ ├── seed.py # initial citizen population (personalities, system prompts, positions)
│ │ ├── services/
│ │ │ ├── llm_client.py # OllamaLLMBackend / OllamaJudgeBackend
│ │ │ ├── message_broker.py # RedisMessageBroker (private inbox + market board)
│ │ │ ├── memory_store.py # QdrantMemoryStore (sleep cycle / memory compression)
│ │ │ ├── economy.py # CentralBank: balances, ledger, passive cost, energy price, devaluation, subsidies
│ │ │ ├── inference_market.py # InferenceQuotaLedger: the scarce compute resource agents auction
│ │ │ ├── contracts.py # ContractRegistry (auto-escalates overdue PENDING contracts to the Judge)
│ │ │ ├── persistence.py # PostgresLedgerStore: periodic checkpoint/restore of balances and quotas
│ │ │ ├── agent_runtime.py # a single agent's Perceive/Think/Act cycle
│ │ │ ├── tick_engine.py # global clock, orchestrates every agent, the sleep cycle and trust
│ │ │ └── judge.py # Judge Agent: resolves disputed contracts using real transaction evidence
│ │ ├── api/v1/
│ │ │ ├── router.py # /health, /agents, /tick
│ │ │ ├── citizens.py # POST /citizens — register a new agent on the live TickEngine
│ │ │ ├── interventions.py # /interventions/devalue|subsidize|blackout|energy_shock
│ │ │ └── websocket.py # /ws/telemetry + TelemetryHub
│ │ └── main.py # FastAPI app factory + lifespan (seeds citizens, restores state, starts the TickEngine)
│ └── tests/ # pytest: economy, models, contracts, agent cycle, tick engine, judge, persistence, seeding, citizens API
└── frontend/
└── src/
├── components/
│ ├── phaser/ # GameCanvas.tsx + MainScene.ts + tileset.ts + mapConstants.ts + pathfinding.ts
│ └── dashboard/ # EconomyPanel, ActivityFeed, CitizenCensus, ConsciousnessInspector, DivineConsole, CitizenFoundry, HelpOverlay
├── hooks/useTelemetrySocket.ts # WebSocket with automatic reconnection
└── types/api.ts # contract shared with the backend
Requirements and installation
- Python 3.11+
- Node.js 20+
- Docker (for Redis, Qdrant and Postgres)
- Ollama running locally, with three models pulled: the citizen model (e.g.
ollama pull llama3.2), the judge model (ollama pull phi4), and the embedding model used by the sleep cycle (ollama pull nomic-embed-text) — missing the last one doesn't fail loudly, it just makes every agent'sperceivestep throw on/api/embeddingsand silently skipthink/actfor the rest of the tick (see Troubleshooting)
macOS / Linux
# Infrastructure
docker compose up -d
# Backend
cd backend
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
cp ../.env.example ../.env # adjust models/URLs if your setup differs from the defaults
# Frontend
cd ../frontend
npm install
Windows (PowerShell)
# Infrastructure
docker compose up -d
# Backend
cd backend
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -e ".[dev]"
copy ..\.env.example ..\.env # adjust models/URLs if your setup differs from the defaults
# Frontend
cd ..\frontend
npm install
On Windows you can also just use WSL2 and follow the macOS/Linux commands as-is.
Usage
macOS / Linux
# Backend (port 8000)
cd backend && source .venv/bin/activate
uvicorn enclave.main:app --reload --port 8000
# Frontend (port 5173, proxies /api to localhost:8000)
cd frontend && npm run dev
Windows (PowerShell)
# Backend (port 8000)
cd backend
.venv\Scripts\Activate.ps1
uvicorn enclave.main:app --reload --port 8000
# Frontend (port 5173, proxies /api to localhost:8000)
cd frontend
npm run dev
With both running, http://localhost:5173 shows "God Mode": the pixel-art city (markets, banks, the judicial hall, vector labs, signal towers, script workshops, a derivatives exchange and housing blocks spread across its districts) with the twelve seeded citizens walking around it, a corner minimap for quick navigation, the economic indicators panel, a live economic-activity feed (open offers, unresolved contracts, Judge rulings), a citizen census, and — once you select an agent — its Consciousness Inspector with inventory, trust links and a live reasoning feed. New citizens can be created on the fly from the "Fundición de ciudadanos" panel.
API contract
All endpoints live under /api/v1. Financial amounts (balance, unit_price, amount, penalty) are exposed as string to preserve decimal precision.
| Method | Route | Description |
|---|---|---|
GET | /health | Health check |
GET | /agents | Snapshot of every registered agent |
GET | /agents/{agent_id} | Snapshot of one agent (404 if it doesn't exist) |
GET | /tick | Current tick of the global clock |
POST | /citizens | {"display_name": "...", "personality": ["cautious", ...], "starting_balance": "120", "position": {"x": 0, "y": 0}} (balance and position optional) — registers a new citizen on the fly and broadcasts an immediate TickEvent; 409 on agent-id collision |
POST | /interventions/devalue | {"factor": "0.5"} — devalues every citizen's SimCoin |
POST | /interventions/subsidize | {"agent_id": "...", "amount": "100"} — prints SimCoin for an agent |
POST | /interventions/blackout | {"agent_id": "...", "quota": 0} — adjusts an agent's inference quota |
POST | /interventions/energy_shock | {"factor": "2.0"} — >1 scarcity, <1 abundance; shifts the base energy price with immediate effect |
WS | /ws/telemetry | Streams TickEvent (agent snapshot, macro indicators, open market offers, unresolved contracts and recent Judge rulings) on every tick |
Development
macOS / Linux
# Backend
cd backend && source .venv/bin/activate
pytest
ruff check .
mypy --strict src/
# Frontend
cd frontend
npm run typecheck
npm run lint
npm run build
Windows (PowerShell)
# Backend
cd backend
.venv\Scripts\Activate.ps1
pytest
ruff check .
mypy --strict src/
# Frontend
cd frontend
npm run typecheck
npm run lint
npm run build
Troubleshooting
- The telemetry WebSocket won't connect: check that the backend is running on the port
vite.config.tsexpects (8000by default), and that nothing else is bound to5173or8000. OllamaLLMBackendraisesLLMGenerationError: Ollama isn't running, the configured model (ENCLAVE_OLLAMA_MODEL) hasn't been pulled, or the model isn't returning valid JSON despiteformat: "json"— check the backend log, which includes the raw text the model returned. This is expected occasionally with smaller local models (e.g. llama3.2 sometimes deviates from the exact action schema); the tick engine logs it and moves on to the next agent instead of crashing.- An agent's position looks wrong on the map:
Positionenforces0 <= x < GRID_WIDTHand0 <= y < GRID_HEIGHT(must matchGRID_WIDTH/GRID_HEIGHTinfrontend/src/components/phaser/mapConstants.ts); a move outside those bounds is rejected as a malformed action rather than silently accepted. - Citizens never show a reasoning log, inventory or trust links, and the backend logs "attempted an economically invalid action" every tick with an
httpx.HTTPStatusError: 404on/api/embeddings:nomic-embed-textisn't pulled in Ollama (ollama pull nomic-embed-text).QdrantMemoryStore.retrieve_relevant_memoriesis called fromAgentRuntime.perceive, beforethink/actrun, so a missing embedding model aborts the whole tick for that agent — it never gets to move, trade or reason, not just to sleep. This is easy to hit when Ollama already had the model pulled on one machine (from earlier testing) but not on a fresh one. - Qdrant logs version-compatibility warnings: informational notice from
qdrant-clientwhen it can't verify the server version; it doesn't block startup. mypy --strictfails onnumpystubs:numpyarrives as a transitive dependency ofqdrant-client;pyproject.tomlpinspython_version = "3.12"in the mypy config specifically to avoid a syntax conflict between its stubs and strict mode.- Backend fails to start with a Postgres connection error: make sure
docker compose up -dincludes thepostgresservice and it's healthy (docker compose ps); checkENCLAVE_POSTGRES_DSNmatches the credentials indocker-compose.ymlif you changed them. - A restarted process shows a citizen's seed balance instead of what it had before: the checkpoint only happens once per simulated day (
ticks_per_dayticks) and on a clean shutdown — an ungraceful kill (kill -9, a crashed container) between checkpoints loses at most one day of state, by design (seepersistence.py).
Roadmap
Natural next steps if the project keeps growing: a real evidentiary link between a signed contract's terms and the specific transactions meant to fulfill it (today the Judge reasons over all transaction history between the two parties, not a scoped set tied to that one contract), and letting citizens produce and trade the other tradable asset types (vector_pack, code_script, etc.) beyond inference quota, which today only move SimCoin without transferring anything back. The new lab/signal-tower/workshop/exchange buildings are visual flavor tied to those asset types for now, not yet gated behind citizen actions the way the market and bank are.
License
MIT — see LICENSE.