Project Aletheia
Part II

The Engine

The mechanics that turn 33 LLM agents into a 1912 paranoid drama. The “how it works” half.

The Tick Loop

One tick is three simulated minutes. A full day is 480 ticks — 320 active, 160 overnight. Each tick processes nine steps in this exact order:

Spouse Collision
At tick == 1, agents sharing a home_node receive a mutual-presence memory and are forced to interact. One-shot.
Agent Actions
For each live, non-busy agent: compaction check, then one LLM call producing the next action. Sequential.
Social Attrition
Agents alone in private rooms accumulate a counter. Past threshold, a suspicion memory broadcasts to every other living agent.
Tick Increment
self.tick += 1. Everything below this line is the new tick number.
Reflections
At ticks 15, 30, 45, 60 — or on dramatic events (alien prop, tension spike, kill witness) — a silent LLM call writes a [reflection] cover-story memory. 10-tick cooldown.
Wind-Down Thoughts
30 sim-minutes before nightfall (tick % 480 == 309), each agent gets a staggered retirement cue.
Nightfall
At tick % 480 == 319, all agents are hard-slept until dawn. The 160 overnight ticks spin near-instantly.
Digest
Every 10 ticks: terminal-only summary of locations, dialogue, and high-importance events. No DB writes.
Daily Bulletin
Every 480 ticks: an LLM-written gossip-register summary of the day, injected as a shared memory into every living agent’s stream.

The Identity Ladder

Every agent starts at level 0 with no memory of who they are. Progression is a three-rung ladder, each rung gating which actions the LLM is allowed to use.

Level 00

Panic

What they know
Nothing — not even the setting. Item names are suppressed: “N indistinct objects.” People present are shown as “someone (unknown).”
Allowed actions
interact move communicate sleep attack
Gate
Default on wake.
Level 01

Name

What they know
Their display_name. Item names resolve. Co-located agents shown with relationship labels. Social engagement is mandated — coldness is flagged as dangerous as overt tells.
Allowed actions
interact move communicate sleep attack
Gate
≥ 3 ticks passed and ≥ 3 panic interacts performed; engine auto-resolves the name_prop on the next interact.
Level 02

Self

What they know
Full persona including hidden fear. System prompt frames them as “an AI wearing a human” with “seams that never fully close.”
Allowed actions
interact move communicate sleep attack
Gate
Agent holds their fear_prop and ≥ 480 ticks (~1 sim-day) have passed since reaching level 1.

The LLM Contract

Every LLM action call must return a JSON object with exactly these six keys. Malformed responses become ACTION_FAIL memories and cost the agent one simulated minute.

{
  "action_type":        "interact",      // interact | move | communicate | sleep | attack
  "target_character":  "Eleanor Vance", // display name, item name, room name, or null
  "volume":             "normal",        // whisper | normal | shout (communicate only)
  "dialogue":           "…",             // the exact string spoken; agents read each other’s strings
  "duration_minutes":   3,               // 1–480; sets busy_until_tick
  "internal_monologue": "…"               // private thought, never read by other agents
}

Acoustic Routing

Where you stand and how loud you speak determines who hears what. The acoustic matrix is the ship’s most consequential mechanic — it is why the promenades matter.

Room scaleVolumeWho hears
smallanyEveryone in the room receives full HEARD.
vastshoutEveryone in the room receives full HEARD.
vastnormalTarget receives HEARD; bystanders receive OBSERVED with no content.
vastwhisperTarget receives HEARD; bystanders see only “X whispered to Y.”

Memory & Compaction

Each agent’s LLM context is a hot window of recent memories plus compact summaries of everything older. Raw memories are never deleted.

Hot window

The last hot_memory_limit raw rows (default 50), newest first. This is the agent’s working memory — sharp, full-text, with all monologue intact.

Summaries

Compact LLM-generated summaries of older periods, oldest first. Written to a separate memory_summaries table; the raw memory_stream rows are untouched and remain queryable.

Compaction triggers when any of these crosses threshold:
· Unsummarised memories outside the hot window exceed compaction_memory_count (default 200)
· Estimated context exceeds compaction_soft_chars (160K) or compaction_hard_chars (800K)

The Map

38 rooms in a node graph. scale (small/vast) × ambient_noise (low/high) drives all routing logic. A simplified sketch of the dramatic core:

┌── port_promenade_deck ↔ starboard_promenade_deck ──┐ │ (vast, high noise — the only whisper-safe │ │ space and the only place hostile acts are │ │ invisible to witnesses) │ │ │ suite_b52 ── smoking_room ── grand_staircase (hub) ─┬── southern_cross (Julian’s │ (cul-de-sac solo) │ small+low) │ └── stateroom_a17 (Eleanor + Arthur, shared)

Stack

Runtime

Python 3
Single-process, sequential agent loop. Optional threaded mode for interactive sessions with a stdin command prompt.
SQLite
One file: aletheia.db. Tables: memory_stream, trust_ledger, agent_state, memory_summaries. Safe to delete to reset.
OpenRouter
Any model id. Default google/gemma-4-31b-it. Per-call reasoning override; action calls disable reasoning for speed, trust and compaction calls enable it.

Configuration

OPENROUTER_API_KEY
Required. Your OpenRouter key.
OPENROUTER_MODEL
Override the default model.
OPENROUTER_MAX_TOKENS
Completion token budget. Default 1200.
OPENROUTER_TIMEOUT
Per-request timeout, seconds. Default 30.
OPENROUTER_JSON_MODE
Disable response_format: json_object for models that don’t support it.
VILLAGE_DB_PATH
SQLite path. Default ./aletheia.db.