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 amsg. - 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
mergeblock 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 amsgblock) and a sharedboarddict.
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.
townis the global board. Any cell can open a board that anyone may join, or a gated one with explicit members (inviteto 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.addputs 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 untildone,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) andr_<name>(the source's). - Consent.
merge(..., wait=False)andcluster()first ask live targets to wind down: they see a<wind-down>block and a message, getconsent_timeoutseconds to finish or park their work and calldone(), 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 pastexec_timeoutretires the citizen and releases its claims and locks. - Atomic kv.
kv_update(key, fn)andkv_cas(key, expected, new)for shared counters and flags. - Human reach. Citizens at depth ≤
human_reachmayask()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.readandboards.waitare, andboards.waitreturns 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,forkwith a list of foci,wait,merge,send,boardnaturally, 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 (
thinkblocks) 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)
| File | Size | Uploaded | |
|---|---|---|---|
| town-0.2.0.tar.gz | 132.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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