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.
176 lines (158 loc) • 13.4 kB
Markdown
# The first-party agent harness — a BYOK AI-SDK driver, dual-harness fidelity, the config matrix, and dogfooding the SDK
**Status:** Proposed (kestrel-5zl / kestrel-m9i). Extends ADR-0004 (typed core, text as
projection), ADR-0006 (grade everything, names are data), ADR-0011 (the Blotter projector is
a pure function of the Bus), and the Simulator seam ADR. The byte-stable
inline-comments ADR referenced under Decision (b) is now written and its guarantee delivered:
**ADR-0033** (comments round-trip byte-identically under `print(parse(x))`), so the Decision (b)
dependency below is **satisfied**, not pending.
## Context
The Simulator ADR defines the *seam* — a typed, session-stateful `Agent` (`open / decide / close?`)
consuming a frozen, date-blind ActingFrame and returning an `AgentTurn`, behind one
`runSimulateSession` driver with four adapters. It deliberately leaves the `liveAgent`
adapter's provider boundary — Fork 1's `liveAgent(config, llm: LlmClient)` — **undefined**:
`LlmClient` is a name with no body, and the deterministic core imports no provider SDK today
(`grep` finds no `@ai-sdk`, no `generateText`, no key handling anywhere in `src/`). This ADR
fills exactly that hole: it decides what the first-party agent *is*, how a live model call
sits inside `liveAgent`, and how that call stays fenced off the graded path.
Kestrel already exposes authorship four ways — the api, the cli, the mcp face, and the SDK
(the djm epic). Why build a *first-party* agent alongside them rather than leave every caller
to bring their own? Two reasons, and they are the same reason twice. **Flexibility:** a
built-in, provider-agnostic driver lets an owner point Simulate at any model with a key and
nothing else — no bespoke integration per experiment. **Evaluation:** Simulate is the
platform's primary harness (the Simulator ADR), and the thing it measures is a model-interface
*system*, not a model in the abstract. m9i draws the line the whole research program turns on:
the **controlled** division measures raw model capability (one baseline prompt-profile held
constant across every model); the **optimized** division measures the best deployable system
per model (model-specific variants allowed). Those two divisions must **never** be conflated
(m9i acceptance). A first-party agent is what makes both divisions executable through one
seam, and — because it is built on the public SDK — building it *is* the acid test of that
SDK (Decision f). The agent is the system-under-test for m9i, and the driver of the
Simulator seam.
The tension: the live model call is the one deliberately non-deterministic act in the system
(judgment is the variable), yet the platform's non-negotiables demand a deterministic record,
a byte-stable round-trip, fail-closed authorship, bounded risk, and — for anything OSS —
provably key-free artifacts. Every decision below is placed to keep the wire call *above* the
determinism line of ADR-0011 and the Simulator ADR while giving the benchmark a real, swept, multi-provider
model to test.
## Decision
**(a) The agent authors Kestrel; a parse escape ⇒ STAND_DOWN.** The agent's act at a Wake is
not a tool-call schema or a JSON blob — it is a **Kestrel document** (View / Wake / Plan),
which is decision, record, and executable in one object (ADR-0001, ADR-0004). `liveAgent`
renders the ActingFrame to a prompt, calls the model, and feeds the raw reply straight to the
deterministic `parse()`. Valid surface statements build the `AgentTurn`
(`supersede / scheduleWake / placeOrder / cancelOrder`); a **parse escape becomes a
`standDown` turn** — the fail-closed default (de-arm clean, inventory rides its TP/EXIT, log
the reason, the Session still settles). Output is **never silently repaired**. Following m9i,
the harness keeps three outcomes distinct and never collapses them: *invalid output*,
*provider failure*, and an *explicit STAND_DOWN* the agent chose. `print(parse(text))`
round-trips, so an authored document survives capture and replay byte-for-byte and the golden
fixtures hold.
**(b) Reasoning is captured evidence, not a second authored artifact.** Three separable
channels, each with its own owner, and none of them a mandate:
- **The raw model reasoning trace is captured as free evidence.** Reasoning models already
emit it; capturing it costs no extra generation. The harness records it as wire evidence
(Decision c) — it never asks the agent to *re-summarize* its own thinking, which would cost
tokens to manufacture a worse copy of something already on the wire.
- **Inline comments carry the human-readable "why" *with* the action**, byte-stably, as part
of the authored Kestrel. This was a separate grammar workstream; its ADR is now written —
**ADR-0033 (byte-stable inline comments)** — and its guarantee **delivered**: a `#` comment
round-trips verbatim under `print(parse(x))` and folds into `sha256(print(module))`, so the
reasoning-in-the-document is preserved and tamper-evident once armed. This ADR's dependency is
therefore satisfied, not pending.
- **A JOURNAL note is OPTIONAL and MINIMAL:** a falsifiable pre-hoc commitment (thesis +
invalidation), written *before* arming and graded by a57.2 (claim vs realized outcome).
Whether a JOURNAL commitment is *required* is itself a **swept benchmark variable** in the
config matrix, not a fixed rule — some cells demand it, some don't, and Grade tells us
whether demanding it helps. `AgentTurn.journal → a57.2 pre-hoc author`, `Agent.close? →
a57.2 debrief`; the harness consumes that stream and never re-mints the JOURNAL schema.
**(c) A dual harness: the Vercel AI SDK (BYOK) is the default driver; a direct-provider path
supplies the fidelity rungs.** The default `LlmClient` behind `liveAgent` is the **Vercel AI
SDK** — TypeScript, runs on Bun, uniform multi-provider model-swap — constructed with a
**pinned direct-provider model object** (never a gateway or fallback router) selected by
`AgentConfig`. This is the flexible path: change the model identity, keep everything else.
Alongside it, a thin **direct-provider path** supplies the wire-fidelity rungs the SDK
abstracts away — prompt-cache economics (m9i.5), latency-causal evidence (m9i.6), and
provider-native usage/cache counts as the shadow oracle. Retries, silent fallbacks,
middleware repair, and unsupported-setting coercion are **disabled or fail the cell
explicitly** (m9i.1) — the harness must never quietly turn one config into another. Both
drivers meet the *same* `Agent` seam; **m9i.7 tournaments HTTP × SDK × CLI × MCP × model ×
harness** over identical Frames, so the AI-SDK path is one face under measurement, not a
privileged one. The invariant across both: **the deterministic core imports no provider SDK**
— BYOK and the wire live only at the `LlmClient` seam inside `liveAgent` (or m9i.1's external
runner), and only parsed Kestrel (or STAND_DOWN) plus redacted wire-evidence/usage cross back
into the graded path.
**(d) The AgentConfig axis IS the point of Simulate, and prompt-profile is its most loaded
dimension.** The comparison grid sweeps **model × tokenizer × Rendering/format × temperature
× thinking-level × prompt-profile**. The first five are mechanical. The sixth is where the
two m9i divisions live:
- **prompt-profile = baseline ⊕ model-variant**, resolved to a **hashed, versioned artifact**
(`AuthorPolicy.prompt_sha256` over the exact system bytes). The **active variant-set is the
division selector**: baseline-only, held constant across all models ⇒ the **controlled**
division (raw capability); baseline plus model-specific variants ⇒ the **optimized**
division (best deployable system per model). A run is stamped with which division it belongs
to, and **the two are never averaged into one number** (m9i acceptance) — a controlled
result and an optimized result are different questions and rank on different boards.
- **Confidentiality rides the profile split.** A generic baseline may be public / OSS; a
strategy-bearing variant is private — the same public/private separation the provenance
scrub already enforces (Decision e). The variant-set selector is therefore also the
publish-safety selector.
- Every run's full config is **stamped on its Blotter** as the a57.14 experimental envelope
(`AgentConfig` *is* that envelope's input half — a single definition, never a second one)
and **frozen into a System Profile** (m9i.2 / m9i.8) before any private forward evaluation.
`ConfigId = sha256(canonical config)` using the Blotter's existing sorted-key /
drop-undefined canonicalizer, so any edit mints a new grid column — a config change can
never silently contaminate an old one. Config identity is content; the credential is not
part of it (Decision e).
**(e) BYOK: keys via env / keyring only, scrubbed at capture, artifacts provably key-free.**
The API key is supplied through environment or keyring configuration and injected into the
`LlmClient` at construction. It **never enters `AgentConfig`** (which is content-hashed into
`ConfigId`), **never touches the Bus, Blotter, System Profile, or any committed fixture**, and
a57.14 stamps *model identity*, not credentials. The recording `fetch` that captures wire
evidence **redacts auth headers and key material at write time** — redaction happens before
bytes ever hit disk, not after. Committed OSS artifacts are provably key-free, gated by
`scripts/check-public-provenance.ts` (which already fails the build on private-provenance
leakage; the BYOK rule extends its remit to credentials). No credential in a committed
artifact is an m9i acceptance criterion, and this is how it is met structurally rather than by
convention.
**(f) The built-in agent dogfoods the SDK: it is the first client of djm.5, not a privileged
internal path.** `liveAgent` drives the runtime through the **public TypeScript SDK
(djm.5)** — deliver the date-blind OPEN Frame, accept a Kestrel document or STAND_DOWN,
advance only to eligible Wakes, accept revisions, finalize through the same Bus / Blotter /
Grade — exactly the surface an external caller gets. Building the first-party agent this way
**validates the external surface**: if the SDK is missing something the agent needs, that is a
djm bug found by dogfooding, not a private back-channel the agent quietly uses. The cli face
(djm.6, agent-mode as an SDK projection) and the mcp face (djm.7) are sibling projections of
the same SDK, and m9i.7 tournaments all four. This is the reconciliation of the two drivers
the Simulator ADR flagged: the first-party agent does not fork a second incremental-Session controller;
it is a *client* of djm.4/djm.5.
**(g) Phasing: an eval-internal driver now, graduating to a shipped reference agent later.**
Phase 1 builds the agent as the **benchmark/Simulator driver** — the `liveAgent` adapter and
its BYOK `LlmClient`, exercised through `runSimulateSession`. It is the system-under-test for
m9i and needs no shipped-product polish. It **graduates** to a shipped *reference* agent once
the surface and the config discipline are proven — same seam, same SDK, additional operational
hardening. The phasing changes only maturity and packaging, not the seam or the determinism
contract, so a phase-1 recorded run re-grades identically under the phase-2 agent.
## Consequences
- **A new external dependency enters the tree — but only at the seam.** The Vercel AI SDK and
the pinned provider packages are added, imported **exclusively** inside `liveAgent` /
`src/session/agent.ts` and the m9i external runner. The deterministic core, the projector
(ADR-0011), and the record/replay path stay provider-free; `recordedAgent` and
`fixedPlanAgent` carry no provider import. A BYOK live run touches the wire **once**; every
re-projection, re-grade, and Backtest-through-the-seam is provider-free and byte-identical.
- **BYOK becomes an operational surface.** Keys are an env/keyring concern with a redact-at-
capture rule and a build-time provenance gate; running the benchmark now requires supplying
credentials the platform deliberately refuses to persist. Owner approval gates paid model
calls, System-Profile freeze, and any publication wording (m9i).
- **Building the agent validates djm.** Because the first-party agent is the SDK's first
client, the SDK's completeness is tested by real use before any third party depends on it —
and the cli/mcp faces inherit that validation as SDK projections.
- **The benchmark can now sweep the full system.** With the agent in place, Simulate sweeps
**prompt-profile × model × tokenizer × Rendering × temperature × thinking-level** over a
fixed tape and fixed fill model, producing a grid of graded Blotters and a config
leaderboard (the Simulator ADR). Controlled and optimized divisions run through the same seam and
never share a board. The raw reasoning trace and provider-native usage land as free evidence,
feeding a57.2's claim-vs-outcome grade and the leaderboard's `ev_per_ktoken` attention axis.
- **Reasoning is evidence, not overhead.** By capturing the emitted trace rather than
commissioning a summary, and by making the JOURNAL commitment optional and itself a swept
variable, the harness measures whether structured reasoning helps instead of assuming it and
paying for it everywhere.