town 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- town-0.1.0/.gitignore +9 -0
- town-0.1.0/LICENSE +21 -0
- town-0.1.0/PKG-INFO +492 -0
- town-0.1.0/README.md +467 -0
- town-0.1.0/pyproject.toml +40 -0
- town-0.1.0/town/__init__.py +5 -0
- town-0.1.0/town/__main__.py +3 -0
- town-0.1.0/town/auth.py +73 -0
- town-0.1.0/town/blocks.py +73 -0
- town-0.1.0/town/cell.py +559 -0
- town-0.1.0/town/cli.py +122 -0
- town-0.1.0/town/commons.py +554 -0
- town-0.1.0/town/config.py +117 -0
- town-0.1.0/town/llm.py +244 -0
- town-0.1.0/town/memory.py +95 -0
- town-0.1.0/town/organism.py +862 -0
- town-0.1.0/town/presets.py +250 -0
- town-0.1.0/town/prompts.py +230 -0
- town-0.1.0/town/trace.py +24 -0
- town-0.1.0/town/viewer.py +339 -0
- town-0.1.0/town/watch.py +201 -0
town-0.1.0/.gitignore
ADDED
town-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dwahdany
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
town-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: town
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One entity, one tool, a context that never stops being rewritten; grows into districts of citizens for big tasks. An experimental agent harness for Claude.
|
|
5
|
+
Project-URL: Homepage, https://github.com/dwahdany/town
|
|
6
|
+
Project-URL: Repository, https://github.com/dwahdany/town
|
|
7
|
+
Project-URL: Issues, https://github.com/dwahdany/town/issues
|
|
8
|
+
Author-email: dwahdany <dw@wasc.io>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,claude,context,harness,multi-agent
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: anthropic>=1.3.0
|
|
23
|
+
Requires-Dist: rich>=13
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# town
|
|
27
|
+
|
|
28
|
+
Experiments in out-of-distribution agent harnesses. One entity, one tool, a context that is
|
|
29
|
+
never a transcript and never stops being rewritten, and a town it can grow into when the task is
|
|
30
|
+
too big for one mind.
|
|
31
|
+
|
|
32
|
+
The two seeds:
|
|
33
|
+
|
|
34
|
+
- **prime-style** — the agent gets exactly one tool, `python`. Everything else is code.
|
|
35
|
+
- **headlong-style** — the agent never waits for a user turn; it thinks continuously, and its
|
|
36
|
+
thinking is text in its own context that it can read, edit, and forget.
|
|
37
|
+
|
|
38
|
+
Then we stop designing around prompt caching, which unlocks the actual experiment: **the harness
|
|
39
|
+
lives inside the agent's Python namespace**, so the entity reshapes its own context, forks, splits,
|
|
40
|
+
merges, clusters, rewinds, compresses and recalls itself by executing code against itself. And
|
|
41
|
+
when a task is the size of "build a browser", the same code forms orgs, opens message boards, puts
|
|
42
|
+
work on a ledger, and hires workers that pull it under lock.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
uvx town # first run: interactive setup (API base URL + key, validated, saved to ~/.config/town)
|
|
46
|
+
town auth status # what is in effect; `town auth` to change it
|
|
47
|
+
town run "Find out which stdlib module has the most public names. Tell me once."
|
|
48
|
+
town chat "Keep me company while I debug; I'll paste things." -v
|
|
49
|
+
town preset list # built-in experiments; `town preset lang -v` builds a language
|
|
50
|
+
town watch traces/<run>.jsonl # live terminal view, in a second terminal
|
|
51
|
+
town viz traces/*.jsonl # HTML viewer: timelines + step-by-step trajectories
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Install with `uv tool install town` or `pip install town`; from a checkout, `pip install -e .`.
|
|
55
|
+
Configuration precedence: environment (`TOWN_API_KEY`, `TOWN_BASE_URL`, `TOWN_MODEL`, ...), then a
|
|
56
|
+
`.env` in the working directory, then `~/.config/town/config.toml` written by `town auth`. The base
|
|
57
|
+
URL is Anthropic's API by default; a LiteLLM or similar gateway works with its own key. Runs write
|
|
58
|
+
`traces/` and `workspace/` under the current directory.
|
|
59
|
+
|
|
60
|
+
`-v` streams the entity's internal thoughts, code and results to stderr. Without it the human
|
|
61
|
+
sees only what the entity chooses to `say()`.
|
|
62
|
+
|
|
63
|
+
## The model of the thing
|
|
64
|
+
|
|
65
|
+
### Context is a document, not a transcript
|
|
66
|
+
|
|
67
|
+
A cell's context is an ordered list of **blocks** (`task`, `user`, `thought`, `think`, `code`,
|
|
68
|
+
`result`, `note`, `msg`, `recall`, `stub`, `merge`, `system`). Every step, the whole list is
|
|
69
|
+
re-rendered into a single user message and sent as a fresh single-turn request. Nothing is ever
|
|
70
|
+
replayed, so nothing has to stay put: any block can be dropped, rewritten, pinned, or compressed
|
|
71
|
+
between two steps. The last few steps (`tail_turns`, default 4) are the exception: they are replayed
|
|
72
|
+
behind the document as a real transcript, signed thinking blocks included, so the model's most
|
|
73
|
+
recent reasoning is verbatim. Older steps live on as blocks, with the thinking *summary* folded in
|
|
74
|
+
as a `think` block, i.e. the inner voice becomes editable memory. Rewriting the document in front
|
|
75
|
+
of a replayed tail is accepted by this gateway; on an account that enforces preserved thinking the
|
|
76
|
+
harness strips the thinking blocks and retries once.
|
|
77
|
+
|
|
78
|
+
Each block carries a **salience** 1–5. Salience 5 is pinned. When the context passes its token
|
|
79
|
+
budget the oldest, lowest-salience blocks are compressed into an **engram** in the memory store and
|
|
80
|
+
replaced by a one-line **stub** that names the engram, so the cell can recall it later.
|
|
81
|
+
|
|
82
|
+
### The only tool is python, and the harness is in the namespace
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
ctx.blocks() / ctx.get(id) / ctx.forget(*ids) / ctx.pin(*ids) / ctx.rewrite(id, text)
|
|
86
|
+
ctx.note(text) / ctx.compress(*ids, hint=...) / ctx.focus(text)
|
|
87
|
+
ctx.history() / ctx.rewind(step, intent=..., keep=[ids], namespace=False) # go back, on the record
|
|
88
|
+
mem.recall(query, k) / mem.get(eid) / mem.list() / mem.store(text, tags, summary)
|
|
89
|
+
fork(focus_or_list, carry="all"|"summary"|[ids], n=1, at=step) # copy yourself (or a past self), focused
|
|
90
|
+
split(foci=None, n=2) # partition yourself by topic
|
|
91
|
+
merge(*ids, how="synthesize", wait=True) # fold cells into yourself
|
|
92
|
+
cluster() # group + aggregate all live cells
|
|
93
|
+
wait(*ids) / send(id, text) / broadcast(text) / cells() / board # talk to the rest of you
|
|
94
|
+
say(text) / ask(question) / done(result) # the only holes to the outside
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
boards.create(name, topic, members=None|[ids]) / post / read / wait / join / invite / list
|
|
99
|
+
work.add(title, detail, org, deps, tags) / work.next(wait=True) / claim / release / done / fail / list
|
|
100
|
+
locks.acquire(name, timeout) / locks.release(name)
|
|
101
|
+
orgs.create(name, purpose, lead) / orgs.join(name) / orgs.list() / orgs.mine()
|
|
102
|
+
hire(foci, org=...) # bulk-fork workers into an org
|
|
103
|
+
kv # shared dict; WORKSPACE for files
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The namespace persists across steps, so state, helpers and imports survive; only the *context*
|
|
107
|
+
is rewritten. The two are different things and the entity is told so.
|
|
108
|
+
|
|
109
|
+
### One entity, many cells
|
|
110
|
+
|
|
111
|
+
- **fork** copies a context (all blocks, a compressed summary, or a chosen subset) into a child
|
|
112
|
+
with a new focus. Children run concurrently and report their `done()` result back into the
|
|
113
|
+
parent's context as a `msg`.
|
|
114
|
+
- **split** is fork's dual: a helper model partitions the parent's blocks by topic and each child
|
|
115
|
+
gets only its share.
|
|
116
|
+
- **merge** synthesizes several cells' contexts into one `merge` block in the caller, then retires
|
|
117
|
+
them. Disagreements are kept explicit.
|
|
118
|
+
- **cluster** looks at every live cell, groups them by theme, and aggregates each group into a new
|
|
119
|
+
cell. It is callable, and it fires on its own when a fork would exceed `max_cells` — the
|
|
120
|
+
organism's homeostasis.
|
|
121
|
+
- Cells nest: forks of forks up to `max_depth`; lineage and depth are visible in every render.
|
|
122
|
+
- Cells communicate with `send`/`broadcast` (arrives as a `msg` block) and a shared `board` dict.
|
|
123
|
+
|
|
124
|
+
### Going back: snapshots and rewind
|
|
125
|
+
|
|
126
|
+
The harness snapshots a cell's state at every step: its blocks, its focus, its transcript tail, and
|
|
127
|
+
a best-effort deep copy of its Python variables. `ctx.history()` lists them with what the cell did
|
|
128
|
+
at each step. `ctx.rewind(step, intent=...)` restores that state. Two things make it more than an
|
|
129
|
+
undo: the branch being abandoned is compressed into an engram first, so the lesson survives, and a
|
|
130
|
+
`rewind` block is left in the restored context recording where the cell came back from and why.
|
|
131
|
+
Rewinds are capped per cell, an intent is mandatory, and `fork(..., at=step)` lets a cell branch a
|
|
132
|
+
child off a past state instead of its present one.
|
|
133
|
+
|
|
134
|
+
### The commons: boards, ledger, locks, orgs
|
|
135
|
+
|
|
136
|
+
Everything above is one cell reshaping itself or its children. The commons is what lets many
|
|
137
|
+
cells act as one town without routing through a root:
|
|
138
|
+
|
|
139
|
+
- **Boards.** `town` is the global board. Any cell can open a board that anyone may join, or a
|
|
140
|
+
gated one with explicit members (`invite` to add). Each cell keeps a read cursor per board, and
|
|
141
|
+
boards it subscribes to show their unread posts inside its render, so nobody polls.
|
|
142
|
+
`boards.wait(name)` blocks on a condition until a post lands.
|
|
143
|
+
- **Ledger.** `work.add` puts a task on the ledger with optional dependencies and an org.
|
|
144
|
+
`work.next(wait=True)` atomically claims the next *ready* task; a claim is a lock that holds
|
|
145
|
+
until `done`, `fail`, `release`, or the claimant's death (then it is auto-released and the
|
|
146
|
+
history says so). Tasks can be tagged so workers specialise.
|
|
147
|
+
- **Locks.** Named locks for files and resources, released on death.
|
|
148
|
+
- **Districts.** A name, a purpose, a lead, members. Creating one opens its gated board
|
|
149
|
+
`org:<name>` and scopes its slice of the ledger. Forks inherit their parent's district;
|
|
150
|
+
`hire(foci, org=...)` bulk-forks workers into one. Clustering is scoped to a district and never
|
|
151
|
+
folds a lead. Districts nest: a lead founding one from inside their own makes a sub-district, and
|
|
152
|
+
the parent's lead can read the child's board. `districts.split(name)` moves members and tagged
|
|
153
|
+
tasks into new sub-districts, from an explicit plan or one a helper proposes from the open tasks
|
|
154
|
+
and the members' foci. Every district render shows its size (live citizens, work, open tasks by
|
|
155
|
+
tag, sub-districts); whether that means "split" is the lead's judgement, never a rule.
|
|
156
|
+
|
|
157
|
+
The cell prompt says how to use this for big tasks: split into ledger tasks, form orgs with leads
|
|
158
|
+
who plan and review, hire workers that pull work instead of being told, coordinate on boards,
|
|
159
|
+
lock before writing, use cheaper models and lower effort for narrow work, and never sleep-poll.
|
|
160
|
+
Two caps: `TOWN_MAX_CELLS` is the number of citizens, `TOWN_CONCURRENCY` the number of model calls in
|
|
161
|
+
flight at once (default 200, i.e. effectively uncapped; a citizen waiting on it is alive but idle).
|
|
162
|
+
Nothing in the design is per-citizen-expensive beyond the model call, so the limit is the gateway.
|
|
163
|
+
|
|
164
|
+
### Town mechanics
|
|
165
|
+
|
|
166
|
+
- **Merge is a union.** The merging citizen gets a synthesis block on top, the sources' own blocks
|
|
167
|
+
underneath tagged with their origin, and the sources' Python bindings; a name both sides define
|
|
168
|
+
becomes `l_<name>` (the merger's) and `r_<name>` (the source's).
|
|
169
|
+
- **Consent.** `merge(..., wait=False)` and `cluster()` first ask live targets to wind down: they see
|
|
170
|
+
a `<wind-down>` block and a message, get `consent_timeout` seconds to finish or park their work and
|
|
171
|
+
call `done()`, and are folded after that.
|
|
172
|
+
- **Fair claims.** `work.next()` lets a hungrier waiting citizen take the next task instead of the
|
|
173
|
+
one that just finished, so a fast worker cannot drain the ledger while its siblings wait.
|
|
174
|
+
- **Review gates dependencies.** A task's dependents stay blocked until a reviewer accepts it
|
|
175
|
+
(`require_review`); districts with no live reviewer are exempt so nothing deadlocks.
|
|
176
|
+
- **Roles are gated.** Only reviewers, leads and overseers may review; only leads, overseers and the
|
|
177
|
+
root may hire; overseer powers (`retire`, `reassign`, `nudge`, `consolidate`) need the role. The
|
|
178
|
+
overseer's render carries a privileged view: dead citizens, stalled claims, idle citizens,
|
|
179
|
+
unreviewed work, roles per district.
|
|
180
|
+
- **Runaway calls retire the citizen.** Time inside harness waits (`work.next`, `boards.wait`,
|
|
181
|
+
`locks.acquire`, `wait`, `merge`) is free; a Python call that computes past `exec_timeout` retires
|
|
182
|
+
the citizen and releases its claims and locks.
|
|
183
|
+
- **Atomic kv.** `kv_update(key, fn)` and `kv_cas(key, expected, new)` for shared counters and flags.
|
|
184
|
+
- **Human reach.** Citizens at depth ≤ `human_reach` may `ask()` the human and are addressable with
|
|
185
|
+
`@c7 …` in chat mode; deeper ones have questions and messages relayed through their lead.
|
|
186
|
+
`@org:doc …` reaches a district's lead, `@board:town …` posts as the human.
|
|
187
|
+
- **Memory scope.** Engrams remember the district they were written in; recall prefers your own
|
|
188
|
+
district, then the town, and reranks the top lexical hits with a helper model. The overseer can
|
|
189
|
+
consolidate a district's engrams into fewer, denser ones.
|
|
190
|
+
- **Boards.** A post shown in your render is not "read"; `boards.read` and `boards.wait` are, and
|
|
191
|
+
`boards.wait` returns at once if unread posts exist.
|
|
192
|
+
- **Scale.** Renders list only the citizens in reach (own district, children, parent); snapshots keep
|
|
193
|
+
the last 30 steps in full and every tenth before that.
|
|
194
|
+
|
|
195
|
+
### Caching, without compromising
|
|
196
|
+
|
|
197
|
+
Nothing in the design is arranged to hit the cache, but where the prefix happens to be stable we
|
|
198
|
+
mark it: one breakpoint on the system prompt (identical for every citizen) and one on the stable
|
|
199
|
+
part of the document (focus and blocks, append-only between edits and compressions); the volatile
|
|
200
|
+
sections (step counter, vitals, roster, boards, memory hints) come after it. The stable part is
|
|
201
|
+
emitted as one content block per step so earlier block boundaries persist and the lookup finds
|
|
202
|
+
them; blocks from the replay window render after the breakpoint so that turns aging out of the
|
|
203
|
+
transcript append to the prefix instead of being inserted into it. On a small run 89% of input
|
|
204
|
+
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
|
|
205
|
+
`cache_write`, and the vitals line shows the running total. `--set cache=false` turns it off.
|
|
206
|
+
|
|
207
|
+
### Knobs and ablations
|
|
208
|
+
|
|
209
|
+
Every `Config` field can be overridden per run with `--set key=value` (CLI) or as trailing
|
|
210
|
+
`key=value` arguments to `town preset <name>`, and the values are recorded in the trace's `start` event.
|
|
211
|
+
`python experiments/results.py` builds a results table from traces with the knobs alongside, so
|
|
212
|
+
runs with `require_review=false` or `fair_claims=false` sit next to their defaults.
|
|
213
|
+
|
|
214
|
+
### Watching it live
|
|
215
|
+
|
|
216
|
+
`python -m town watch traces/<run>.jsonl` in a second terminal: citizens as rows with their last
|
|
217
|
+
events as glyphs (bright when fresh), district and role, the ledger, the districts, the root's
|
|
218
|
+
latest thought and code, what reached the human, and a ticker of forks, merges, reviews and
|
|
219
|
+
rejections. Works on finished traces too.
|
|
220
|
+
|
|
221
|
+
### Memory: compress and recall
|
|
222
|
+
|
|
223
|
+
Compression is a helper-model call that turns a run of blocks into a dense engram with tags and a
|
|
224
|
+
one-line summary. Recall is lexical (no embedding model on the gateway): `mem.recall(query)` pulls
|
|
225
|
+
the best engrams back in as `recall` blocks; every render also shows a *memory hint* listing
|
|
226
|
+
engrams related to the cell's focus and last block. Engrams are global to the organism, so a fork
|
|
227
|
+
can recall what its parent or a sibling compressed.
|
|
228
|
+
|
|
229
|
+
### I/O: the human sees almost nothing
|
|
230
|
+
|
|
231
|
+
The human is not reading the context. Only `say()`, `ask()`, and the root's final `done()` result
|
|
232
|
+
reach them. Everything else goes to a JSONL trace. The human's typed input goes into the inbox of
|
|
233
|
+
the root cell (or answers a pending `ask()`), gets rendered as a `user` block on the next step, and
|
|
234
|
+
if the root has already finished it is revived with the new input. A one-line vitals string on
|
|
235
|
+
stderr (cells, engrams, tokens) is the only continuous signal of the entity's shape.
|
|
236
|
+
|
|
237
|
+
## Layout
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
town/
|
|
241
|
+
blocks.py Block, salience defaults, token estimate
|
|
242
|
+
memory.py Engram + MemoryStore (lexical recall)
|
|
243
|
+
commons.py Boards, work Ledger, Locks, Districts (thread-safe; shared by every citizen)
|
|
244
|
+
watch.py live terminal view of a trace
|
|
245
|
+
cell.py Cell: render, headlong loop, python exec, snapshots/rewind, auto-compress
|
|
246
|
+
organism.py fork/hire/split/merge/cluster/compress, inbox, kv, say/ask, scheduler, namespace API
|
|
247
|
+
llm.py AsyncAnthropic wrapper (document + replayed tail, schema-constrained helper calls)
|
|
248
|
+
prompts.py the cell's system prompt + helper prompts/schemas
|
|
249
|
+
trace.py JSONL trace · viewer.py traces → HTML viewer · cli.py
|
|
250
|
+
presets.py the built-in experiments (town preset <name>) · auth.py onboarding and status
|
|
251
|
+
experiments/results.py results table from traces (experiments/RESULTS.md)
|
|
252
|
+
traces/ committed runs (.jsonl) + index.html
|
|
253
|
+
workspace/ files produced by runs (git-ignored)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Every step's trace record carries the exact rendered context the cell saw and its block index, so
|
|
257
|
+
the viewer can show the block-level diff between consecutive steps: what appeared, what was
|
|
258
|
+
forgotten or compressed away, what was rewritten. Click a lane or a step in the timeline.
|
|
259
|
+
|
|
260
|
+
Defaults: `claude-opus-5` for cells and helpers, adaptive thinking with summarized display,
|
|
261
|
+
effort `medium` for cells and `low` for helpers. Override with `TOWN_MODEL`, `TOWN_HELPER_MODEL`,
|
|
262
|
+
`TOWN_EFFORT`, `TOWN_BUDGET`, `TOWN_MAX_CELLS`, `TOWN_MAX_STEPS`, `TOWN_CONCURRENCY`, `TOWN_TAIL`.
|
|
263
|
+
|
|
264
|
+
## What happened when it ran
|
|
265
|
+
|
|
266
|
+
All runs are in `traces/` (`traces/index.html` is the viewer over all of them; each `.jsonl` has the
|
|
267
|
+
full record, including the exact context every cell saw at every step). Cells and helpers on
|
|
268
|
+
`claude-opus-5` via the gateway; no prompt caching anywhere; the last four steps of each cell
|
|
269
|
+
replayed as a signed-thinking transcript.
|
|
270
|
+
|
|
271
|
+
| run | cells | engrams | calls | in tok | out tok | seconds | rewinds | things the human saw |
|
|
272
|
+
|---|---|---|---|---|---|---|---|---|
|
|
273
|
+
| fork_merge | 4 | 4 | 12 | 50k | 7.8k | 111 | 0 | 1 |
|
|
274
|
+
| memory (budget 3.5k) | 1 | 13 | 15 | 61k | 10.5k | 274 | 0 | 1 |
|
|
275
|
+
| comms | 4 | 0 | 18 | 66k | 3.3k | 413 | 0 | 1 |
|
|
276
|
+
| nested (depth 3) | 7 | 3 | 26 | 100k | 9.5k | 233 | 0 | 1 |
|
|
277
|
+
| split | 3 | 0 | 8 | 31k | 3.6k | 96 | 0 | 3 |
|
|
278
|
+
| cluster (6 → 2) | 9 | 3 | 43 | 190k | 24.3k | 246 | 0 | 1 |
|
|
279
|
+
| rewind | 2 | 4 | 13 | 47k | 8.2k | 159 | 1 | 1 |
|
|
280
|
+
| headlong chat | 1 | 0 | 5 | 17k | 1.6k | 208 | 0 | 3 |
|
|
281
|
+
| ledger (24 tasks, 8 workers) | 9 | 0 | 11 | 48k | 5.8k | 102 | 0 | 1 |
|
|
282
|
+
| browser, no reviewers (4 orgs, 8 tasks) | 14 | 1 | 180 | 3.5M | 0.2M | 715 | 0 | 1 |
|
|
283
|
+
| browser, with reviewers (3 orgs, 17 tasks) | 29 | 11 | 562 | 10.2M | 0.29M | 2551 | 0 | 1 |
|
|
284
|
+
| bigger browser (5 districts, 45 tasks; cut off) | 29 | 43 | 769 | 16.9M | 0.51M | 3192 | 0 | 0 |
|
|
285
|
+
| bigger browser, concurrency 200, cache on | 28 | 14 | 470 | 3.1M + 7.0M cached | 0.37M | 2183 | 0 | 1 |
|
|
286
|
+
| language, first run (root retired by harness) | 26 | 4 | 1111 | 8.5M + 45.3M cached | 1.15M | 2228 | 0 | 0 |
|
|
287
|
+
| language, complete | 59 | 32 | 1654 | 11.2M + 60.0M cached | 1.49M | 11659 | 0 | 1 |
|
|
288
|
+
|
|
289
|
+
**rewind.** Asked for ∫₀¹ sin(1/x) dx to 8 digits, the root tried uniform composite Simpson for
|
|
290
|
+
three steps, watched it stall near five digits and wander, then called `ctx.history()` and
|
|
291
|
+
`ctx.rewind(1, intent=...)`. The intent it wrote: "Uniform composite Simpson on sin(1/x) over [0,1]
|
|
292
|
+
stalls near 5 digits and then wanders non-monotonically … infinitely many oscillations as x→0 make
|
|
293
|
+
equal-spaced quadrature hopeless … Lesson: for endpoint-oscillators, substitute first." It kept one
|
|
294
|
+
block across the rewind, the abandoned branch became engram e2, and it also forked a child from the
|
|
295
|
+
step-1 snapshot (`fork(..., at=1)`) to try Gauss-Legendre per period. Root (via u=1/x and the
|
|
296
|
+
closed form sin(1) − Ci(1), in `decimal`) and child agreed to 15 digits: 0.50406706190692837.
|
|
297
|
+
|
|
298
|
+
**fork_merge.** Three method-cells forked with `carry="summary"`, waited, merged. The synthesis
|
|
299
|
+
kept the exact sieve as ground truth and ranked the estimators; every child also wrote its result
|
|
300
|
+
into an engram with `mem.store` before finishing.
|
|
301
|
+
|
|
302
|
+
**memory.** With a 3.5k-token budget the cell was auto-compressed repeatedly and ended with 13
|
|
303
|
+
engrams, most of them written explicitly with `mem.store` as it learned not to trust
|
|
304
|
+
auto-compression with its findings. The closing paragraph cited a concrete quirk from all eight
|
|
305
|
+
modules.
|
|
306
|
+
|
|
307
|
+
**comms.** Three siblings with empty contexts coordinated through `board['words']`, one `send()`
|
|
308
|
+
of a top-5 list, and two completion messages, without routing data through the root. Slow because
|
|
309
|
+
two cells chose to poll with `time.sleep`, which is also what exposed the shutdown bug fixed in
|
|
310
|
+
`Cell.run_code`.
|
|
311
|
+
|
|
312
|
+
**nested.** Root → two leads → four workers; leads merged their workers, root merged the leads.
|
|
313
|
+
|
|
314
|
+
**split.** The root gathered raw material, then `split()` had a helper partition its blocks into
|
|
315
|
+
two children by topic; each child got only its share and finished in one step.
|
|
316
|
+
|
|
317
|
+
**cluster.** Six investigators with overlapping foci were forked in one call and the root called
|
|
318
|
+
`cluster()`. The helper grouped them into floats and ordering, each group was synthesized into a
|
|
319
|
+
new aggregate cell that continued the work, and the sorting singleton finished on its own.
|
|
320
|
+
|
|
321
|
+
**headlong chat.** The root finished the initial task, was revived by each later human message with
|
|
322
|
+
its old context and namespace intact, and answered each in one step.
|
|
323
|
+
|
|
324
|
+
**ledger.** The root put 24 tasks on the ledger and hired 8 Sonnet workers into an org. Every task
|
|
325
|
+
was claimed exactly once and finished by its claimant; no claim was ever auto-released. But only
|
|
326
|
+
two workers ever won a claim (17 and 7 tasks): a worker loops `work.next(wait=True)` inside one
|
|
327
|
+
Python call, so the first two to reach the ledger drained it before the other six had taken their
|
|
328
|
+
first step. Distribution across cells needs either a per-claim step (slower, fairer) or a ledger
|
|
329
|
+
that rations claims per cell.
|
|
330
|
+
|
|
331
|
+
**browser, first attempt.** The root formed five orgs with leads, wrote interface contracts, and
|
|
332
|
+
hired leads and workers on `claude-opus-4-5`, which rejects adaptive thinking; the gateway dropped
|
|
333
|
+
every one of those streams, so all eight children burned their step budgets in 160 seconds without
|
|
334
|
+
a single model call, and the root, told by the harness that each child had died, wrote the whole
|
|
335
|
+
package itself (50 tests passing). Fixed in the harness: per-model request parameters, model
|
|
336
|
+
validation at fork time, and a failed call no longer counting as a step, with backoff, fallback to
|
|
337
|
+
the default model after three failures, and an explicit give-up after six (ten minutes of retries
|
|
338
|
+
for overload and other transient errors).
|
|
339
|
+
|
|
340
|
+
**browser, second attempt** (`traces/20260903-092542-browser.jsonl`, the default run in the viewer).
|
|
341
|
+
The root wrote `CONTRACT.md`, posted it on `town`, created four orgs (fetch, parse, render, shell),
|
|
342
|
+
forked a lead into each, and later hired two Sonnet builders per org. Each lead posted a detailed
|
|
343
|
+
contract for its area on its org board and a summary on `town`, put implementation and test tasks
|
|
344
|
+
with dependencies on the ledger, and reviewed. Builders pulled 8 ledger tasks under 15 file locks.
|
|
345
|
+
A shell worker read another org's `render.py`, found that `page_title()` returned an empty string
|
|
346
|
+
because of a short-circuit in `_text_content()`, and posted the bug on `town`; the render lead fixed
|
|
347
|
+
it. The root ran the whole suite: 97 tests passing (fetch 16, parse 16, render 35, shell 30), in 715
|
|
348
|
+
seconds and 180 model steps. The log never showed the run's final two lines, which I first read as
|
|
349
|
+
a hang in interpreter shutdown and papered over with a hard exit. It was not a hang: cell output
|
|
350
|
+
capture used the process-global `redirect_stdout`, concurrent threads restored each other's streams
|
|
351
|
+
out of order, and the last prints went into a dead buffer while the process exited normally. The
|
|
352
|
+
capture is now per thread, the hard exit is gone, and a test with four workers cancelled mid-wait
|
|
353
|
+
exits two seconds after the root finishes. `kill -USR1 <pid>` still dumps every thread's stack.
|
|
354
|
+
|
|
355
|
+
**browser, with reviewers** (`traces/20260903-153244-browser.jsonl`, the default run in the
|
|
356
|
+
viewer). Same task, with the team-shape guidance and roles in place. The root formed three orgs
|
|
357
|
+
(net, doc, ui), forked a lead into each, and hired builders, an adversarial reviewer and a fixer
|
|
358
|
+
per org plus one town overseer: 29 cells, Opus 5 for leads and reviewers, Sonnet 5 for builders and
|
|
359
|
+
fixers. 17 ledger tasks, 73 board posts, 113 direct messages, 19 file locks. The reviewers did what
|
|
360
|
+
they were there for: 20 reviews, 5 rejections, each of which opened a `fix` task that a fixer
|
|
361
|
+
pulled and a reviewer re-checked, and every one of the rejections named a real defect: a corrupted
|
|
362
|
+
back-stack when `open()` failed, `follow()` with a non-integer, an untested `main()`, `render`
|
|
363
|
+
dropping block children of `<li>`/`<p>`, and a multi-blank-line bug on anonymous inline runs. One
|
|
364
|
+
fix went through three rounds before a fresh reviewer accepted it. The chain of custody held:
|
|
365
|
+
every task was accepted by someone other than its author. Final suite from the root: 118 tests
|
|
366
|
+
passing, in 43 minutes and 562 model steps, at 10.2M input tokens.
|
|
367
|
+
|
|
368
|
+
**bigger browser** (`traces/20260903-203012-browser_xl.jsonl`, `browser_xl` preset: fetch with
|
|
369
|
+
cache and cookies, a CSS selector subset, tables and forms in the renderer, tabs and bookmarks in
|
|
370
|
+
the shell, a local test server for integration tests). The root founded five districts (net, parse,
|
|
371
|
+
render, ui, qa) with a lead, builders, an adversarial reviewer and a fixer each, plus a town
|
|
372
|
+
overseer: 29 citizens, 26 of them on Opus 5. The ledger carried 45 tasks, 11 of them fix tasks
|
|
373
|
+
opened by the 3 rejections among 42 reviews; builders took 77 file locks and the town exchanged 164
|
|
374
|
+
board posts and 138 direct messages. The qa district ran the full-suite gate repeatedly and the
|
|
375
|
+
overseer posted round-by-round status. At 53 minutes the session's container restarted, which moved
|
|
376
|
+
the outbound proxy under the running process; every call failed to connect from then on and the
|
|
377
|
+
run never wrote its finish event. The package it left behind is 2,557 lines of Python plus tests,
|
|
378
|
+
and `python -m unittest discover -s tests -t .` in its workspace passes **473 tests**. The viewer
|
|
379
|
+
and results table rebuild districts and the ledger from events for a run cut off like this.
|
|
380
|
+
|
|
381
|
+
**bigger browser, second run** (`traces/20260903-220645-browser_xl.jsonl`, the default in the
|
|
382
|
+
viewer). Same preset, relaunched with model-call concurrency uncapped (200) and prompt caching on.
|
|
383
|
+
The root founded five districts (net, dom, view, ui, qa), picked Fable 5.1 for the leads and
|
|
384
|
+
reviewers, Opus 5 for builders and Sonnet for two narrow jobs, and staffed each district with a
|
|
385
|
+
lead, builders, an adversarial reviewer and a fixer plus a town overseer: 28 citizens. 24 ledger
|
|
386
|
+
tasks, 23 non-author reviews with 3 rejections that opened 6 fix tasks, one merge, 40 file locks,
|
|
387
|
+
84 posts. The town finished in 36 minutes and 470 steps, said one thing to the human, and the root's
|
|
388
|
+
final suite matched mine: **438 tests passing** over a 2,909-line package (fetch with cache and
|
|
389
|
+
cookie jar, a CSS selector subset, tables and forms in the renderer, tabs and bookmarks in the
|
|
390
|
+
shell, a threaded fixture server for 28 integration tests). Cost: 3.1M uncached input tokens plus
|
|
391
|
+
7.0M served from cache and 1.0M cache writes, i.e. 63% of input came from the cache. With every
|
|
392
|
+
citizen allowed to think at once, steps came at 13 per minute against 8 per minute in the
|
|
393
|
+
concurrency-12 run.
|
|
394
|
+
|
|
395
|
+
**language, first run** (`traces/20260903-230953-lang0.jsonl.gz`, the `lang` preset: a statically
|
|
396
|
+
typed language with lexer, parser, type checker, compiler, VM, standard library, REPL, formatter,
|
|
397
|
+
linter, debugger, test runner, docs, benchmarks and 30 example programs; organization
|
|
398
|
+
unprescribed). The root founded three districts, core, tools and content, and a default-nesting
|
|
399
|
+
bug (fixed since) chained them core → tools → content instead of siblings; it staffed them with
|
|
400
|
+
leads, builders, a reviewer and a fixer each plus two overseers, 26 citizens, and wrote a language
|
|
401
|
+
spec first. In 37 minutes the town put 77 tasks on the ledger, finished 30, failed 18 (mostly
|
|
402
|
+
example programs whose expected output did not yet match), reviewed 25 with 7 rejections, took 136
|
|
403
|
+
file locks and exchanged 179 posts and 192 direct messages, at 1,111 model calls with 84% of input
|
|
404
|
+
served from cache. Then the root ran the full test suite inside one Python call, the call exceeded
|
|
405
|
+
the 120-second runaway limit, and the runaway rule retired the root, which ends the town. Verified
|
|
406
|
+
in the workspace: 14,790 lines of Python, 41 example programs, and 497 tests of which 491 pass. Two
|
|
407
|
+
fixes followed: a long call on the root is abandoned rather than retiring it, and the default limit
|
|
408
|
+
is 15 minutes so builds and suites fit. Traces above GitHub's file limit are stored as `.jsonl.gz`;
|
|
409
|
+
every tool reads both.
|
|
410
|
+
|
|
411
|
+
**language, complete** (`traces/20260904-000225-lang.jsonl.gz`, the default in the viewer). Same
|
|
412
|
+
preset, under the fixed harness. The root wrote a contract first (grammar, types, standard library
|
|
413
|
+
signatures, opcode list, error format, exit codes), founded three sibling districts along the
|
|
414
|
+
dependency seams, core, tools and content, and staffed each with a lead, builders, a reviewer, a
|
|
415
|
+
fixer and an overseer: 59 citizens over the run, Opus 5 for most, Fable 5.1 for five leads and
|
|
416
|
+
reviewers, Sonnet for six narrow jobs. It never split a district, but it did reshape itself twice:
|
|
417
|
+
when the first core builders stalled for twenty-five minutes with everyone else waiting, the root
|
|
418
|
+
retired seven of them and re-hired six with direct file assignments instead of ledger pulls; and
|
|
419
|
+
when tools built against the spec drifted from the real core, it switched them to building against
|
|
420
|
+
the core. On the ledger: 80 tasks, 89 reviews with 20 rejections, every task accepted by a
|
|
421
|
+
non-author; 134 file locks, 159 posts, 32 engrams. Five citizens died after an hour of consecutive
|
|
422
|
+
gateway failures each, and the town carried on without them. Wall time 3 hours 14 minutes, 1,654
|
|
423
|
+
model calls, 79% of input tokens served from cache. Verified in the workspace: 15,227 lines of
|
|
424
|
+
Python, **706 tests passing**, and all **40 example programs** running to exit 0, among them a JSON
|
|
425
|
+
parser, a Brainfuck interpreter, a raytracer that writes a PPM, a Sudoku solver, a Markdown-to-HTML
|
|
426
|
+
converter, Dijkstra, N-queens, Game of Life and a Mandelbrot renderer, all written in the language
|
|
427
|
+
the town built.
|
|
428
|
+
|
|
429
|
+
Two harness bugs came out of that run. A builder's own `except Exception:` swallowed the `done()`
|
|
430
|
+
signal, so it kept running after the harness thought it had ended and claimed a fix task that was
|
|
431
|
+
then stuck; `CellDone` is now a `BaseException` and the ledger refuses claims from ended cells.
|
|
432
|
+
And cell output capture used the process-global `redirect_stdout`, which concurrent threads restore
|
|
433
|
+
out of order; output is now routed per thread.
|
|
434
|
+
|
|
435
|
+
Getting this run to happen took four launches: the gateway returned "overloaded" for every model
|
|
436
|
+
for most of an hour and a half. Cells now wait out transient failures for a configurable window
|
|
437
|
+
(`TOWN_PATIENCE`) instead of dying.
|
|
438
|
+
|
|
439
|
+
## Observations
|
|
440
|
+
|
|
441
|
+
- **The harness-in-namespace idea works.** The model uses `ctx.note`, `mem.store`, `fork` with a
|
|
442
|
+
list of foci, `wait`, `merge`, `send`, `board` naturally, without examples. It also invents
|
|
443
|
+
reasonable protocols ("you are B, send your top-5 to c4").
|
|
444
|
+
- **Single-turn re-rendering is fine for the model.** No transcript, no replayed thinking, and the
|
|
445
|
+
entity still keeps a coherent thread through summaries, notes and results. Thinking summaries as
|
|
446
|
+
editable text (`think` blocks) are low-salience and are the first thing compressed, which is right.
|
|
447
|
+
- **Auto-compression is a blunt instrument; explicit memory is sharper.** Under a tight budget the
|
|
448
|
+
model shifted to writing engrams itself. The memory hint line in each render (lexically related
|
|
449
|
+
engrams) was what made recall cheap.
|
|
450
|
+
- **The town reshapes itself, and not through the operations I built for it.** With nesting,
|
|
451
|
+
splitting, merging and clustering all available, the language town used none of them; it retired
|
|
452
|
+
stalled builders and re-hired with a different work protocol, and re-pointed a whole district at a
|
|
453
|
+
different source of truth. The reshaping operations that matter most so far are hire and retire.
|
|
454
|
+
- **Cluster is the strangest and most promising op.** Aggregating *in-progress* cells into a new
|
|
455
|
+
cell that inherits a synthesis of their contexts is a real morph, not a fan-in. The retired cells'
|
|
456
|
+
notes survive through the synthesis; their namespaces do not.
|
|
457
|
+
- **Rewind with intent is cheap and the model uses it well.** The intent it wrote is a better
|
|
458
|
+
record of the dead end than the dead end itself, and the engram keeps the numbers.
|
|
459
|
+
- **I/O discipline held.** Across eight runs and 140 model calls, 12 things were said to the human.
|
|
460
|
+
- **Diagnose from the trace, not the terminal.** The trace said every long run finished; the
|
|
461
|
+
terminal said otherwise, and the terminal was the thing that was broken.
|
|
462
|
+
- **The harness must fail loudly to the parent, not just to the trace.** In the first browser
|
|
463
|
+
attempt the root recovered only because the death of each child arrived in its context as a
|
|
464
|
+
message. The fix is to make death rarer, not to hide it.
|
|
465
|
+
- **Costs are real.** Without caching every step re-sends the whole context; the cluster run was
|
|
466
|
+
190k input tokens for 43 calls, and the browser build 3.5M input tokens for 180 calls.
|
|
467
|
+
- **The gateway is the scaling limit, not the harness.** Fourteen cells were comfortable; the runs
|
|
468
|
+
that failed did so because Opus 5 was overloaded upstream, which is why cells now wait through
|
|
469
|
+
overload for ten minutes and why worker models are chosen per fork. That is the price of the experiment, and it was the premise.
|
|
470
|
+
|
|
471
|
+
## Ideas not yet built
|
|
472
|
+
|
|
473
|
+
- Namespace inheritance on fork (pickle what pickles) so children get functions, not just prose.
|
|
474
|
+
- A structural merge (union of blocks with provenance) next to the synthesizing one, and merges
|
|
475
|
+
that the merged cells can see coming (a proposal via `send`, a "do not fold before step N" note).
|
|
476
|
+
- Salience decay over steps and a `ctx.reflect()` that rewrites the whole context in the cell's own
|
|
477
|
+
voice every N steps (the context as a living document rather than an append log).
|
|
478
|
+
- Cells with different models/efforts chosen by the entity (`fork(..., model="claude-haiku-4-5")`
|
|
479
|
+
is wired but untested).
|
|
480
|
+
- Semantic recall (embeddings) instead of lexical; automatic reminiscence that injects an engram
|
|
481
|
+
when a cell's thought strongly matches one.
|
|
482
|
+
- A "nervous system" view: the trace HTML already draws lanes and fork/merge/cluster edges; a live
|
|
483
|
+
version would be the right human-facing surface for an entity that mostly stays quiet.
|
|
484
|
+
|
|
485
|
+
## Releasing
|
|
486
|
+
|
|
487
|
+
Tagging `vX.Y.Z` runs `.github/workflows/publish.yml`, which builds the sdist and wheel and uploads
|
|
488
|
+
them to PyPI through trusted publishing (no token in the repo). One-time setup on PyPI: add a
|
|
489
|
+
pending publisher for project `town` with owner `dwahdany`, repository `town`, workflow
|
|
490
|
+
`publish.yml`, environment `pypi`; then create the `pypi` environment in the GitHub repository
|
|
491
|
+
settings. Bump `version` in `pyproject.toml`, tag, push the tag. Locally, `uv build` produces the
|
|
492
|
+
same artifacts and `uvx --from dist/town-*.whl town` exercises them before a release.
|