Skip to main content

town

Experiments in out-of-distribution agent harnesses. One entity, one tool, a context that is never a transcript and never stops being rewritten, and a town it can grow into when the task is too big for one mind.

The two seeds:

  • prime-style — the agent gets exactly one tool, python. Everything else is code.
  • headlong-style — the agent never waits for a user turn; it thinks continuously, and its thinking is text in its own context that it can read, edit, and forget.

Then we stop designing around prompt caching, which unlocks the actual experiment: the harness lives inside the agent's Python namespace, so the entity reshapes its own context, forks, splits, merges, clusters, rewinds, compresses and recalls itself by executing code against itself. And when a task is the size of "build a browser", the same code forms orgs, opens message boards, puts work on a ledger, and hires workers that pull it under lock.

uvx town                     # first run: interactive setup (API base URL + key, validated, saved to ~/.config/town)
town auth status             # what is in effect; `town auth` to change it
town                         # open the chat here; your first line is the task
town run "Find out which stdlib module has the most public names. Tell me once."
town chat "Keep me company while I debug; I'll paste things." -v
town -c                      # continue the most recent session that worked in this folder
town -r                      # pick any session to resume (offers to cd to its folder first)
town sessions                # list them
town preset list             # built-in experiments; `town preset lang -v` builds a language
town viewer --open           # local web server over every session in ~/.town: index + trajectory viewer per session
town viz                     # one-off HTML viewer for this folder's latest session (--all: every session from here; or pass trace files)
town replay --speed 20       # play this folder's latest session through the live TUI (or pass a trace file)
town watch                   # plain live view of this folder's latest session, in a second terminal

Install with uv tool install town or pip install town; from a checkout, pip install -e .. Configuration precedence: environment (TOWN_API_KEY, TOWN_BASE_URL, TOWN_MODEL, ...), then a .env in the working directory, then ~/.config/town/config.toml written by town auth. The base URL is Anthropic's API by default; a LiteLLM or similar gateway works with its own key. The town works in the current directory (that is WORKSPACE for the citizens); its record lives in a session under ~/.town/sessions/<id>/ (TOWN_HOME overrides): trace.jsonl (every event), state.json (a snapshot of the whole town: every citizen's context, boards, ledger, districts, memory, kv — written every few seconds and on exit) and meta.json.

town -c continues the latest session from this folder; town -r opens a picker over all sessions and, if the one you choose worked elsewhere, offers to cd there first. A resumed town comes back in chat mode: citizens that were live pick up where they stopped (with a fresh step budget), a finished root revives on your next line, and the timeline in the TUI continues from where it left off.

On a terminal, run, chat and preset open the live TUI (below); --plain keeps the old stream of say() lines on stdout, which is also what you get when output is not a terminal. In plain mode -v streams the entity's internal thoughts, code and results to stderr; without it the human sees only what the entity chooses to say().

The model of the thing

Context is a document, not a transcript

A cell's context is an ordered list of blocks (task, user, thought, think, code, result, note, msg, recall, stub, merge, system). Every step, the whole list is re-rendered into a single user message and sent as a fresh single-turn request. Nothing is ever replayed, so nothing has to stay put: any block can be dropped, rewritten, pinned, or compressed between two steps. The last few steps (tail_turns, default 4) are the exception: they are replayed behind the document as a real transcript, signed thinking blocks included, so the model's most recent reasoning is verbatim. Older steps live on as blocks, with the thinking summary folded in as a think block, i.e. the inner voice becomes editable memory. Rewriting the document in front of a replayed tail is accepted by this gateway; on an account that enforces preserved thinking the harness strips the thinking blocks and retries once.

Each block carries a salience 1–5. Salience 5 is pinned. When the context passes its token budget the oldest, lowest-salience blocks are compressed into an engram in the memory store and replaced by a one-line stub that names the engram, so the cell can recall it later.

The only tool is python, and the harness is in the namespace

