# CLAUDE.md Guidance for working on **The Echo** — an agentic tree of the lives you didn't live. ## What this project is The Echo takes one fork in a person's life ("I left Brazil instead of staying") and grows a **tree of alternate selves**. Each node is a coherent alternate life; each branch is a dramatic turning point. The user navigates the tree, and each echo speaks back in a subtly altered version of their own voice. It is built for the **Thousand Token Wood** track of a small-model hackathon (all models ≤32B, Gradio + HF Spaces, demo video + social post). The award surface it targets: Thousand Token Wood (delight), Best Agent (true multi-agent + tools), Off-Brand (the living tree UI), Best Demo (voice + emotional reveal), and Tiny Titan (if a ≤4B model holds coherence). **This is an agentic inference system, not an RL project.** There is no training loop, no reward, no policy gradient. The "intelligence" is three coordinated agents using a frozen LLM zero-shot. Do not add RL unless explicitly asked. ## The core idea that must never break The single hardest, most important property: **"the same you" stays recognizable across every branch.** A child life is always generated as a *causal consequence* of its parent's `WorldState` — never invented from scratch. Age math must add up, relationships and dependents must persist or change for a reason, carried losses must echo forward. The `Verifier` exists to enforce this. If a change would let branches drift into unrelated people, it is wrong. ## Architecture (dependency order — leaves first) ``` core/world_state.py data: WorldState, LifeFacts, EmotionalTone (no deps) core/tree.py LifeTree: branching graph + history (-> world_state) llm/client.py LLMClient; MockLLM (offline) + LocalLLM (HF model) tools/research.py world-grounding (real place/era detail) (-> world_state) tools/voice.py TTS; pitch shifts by emotional valence (-> world_state) agents/base.py thin Agent base (-> llm) agents/curator.py grows child life from parent (causal) (-> base, world_state) agents/screenwriter.py plans the next two dramatic forks (-> base, world_state) agents/verifier.py audits branch coherence; triggers regen (-> base, world_state) core/orchestrator.py conductor: curator→verifier loop, voice, plan (-> everything) smoke_test.py full pipeline with mocks, no GPU (-> everything) ``` When rewriting files, follow this same order: leaves before roots, so the project never sits in a broken state. ## The agentic loop (one user choice → one new node) 1. **Curator** reads parent + full branch history (+ research grounding) → child `WorldState` 2. **Verifier** audits child vs branch; orchestrator regenerates up to `max_regen` times 3. **Voice tool** synthesizes the child's spoken line (pitch nudged by valence) 4. **Screenwriter** plans the child's next two forks 5. node added to `LifeTree` The orchestrator is UI-agnostic. The (future) Gradio app calls `seed()` once, then `choose_fork(node_id, i)` per click, and renders `graph()`. ## Hard rules - **Keep `MockLLM` and the mock tools working at all times.** `python -m echo.smoke_test` must pass with zero ML dependencies installed. This is the project's correctness gate. Every change is validated here first. - **Lazy-import heavy deps.** `torch`, `transformers`, `peft`, `bitsandbytes`, TTS backends are imported *inside* the methods that need them, never at module top level. Importing any `echo` module must stay cheap and dependency-free. - **Never import `orchestrator` from `core/__init__.py`.** It pulls in agents + tools, and `tools/voice.py` imports `core.world_state` — importing the orchestrator at package load creates a circular import. Import it explicitly: `from echo.core.orchestrator import Orchestrator`. - **Agents stay single-purpose and thin.** One responsibility each. This is what makes the ≤4B (Tiny Titan) path viable — small focused calls instead of one giant generation. Don't merge agents. - **All model access goes through `LLMClient`.** Agents never touch `transformers` directly. Swapping 14B ↔ 3B must remain a one-line config change. - **Favor concrete, mundane, specific detail over vague profundity** in any prompt or generated content. Specificity is what makes a life feel real; vagueness is the main failure mode. ## Quantization & LoRA (planned, on the `LocalLLM`) These are the next technical additions and belong only in `llm/client.py`: - **4-bit quantization** via `BitsAndBytesConfig` (bitsandbytes): `load_in_4bit`, NF4, double quantization. This is what lets a 14B fit on a free-tier GPU and makes the honest small-model fit real. - **Optional LoRA adapters** for light *supervised* fine-tuning of an agent (e.g. teaching the Curator format + tone). This is SFT, not RL. Per-agent adapters are loadable but training is out of scope unless asked. The `MockLLM` must remain untouched by either — it stays the offline test path. ## Tiny Titan experiment The orchestrator tracks `GrowthStats.regen_rate` (regenerations / nodes grown). This is the metric that decides 14B vs ≤4B: run the same pipeline on both and compare how often each model breaks branch coherence. A ≤4B model with a low regeneration rate wins the honest-fit argument and the Tiny Titan award. ## Tone & safety of the experience Dramatic forks touch real emotion (loss, regret). The framing must be **wonder, not regret** — "look how many vibrant lives lived inside one choice," never "look what you lost." The closing artifact (`final_map_summary`) deliberately ends with gentleness ("none of them are more real than the one you're in"). Keep that spirit in any new generated content. This is a toy that should leave people lighter, not heavier. ## How to verify any change ```bash python -m echo.smoke_test # must print "ALL STAGES PASSED ✓" ``` If that passes, the agentic loop, tree, tools, and serialization are all intact. Only after that should you test with a real model. ## Commands ```bash # offline correctness gate (no GPU, no ML deps) python -m echo.smoke_test # real model run (requires: pip install -r requirements.txt) python -c " from echo.llm.client import LocalLLM, LLMConfig from echo.tools.research import MockResearch from echo.tools.voice import MockVoice from echo.core.orchestrator import Orchestrator llm = LocalLLM(LLMConfig(model_name='Qwen/Qwen2.5-3B-Instruct')); llm.load() orch = Orchestrator(llm, MockResearch(), MockVoice()) root = orch.seed('I stayed instead of moving abroad', base_age=24) print(root.facts.constraints_text()) " ```