kestrel.markets
Version:
A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.
535 lines (476 loc) • 41.5 kB
Markdown
# The agent-requested, emergent View authoring loop
**Status:** **Accepted (owner, 2026-07-13)** — the v1 slice is under implementation (epic
kestrel-d72; the Percept v5 emergence study's measurement instrument). Extends ADR-0001 (one
language, four statement kinds — `View` is already one of them), ADR-0009 (the screen is measured,
not designed), ADR-0011 (the Blotter projector is a pure function of the Bus), ADR-0012 (the
Simulator seam — `open`/`decide`/`close`, the `AgentTurn`, the determinism line at the returned
turn), ADR-0013 (the BYOK live harness, the versioned prompt-profile, the controlled/optimized
divisions), and ADR-0019 (the kernel leads every frame). Its sibling ADR-0030 (**the grammar is
measured, not designed**) records the principle that this loop's aggregated authoring-failure
clusters are the evidence for BOTH prompt refinement AND grammar/syntax evolution.
> **Amendment (2026-07-16).** ADR-0040's rule that **deliberation consumes tape time**
> overtakes the framing in §3 and §7 that *"a view request costs attention, not time."*
> Under ADR-0040 a wake's deliberation — including the deliberation spent authoring or
> reading a view — is measured wall time recorded on the Bus and lands the returning
> control later on the tape. The §3 T-5m / date-blind *look-ahead* invariant (a view
> reveals no future data, advances no clock of its own) stands unchanged; what is amended
> is only the "costs no time" half — under a moving clock, thinking is never free.
## Owner-confirmed defaults (2026-07-13) — the answers to the Open questions this ADR builds to
The open questions below were answered by the owner; the v1 slice builds to these. They are recorded
here so the ADR reads as accepted, not as a design still awaiting input:
1. **Scope (Open q1):** OPEN-only for v1. Mid-session `decide()` shopping is a measured-separately
follow-up (its cost regime differs — a standing book is already managing).
2. **Budgets (Open q2):** `viewRequestCap = 3` (a small integer cap on lenses) and a conservative
`authoringTokenBudget` default, both **config axes** (a different budget is a different grid
column). The production values remain an owner call, as `min_replicates` was in ADR-0012.
3. **Budget-exhaustion terminal (Open q3):** **fail-closed `standDown`** ("view-request budget
exhausted"). NEVER force a Plan — that would reintroduce the exact bad choice this ADR removes.
The same terminal covers repair-budget exhaustion (see the repair-retry sibling loop below).
4. **Discovered-default-View promotion (Open q4):** **owner-gated** freeze — out of scope for the v1
slice. The slice only *logs the evidence* (the emergence log); the `discover` aggregation
projection and any promotion are deferred follow-ups.
5. **Control arms (Open q5):** the **baseline profile stays byte-frozen** (the controlled division);
the loop is a **new `viewshop-v1` profile** (an optimized-division arm). `ConfigId` splits them
into distinct grid columns that never average.
**The repair-retry loop is an approved sibling, unified into the SAME machinery (owner, 2026-07-13).**
The bounded OPEN loop carries *two* kinds of non-terminal iteration under **one** dual budget
(`viewRequestCap` + `authoringTokenBudget`): (a) a **requestView** iteration (re-materialize a
different lens at the same frozen cutoff and re-ask, §2); and (b) a **repair** iteration — an invalid
author (a parse/validate defect) surfaces the **repair-guiding parser error** back to the model and
re-asks it to fix and resubmit, consuming the same budget. Both terminate identically: a valid Plan
supersede / `standDown` / empty pass, or **budget-exhaust → fail-closed `standDown`**. This unifies
what were two separate "ask again in-window" mechanisms into one bounded loop; the parser's
fail-closed pass (ADR-0013 (a), the three m9i outcomes) is the *classification* the driver loop reads
to decide repair-vs-terminate — it stays the terminal outcome only once the budget is spent.
Builds on the single-source **pane catalog** (a parallel workstream — a pane registry
`{ id, title, description, attribution, tokenCost, builder }` that `render` / `View` / the prompt
all read from one place; this ADR designs against that shape and does not assume it has landed) and
on the **brief-rendering + `brief_hash`** workstream landing alongside it (the OSS runtime renders a
platform-supplied `Brief` into the prompt and binds its content hash into grade provenance). Also
folds in **KM-ADR-0026** (kestrel.markets — "Language is interface: Brief, Journal, and the
pre-registered Thesis"): the Brief/Mandate two-channel split, the hard guard that a Brief can never
enter admission, and the mode-collapse thesis (realized behavioral diversity is read from blotters,
never from prompt-space). Depends on nothing new in the determinism core.
> This ADR specifies a design that is being co-shaped live. It is precise enough to implement from,
> but it must be reviewed and the open questions (§Open questions) answered before any code lands.
## Context
Today the slow authoring path is a single shot. At the OPEN vantage the Simulate driver projects a
frozen, date-blind briefing from the bus prefix bounded by the author acting cutoff (`cutoffTs`,
T-5m by default, enforced by kestrel-m9i.11 — `src/session/simulate.ts` ~L895), renders it under the
config's **one** default View, and calls `agent.open(briefing)` **exactly once**. Whatever the agent
returns is routed to the Bus and captured at `OPEN_ORDINAL`. The agent gets one look at one screen
and must either arm a Plan or stand down.
That forces a bad choice. A capable model handed a screen that is missing the one pane it needs to
form a thesis — the GEX profile, the IV-skew term structure, deeper tape, the failed-break count —
has only two moves: **force a Plan it does not believe** (bad standing authority, graded as such) or
**stand down** (no authority at all, and the horizon is wasted). Neither is what a good trader does.
A good trader asks for a different look at the *same* market and then decides.
This is also the mechanism the research program calls for. `docs/paper/research-program.md`
(§"Percept v5 emergence study", §"Track B — measure the reasoning prosthetic") wants the default
screen **discovered, not hand-blessed**: "a frontier-model emergence cohort chooses panes under
opportunity cost; only the converged layout becomes a stable SFT/RL curriculum … Log every pane
addition/removal and its stated reason. Cross-agent convergence and randomized pane ablations
determine which panes earn default status. Freeze the discovered View before small-model SFT/RL." An
agent that can *ask for panes* under a token budget, with every request logged, is exactly the
instrument that experiment needs. ADR-0009 already committed to "the screen is measured, not
designed"; this ADR is how the measurement is taken.
The tension is the platform's usual one, in a new place. The authoring loop is deliberately
non-deterministic (the agent's choice of what to look at is the variable), yet the record path must
stay a pure function of Bus bytes, replay must be byte-identical, the T-5m / date-blind contamination
fence must hold with zero leakage, and the grade must remain a grade of the *Plan*, not of the
shopping. And the affordance is worthless unless the prompt teaches it: **an affordance the prompt
does not advertise is dead.** Every decision below is placed to keep the loop above the determinism
line, keep the view request provably incapable of look-ahead, and keep the whole thing a *measured*
arm rather than a silently-enabled default.
Note two existing surfaces this must not be confused with. `ScheduleWakeAction.view?`
(`src/session/agent.ts`) and `DELIVER <view>` (`WakeStatement`, `src/lang/ast.ts`) both answer *what
to show me at a **future** wake* — a standing, one-shot selection, no re-ask. The loop here answers a
different question: *re-materialize a different lens **now**, at the same frozen vantage, and ask me
again* — iterative, pre-commit, and entirely inside the T-5m window. Both draw panes from the one
catalog; they are otherwise unrelated.
## Decision
### 1. A new authoring outcome: `requestView` (the turn-kind fork, resolved)
At the OPEN vantage the agent may reply with EITHER a normal `AgentTurn` (`supersede` a Plan,
`standDown`, or an empty pass) OR a **view request**: "I am not ready to commit standing authority;
give me *this* lens instead." There is a genuine fork in how to model that reply.
- **Option A — `requestView` as a member of the `Action` union** (`supersede | scheduleWake |
placeOrder | cancelOrder | standDown | requestView`). Uniform: it flows through the one
`validateAction` switch and the one fail-closed parser. But it breaks an invariant ADR-0012 §4
leans on hard — *every `Action` serializes to exactly one Bus event*. A view request must NOT
become a Bus event (see §3, §5), so Option A needs a carve-out in the one place the codebase is
currently carve-out-free.
- **Option B — the agent authors a bare `VIEW` document as its whole turn.** The language already
has a `ViewStatement` (`VIEW <name> [budget N]` + pane lines; `src/lang/ast.ts` L498-519) and
panes are "references + args only — a View never computes." So a `supersede` whose document parses
to a `View` (not a `Plan`) could *be* the request. Maximally faithful to "the agent authors
Kestrel" (ADR-0013 (a)) and reuses the pane grammar verbatim. But it overloads `supersede`, which
already means *register standing authority*; the driver would have to disambiguate "materialize
this View now and re-ask" from "register this View for future wakes" by document-kind **and**
authoring-phase. That is subtle, and a phase bug would silently arm or silently loop.
- **RECOMMENDED — Option C, the hybrid.** Model the view request as a **distinct top-level turn
outcome**, not an `Action`, whose **payload is a Kestrel `VIEW` document**:
```jsonc
// the reply is ONE JSON object; it is EITHER a normal turn …
{ "actions": [ … ], "journal": "…" }
// … OR a view request (mutually exclusive with actions):
{ "requestView": { "view": "VIEW probe budget 900\n gex 0dte\n skew 25d\n tape 5m", "reason": "need dealer-gamma + skew before I commit" } }
```
It is a *distinct outcome* (not an `Action`) because it is the **only** agent output that lives
*above* the determinism line and never becomes a Bus event — folding it into the `Action` union
would break the "every Action is a Bus event" invariant. Its **payload is a Kestrel `VIEW`
document** (not a bare pane-name list) so the agent still authors Kestrel, `print(parse(view))`
round-trips, and the panes come from the one catalog/grammar rather than a second ad-hoc vocabulary.
This takes the control-flow clarity of A and the grammar-fidelity of B without either's cost.
**Fail-closed composition (the three m9i outcomes stay distinct).** The reply parser
(`parseTurn`, `src/session/harness/prompt.ts`) gains one branch: if the top-level object carries
`requestView`, validate it — `view` is a non-empty string, `parse(view)` succeeds **and** yields a
single `View` statement (a `Plan`/`Wake`/`Grade` payload is a defect), `reason` is a string, and
**no `actions` are present** (a mixed reply is a defect). Any defect ⇒ the existing **fail-closed
pass** (`{ actions: [] }` + a JOURNAL naming the defect): the *invalid-output* outcome, exactly as
a bad `supersede` document is today — never a fabricated `standDown`, never a crash. An
authored-but-well-formed `requestView` is the *authored* outcome (it is a legal move), but a
*non-terminal* one that the driver consumes rather than routes. A provider failure / handshake
timeout stays the third, distinct outcome (unchanged). So the m9i three-way distinction —
*authored / invalid / provider-failure* — is preserved, with `requestView` sitting inside
*authored* as a control sub-outcome the driver routes on.
**The affordance is config-gated.** `requestView` is honored ONLY when the config's author policy
enables the authoring loop (§4). Under the baseline (loop disabled) an emitted `requestView` is an
**unknown/illegal outcome ⇒ fail-closed pass** — the controlled division genuinely lacks the move,
so a baseline model cannot stumble into it.
> **AMENDMENT — Option C wire surface, owner 2026-07-15.** Ratified in the canon-reconciliation grill
> (tracker **kestrel-2sy**; enumeration `docs/adr/NOTE-agent-architecture-reconciliation-0029-0031-0032.md`;
> ratified companion **ADR-0031**). **The canonical view-request surface is the pure-Kestrel
> `SHOW VIEW` directive (ADR-0031 §2b), NOT the JSON `{requestView:{view,reason}}` key.** Option C's
> JSON key is **demoted to forgiving-ingest compatibility only**: the driver *may* accept a
> `{requestView}` object and normalize it to the same typed request, but the **logged / canonical /
> corpus** form is always the `SHOW VIEW` Kestrel projection. This **amends** Option C's original
> choice — Option C explicitly *rejected* authoring a bare Kestrel verb and picked the JSON key as the
> request-vs-registration disambiguator; ADR-0031 re-supplies that disambiguator *as a grammar keyword*
> (`SHOW VIEW` vs a bare standing `VIEW`), which achieves the same anti-ambiguity guarantee without a
> JSON envelope. **Rationale:** JSON envelopes add heavy token + cognitive overhead vs pure text (the
> percept thesis applied to the authoring side); one grammar = one corpus = one CFG constraint; the
> free-is-licensed corpus (platform L2) must not bifurcate. **Everything else in this ADR survives
> verbatim** — the `requestView` *outcome model* (`AuthoringReply.requestView`, the distinct
> above-the-line outcome that is not an `Action`), the bounded loop (§2), `viewRequestCap` +
> `authoringTokenBudget` (§2), the fail-closed `standDown` terminal (§2/§5), the T-5m/date-blind
> invariant (§3), and the emergence log (§5). Only the **wire byte-surface of the request** changes,
> from a JSON key to the `SHOW VIEW` keyword, routing to the same `requestView` variant. The typed
> `standDown` **Action** in this ADR is unchanged and is a *different layer* from ADR-0031's
> `STAND-DOWN` grammar directive and ADR-0012's `STAND_DOWN` fail-closed sentinel (see ADR-0031's
> ratification-note mapping table — the three spellings are three layers; do NOT mass-rename).
### 2. The bounded authoring loop
The driver's OPEN phase becomes a **bounded iteration**, owned by the driver (`runSimulateSession`),
not the adapter — because materializing a View is *re-projecting the frozen snapshot* (deterministic
core) plus *re-rendering* (adapter), and only the driver holds the frozen snapshot and the pane
catalog. Sketch:
```
view := config.defaultView // the frozen incumbent View
iteration := 0
loop:
frame := materialize(briefingSnapshot@cutoff, view) // SAME frozen prefix, SAME cutoffTs — §3
turn := await agent.open/reAuthor(frame) // §6 — the adapter re-decides in-window
if turn is requestView(v):
if iteration+1 > viewRequestCap OR attentionSpent+lastCall ≥ authoringTokenBudget:
terminal := standDown("view-request budget exhausted") // fail-closed default
break
emergenceLog.push({ ordinal: OPEN, iteration, panes: panesOf(v), budget: budgetOf(v),
reason, usage, latency }) // §5 — evidence, OFF the graded path
view := parse(v) // the requested View is the next lens
iteration += 1
continue
else:
terminal := turn // supersede(Plan) | standDown | empty pass
break
routeTurn(terminal, firstTs) // the ONLY value that crosses to the Bus
captured.set(OPEN_ORDINAL, terminal) // the ONLY turn recordedAgent replays — §5
```
**What terminates the loop.** (a) a `supersede` carrying a `Plan` (the arm — the normal happy path);
(b) a `standDown` (the agent chose to step aside); (c) an empty pass (arm nothing — degenerate but
legal); (d) **budget exhaustion → the fail-closed default `standDown`** ("view-request budget
exhausted", logged). Budget exhaustion never *forces a Plan* — that would reintroduce exactly the bad
choice this ADR removes. The book settles flat, honestly.
**The bound is dual — a request cap AND a token budget.** `viewRequestCap` (a small integer, e.g. 3)
caps the number of lenses; `authoringTokenBudget` caps the cumulative model spend (input + output +
thinking, summed across iterations) so the agent literally *spends attention* and cannot loop
forever even under a generous count cap. Whichever binds first terminates. Both are ConfigId axes
(§4) — different budgets are different grid columns. A `View`'s own `budget` field (the token cost
of the *screen* it asks for) is a separate, per-request quantity the catalog enforces at
materialization, distinct from the loop's `authoringTokenBudget` (the cost of the *shopping*).
**Scope: OPEN-only for v1.** The loop runs in the pre-open authoring window. Extending the same
bounded loop to mid-session `decide()` wakes is a deliberate later step (Open question 1): a wake has
a tighter attention regime and a standing book already managing, so shopping mid-session is a
different cost story and should be measured separately.
### 3. The T-5m / date-blind INVARIANT (hard, non-negotiable)
> **A view request is a LENS on the SAME information horizon (≤ the author cutoff). It NEVER advances
> the clock, consumes a wake, or reveals future data. Requesting a view costs tokens (attention), not
> time.**
Precisely. Fix, once, before the loop begins, the author acting cutoff as an immutable
`(cutoffTs, cutoffSeq)` pair (the T-5m default, kestrel-m9i.11). For every iteration, the materialized
frame is a pure projection of the **identical** frozen prefix:
```
frame_V = render( projectAuthorFrame( briefingSnapshot(events[ts < cutoffTs], …), view=V ), format )
```
Across every View `V` the loop ever materializes, the set of Bus events readable is **identical** —
every one bounded by `seq < cutoffSeq`. Only the field/pane **selection** differs (per research-program
C1: choosing which fields to project is *a different View*, not a look-ahead). Therefore, term by term:
1. **Never advances the clock.** `cutoffSeq`/`cutoffTs` are captured once and are `const` for the
whole loop. No code path in the loop re-reads a later prefix; `briefingSnapshot` reads only
`events` with `ts < cutoffTs` (already enforced, m9i.11).
2. **Never consumes a wake.** The loop is entirely pre-open, at the OPEN vantage; no `scheduleWake`
is routed, no wake ordinal advances. Wakes are strictly post-open.
3. **Never reveals future data.** No materialized View can surface an event with `seq ≥ cutoffSeq` —
there is no parameter by which a View could request one.
4. **The date-blind fence applies identically to every View.** `projectAuthorFrame`'s date-blind
projection and the `scanDateLeak` / `DATE_BLIND_PLACEHOLDER` fence (`src/session/simulate.ts`) run
over *every* materialized frame, so no pane — however exotic — can smuggle a calendar token.
**The guard is structural, not a check-after-the-fact.** Look-ahead is impossible *by construction*
because of two properties, which the ADR requires the pane catalog to honor:
- **Panes are pure functions of the already-frozen projection.** A catalog `builder` receives the
frozen `AuthorFrame` at `cutoffSeq` — **not** a Bus cursor, **not** a clock. It has no channel by
which to read past the cutoff. (This constrains the catalog's `builder` signature: it takes the
projected frame, never raw bus/time.)
- **A View never computes and never widens the horizon.** `View`/`Pane` are references + args only
(`src/lang/ast.ts` L501). A pane window (`5m`, `20d`) *clamps* to `[…, cutoffSeq)`; a window that
would extend past the cutoff yields the in-horizon slice only. A pane whose data does not exist at
`cutoffSeq` resolves to explicit **UNKNOWN** (fail-closed, ADR-0011 C1.4) — never a peek forward,
never a guess.
**Conformance obligations** (mutations that must fail to break the invariant, in the style of
research-program §"conformance mutations"): (i) *same View twice at the same cutoff ⇒ byte-identical
frame*; (ii) *a pane window extending past the cutoff still reads only `seq < cutoffSeq`*; (iii)
*no materialized View, under any pane selection, emits a calendar token* (date-leak fuzz over the
whole catalog); (iv) *a `requestView` never emits a Bus event* (the graded Bus is byte-identical
whether the agent shopped 0 or N times before the same terminal Plan). A "materialize a forward pane"
mutation must be structurally unrepresentable, not merely rejected.
### 4. The prompt / context co-design (the coupling)
The affordance is dead unless the prompt teaches it, so this ADR defines a **new, versioned prompt
profile** — call it `viewshop-v1` — *distinct from* `baseline-v0` (`BASELINE_SYSTEM_PROMPT`,
`src/session/harness/prompt.ts`), which stays byte-frozen. The new profile teaches:
- **that `requestView` is a legal move**, and its grammar (the top-level `{ requestView: { view,
reason } }` shape, the `VIEW` document payload);
- **WHEN to use it**: *if you are not ready to commit standing authority, ask for a different View —
do not force a Plan you do not believe, and do not stand down for lack of a look you could have
had*;
- **the PANE MENU**, generated **from the single pane catalog** — each pane's `id`, `title`,
`description`, `attribution`, and `tokenCost` — so the menu is the *same source* `render` and the
`View` grammar read. The menu is interpolated from the catalog into the system bytes; it cannot
drift from what is materializable, because there is one source.
- **the two prompt channels, kept explicitly separate** (KM-ADR-0026). The agent's context is not
one blob; it is a **Mandate** and a **Brief**, and the prompt must teach it to read both and keep
them apart:
- **MANDATE** — hard, machine-checkable, *narrowing-only*: what the agent may **not** do
(envelope, budget, never-naked, R limits). It is the **only** admission input. The prompt frames
it as a fence, not a suggestion.
- **BRIEF** — soft, directional English: the **goal + philosophy/approach + persona** — what the
agent **should go look for**. It is versioned, content-hashed (`brief_hash`), and rendered from a
platform-supplied artifact; it is bound into grade provenance so "this performance came from this
thesis" is certifiable. **Hard guard (KM-ADR-0026, non-negotiable): the Brief can NEVER enter an
admission check** — mandate/envelope remain the sole inputs to the narrowing algebra. The Brief
directs *perception and authoring*; it never *authorizes* anything.
- **The Brief and the `requestView` loop are directly coupled.** When the agent decides *am I ready
to commit a Plan, or do I request a different View*, it reasons **from its Brief** (goal /
philosophy / persona) against **what the current View shows**. An agent whose Brief says "hunt
failed breakouts" should request the panes that *reveal* failed breakouts (failed-break counts,
the level-interaction pane, deeper tape); a mean-reversion persona should reach for different
panes on the same frozen market. So the requested-View sequence is a *readout of the Brief acting
through perception* — which is exactly what makes it a diversity signal (§5). The prompt teaches:
*read your Brief; if the current View does not let you test the thesis your Brief points at,
request the View that would, within budget.*
- **the T-5m invariant** in the agent's own terms: *a view costs attention, not time; it never
reveals future data; you are looking again at the same moment.*
Because the menu is interpolated from the catalog, **a catalog change changes the system bytes ⇒ the
`promptHash` (`AuthorPolicy.prompt_sha256`) changes ⇒ a new `ConfigId` ⇒ a distinct grid column**.
That is correct: a catalog change is a config change and must never silently contaminate an old
column (ADR-0013 (d), SYSTEM-PROFILE v1.1). `viewshop-v1` is byte-stable *given a fixed catalog*, and
`liveAgent` hashes the exact assembled bytes as it does today.
**This makes the loop a measured arm, not a silent default.** Mapping onto m9i's two divisions
(ADR-0013 (d), which must never be conflated): `baseline-v0` — no `requestView`, the one frozen
default View, held constant across models — is the **controlled** division (raw model capability).
`viewshop-v1` — the loop enabled, the pane menu advertised — is an **optimized**-division arm. Because
`ConfigId` splits them into different columns and the two boards never average, we can *measure*
whether the affordance + prompt improves trading (the `management_alpha` of view-shopping, per
EVALUATION Part 2) rather than assume it. The research program's three-arm shape falls straight out:
*minimal fixed View* and *corrected fixed-v5 View* are two `baseline`-style no-loop configs; `viewshop-v1`
is the configurable cohort.
### 5. Determinism + evidence
**The whole loop is above the determinism line.** Materializing a View is Rendering/projection
(above the line, ADR-0011/0012); the model call is non-deterministic (above the line). The **only**
value that crosses into the graded path is the **terminal** turn (`Plan` supersede / `standDown` /
pass). View-request iterations emit **zero Bus events**. This is the design's keystone: it makes
everything else fall out cleanly.
- **The grade is on the authored Plan, not the shopping** — unchanged from ADR-0012 §5 / ADR-0006.
Two runs that shop differently but land the same terminal Plan produce the **same graded Bus**,
hence the same `SimRunId = sha256(gradedBus)`. Shopping is metadata, not run identity.
- **View-requests are captured as evidence — the emergence log — OFF the graded path.** Extend the
existing off-Bus capture (`TurnCapture` in `live-agent.ts`, `HandshakeCapture` in
`file-handshake.ts`) with per-iteration records keyed by `(OPEN_ORDINAL, iteration)`: panes
requested (ids + args), the `VIEW` document, its budget, the iteration order, the stated reason,
the tokens spent, latency, and outcome. This IS the Percept v5 emergence study's raw data. It is
private application data (Open question 7), never a graded input.
- **`recordedAgent` replays byte-identically — trivially.** `CapturedTurns` holds ONLY the terminal
turn at `OPEN_ORDINAL`. `recordedAgent.open()` returns it directly and **does not shop** (rendering
is above the line and is ignored on replay, exactly as today). No sub-ordinal replay machinery is
needed on the graded path; the emergence log is evidence, not a replay input. This is the payoff
of putting the loop above the line.
- **Aggregation → the discovered default View.** A `discover` projection over the emergence logs
computes, per pane: the frequency it appears in *terminal-committed* authoring loops (optionally
weighted by how well those runs graded), the marginal iteration at which it was added, and
cross-model agreement. Cross-model convergence + randomized pane ablations (research-program
§emergence) nominate a **candidate** default View — the converged request-set. "Discover, then
freeze": the candidate is promoted to a new default View (a new `ConfigId`) and frozen before any
small-model SFT/RL — **owner-gated**, never auto-blessed (Open question 4). The screen becomes
*earned*, which is precisely ADR-0009.
- **The requested-View distribution is behavioral diversity, and mode collapse is the trap
(KM-ADR-0026).** What panes an agent asks for is a fork dimension **co-equal with the Brief/persona
and with the numeric parameters** — a contrarian Brief and a momentum Brief shopping different
lenses on the same frozen market is genuine perspective diversity; N forks of *one* brief+model
shopping the "same" panes is **one trader sampled N times**, not N traders. So two disciplines bind
the emergence analysis:
- **Convergence must be read from realized behavior, not from what agents SAY they want.**
Prompt-space distance is not behavior-space distance. A pane's default-status verdict, and any
"the cohort agrees" claim, must be corroborated against **realized blotter correlation** (the
certified diversity telemetry — trade/holdings overlap v1, pairwise blotter-return correlation
with `effective_bets` v2, KM-ADR-0026), **never** against the raw requested-View strings alone.
Two agents that *asked for* different panes but produced correlated blotters did not diversify;
two that converged on the same panes but traded decorrelated did. The emergence log is the
*stated* signal; the blotter is the *realized* one, and the realized one adjudicates.
- **Do not select the default View on convergence alone.** Selecting panes purely by "most
requested by the best runs" re-collapses the population toward one screen for one style —
decorrelation-aware selection (KM-ADR-0026 `keep(k, …, min_pairwise_decorrelation)`) applies to
*which panes earn default status* just as it does to which traders survive. A pane that only a
high-decorrelation minority requests may still be worth keeping in the catalog even if it never
joins the single default View. Persona is a catalog fork dimension co-equal with the numerics, so
the "default View" may ultimately be **persona-conditioned**, not a single global screen (a
finding to report, not to assume).
### 6. Interaction with the harnesses
Both live and file-handshake harnesses realize the loop; the affordance MUST be symmetric or the
m9i.7 tournament (HTTP × SDK × CLI × MCP) is unfair.
- **`liveAgent` (conversation-cached).** A materialized View is an appended frame — the loop reuses
the growing transcript the adapter already maintains (`live-agent.ts`): each iteration's frame is a
new `user` message, each `requestView` reply a new `assistant` message, so the model **shops with
memory** (it sees its prior requests and the panes it got). The byte-stable prefix (system + pane
menu + prior iterations) is prompt-cached, so the marginal cost of one more lens is mostly the new
frame + reply, not the whole prefix (measured via `cacheReadTokens` — the economics of
shopping-under-cache is itself an m9i.5-style sweep). The adapter change: an in-window re-decide
entry (§Open questions notes the exact seam shape); only the terminal turn is returned for
`CapturedTurns`, the iterations go to the emergence-log sink.
- **`file-handshake` (external CLIs — codex / claude-code): yes, the external CLI gets the same
affordance.** The runtime writes `frame-open.json` + rendered `frame-open.txt` (materialized under
the current View, carrying the pane menu in the rendered text); the brain writes `turn-open.json`.
If that turn is a `requestView`, the runtime materializes the requested View and writes a
sub-iteration frame (`frame-open.1.json` / `.txt`), then blocks for `turn-open.1.json` — looping
under the same dual budget until a terminal turn. Fail-closed identically: an invalid `VIEW` ⇒ pass
(invalid outcome); a handshake **timeout mid-shop** ⇒ the fail-closed default `standDown` (pre-open
there is no standing book to ride). The same `viewshop-v1` menu reaches the external brain through
the rendered frame text, so `first-party-liveagent`, `claude-code`, `codex`, and `opencode` all see
a byte-identical menu and a byte-identical affordance — the matched-interface discipline
(m9i.7 stage-0) extends to view-shopping.
### 7. Token accounting / the attention axis
View-shopping spends tokens, and that spend is part of the efficiency measurement — not free.
- Each iteration is a full model call; its `usage` (input + output + thinking, plus cache splits) is
captured per iteration in the off-Bus evidence, exactly like existing turn usage.
- The authoring-loop tokens are **added to the session's cumulative token spend**, so the
leaderboard's `ev_per_ktoken` attention axis (ADR-0012 §5) already reflects them: **a model that
needs 5 views to commit scores lower on attention-efficiency, for the same EV, than one that
commits from the default View.** That is the intended incentive — the default View exists to be
good enough that shopping is rarely worth its cost.
- A pane's catalog `tokenCost` lets the emergence analysis attribute *how much* of the shopping spend
each pane cost, feeding the **cost** side of the Track-B reasoning-prosthetic Pareto frontier
(research-program: "measures the benefit and cost of moving arithmetic/structure out of the
model"). A pane that is frequently requested but expensive is a different verdict from one
frequently requested and cheap.
- The grade itself is unchanged (EV on the terminal Plan); the shopping cost lands purely in the
attention/efficiency axis and the emergence log.
## Consequences
- **A new agent-output shape, above the determinism line.** `requestView` is the first and only agent
output that never becomes a Bus event. The `Action` union and the "every Action is a Bus event"
invariant are preserved by keeping `requestView` a distinct top-level outcome, not an `Action`.
- **The determinism contract is untouched.** The graded Bus, `SimRunId`, and `recordedAgent`
byte-identity all hold with zero new machinery, because the loop is above the line and only the
terminal turn crosses. Replaying a run that shopped 5 times is identical to replaying one that
committed immediately to the same Plan.
- **Fail-closed stays structural and proportionate.** Invalid `requestView` ⇒ pass + journal (invalid
outcome, distinct from provider failure and from an authored `standDown`); budget exhaustion ⇒
`standDown` (never a forced Plan); a `requestView` under the baseline profile ⇒ pass (the move is
not offered). The three m9i outcomes stay distinct everywhere.
- **The contamination fence is strengthened, not stretched.** By constraining the catalog `builder`
to a pure function of the frozen projection, the T-5m guard becomes structural — a forward-looking
pane is unrepresentable, not merely rejected.
- **The default screen becomes measurable and discoverable.** The emergence log turns "which panes
earn default status" from an opinion into an aggregate over logged, cross-model requests under
opportunity cost — ADR-0009 and the Percept v5 emergence study made executable.
- **The catalog and the prompt are welded.** The pane menu is generated from the one catalog, so the
prompt cannot advertise a pane that does not materialize, and a catalog edit correctly mints a new
`ConfigId`. This raises the bar on the pane-catalog workstream: its `builder` signature (frozen
projection in, rendered pane out) and its advertised metadata (`tokenCost`, `attribution`) are now
load-bearing for both the fence and the prompt.
- **The Brief becomes load-bearing for perception, and a cross-repo seam.** The requested-View
sequence is the Brief acting through perception, so the emergence study measures Brief × pane-menu
jointly. `Brief`/`Journal`/`Thesis` are **platform (kestrel.markets) artifacts** (KM-ADR-0026); the
OSS runtime only *renders a supplied Brief* into the prompt and *binds its `brief_hash` into grade
provenance* — the parallel brief-rendering workstream this ADR builds on. The hard guard rides
along: the Brief reaches the prompt (perception/authoring) but is structurally barred from
admission (the Gate reads mandate/envelope only), so no view-shopping heuristic can smuggle the
Brief into the narrowing algebra.
- **Diversity is measured from blotters, not from the shopping.** The emergence log is the *stated*
perspective signal; certified blotter-correlation telemetry is the *realized* one, and the ADR
binds the default-View discovery to the realized signal — inoculating the emergence study against
mistaking prompt-space variety for behavioral diversity (KM-ADR-0026 mode collapse).
- **New surfaces to build:** the `requestView` outcome + parser branch (`prompt.ts`), the
driver-owned bounded OPEN loop (`simulate.ts`), the `viewshop-v1` profile with catalog-interpolated
menu (`prompt.ts`), the emergence-log capture + `discover` projection, the file-handshake
sub-iteration protocol, and the conformance/date-leak mutations. All additive; no determinism-core
change.
## Rejected alternatives
1. **`requestView` as a bare pane-name list (no Kestrel `VIEW` document).** Rejected: loses
`print(parse(view))` round-trip, abandons "the agent authors Kestrel," and forks a second pane
vocabulary when `ViewStatement` already is the canonical one. The `VIEW` document is the payload.
2. **Pure Option B — overload `supersede`-of-a-`View` as the request.** Rejected as the primary
shape: it conflates *register a standing View for future wakes* (`DELIVER`) with *re-materialize
now and re-author*, forcing the driver to disambiguate by document-kind + phase — subtle, and a
phase bug silently arms or silently loops. (The `VIEW` document is kept as the *payload* of the
distinct outcome — the useful half of B without the overload.)
3. **Put the loop below the determinism line / emit view-requests as Bus events.** Rejected: it makes
the graded Bus depend on shopping, breaks "grade the Plan, not the shopping," forces
`recordedAgent` to re-shop (re-introducing non-determinism into replay), and pollutes `SimRunId`.
Above-the-line is what makes replay trivially byte-identical.
4. **Always-on affordance (no separate profile / no gate).** Rejected: silently enabling it destroys
the controlled/optimized division (ADR-0013) and forfeits the ability to *measure* whether it
helps. It must be a versioned profile and a config-gated arm.
5. **Unbounded shopping.** Rejected: an agent could loop forever or burn arbitrary attention —
violates bounded attention. Hence the dual (count + token) cap and the fail-closed terminal.
6. **A view request that advances the clock or peeks forward (e.g. "show me the next 5 minutes").**
Rejected, non-negotiable: that is look-ahead / contamination. A View is a lens on `seq <
cutoffSeq`, full stop; a pane that would need future data resolves to UNKNOWN.
## Open questions (need owner input before implementation)
1. **Loop scope.** OPEN-only for v1 (recommended) vs. also extending the bounded loop to mid-session
`decide()` wakes. The mid-session cost regime differs (standing book managing, tighter attention);
it should be measured on its own.
2. **Budget defaults.** The production `viewRequestCap` (e.g. 3) and `authoringTokenBudget`. Buildable
now with a conservative default; the production values are an owner call (as `min_replicates` was
in ADR-0012).
3. **Budget-exhaustion terminal default.** `standDown` / arm-nothing (recommended — never force a
Plan) vs. any alternative. Confirm.
4. **Promotion of the discovered default View.** Owner-gated freeze (recommended — like the System
Profile freeze) vs. an automatic promotion rule from convergence stats. Confirm the gate.
5. **The control arms.** Is the controlled division the single frozen incumbent View only, or should
it include a *corrected fixed-v5* fixed-View arm alongside (research-program's three-arm shape)?
This ADR assumes `viewshop-v1` is the third (cohort) arm and the fixed Views are separate no-loop
configs.
6. **Pane-menu cost.** Advertising the full catalog in every system prompt costs input tokens every
call (mostly cache-mitigated). If the catalog grows large the menu itself is an attention cost —
cap it, tier it, or let it grow? A measurement question as much as a design one.
7. **Emergence-log confidentiality.** The requested-View sequences are strategy-bearing (which panes
a strong model wants, in what order). Recommended: emergence logs are private application data
(EVALUATION boundary rules — registry + gitignored run dirs), and only aggregate convergence
statistics are publishable.
8. **One global default View, or persona-conditioned defaults?** If the emergence study finds that
different personas converge on genuinely different decorrelated screens (KM-ADR-0026 — persona is a
fork dimension co-equal with numerics), the "discovered default" may be a *set* of persona-keyed
Views rather than one global screen. This changes what gets frozen for SFT/RL. Recommend: report
the finding; do not pre-commit to a single global View.
9. **The exact in-window re-decide seam.** Whether the adapter loops internally on a driver-supplied
re-materialized frame, or the driver re-invokes a dedicated `reAuthor(frame)` method, or the loop
reuses `decide()` with an `phase: "authoring"` marker on the frame (`wakeOrdinal = OPEN_ORDINAL`).
Recommended: a driver-owned loop calling an explicit in-window re-entry, so the adapter's
conversation reuse handles shopping-with-memory for free and only the terminal turn enters
`CapturedTurns`. Pin the method shape at implementation.