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.
226 lines (198 loc) • 16.9 kB
Markdown
# The human oversight surface — the Caller-selected interactive session over the harness
**Status:** **Proposed — design for owner review (2026-07-14).** Design only; introduces no
new determinism-core plumbing. Extends ADR-0002 (the Pod is the unit of autonomy; humans sit
above the root), ADR-0009 (the screen is measured, not designed), ADR-0012 (the Simulator
seam — `open`/`decide`/`close`, the determinism line at the returned `AgentTurn`), ADR-0013
(the first-party BYOK harness), ADR-0019 (the kernel leads every frame), ADR-0029 (the
agent-requested emergent View authoring loop), and ADR-0031 (the envelope-free pure-Kestrel
turn). **Composes with ADR-0032 (two-tier Strategist + Watcher)**, whose seven questions are now
**owner-approved** — the cascade is a **parallel track** that meets this one at the protocol
contract (clause (h)), not a downstream phase. The parts deliberately scoped out here are
**platform-bound** and take the platform repo's own numbering (drafted as stubs under
`scratchpad/platform-drafts/`): *access tiers & API-key custody*, *the agentic-account risk
model*, and *the Web dashboard Rendering*. No OSS ADR number is reserved for them.
**Nothing here weakens a determinism or fail-closed non-negotiable.** The interactive surface
is a *view over harness state*; only the returned `AgentTurn` crosses into the graded Bus.
## Context
Kestrel is an **agentic** trading platform, not a human trading platform: the human never
places a ticket — agents do. The glossary already says so — *"humans sit above the root; a
pod is the largest thing that runs on its own"* (CONTEXT.md, Pod), and *"a PM never authors
tickets — PM actions are allocations and envelope changes"* (CONTEXT.md, PM). Yet the CLI has
no surface that expresses this: today it resolves one of three renderings (`human`/`text`/
`json`) purely by TTY and flags (`src/cli/context.ts` `resolveOutputCtx`), a bare invocation
prints help, and there is no interactive session at all.
Two facts force a redesign:
1. **There are two Callers with opposite needs.** An **agent** (an automated harness) wants
token-cheap, non-interactive machine output and authors Kestrel grammar back. A **human**
wants an interactive session — the PM/overseer's view of the desk — and *chats*. The same
`kestrel` binary must serve both, and the acceptance test is blunt: **`npx kestrel.markets`
with no flags must do the right thing whether a person pasted it or an agent ran it.**
2. **The human's view already exists as data.** The **Kernel** the runtime projects at every
wake is *"pod-state for a PM"* vs *"book-state for a Trader"* (CONTEXT.md, Kernel). So the
"PM cockpit" is not a new model — it is a **human Rendering of a PM's acting Frame**
(positions, plans, P&L, risk envelope), and CLI vs Web is just the `ascii` vs `html`
Rendering of it (CONTEXT.md, Rendering).
The tension is the platform's usual one: the interactive/oversight layer touches wall-clock,
network, and (on Web) OAuth — none of which may perturb the deterministic runtime. Every
decision below keeps the interactive surface strictly *above* the determinism line.
## Decision
**(a) The Caller is a first-class concept, resolved once.** A new `resolveCaller()`, factored
out of `src/cli/context.ts`, returns whether the invoker is an **agent** or a **human** (plus
the detected harness + detection source). It is the **single source** read by *both* the
render-mode resolver and the platform-bound telemetry — the `Caller` rides a `User-Agent`
**only on requests to the platform** (the substantive analytics is a *platform* ADR; a
pure-OSS-direct run with no platform requests emits none) — so rendering and analytics can
never disagree about who is calling. Detection is **env-first, TTY-second**: the agent
env-var table (`CLAUDECODE`, `CLAUDE_CODE_CHILD_SESSION`, `CURSOR_AGENT`, `CODEX_*`,
`GEMINI_CLI`, `OPENCODE`, `AI_AGENT`/`AGENT`) and `CI` are checked **before** `isatty`,
because an agent can hold a PTY (where `isatty` is true). `Caller` is added to CONTEXT.md.
**(b) Human is the default — but the interactive session launches only on a *confident*
human.** Resolution ladder: explicit flag (`--json`/`--format`/`--agent`) wins; else a
detected agent/CI → machine `text`/`json`, **never** the interactive session; else, if
**stdin and stdout are both real TTYs and no agent env is detected**, a bare invocation
launches the interactive session; else (piped/redirected human) static `text`. **The safety
rule is load-bearing: under any ambiguity, resolve to non-interactive.** A wrongly-launched
TUI hangs an agent waiting on input — the worst failure; guessing "agent" wrong merely gives
a human plain text they can override with a flag. One-shot commands (`parse`, `frame`,
`runs`, …) keep today's per-command `human`/`text`/`json` rendering; the interactive session
is a **new surface**, not a new render mode of every verb. With no account, the session opens
on a **local paper Pod** (Mode = paper/sim; no broker, no signup — the on-ramp).
**(c) The bare, no-flags invocation renders an orientation: live market + task-grouped
commands.** `npx kestrel.markets` with no args produces (1) a **spectator Frame** of current
market conditions (levels/tape for a default watchlist, no acting kernel — spectator is
off the deterministic path, so live data + wall-clock there are fine) and (2) commands
grouped by intent (*see the market / write a plan / test it / review / go live*), never an
alphabetical dump. **Same content, two renderings:** an agent gets it as static `text` (never
hangs); a human gets that same orientation as the *opening view* of the interactive session.
**(d) The oversight surface is one model, rendered across two repos.** The human experience is
a human Rendering of a PM's acting Frame (plus Pod aggregate). The **model is a shared
contract** — the session/oversight types in the OSS `src/protocol` — and *that contract is the
cross-repo seam*. **OSS `kestrel` owns the CLI Rendering** (`ascii`/interactive TUI) and the
model contract; **the platform `kestrel.markets` owns the Web Rendering** (`html` dashboard),
in its own platform-repo ADR. Both consume the one model over the same API, so "one model, two
Renderings" holds *across the repo boundary* — neither surface is a silo, because divergence
would break the shared protocol. OAuth (broker connect) and Stripe (subscribe) are
browser-native and platform-owned, so the CLI **deep-links to Web** for those flows. This ADR
fixes the unification principle and the protocol seam; the Web Rendering's specifics are a
platform-repo ADR, not an OSS one.
**(e) One input box: parse-first, chat-fallback.** The interactive session has a single input
(the Claude-Code pattern). Input that parses as **Kestrel grammar** — including terse
**shortcuts** (`SPY` → a multi-timescale **View**; `SPY 0DTE` → a scoped chain View; full
grammar → Plans/Wakes/owner acts) — executes deterministically. Input that does **not** parse
is **natural-language chat with the PM**, who may research, propose, and *author grammar* in
reply (flowing through the normal path). Shortcuts reuse the existing `View`/`Rendering`
machinery and only ever *show* — they add no authority.
**(f) Oversight is typed acts, not tickets; chat is sugar over authoring.** Every structured
human effect is a **PM/owner act** (allocate/adjust an envelope, arm/de-arm, assign Coverage,
approve/veto) that lands on the **Bus** as an event — auditable, `seq`-ordered, replayable —
exactly like a wake. **Chat has no direct authority**: it is a convenience over authoring +
owner acts, never a fourth mutation path. Two guardrails: **chat never arms risk directly**
(anything that commits risk is an explicit typed act the human confirms — see (g)), and
**malformed-looking grammar surfaces its parse error** rather than silently becoming a
consequential chat instruction (fail-closed: parse escape → don't act; clear prose still goes
straight to chat).
**(g) Risk approval gates capital, not trades (machinery deferred to the platform-bound
agentic-account risk model).** The funded
"agentic account" balance **is the owner envelope** (`plan ⊆ book ⊆ owner`, already in the
kernel). Authorizing the account and **funding it** (money in = more risk capacity) are
**human-signed, OAuth-style Web approvals**; within the funded owner-envelope agents trade
freely at machine speed with **no per-trade approval**; **de-risking is always free** (money
out, pause, de-arm — authority only narrows). Bounded-risk becomes structural: you cannot
lose more than you funded. **Two authorization paths (detail in the platform-bound risk-model
draft):** *CLI-direct
BYO-broker* (IBKR via IB Gateway, Alpaca via BYO API key) authorizes at the user's *own*
broker — **no platform, no Web approval, no Stripe** — with risk = broker balance + local
owner-envelope, live submit human-signed by broker creds, guarded by the BYO-broker
safety-envelope (`kestrel-7o2.9`); *platform-managed* uses the funded agentic-account +
OAuth-style Web approval above. So the **OSS CLI does paper *and* live standalone** via a BYO
broker (local socket or API key); the platform adds managed convenience (OAuth brokers, our
keys, funded accounts). This surface *initiates* the platform flows and deep-links to Web.
**(h) The multi-agent picture: two orthogonal axes; the human sits above the Strategist.**
The "one agent per session" rule in today's code (`simulate.ts` single `opts.agent`) is
already being retired by **ADR-0032**, which splits the in-loop agent into a fast **Watcher**
(small model, wake cadence) and a rare **Strategist** (frontier). This ADR requires that
cascade to run in the **local BYOK agent** (`src/session/harness/live-agent.ts`), not just
the managed service, and to be **rendered in the cockpit** (per Book: the Strategist's
theses/re-frames + the Watcher's in-loop actions). Two axes stay **orthogonal**: **tier**
(Strategist ↔ Watcher, by clock/price) × **org** (PM/Pod ↔ Trader/Book, by allocation). The
**human is one tier above the Strategist** — oversight, not a fourth model.
**"Single agent" never meant "single model" (owner decision, 2026-07-14).** Read the two axes
strictly: **ORG** is one Book now, and a Pod fan-out of N Books is **Phase 2**; **TIER** is
Strategist + Watcher (ADR-0032) — **two models inside that one Book, and that is Phase-0
territory, not Phase 2.** So **Phase 0 = one Book, up to two model tiers.** The consequence is
a schedule fact, not a nuance: the **CLI-session build and the cascade build** (beads epic
`kestrel-8di`, now **UNBLOCKED**) are **parallel tracks that meet at the protocol contract** —
neither blocks the other. The cascade driver issue is accordingly **re-homed** out of the
Phase-2 epic and into `kestrel-8di` (it is `kestrel-1xno.1`, P1). Build is **phased** (claimed,
not pretended): **Phase 0** — the interactive session over one Book (up to two tiers),
buildable now on existing seams (`controller.ts` turn-stream + `live-agent.ts` transcript);
**Phase 1** — the human→agent chat channel, grounded in the live-agent transcript but strictly
*above* the determinism line; **Phase 2** — Pod fan-out (one driver per Book, kernels
multiplexed into panes, aggregated to the PM) — the org "charter → implementation" milestone.
**Tiers are EXPOSED, not hidden (owner decision, 2026-07-14).** Every action carries
**`authoredBy: { tier, model, version }`** in the protocol contract **from day one**, and
**escalation (Watcher → Strategist) is a first-class typed event** — not a flag on a turn. The
reason is principled, not cosmetic: **Kestrel already refuses unattributed judgment.** A
`MODEL` Field is refused at construction without its receipt (source + modelVer + confidence;
CONTEXT.md *Attribution* — *"nothing above CALC goes unattributed"*). An action **authored by a
model** deserves the same rule; hiding which tier decided would be honest about where a
**number** came from and cagey about where a **decision** came from. Two further reasons:
escalation is the **highest-signal event in the system** — *the cheap tier wasn't sure and
called the boss* — and exposing tiers is what lets the human **intervene at the right level**
(re-brief the Strategist vs. correct the Watcher). **UX: exposed by default, collapsible**
("just show me the book") — **never hidden, only folded**, the same discipline as
absent-not-hidden. Because the tier stamp is in the contract from day one, **no Rendering needs
rework when the second tier lights up.**
**(i) The determinism boundary (the invariant under all of the above).** The interactive
layer is a **view over harness state**. Only the returned `AgentTurn` crosses into the graded
Bus (`live-agent.ts`), so panes, chat context, shortcuts, and multiplexing sit *above* the
line and must never perturb graded bytes. A human chat message may be *context* above the line
but **must not alter the returned turn's graded bytes**, or it breaks `recordedAgent` replay.
## Consequences
- **The bare-invocation default flips:** `npx kestrel.markets` on a confident-human TTY opens
the interactive session (on a local paper Pod) instead of printing help. Agents/CI/pipes are
unaffected and never hang.
- **The local agent grows the Strategist+Watcher cascade** (ADR-0032 becomes a shipped,
public-package concern, not platform-only). ADR-0032 moves to this OSS repo as its single
canonical home; its 7 questions are **answered and owner-approved**, so the cascade epic
(`kestrel-8di`) is **unblocked and runs in parallel** with the CLI-session build — the two
meet at the protocol contract.
- **Judgment is attributed, from day one.** `authoredBy: { tier, model, version }` rides every
action in the contract and escalation is a typed event, so the cockpit can always say **which
tier decided** — and the human can intervene at the right level. A Rendering may **fold** the
tier detail; it may never **hide** it.
- **`resolveCaller()` is the one detector**, shared by rendering and telemetry (ADR-0031).
- **A new interactive surface** exists above the determinism line; the agent-facing
`text`/`json` renderings stay byte-stable and the graded Bus is untouched.
- **Runtime `BookState` → `InventoryLedger`** rename is queued (the org `Book` ≠ the
per-instrument ledger); the collision is documented in CONTEXT.md now.
- **Web, tiers/custody, and the risk model are deferred — and mostly land in the *platform*
repo.** The Web-dashboard Rendering, the substantive access-tiers + API-key custody, and the
agentic-account/funding/risk + broker-OAuth machinery all live in `kestrel.markets` (its own
ADRs, its own numbering); the OSS side is the thin CLI client that *initiates* and
*deep-links*. Each concern stays in its right repo, joined by the `src/protocol` contract.
## Resolved (owner-approved 2026-07-14)
Three of the questions this ADR opened are **answered**; they bind the build rather than block
it. Nothing here relaxes a determinism or fail-closed non-negotiable.
1. **ADR-0032's seven questions are ANSWERED and owner-approved** (2026-07-14). This ADR still
*composes with* the cascade rather than owning it, but the dependency no longer blocks:
0032's answers bind, and the cascade is a **parallel track**, not a downstream phase —
clause (h).
2. **The human-message channel — a TYPED BUS EVENT.** A human chat message is an
owner/journal-class event on the Bus: `seq`-ordered, auditable, replayable. The harness feeds
it to the agent **from the Bus**, so `recordedAgent` replay stays **byte-identical** — a
message is visible to a turn **iff `message.seq ≤ frame.asofSeq`**; one arriving mid-turn
lands at a later `seq` and is seen at the **next** wake. There is no mid-turn injection, no
side-channel, no context patch. **Recording ≠ authority**: the returned `AgentTurn` is still
the only thing that can place risk.
3. **Shortcut vocabulary — SHOW-only, fail-closed.** Bare instrument/expiry tokens (`SPY`,
`SPY 0DTE`) resolve **only** to a shortcut, and a shortcut only ever **shows a View**.
**Arming requires an explicit verb** — a `PLAN`/`COVERAGE` statement or a typed act, which
routes through (g)'s capital gate. Under ambiguity, resolution goes to the **shortcut** (the
zero-authority branch): never toward risk.
## Open questions
1. **The shortcut vocabulary *itself*** — *which* token shapes expand to *which* View. The
safety rule is settled (above); the terse grammar is a grammar-*measurement* question
(ADR-0030: measured, not designed), not a protocol one.
2. **Web dashboard specifics** — deferred to its own ADR; this one only fixes the
one-model-two-renderings unification and the CLI↔Web OAuth/billing deep-link.