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
# 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
# (`sed -n '120,180p'`) costs one cheap tool call, where carrying every
# whole file forever costs every later step. 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.
#
# Deliberately not configured here (operator ruling 2026-08-16): 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. So
# nothing ships bounded: tokens, wall and depth are all unbounded, and a
# ceiling is config an operator adds. To wire one:
# budgets:
# max_total_tokens: 2000000
# max_wall_seconds: 3600
# max_depth: 4