# Thousand Token Wood — Build Plan Build Small Hackathon (Gradio + Hugging Face). Track: An Adventure in Thousand Token Wood. Hack window June 5-15 2026. Build effort: both weekends full (June 6-8, 13-15). Weekday prep floor untouched. ## Concept A tiny emergent economy. 4-6 woodland creatures, each a small-model agent (<=32B), trade goods, set prices, gossip, hoard, and panic. The player pokes the economy (shocks, rumors, taxes) and watches bubbles, crashes, and a widening wealth gap emerge. Nobody scripts the drama; the agents do it. ## Architecture - **Model**: Qwen2.5-7B-Instruct (decide 7B vs 3B after smoke test; 7B for tool-call reliability). - **Serving**: Modal GPU-backed endpoint (vLLM for throughput across many agent calls). Targets the separate $20K Modal prize category. - **Frontend**: Gradio app hosted as an HF Space under the build-small-hackathon org, on cheap CPU, calling the Modal endpoint. - **Why small is load-bearing**: a living economy needs many agents x many turns; frontier models are too expensive/slow for that. Small local models make the simulation feasible. That is the thesis. ## Acceptance criteria (definition of done) - [ ] Public HF Space under the org, runs without crashing. - [ ] >=4 creature-agents trade autonomously over >=20 turns. - [ ] Player can trigger >=2 shock types (e.g. drought, rumor) and the economy visibly reacts. - [ ] UI shows: town square, narrated event ticker, price + wealth(Gini) charts, clickable per-agent reasoning trace. - [ ] Seeded reproducible "demo run" that reliably produces a fun arc (bubble or panic). - [ ] Demo video recorded + social post written + submitted by June 15. - [ ] Bonus badges: Sharing is Caring (published agent traces) + Field Notes (blog post). ## Milestones ### Pre-weekend (this week) — de-risk - [x] Claim Modal ($250) + HF ($20) credits. - [x] Install modal + gradio, authenticate Modal CLI. - [x] GPU smoke test: small model loads + generates one in-character line on Modal. PASS (Qwen2.5-7B on L4; Fenn the fox produced a coherent in-character buy-the-dip decision). Cold start ~3-4 min (build 80s + model download 76s + load 38s) -> use vLLM + keep-warm for the live demo. - [x] Sign off on this plan (go given 2026-06-05, hack window open). ### Weekend 1 (June 6-8) — the engine - [x] World model: goods, currency (pebbles), inventories, needs, wealth. (ttw/world.py) - [x] Market engine: offer book, double-auction matching, price updates, consumption — deterministic, tested with dummy random agents first (no LLM). (ttw/market.py, ttw/sim.py, ttw/dummy.py) Reviewer-loop: 2 blocking bugs found + fixed (matching retirement logic; Infinity/NaN crash), re-reviewed APPROVED. 9 tests pass. Random sim runs 12 turns, valid, emergent (Pip squeezed). - [x] Action schema (offers + gossip) + JSON parse-and-repair loop so a small model can't break the sim. (ttw/actions.py) - [x] vLLM serving layer on Modal (serve.py, app "ttw-serve" deployed; L4, model cache volume, keep-warm 300s). vLLM 0.6.6 loads cleanly with transformers 4.50.3 (resolver pick) -- no conflict. - [x] Wire real small-model agent emitting valid actions (ttw/agents.py drop-in Policy; ttw/llm.py client). Batches all creatures into ONE GPU call/turn, memoized by turn; keeps raw text for traces. - [x] Scale to 5 agents on the real model (scripts/run_llm.py). RELIABILITY: 3B = 100% valid JSON, 97% valid offer over 30 calls. MODEL LOCKED: Qwen2.5-3B (Tiny Titan eligible). ### economic-pressure tuning -- ITERATION 1 DONE (big progress) Added scarcity mechanics: diet variety (MAX_FOOD_PER_GOOD), spoilage (SPOIL_RATE), rising winter fuel need (fuel_need), production drought multipliers, and player shocks (ttw/shocks.py: drought, rumor, gold_rush, harvest). Strengthened agent prompt. 14 tests pass. 15-turn LLM run (model 3B, reliability 100%/100% over 75 calls) RESULTS: - WORKS: sustained trade across all turns; planted rumor PROPAGATED organically (creatures repeated + embellished it); firewood panic on turn 11 (price 5->7); Gini climbed 0.18->0.41; Pip (woodcutter) ended richest. The "plant rumor -> panic -> price spike -> winner" demo arc emerged unscripted. - BROKEN, fix next: - [ ] TOO LETHAL: all 5 creatures ended wellbeing=0 (starved/froze). Scarcity outpaces trade speed. Soften: lower food_need (3->2) and/or raise MAX_FOOD_PER_GOOD, slow FUEL_RAMP_EVERY, maybe a starting buffer. Target: most survive a 20-turn run, 1-2 may collapse for drama. - [ ] STICKY PRICES: reference prices barely move on the chart because the prompt shows agents the exact reference price and they quote it. Stop spoon-feeding exact prices (show a band, or last trade only) so supply/demand moves prices visibly for the chart. - [ ] last_price reflects only the final trade of a turn; consider volume-weighted or high/low capture for the price chart. ### economic-pressure tuning -- ITERATION 2 (softened scarcity) -- DID NOT FIX Lowered food_need 3->2, FUEL_RAMP 5->8, SPOIL 0.25->0.20, added starting buffers. Result: trade got QUIETER and all 5 still ended wellbeing=0. Root causes found (deeper than balance numbers): - Wellbeing is a one-way ratchet: any food OR fuel shortfall subtracts, recovery is only +1/turn and only when perfectly provisioned -> chronic small shortfalls grind everyone to 0. It's a death clock, not a mood. - 3B trades suboptimally: agents buy their OWN product (Mossback produces acorns yet bid to BUY acorns) and don't reliably buy what they lack. Valid JSON, weak economic judgment (small-model limitation). - The rumor is so salient it crowds out survival trades (turns 7-15 mostly gossip, little trading). ### DESIGN FORK (needs Lester's call): survival game vs economic-drama sandbox The delightful, working parts are the ECONOMIC drama (rumor spread, firewood panic, Gini divergence, Pip getting rich). The survival/death layer fights it. PROPOSED: reframe as a sandbox -- - [ ] Make wellbeing a real-time MOOD = f(current food+fuel provisioning), mean-reverting, floored so nobody death-spirals. Stakes live in pebbles/prices/status, not starvation. - [ ] Prompt fix: "you PRODUCE {x}; never buy {x}; buy the OTHER foods you are low on." Add a 1-shot example of a good move. Consider 7B if 3B judgment stays weak (would cost Tiny Titan eligibility). - [ ] Optionally keep mild stakes: only a genuinely broke/empty creature visibly suffers (drama), not all. ### economic-pressure tuning -- ITERATION 3 (sandbox redesign) -- SUCCESS Reframed wellbeing as a mean-reverting MOOD (_update_mood, range [4,10], no death spiral) + prompt fix ("you produce X, never buy X; buy what you lack" + 1-shot example + computed lacking list). 15 tests pass. 15-turn LLM run (3B, reliability 100%/100%): - Nobody dies (wellbeing 6-7). Trade robust (4-9/turn, never silent). Self-buying bug GONE (Mossback sells acorns, buys what it lacks). Roles emerged: Pip (firewood) richest net 387; Bramble (hoarder) broke net 25. Gini 0.14->0.38. Rumor propagates. This is the demo-worthy economy. ### LAST ECONOMY GAP: price dynamics -- SOLVED + reviewer-APPROVED drift_prices (ttw/market.py) nudges reference price by RESIDUAL (post-match) supply/demand imbalance. Live 15-turn run: prices now tell stories -- honey CRASHED 10->3 (Oona liquidated her hoard during the "Run on Oona's Hoard" legend), mushrooms 5->2 (Fenn dumped), firewood BOOMED 4->7 (winter scarcity). Wood Legends fired and integrated narratively (the bank-run legend literally caused Oona to dump honey -> honey price crash). Reliability 100%/100%. Gini 0.13->0.32. Nobody died (wellbeing 4-9). Pip richest. THE ECONOMY IS DEMO-COMPLETE. - Minor cosmetic backlog: rumor field drifts into verbose self-narration instead of punchy gossip; constrain gossip in the prompt later (not blocking). ### Wood Legends event deck (ttw/events.py) -- BUILT, unit-tested (Lester's idea 06-05) Famous market manias/panics reskinned as random woodland chapters, composed from shocks.py. 6 events (Tulip/Acorn Mania, South Sea/Hollow Log Co, 1929 run/Oona's Hoard, 2020 TP/Firewood Panic, Hunt silver/Honey Corner, Dust Bowl/Long Frost). Each has real `inspired_by` for a UI reveal + Field Notes + allocator-credibility tie-in. EventDeck draws w/o repeat until exhausted. 17 tests pass. - [ ] Wire EventDeck into the sim/run loop (fire a legend every N turns or on a player "Tempt Fate" button) and validate live on 3B. Not yet LLM-tested. - Tone guardrail set in code: economic history only, no real tragedies/living people. ### Weekend 2 -- Gradio UI -- BUILT + reviewer-APPROVED (2026-06-05) - [x] ttw/narrate.py (event -> ticker prose), ttw/game.py (Game wrapper: history, step/shock/tempt_fate, pandas frames for charts/town, markdown for ticker/traces), app.py (Gradio Blocks). - [x] UI: town square (Dataframe), price LinePlot + Gini LinePlot, news ticker, reasoning-traces accordion, controls (Step, Auto-run via gr.Timer, Tempt Fate, Gold rush, Drought, Plant rumor, Reset). - [x] TTW_DUMMY=1 = no-GPU dummy-policy mode for local dev; default = ModalLLM live. - [x] 25 tests pass (18 engine + 7 game). App builds, callbacks run, server HTTP 200. Reviewer APPROVED (fixed gr.Timer interval on toggle; verified make_llm_policy signature for live path). - Gradio 6 notes: theme -> launch(), no overlay_point on LinePlot. ### REMAINING to submit (by Jun 15) - [ ] Deploy as HF Space under build-small-hackathon org (Gradio on CPU calling Modal endpoint). Needs: requirements.txt, README.md w/ Space metadata, modal token as a Space secret, git push. Verify live: does the Space reach the deployed ttw-serve Modal app? (auth across HF<->Modal.) - [ ] Visual QA in browser (charts render, traces readable, buttons wired) -- Lester's eyes. - [ ] Optional polish: tighten gossip prompt; Off-Brand (gr.Server custom UI) for Bonus Quest stack. - [ ] Lester's hands: demo video (honey-crash legend chain = money shot), Field Notes blog, social post, SUBMIT. ### Known minor items (not blocking) - _extract_json brace scanner doesn't skip braces inside JSON string literals. Future hardening. - 3B occasionally rambles/repeats (Bramble "bits of fabric and bits of fabric"). Cosmetic; cap tokens. - _extract_json brace scanner doesn't skip braces inside JSON string literals (e.g. gossip with "}"). Future hardening; harmless today. ### Weekend 2 (June 13-15) — make it sing - [ ] Gradio UI: town square, event ticker, price/Gini charts, per-agent reasoning expander. - [ ] Player controls: shock buttons (drought, frost, gold rush), rumor text input, step/auto-run. - [ ] Personas + per-agent memory (short-term trades + long-term grudges/relationships). - [ ] Narrator layer: structured events -> charming prose. - [ ] Seed + tune a reproducible demo run with a clean dramatic arc. - [ ] Deploy Space, confirm public URL works. - [ ] Record demo video, write Field Notes post, publish traces, write social post, SUBMIT. ## Stretch (only if ahead) - [ ] Off the Grid badge (fully local llama.cpp fallback path). - [ ] Small-vs-bigger-model behavior comparison for the blog post. - [ ] More creatures / more goods / seasons. ## Risks - Modal cold-start latency on the demo — mitigate with keep-warm container during recording. - Small-model invalid actions — the parse-and-repair loop is the critical reliability primitive. - Scope creep into "real economics" — keep it a toy; drama > accuracy. - ZeroGPU/queue not used; serving is on Modal credits ($250 budget, watch burn). ### Well-Tuned fine-tune pipeline Self-contained fine-tuning pipeline under `scripts/finetune/` targeting the Well-Tuned badge (a fine-tuned model published on Hugging Face). Built 2026-06-06. - `build_sft_dataset.py`: traces (`--from-file` default repo `traces.jsonl`, or `--generate` via ModalLLM over seeded decks) -> `sft.jsonl` chat-messages format. Quality filter parses each row with `_extract_json` / `parse_actions`, drops invalid JSON, strips self-buy offers (buying the good a creature produces, derived from `seed_world()`), and re-serializes a canonical compact target. Prints kept/dropped tallies; optional `--push` to an HF dataset. Runs locally, no GPU. - `train_modal.py`: Modal app, LoRA fine-tune (r=16/alpha=32/dropout=0.05 on attn proj) of a tiny base (default `Qwen/Qwen2.5-0.5B-Instruct`, 1.5B overridable) via TRL SFTTrainer, merge, push merged model to the Hub for serving via `TTW_MODEL`. Pinned training stack (transformers 4.46 / trl 0.12 / peft 0.13 / datasets 3.1 / accelerate 1.1). Needs Modal + GPU; not run here. - `eval_model.py`: reliability harness (valid_json_pct, valid_offer_pct, self_buy_rate, avg_offers_per_turn) over real agent prompts. Backends modal/local/dummy; `--backend dummy` verifies harness logic with no network/GPU. - `README.md`: step-by-step (build -> train -> eval -> serve -> Field Notes), GPU/local split. - Verified: ruff clean, syntax OK, fixture build (valid kept / self-buy dropped / invalid dropped), dummy eval runs (self_buy_rate 0.0%), full suite 28 passed. ## Review (to be filled at the end) --- # v4 — "The Wood Fights Back" (adversarial adaptation) + UI causation polish Branch `v4-wood-fights-back`. Post-v3 arc. v3 thesis: *you cannot steer agents with input shocks, so author the crash at the settlement seam* (the gambit reliably pays +40). v4 inverts it: **the agents you keep burning learn to stand on the other side of your authored crash, and the same gambit decays toward zero.** The lesson is adverse selection — a repeatable manipulation is a self-destroying strategy because the counterparties price you in. ## The mechanic (deterministic spine, LLM texture on top) Builds entirely on state that already exists. No new creature fields. The load-bearing signal is the per-creature `sentiment toward "Patron"` (already soured -4 by a resolved false tip via `tipped_false`, -3 by `shorted_my_good`; already injected to prompts as buckets via `relationships.summary_line`). We just *read* it and give it teeth. Define a creature as **wary of the Patron** at `sentiment(Patron) <= WARY_THRESHOLD` (reuse `REL_HOSTILE_THRESHOLD = -5`, or a new `WARY_THRESHOLD = -3` — architect's call; -3 = one false tip flips them, -5 = two strikes). Three effects, in order of importance: 1. **Wood resistance blunts the gambit (THE TEETH — at the settlement seam, sim block 3b).** When a legend crashes its good, soften the crash in proportion to how many wary creatures are positioned to counter it (wary AND produce/hold the legend good). Define `resistance = min(MAX_RESIST, n_wary_counterparties * RESIST_PER)` in [0,1] and apply `effective_crash = crash + (1 - crash) * resistance` BEFORE overwriting `world.last_price` and BEFORE settling shorts. As the wood wises up, the price falls less, so the short pays less — the same +40 gambit pays +10, then ~0, then can lose. Policy-independent, measurable in P&L, sits at the exact seam v3 established. Emit a `wood_braces` event carrying the resistance level. 2. **Informant heat (the wood talks to Heron — new block, after settlement).** Each turn, add heat proportional to the count of wary creatures (`heat += n_wary * INFORMANT_HEAT_PER`, small, e.g. 3). Lying now has a standing cost: burned creatures accelerate the inquiry you already have. Emit `wood_testifies` event when it fires. This is what makes heat climb toward Heron's 70 threshold *because* of your manipulation history, not just per-move heat. 3. **LLM texture (lower stakes; spine carries the result).** Wary creatures already see "you feel bitterly toward Patron" in-prompt. When a short's "Word spreads…" context reaches a wary creature, optionally append a nudge so emergence *can* echo the counter-trade. Per the v3 lesson, the deterministic seam (effect 1) carries the outcome; this only adds flavor. Skippable if it risks the firewall or the budget. ### Truth firewall (unchanged, must be preserved) `tip.truth` / `legend_key` still never enter any prompt or user-facing view. Wariness is derived from *already-settled* sentiment, so no new leak path. Reviewer must confirm `summary_line`'s banned-substring guard still holds and no new code surfaces truth. ## UI causation polish (the Facade-of-Jade borrow + v4 surfacing) The point of Facade's "Drama Engine" panel: state visibly *moves on your action*. Apply it to make the v4 mechanic legible — the player should watch their expected gambit profit shrink as they burn the wood. 1. **Exposure / expected-payoff meter (new view).** A live panel for the open gambit: short exposure, **expected payoff if the legend fires now** (uses current price + the live wood- resistance factor), and a **"wood wariness" gauge** (how many creatures are wary of the Patron). This is the causation made visible: queue a false tip, sour a creature, watch expected payoff drop. Surface in the patron console. 2. **React on the queued move.** The exposure meter updates immediately when a short/tip is queued (before settlement), so cause→effect is felt, not just read. 3. **Hierarchy (light touch).** Keep the one-click gambit preset as the dominant CTA; demote the mode-picker to "advanced." No frontend-stack change — stay on Gradio Blocks. ## Files in scope (architect to confirm/refine) - `ttw/events.py` or a constants module — `WARY_THRESHOLD`, `MAX_RESIST`, `RESIST_PER`, `INFORMANT_HEAT_PER`. - `ttw/relationships.py` — helper: count creatures wary of the Patron / wary-and-holding-good. - `ttw/sim.py` — block 3b: apply wood-resistance to crash + short payoff; new informant-heat block; new event types in the log whitelist. - `ttw/narrate.py` — `wood_braces`, `wood_testifies` ticker lines. - `ttw/game.py` — `exposure_markdown()`/`exposure_html()` view + wood-wariness gauge; widen `_views`. - `app.py` — add the new view (14→15), output arity (17→18), wire ALL handlers (do_watch/do_step/do_tempt/do_move/do_preset) + replay tail; **the delicate arity migration — new view goes BEFORE the mode_pill/raw_md tail so `replay.frame_to_views` width stays consistent.** - `scripts/record_attract.py` + `data/attract_run.json` — re-record an arc that SHOWS the decay: gambit pays early, wood wises up, a later identical gambit is blunted. - `tests/` — arity lock (now 18), wood-resistance softening, informant heat, firewall preserved, exposure-view content. Reuse the v3 `EventDeck(seed, events=[...])` forcing pattern. ## Acceptance criteria (v4) - [ ] A repeated short-into-legend gambit pays strictly LESS once N creatures are wary (assert P&L monotonically decreases with wariness in a forced-deck test). - [ ] Informant heat reaches Heron's threshold faster with more wary creatures (asserted). - [ ] Truth firewall preserved (no `truth`/`legend_key`/banned substrings in any prompt or view; test). - [ ] Exposure meter shows expected payoff + wariness and updates on a queued move. - [ ] Output arity is internally consistent across every handler (arity test green); replay width OK. - [ ] Re-recorded attract run shows the decay arc; content constraints clean (0 emoji-in-notes etc.). - [ ] Full suite green; ruff clean; Space boots. - [ ] reviewer APPROVED after each fix round; QA PASS before any deploy. ## Process architect -> implementer (stepwise) -> reviewer loop (targeted, per fix round) -> QA. NOT deployed to the Space until the user approves the re-record + the Modal spend (their gated call), per the standing deploy gate. Field Notes v4 written after QA PASS.