1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# The **learning loop** (`docs/DESIGN_LEARNING_LOOP.md`) — the basic
# agentic loop plus a reviewer. `litany prime` seeds this file beside
# `basic-agentic-loop.yaml` in `<config-root>/workflows/`, seed-if-absent
# like every other pool entry, so it is there to copy or fork a variant
# from. It is **not** the default: a reviewer is model spend the operator
# opts into, and the basic agentic loop stays exactly today's stock
# behavior (`docs/DESIGN_WORKFLOW_SWITCH.md` §3).
#
# Everything below the `events:` block is the basic agentic loop's own
# declaration, unchanged — the compaction clock this loop rides, the
# retry policy, the tool-output bounds. Only the two bindings marked
# below are added.
#
# To adopt it:
#
# litany config <ws> learning --from default
# # replace the new lineage's workflow.yaml with this file
# litany workflow <ws> <agent> --config learning
#
# Off switch: delete the `dispatch(reviewer)` line. A config with no
# reviewer binding never forks one — policy lives in the config, never in
# code.
events:
user_message:
- dispatch(worker)
worker_return:
- deliver_result
# The reviewer rides the compaction checkpoint (§2 there): it forks off
# the same compaction point as the compactor, so the span it inspects is
# exactly the span about to be squashed out of the transcript. No second
# clock, no new trigger, and never on the critical path — its return is
# consumed, so the reviewed agent never reads a review.
worker_flush:
- dispatch(compactor)
- dispatch(reviewer)
compactor_return:
- land_compaction
# The reviewer's landing (§3 there): one config commit on
# `proposal/<reviewer-id>`, parented on the followed config commit the
# reviewer read, which no lineage points at until `litany proposal
# --accept` fast-forwards it. Shipped since bl-5b62: the action stages
# the proposal, and `litany proposal <ws>` is where an operator reads,
# accepts or rejects it.
reviewer_return:
- stage_proposal
branch_stopped:
- mark_abandoned
- notify_ui
# Intermediate compaction checkpoints (ARCH §2.6–§2.7, §6). The executor
# reads this at each step boundary: when the trigger fires it dispatches a
# compactor off the compaction point — the branch tip, or `HEAD~keep_recent`
# when `keep_recent` is set — and the compactor's return lands by
# rebase-forward (the `compactor_return: land_compaction` binding above):
# the span before the point squashes into a compaction base and the live
# tail replays on top, zero downtime. `trigger` is one of
# `every_n_commits`, `every_t_seconds` (both take `n`), or `on_flush` (the
# agent-elected `flush`, no `n`). `keep_recent` (optional, default 0) keeps
# the most recent commits out of the span; it must stay below `n` under
# `every_n_commits`. `extract_bytes` (optional) caps the extract the
# landing itself derives — `summary/<NNN>.refs.md`, the verbatim user
# messages, error strings, pull-request numbers, commit shas and paths
# the compaction takes out of context, written by code beside the
# compactor's prose (docs/DESIGN_CONTEXT_ECONOMY.md §5.3); omit it and no
# extract is written. Omit the whole block and the branch never compacts.
#
# `n: 60` and `extract_bytes: 8192` are chosen together with the
# `tool_output:` bound below, because the clock counts commits and what
# a commit COSTS is that bound (bl-ce09). A step writes two commits, so
# 60 is about thirty steps. At the shipped 4 KiB-per-result bound a
# tool-heavy step appends a few thousand prompt tokens, so a span of
# thirty steps is tens of thousands — inside every shipped model's
# window, with the retained tail and the pinned head on top. The
# previous `n: 20` fired every ten steps whatever the context held: on
# the measured goals an ordinary conversation compacted six times, and
# each compaction is a model dispatch carrying the whole inherited
# transcript, so the clock cost more than the context it was reclaiming.
# The rule the pair is picked under is: an ORDINARY conversation should
# finish without compacting once, and a long one should compact rarely
# rather than continuously.
#
# `extract_bytes` moved with it for a reason that is not symmetry. Since
# bl-2071 the landing sweeps the span's transcript entries itself, so
# the extract is now derived from the whole span rather than from
# whatever a model happened to nominate — it will actually fill toward
# its cap, and unlike a tool result it stays in context until its
# summary is shed. 8 KiB is about two thousand tokens of references per
# compaction; 32 KiB was a cap nothing reached before and would now be
# paid every time.
compaction:
intermediate:
trigger: every_n_commits
n: 60
extract_bytes: 8192
# Harness-owned retry policy for a step's model call (ARCH §2.10, §4.4):
# brazen never retries — the harness re-invokes `bz` on a retryable
# in-band Error, up to max_attempts, with exponential backoff.
retry:
max_attempts: 3
backoff: exponential
# Bounded transcript projection of tool output (ARCH §3.3, §6). Each
# stream of a tool result (stdout and stderr independently) is bounded
# to its first head_bytes and last tail_bytes before the result envelope
# is rendered; the omitted middle is replaced by a marker stating the
# original byte/line counts and where the full record lives
# (steps/<agent-id>/<NNN>/tools/<tool-id>/output.json — always complete).
# Counts are bytes, never tokens. Omit the block and tool output reaches
# the transcript unbounded.
#
# 2 KiB + 2 KiB is the shipped bound, and it is small on purpose
# (bl-ce09). The number that ships is the one an ordinary conversation
# pays on EVERY step for the rest of its life, so it is chosen against
# the ordinary case and not against the rare one that wants the whole
# capture. At 4 KiB a result is roughly a thousand tokens: a `--help`,
# an `ls -la`, a `git status`, a test summary all land whole or land
# with their two useful ends and a marker between them. The previous
# 16 KiB + 16 KiB made ONE result worth about eight thousand tokens —
# measured, three ordinary calls filled a context, one `find` over a
# home tree put 32,985 bytes into a transcript essentially whole, and a
# conversation reading a repository reached 122,000 prompt tokens by its
# eighth step on `cat` output alone.
#
# What it costs is real and is priced here: a source file read whole is
# cut in the middle, and the model gets the marker instead. That is the
# intended trade — the full capture is on disk, the marker names its
# path and the byte and line counts, and re-reading a named range costs
# one cheap tool call, where carrying every whole file forever costs
# every later step. The range is named for the model rather than
# guessed by it: since bl-cbe0 `read_file` takes `offset`/`limit` in
# lines and every result says which lines it returned, how many the
# file has, and the offset to continue at. Raise both numbers on a
# workspace whose work really is reading long files end to end; that is
# what a severable policy block is for.
tool_output:
head_bytes: 2048
tail_bytes: 2048
# Context files (ARCH §3.3 *Context files ride the next tool result*,
# `docs/DESIGN_CONTEXT_ECONOMY.md` §6). File NAMES, looked for in every
# directory on the path from the enclosing repository's top level down to
# the agent's working directory. Each one the agent has not been shown
# yet is appended to its next tool result, framed <file path="..."> and
# bounded by tool_output above as its own stream; "already shown" is read
# off the transcript, so a compaction that drops the entry shows the file
# again. Omit the block and nothing is discovered.
context_files:
# Tool control (ARCH §3.3 *Tool control*, §6): an adjudicator binary
# consulted before every granted tool invocation executes — it answers
# pass, refuse, or hold (park for out-of-band review). Deliberately not
# configured here: no control ships, and omitting the block leaves the
# tool window unchanged. To wire one:
# tool_control:
# command: /path/to/control
# Whole-tree spend limits (ARCH §6 "Budgets (v0.7)"). One frozen ceiling
# for the whole agent tree, not a per-agent allowance: every driver in
# the tree — root or subagent — checks the tree's total against these
# same numbers, and a dispatch inherits no fresh budget. Checked at
# every model-call boundary before the adapter is invoked; spend, wall,
# and depth are derived from disk each check — no stored counter. Omit a
# limit (or the whole block) to leave that axis unbounded.
#
# What ships bounded is DEPTH, and only depth (bl-c701).
#
# The two SPEND ceilings stay off, for the reason the 2026-08-16
# operator ruling gave: a whole-tree ceiling binds far earlier than its
# number reads, because a root and every agent below it spend one shared
# allowance — an hour of accumulated wall across a tree ends a
# conversation that is working, and raising the number moves that cliff
# rather than removing it. Nothing here bounds tokens or wall; an
# operator who wants a spend ceiling declares one. To wire one:
# budgets:
# max_total_tokens: 2000000
# max_wall_seconds: 3600
#
# max_depth was swept out with them rather than judged on its own, and
# it is a different kind of limit. It is not a spend judgement: it is
# the tree's only prohibition on GROWTH (ARCH §6 "The depth ceiling is
# the tree's only prohibition on growth"). Every agent may dispatch
# children, a parent never blocks, and stopping a parent does not
# cascade downward on its own — so with no depth ceiling a tree that
# re-dispatches itself has nothing structural to stop it. That is not
# hypothetical: the runaway reproduced in yog bl-d023 re-dispatched
# every ~3 seconds per generation and was ended by an operator at
# depth 4, not by the harness. Unlike a token or wall cap, a depth
# ceiling cannot cut off legitimate long work; it refuses only a shape.
#
# Why 5. Depth counts dispatches from the root, which is depth 0, and
# max_depth is the deepest ALLOWED depth: a dispatch that would land a
# child deeper is refused before the fork, so the deepest agent a
# dispatch can create sits at exactly max_depth. Five is therefore a
# root plus five levels of delegation. Observed practice in real fleets
# is two or three levels; five leaves two clear levels of headroom above
# it, while a runaway recursion crosses it in seconds.
#
# The cost, stated rather than hidden: the fork gate makes no
# distinction between a dispatch the model asked for and one the
# workflow ran, so an agent sitting AT max_depth cannot fork a compactor
# either — its compaction checkpoint is refused and the branch steps on
# uncompacted (ARCH §6 "One gate, every dispatch"). That is the second
# reason the number sits above ordinary practice rather than at it: the
# band that cannot compact should be a band nothing ordinarily reaches.
#
# Severable, and already severed for one consumer: delete the two lines
# below and the tree is unbounded on every axis again — config deleted,
# no code touched. yog does exactly that, stripping any top-level
# budgets: block from every workspace it manages at every start (yog
# bl-56af), because a seat holds a dollar ceiling at a conversation's
# birth and an operator watching the board, and two ceilings over one
# concern is the second representation that drifts. So this default
# binds the plain-litany operator — whose fleet nobody is watching,
# which is exactly whose fleet it was filed about.
budgets:
max_depth: 5