UNPKG

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
# 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.