Spaces:
Running
A newer version of the Gradio SDK is available: 6.20.0
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)
- Curator reads parent + full branch history (+ research grounding) → child
WorldState - Verifier audits child vs branch; orchestrator regenerates up to
max_regentimes - Voice tool synthesizes the child's spoken line (pitch nudged by valence)
- Screenwriter plans the child's next two forks
- 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
MockLLMand the mock tools working at all times.python -m echo.smoke_testmust 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 anyechomodule must stay cheap and dependency-free. - Never import
orchestratorfromcore/__init__.py. It pulls in agents + tools, andtools/voice.pyimportscore.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 touchtransformersdirectly. 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
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
# 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())
"