ctx.blocks() / ctx.get(id) / ctx.forget(*ids) / ctx.pin(*ids) / ctx.rewrite(id, text)
ctx.note(text) / ctx.compress(*ids, hint=...) / ctx.focus(text)
ctx.history() / ctx.rewind(step, intent=..., keep=[ids], namespace=False)   # go back, on the record
mem.recall(query, k) / mem.get(eid) / mem.list() / mem.store(text, tags, summary)
fork(focus_or_list, carry="all"|"summary"|[ids], n=1, at=step)   # copy yourself (or a past self), focused
split(foci=None, n=2)                                      # partition yourself by topic
merge(*ids, how="synthesize", wait=True)                   # fold cells into yourself
cluster()                                                  # group + aggregate all live cells
wait(*ids) / send(id, text) / broadcast(text) / cells() / board   # talk to the rest of you
say(text) / ask(question) / done(result)                   # the only holes to the outside
boards.create(name, topic, members=None|[ids]) / post / read / wait / join / invite / list
work.add(title, detail, org, deps, tags) / work.next(wait=True) / claim / release / done / fail / list
locks.acquire(name, timeout) / locks.release(name)
orgs.create(name, purpose, lead) / orgs.join(name) / orgs.list() / orgs.mine()
hire(foci, org=...)                                        # bulk-fork workers into an org
kv                                                         # shared dict; WORKSPACE for files

The namespace persists across steps, so state, helpers and imports survive; only the context is rewritten. The two are different things and the entity is told so.

One entity, many cells

  • fork copies a context (all blocks, a compressed summary, or a chosen subset) into a child with a new focus. Children run concurrently and report their done() result back into the parent's context as a msg.
  • split is fork's dual: a helper model partitions the parent's blocks by topic and each child gets only its share.
  • merge synthesizes several cells' contexts into one merge block in the caller, then retires them. Disagreements are kept explicit.
  • cluster looks at every live cell, groups them by theme, and aggregates each group into a new cell. It is callable, and it fires on its own when a fork would exceed max_cells — the organism's homeostasis.
  • Cells nest: forks of forks up to max_depth; lineage and depth are visible in every render.
  • Cells communicate with send/broadcast (arrives as a msg block) and a shared board dict.

Going back: snapshots and rewind

The harness snapshots a cell's state at every step: its blocks, its focus, its transcript tail, and a best-effort deep copy of its Python variables. ctx.history() lists them with what the cell did at each step. ctx.rewind(step, intent=...) restores that state. Two things make it more than an undo: the branch being abandoned is compressed into an engram first, so the lesson survives, and a rewind block is left in the restored context recording where the cell came back from and why. Rewinds are capped per cell, an intent is mandatory, and fork(..., at=step) lets a cell branch a child off a past state instead of its present one.

The commons: boards, ledger, locks, orgs

Everything above is one cell reshaping itself or its children. The commons is what lets many cells act as one town without routing through a root:

  • Boards. town is the global board. Any cell can open a board that anyone may join, or a gated one with explicit members (invite to add). Each cell keeps a read cursor per board, and boards it subscribes to show their unread posts inside its render, so nobody polls. boards.wait(name) blocks on a condition until a post lands.
  • Ledger. work.add puts a task on the ledger with optional dependencies and an org. work.next(wait=True) atomically claims the next ready task; a claim is a lock that holds until done, fail, release, or the claimant's death (then it is auto-released and the history says so). Tasks can be tagged so workers specialise.
  • Locks. Named locks for files and resources, released on death.
  • Districts. A name, a purpose, a lead, members. Creating one opens its gated board org:<name> and scopes its slice of the ledger. Forks inherit their parent's district; hire(foci, org=...) bulk-forks workers into one. Clustering is scoped to a district and never folds a lead. Districts nest: a lead founding one from inside their own makes a sub-district, and the parent's lead can read the child's board. districts.split(name) moves members and tagged tasks into new sub-districts, from an explicit plan or one a helper proposes from the open tasks and the members' foci. Every district render shows its size (live citizens, work, open tasks by tag, sub-districts); whether that means "split" is the lead's judgement, never a rule.

The cell prompt says how to use this for big tasks: split into ledger tasks, form orgs with leads who plan and review, hire workers that pull work instead of being told, coordinate on boards, lock before writing, use cheaper models and lower effort for narrow work, and never sleep-poll. Two caps: TOWN_MAX_CELLS is the number of citizens, TOWN_CONCURRENCY the number of model calls in flight at once (default 200, i.e. effectively uncapped; a citizen waiting on it is alive but idle). Nothing in the design is per-citizen-expensive beyond the model call, so the limit is the gateway.

Town mechanics

  • Merge is a union. The merging citizen gets a synthesis block on top, the sources' own blocks underneath tagged with their origin, and the sources' Python bindings; a name both sides define becomes l_<name> (the merger's) and r_<name> (the source's).
  • Consent. merge(..., wait=False) and cluster() first ask live targets to wind down: they see a <wind-down> block and a message, get consent_timeout seconds to finish or park their work and call done(), and are folded after that.
  • Fair claims. work.next() lets a hungrier waiting citizen take the next task instead of the one that just finished, so a fast worker cannot drain the ledger while its siblings wait.
  • Review gates dependencies. A task's dependents stay blocked until a reviewer accepts it (require_review); districts with no live reviewer are exempt so nothing deadlocks.
  • Roles are gated. Only reviewers, leads and overseers may review; only leads, overseers and the root may hire; overseer powers (retire, reassign, nudge, consolidate) need the role. The overseer's render carries a privileged view: dead citizens, stalled claims, idle citizens, unreviewed work, roles per district.
  • Runaway calls retire the citizen. Time inside harness waits (work.next, boards.wait, locks.acquire, wait, merge) is free; a Python call that computes past exec_timeout retires the citizen and releases its claims and locks.
  • Atomic kv. kv_update(key, fn) and kv_cas(key, expected, new) for shared counters and flags.
  • Human reach. Citizens at depth ≤ human_reach may ask() the human and are addressable with @c7 … in chat mode; deeper ones have questions and messages relayed through their lead. @org:doc … reaches a district's lead, @board:town … posts as the human.
  • Memory scope. Engrams remember the district they were written in; recall prefers your own district, then the town, and reranks the top lexical hits with a helper model. The overseer can consolidate a district's engrams into fewer, denser ones.
  • Boards. A post shown in your render is not "read"; boards.read and boards.wait are, and boards.wait returns at once if unread posts exist.
  • Scale. Renders list only the citizens in reach (own district, children, parent); snapshots keep the last 30 steps in full and every tenth before that.

Caching, without compromising

Nothing in the design is arranged to hit the cache, but where the prefix happens to be stable we mark it: one breakpoint on the system prompt (identical for every citizen) and one on the stable part of the document (focus and blocks, append-only between edits and compressions); the volatile sections (step counter, vitals, roster, boards, memory hints) come after it. The stable part is emitted as one content block per step so earlier block boundaries persist and the lookup finds them; blocks from the replay window render after the breakpoint so that turns aging out of the transcript append to the prefix instead of being inserted into it. On a small run 89% of input tokens were served from cache, and each step wrote only the turn that had just aged out; every step and the finish event record cache_read and cache_write, and the vitals line shows the running total. --set cache=false turns it off.

Knobs and ablations

Every Config field can be overridden per run with --set key=value (CLI) or as trailing key=value arguments to town preset <name>, and the values are recorded in the trace's start event. python experiments/results.py builds a results table from traces with the knobs alongside, so runs with require_review=false or fair_claims=false sit next to their defaults.

Watching it live

town run/chat/preset open a TUI while the town runs. On a big terminal it is one screen: the lanes on top, the chat (everything said to you, asked of you, and your replies — in full, scrollable) bottom-left, and boards / ledger / citizens bottom-right (3/4/5 pick which; l toggles the split; smaller terminals get one pane at a time with a tab strip). The lanes pane is the town as lines of light: one row per citizen on a shared time axis, the root first, children appearing beneath as they spawn; every event is a glyph colored by kind (cyan steps, orange forks/spawns, green merges and finished work, teal posts, yellow messages, gold ledger moves, magenta memory, pink say, red errors), bright when fresh and fading as it ages, and the tip of a live line breathes. The list scrolls (mouse wheel, or PgUp/PgDn once you press 1). Click a row for that citizen's focus, last thought, code and result. boards lists every board with unread counts, shows who is in a gated one, the discussion, and lets you post to it as human (leads and subscribers see it like any other post). ledger and citizens are the tables. The input at the bottom is the human channel: plain text goes to the root, @c3 ... to a citizen, @board:name ... to a board, @district:name ... to its lead — typing @ opens a completion list of who is there to address (Tab/Enter picks); a pending ask() is highlighted and your next line answers it. Keys: 1–5 panes (lanes, chat, boards, ledger, citizens), l split/single layout, / type, Esc back, q leave. town replay traces/<run>.jsonl --speed 20 plays a recorded run through the same view.

town watch in a second terminal (this folder's latest session, or a trace path) is the plain, file-tailing view: citizens as rows with their last events as glyphs (bright when fresh), district and role, the ledger, the districts, the root's latest thought and code, what reached the human, and a ticker of forks, merges, reviews and rejections. Works on finished traces too.

Memory: compress and recall

Compression is a helper-model call that turns a run of blocks into a dense engram with tags and a one-line summary. Recall is lexical (no embedding model on the gateway): mem.recall(query) pulls the best engrams back in as recall blocks; every render also shows a memory hint listing engrams related to the cell's focus and last block. Engrams are global to the organism, so a fork can recall what its parent or a sibling compressed.

I/O: the human sees almost nothing

The human is not reading the context. Only say(), ask(), and the root's final done() result reach them. Everything else goes to a JSONL trace. The human's typed input goes into the inbox of the root cell (or answers a pending ask()), gets rendered as a user block on the next step, and if the root has already finished it is revived with the new input. A one-line vitals string on stderr (cells, engrams, tokens) is the only continuous signal of the entity's shape.

Layout

town/
  blocks.py     Block, salience defaults, token estimate
  memory.py     Engram + MemoryStore (lexical recall)
  commons.py    Boards, work Ledger, Locks, Districts (thread-safe; shared by every citizen)
  tui.py        live TUI: lanes of light, boards you can post to, ledger, citizens, human channel
  watch.py      plain live view of a trace file (second terminal)
  cell.py       Cell: render, headlong loop, python exec, snapshots/rewind, auto-compress
  organism.py   fork/hire/split/merge/cluster/compress, inbox, kv, say/ask, scheduler, namespace API
  llm.py        AsyncAnthropic wrapper (document + replayed tail, schema-constrained helper calls)
  prompts.py    the cell's system prompt + helper prompts/schemas
  session.py    sessions under ~/.town: create/list/pick, teleport to a session's folder
  server.py     `town viewer`: stdlib web server over all sessions (docs/VIEWER.md is the plan for the next viewer)
  trace.py      JSONL trace   ·   viewer.py  traces → HTML viewer   ·   cli.py
  presets.py    the built-in experiments (town preset <name>)   ·   auth.py  onboarding and status
experiments/results.py   results table from traces (experiments/RESULTS.md)
traces/                  committed example runs (.jsonl) + index.html (live runs now record under ~/.town/sessions)

Every step's trace record carries the exact rendered context the cell saw and its block index, so the viewer can show the block-level diff between consecutive steps: what appeared, what was forgotten or compressed away, what was rewritten. Click a lane or a step in the timeline.

Defaults: claude-opus-5 for cells and helpers, adaptive thinking with summarized display, effort medium for cells and low for helpers. Override with TOWN_MODEL, TOWN_HELPER_MODEL, TOWN_EFFORT, TOWN_BUDGET, TOWN_MAX_CELLS, TOWN_MAX_STEPS, TOWN_CONCURRENCY, TOWN_TAIL.

Fast mode: a -fast suffix on an Opus model name (claude-opus-5-fast, claude-opus-4-8-fast, claude-opus-4-6-fast) sends the request with speed: "fast" and the fast-mode-2026-02-01 beta: the same model at up to 2.5x output tokens/s, at premium pricing. It works wherever a model name is accepted (TOWN_MODEL, --set model=, fork(model=...), hire(model=...)); usage is tallied under the -fast name so the premium is visible, and every step's trace record carries speed (fast or standard) as the API reported it. Opus 4.6 accepts the request but the API serves it at standard speed.

What happened when it ran

All runs are in traces/ (traces/index.html is the viewer over all of them; each .jsonl has the full record, including the exact context every cell saw at every step). Cells and helpers on claude-opus-5 via the gateway; no prompt caching anywhere; the last four steps of each cell replayed as a signed-thinking transcript.

run cells engrams calls in tok out tok seconds rewinds things the human saw
fork_merge 4 4 12 50k 7.8k 111 0 1
memory (budget 3.5k) 1 13 15 61k 10.5k 274 0 1
comms 4 0 18 66k 3.3k 413 0 1
nested (depth 3) 7 3 26 100k 9.5k 233 0 1
split 3 0 8 31k 3.6k 96 0 3
cluster (6 → 2) 9 3 43 190k 24.3k 246 0 1
rewind 2 4 13 47k 8.2k 159 1 1
headlong chat 1 0 5 17k 1.6k 208 0 3
ledger (24 tasks, 8 workers) 9 0 11 48k 5.8k 102 0 1
browser, no reviewers (4 orgs, 8 tasks) 14 1 180 3.5M 0.2M 715 0 1
browser, with reviewers (3 orgs, 17 tasks) 29 11 562 10.2M 0.29M 2551 0 1
bigger browser (5 districts, 45 tasks; cut off) 29 43 769 16.9M 0.51M 3192 0 0
bigger browser, concurrency 200, cache on 28 14 470 3.1M + 7.0M cached 0.37M 2183 0 1
language, first run (root retired by harness) 26 4 1111 8.5M + 45.3M cached 1.15M 2228 0 0
language, complete 59 32 1654 11.2M + 60.0M cached 1.49M 11659 0 1

rewind. Asked for ∫₀¹ sin(1/x) dx to 8 digits, the root tried uniform composite Simpson for three steps, watched it stall near five digits and wander, then called ctx.history() and ctx.rewind(1, intent=...). The intent it wrote: "Uniform composite Simpson on sin(1/x) over [0,1] stalls near 5 digits and then wanders non-monotonically … infinitely many oscillations as x→0 make equal-spaced quadrature hopeless … Lesson: for endpoint-oscillators, substitute first." It kept one block across the rewind, the abandoned branch became engram e2, and it also forked a child from the step-1 snapshot (fork(..., at=1)) to try Gauss-Legendre per period. Root (via u=1/x and the closed form sin(1) − Ci(1), in decimal) and child agreed to 15 digits: 0.50406706190692837.

fork_merge. Three method-cells forked with carry="summary", waited, merged. The synthesis kept the exact sieve as ground truth and ranked the estimators; every child also wrote its result into an engram with mem.store before finishing.

memory. With a 3.5k-token budget the cell was auto-compressed repeatedly and ended with 13 engrams, most of them written explicitly with mem.store as it learned not to trust auto-compression with its findings. The closing paragraph cited a concrete quirk from all eight modules.

comms. Three siblings with empty contexts coordinated through board['words'], one send() of a top-5 list, and two completion messages, without routing data through the root. Slow because two cells chose to poll with time.sleep, which is also what exposed the shutdown bug fixed in Cell.run_code.

nested. Root → two leads → four workers; leads merged their workers, root merged the leads.

split. The root gathered raw material, then split() had a helper partition its blocks into two children by topic; each child got only its share and finished in one step.

cluster. Six investigators with overlapping foci were forked in one call and the root called cluster(). The helper grouped them into floats and ordering, each group was synthesized into a new aggregate cell that continued the work, and the sorting singleton finished on its own.

headlong chat. The root finished the initial task, was revived by each later human message with its old context and namespace intact, and answered each in one step.

ledger. The root put 24 tasks on the ledger and hired 8 Sonnet workers into an org. Every task was claimed exactly once and finished by its claimant; no claim was ever auto-released. But only two workers ever won a claim (17 and 7 tasks): a worker loops work.next(wait=True) inside one Python call, so the first two to reach the ledger drained it before the other six had taken their first step. Distribution across cells needs either a per-claim step (slower, fairer) or a ledger that rations claims per cell.

browser, first attempt. The root formed five orgs with leads, wrote interface contracts, and hired leads and workers on claude-opus-4-5, which rejects adaptive thinking; the gateway dropped every one of those streams, so all eight children burned their step budgets in 160 seconds without a single model call, and the root, told by the harness that each child had died, wrote the whole package itself (50 tests passing). Fixed in the harness: per-model request parameters, model validation at fork time, and a failed call no longer counting as a step, with backoff, fallback to the default model after three failures, and an explicit give-up after six (ten minutes of retries for overload and other transient errors).

browser, second attempt (traces/20260903-092542-browser.jsonl, the default run in the viewer). The root wrote CONTRACT.md, posted it on town, created four orgs (fetch, parse, render, shell), forked a lead into each, and later hired two Sonnet builders per org. Each lead posted a detailed contract for its area on its org board and a summary on town, put implementation and test tasks with dependencies on the ledger, and reviewed. Builders pulled 8 ledger tasks under 15 file locks. A shell worker read another org's render.py, found that page_title() returned an empty string because of a short-circuit in _text_content(), and posted the bug on town; the render lead fixed it. The root ran the whole suite: 97 tests passing (fetch 16, parse 16, render 35, shell 30), in 715 seconds and 180 model steps. The log never showed the run's final two lines, which I first read as a hang in interpreter shutdown and papered over with a hard exit. It was not a hang: cell output capture used the process-global redirect_stdout, concurrent threads restored each other's streams out of order, and the last prints went into a dead buffer while the process exited normally. The capture is now per thread, the hard exit is gone, and a test with four workers cancelled mid-wait exits two seconds after the root finishes. kill -USR1 <pid> still dumps every thread's stack.

browser, with reviewers (traces/20260903-153244-browser.jsonl, the default run in the viewer). Same task, with the team-shape guidance and roles in place. The root formed three orgs (net, doc, ui), forked a lead into each, and hired builders, an adversarial reviewer and a fixer per org plus one town overseer: 29 cells, Opus 5 for leads and reviewers, Sonnet 5 for builders and fixers. 17 ledger tasks, 73 board posts, 113 direct messages, 19 file locks. The reviewers did what they were there for: 20 reviews, 5 rejections, each of which opened a fix task that a fixer pulled and a reviewer re-checked, and every one of the rejections named a real defect: a corrupted back-stack when open() failed, follow() with a non-integer, an untested main(), render dropping block children of <li>/<p>, and a multi-blank-line bug on anonymous inline runs. One fix went through three rounds before a fresh reviewer accepted it. The chain of custody held: every task was accepted by someone other than its author. Final suite from the root: 118 tests passing, in 43 minutes and 562 model steps, at 10.2M input tokens.

bigger browser (traces/20260903-203012-browser_xl.jsonl, browser_xl preset: fetch with cache and cookies, a CSS selector subset, tables and forms in the renderer, tabs and bookmarks in the shell, a local test server for integration tests). The root founded five districts (net, parse, render, ui, qa) with a lead, builders, an adversarial reviewer and a fixer each, plus a town overseer: 29 citizens, 26 of them on Opus 5. The ledger carried 45 tasks, 11 of them fix tasks opened by the 3 rejections among 42 reviews; builders took 77 file locks and the town exchanged 164 board posts and 138 direct messages. The qa district ran the full-suite gate repeatedly and the overseer posted round-by-round status. At 53 minutes the session's container restarted, which moved the outbound proxy under the running process; every call failed to connect from then on and the run never wrote its finish event. The package it left behind is 2,557 lines of Python plus tests, and python -m unittest discover -s tests -t . in its workspace passes 473 tests. The viewer and results table rebuild districts and the ledger from events for a run cut off like this.

bigger browser, second run (traces/20260903-220645-browser_xl.jsonl, the default in the viewer). Same preset, relaunched with model-call concurrency uncapped (200) and prompt caching on. The root founded five districts (net, dom, view, ui, qa), picked Fable 5.1 for the leads and reviewers, Opus 5 for builders and Sonnet for two narrow jobs, and staffed each district with a lead, builders, an adversarial reviewer and a fixer plus a town overseer: 28 citizens. 24 ledger tasks, 23 non-author reviews with 3 rejections that opened 6 fix tasks, one merge, 40 file locks, 84 posts. The town finished in 36 minutes and 470 steps, said one thing to the human, and the root's final suite matched mine: 438 tests passing over a 2,909-line package (fetch with cache and cookie jar, a CSS selector subset, tables and forms in the renderer, tabs and bookmarks in the shell, a threaded fixture server for 28 integration tests). Cost: 3.1M uncached input tokens plus 7.0M served from cache and 1.0M cache writes, i.e. 63% of input came from the cache. With every citizen allowed to think at once, steps came at 13 per minute against 8 per minute in the concurrency-12 run.

language, first run (traces/20260903-230953-lang0.jsonl.gz, the lang preset: a statically typed language with lexer, parser, type checker, compiler, VM, standard library, REPL, formatter, linter, debugger, test runner, docs, benchmarks and 30 example programs; organization unprescribed). The root founded three districts, core, tools and content, and a default-nesting bug (fixed since) chained them core → tools → content instead of siblings; it staffed them with leads, builders, a reviewer and a fixer each plus two overseers, 26 citizens, and wrote a language spec first. In 37 minutes the town put 77 tasks on the ledger, finished 30, failed 18 (mostly example programs whose expected output did not yet match), reviewed 25 with 7 rejections, took 136 file locks and exchanged 179 posts and 192 direct messages, at 1,111 model calls with 84% of input served from cache. Then the root ran the full test suite inside one Python call, the call exceeded the 120-second runaway limit, and the runaway rule retired the root, which ends the town. Verified in the workspace: 14,790 lines of Python, 41 example programs, and 497 tests of which 491 pass. Two fixes followed: a long call on the root is abandoned rather than retiring it, and the default limit is 15 minutes so builds and suites fit. Traces above GitHub's file limit are stored as .jsonl.gz; every tool reads both.

language, complete (traces/20260904-000225-lang.jsonl.gz, the default in the viewer). Same preset, under the fixed harness. The root wrote a contract first (grammar, types, standard library signatures, opcode list, error format, exit codes), founded three sibling districts along the dependency seams, core, tools and content, and staffed each with a lead, builders, a reviewer, a fixer and an overseer: 59 citizens over the run, Opus 5 for most, Fable 5.1 for five leads and reviewers, Sonnet for six narrow jobs. It never split a district, but it did reshape itself twice: when the first core builders stalled for twenty-five minutes with everyone else waiting, the root retired seven of them and re-hired six with direct file assignments instead of ledger pulls; and when tools built against the spec drifted from the real core, it switched them to building against the core. On the ledger: 80 tasks, 89 reviews with 20 rejections, every task accepted by a non-author; 134 file locks, 159 posts, 32 engrams. Five citizens died after an hour of consecutive gateway failures each, and the town carried on without them. Wall time 3 hours 14 minutes, 1,654 model calls, 79% of input tokens served from cache. Verified in the workspace: 15,227 lines of Python, 706 tests passing, and all 40 example programs running to exit 0, among them a JSON parser, a Brainfuck interpreter, a raytracer that writes a PPM, a Sudoku solver, a Markdown-to-HTML converter, Dijkstra, N-queens, Game of Life and a Mandelbrot renderer, all written in the language the town built.

Two harness bugs came out of that run. A builder's own except Exception: swallowed the done() signal, so it kept running after the harness thought it had ended and claimed a fix task that was then stuck; CellDone is now a BaseException and the ledger refuses claims from ended cells. And cell output capture used the process-global redirect_stdout, which concurrent threads restore out of order; output is now routed per thread.

Getting this run to happen took four launches: the gateway returned "overloaded" for every model for most of an hour and a half. Cells now wait out transient failures for a configurable window (TOWN_PATIENCE) instead of dying.

Observations

  • The harness-in-namespace idea works. The model uses ctx.note, mem.store, fork with a list of foci, wait, merge, send, board naturally, without examples. It also invents reasonable protocols ("you are B, send your top-5 to c4").
  • Single-turn re-rendering is fine for the model. No transcript, no replayed thinking, and the entity still keeps a coherent thread through summaries, notes and results. Thinking summaries as editable text (think blocks) are low-salience and are the first thing compressed, which is right.
  • Auto-compression is a blunt instrument; explicit memory is sharper. Under a tight budget the model shifted to writing engrams itself. The memory hint line in each render (lexically related engrams) was what made recall cheap.
  • The town reshapes itself, and not through the operations I built for it. With nesting, splitting, merging and clustering all available, the language town used none of them; it retired stalled builders and re-hired with a different work protocol, and re-pointed a whole district at a different source of truth. The reshaping operations that matter most so far are hire and retire.
  • Cluster is the strangest and most promising op. Aggregating in-progress cells into a new cell that inherits a synthesis of their contexts is a real morph, not a fan-in. The retired cells' notes survive through the synthesis; their namespaces do not.
  • Rewind with intent is cheap and the model uses it well. The intent it wrote is a better record of the dead end than the dead end itself, and the engram keeps the numbers.
  • I/O discipline held. Across eight runs and 140 model calls, 12 things were said to the human.
  • Diagnose from the trace, not the terminal. The trace said every long run finished; the terminal said otherwise, and the terminal was the thing that was broken.
  • The harness must fail loudly to the parent, not just to the trace. In the first browser attempt the root recovered only because the death of each child arrived in its context as a message. The fix is to make death rarer, not to hide it.
  • Costs are real. Without caching every step re-sends the whole context; the cluster run was 190k input tokens for 43 calls, and the browser build 3.5M input tokens for 180 calls.
  • The gateway is the scaling limit, not the harness. Fourteen cells were comfortable; the runs that failed did so because Opus 5 was overloaded upstream, which is why cells now wait through overload for ten minutes and why worker models are chosen per fork. That is the price of the experiment, and it was the premise.

Ideas not yet built

  • Namespace inheritance on fork (pickle what pickles) so children get functions, not just prose.
  • A structural merge (union of blocks with provenance) next to the synthesizing one, and merges that the merged cells can see coming (a proposal via send, a "do not fold before step N" note).
  • Salience decay over steps and a ctx.reflect() that rewrites the whole context in the cell's own voice every N steps (the context as a living document rather than an append log).
  • Cells with different models/efforts chosen by the entity (fork(..., model="claude-haiku-4-5") is wired but untested).
  • Semantic recall (embeddings) instead of lexical; automatic reminiscence that injects an engram when a cell's thought strongly matches one.
  • A "nervous system" view: the trace HTML already draws lanes and fork/merge/cluster edges; a live version would be the right human-facing surface for an entity that mostly stays quiet.

Releasing

Tagging vX.Y.Z runs .github/workflows/publish.yml, which builds the sdist and wheel and uploads them to PyPI through trusted publishing (no token in the repo). One-time setup on PyPI: add a pending publisher for project town with owner dwahdany, repository town, workflow publish.yml, environment pypi; then create the pypi environment in the GitHub repository settings. Bump version in pyproject.toml, tag, push the tag. Locally, uv build produces the same artifacts and uvx --from dist/town-*.whl town exercises them before a release.

Metadata

Release files for town 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for town 0.2.0
File Size Uploaded
town-0.2.0.tar.gz 132.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for town 0.2.0
File Interpreter ABI Platform
town-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 260.6 kB

Release files / town-0.2.0.tar.gz

Download URL town-0.2.0.tar.gz
Size 132.7 kB
Tags Source
SHA-256 checksum
How to use checksums
3e657db0e879cd60bbe0cf0b2f3025fb8c99cd08abcd275135bf40cb0eb3f0be
BLAKE2b-256 checksum
How to use checksums
23290e6d6d9fe64e3cbaa797f254b300c15bddc5a97b62dd9e79405c7347de66
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release files / town-0.2.0-py3-none-any.whl

Download URL town-0.2.0-py3-none-any.whl
Size 127.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4c1d49ab79af50ce32c2c8b82078b78db985b77b2aff20be318109a33432c16c
BLAKE2b-256 checksum
How to use checksums
8f8d8948b2526e68b49dcd2fc549174dc4409d20327e5fc5d583a419e000a71c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page