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.
139 lines (138 loc) • 7.55 kB
JavaScript
/**
* # session/authoring-loop — the bounded OPEN authoring loop (ADR-0029 §2) + its evidence log
*
* The driver-owned iteration the Simulate driver ({@link import("./simulate.ts")}) runs when the agent is
* viewshop-enabled (it exposes {@link Agent.authoringOpen}): {@link runOpenAuthoringLoop} drives a
* view-request / repair loop under ONE dual budget (`viewRequestCap` + `authoringTokenBudget`) and returns
* the TERMINAL {@link AgentTurn} — only that turn crosses to the Bus + CapturedTurns; the iterations are
* logged as {@link EmergenceRecord}s ABOVE the determinism line (private application data, never a replay
* or graded input). Terminal = a valid `supersede` / `standDown` / empty pass, or a fail-closed `standDown`
* on budget exhaustion (NEVER a forced Plan).
*
* The loop is driver-owned because only the driver holds the frozen snapshot + the pane catalog. It is
* extracted here so that surface — the loop, its dual-budget defaults, the emergence-record shape, and the
* View-document → {@link ViewSelection} mapper it fails closed through — lives beside its own interface. A
* baseline / recorded / fixed run never exposes `authoringOpen`, so this module is inert for it and the run
* is byte-identical (single-shot open).
*/
import { parse } from "../lang/index.js";
import { resolveView } from "../frame/pane-catalog.js";
import { OPEN_ORDINAL } from "./agent.js";
import { standDownTurn } from "../engine/disarm.js";
/** The conservative default cap on non-terminal authoring iterations (owner-confirmed 2026-07-13). */
export const DEFAULT_VIEW_REQUEST_CAP = 3;
/** The conservative default cumulative model-token ceiling for the authoring loop (a config axis). */
export const DEFAULT_AUTHORING_TOKEN_BUDGET = 8000;
/** Local message extractor (the driver's own `msgOf`) — kept private so this module carries no
* back-edge onto the driver it was extracted from. */
function msgOf(e) {
return e instanceof Error ? e.message : String(e);
}
/** Total model spend for the loop's `authoringTokenBudget` (input already includes the cache splits). */
function authoringSpend(u) {
return u.inputTokens + u.outputTokens + u.thinkingTokens;
}
/** Map a validated Kestrel VIEW document to the renderer's {@link ViewSelection} — pane names +
* budget + the authored pane ARGS (kestrel-wa0j.3: args thread through `resolveView` →
* `materializePanes`; a pane that does not understand an arg fails closed there, never a silent
* drop). Throws if it is not a single `View` (defensive — the harness parser already validated
* this shape in `parseAuthoringReply`). Exported for the pane-args threading test. */
export function viewSelectionOfDocument(doc) {
const node = parse(doc);
const stmt = node.kind === "module" ? node.statements[0] : node;
if (stmt === undefined || stmt.kind !== "view") {
throw new Error("requestView.view is not a single VIEW statement");
}
return {
name: stmt.name,
panes: stmt.panes.map((p) => (p.args.length > 0 ? { name: p.name, args: p.args } : { name: p.name })),
...(stmt.budget !== undefined ? { budget: stmt.budget } : {}),
};
}
/**
* Drive the bounded OPEN authoring loop (ADR-0029 §2) and return the TERMINAL {@link AgentTurn}. TWO
* kinds of non-terminal iteration share ONE dual budget (`viewRequestCap` + `authoringTokenBudget`):
* - a **requestView** → materialize the requested View (validated against the one catalog, fail-closed
* on an unknown pane) and re-ask under the SAME frozen snapshot / cutoff (the T-5m guard, §3 —
* structural, since the pane builders are pure functions of the frozen frame);
* - an **invalid author** → surface the repair-guiding error and re-ask (repair).
* Terminal = a valid `supersede` / `standDown` / empty pass, or — on budget exhaustion — a fail-closed
* `standDown` (NEVER a forced Plan). Only this returned turn crosses to the Bus + CapturedTurns; the
* iterations are logged as evidence, above the determinism line. The loop is driver-owned because only
* the driver holds the frozen snapshot and the pane catalog.
*/
export async function runOpenAuthoringLoop(agent, briefing, p) {
const standDown = (reason) => standDownTurn(reason);
let iteration = 0; // NON-terminal iterations consumed so far (view-requests + repairs, shared)
let tokensSpent = 0;
let view; // the incumbent default View (undefined ⇒ the frame default)
let repairError;
for (;;) {
const step = await agent.authoringOpen(briefing, {
...(view !== undefined ? { view } : {}),
...(repairError !== undefined ? { repairError } : {}),
});
tokensSpent += authoringSpend(step.usage);
const reply = step.reply;
if (reply.kind === "turn")
return reply.turn; // TERMINAL: supersede(Plan) | standDown | empty pass
// A non-terminal iteration — a view-request OR a repair. Both consume the SAME dual budget; whichever
// bound binds first terminates the loop with a fail-closed standDown (never a forced Plan).
const nextIteration = iteration + 1;
const exhausted = nextIteration > p.viewRequestCap || tokensSpent >= p.authoringTokenBudget;
const latency = step.latencyMs !== undefined ? { latencyMs: step.latencyMs } : {};
if (reply.kind === "requestView") {
let sel;
let viewError;
try {
const candidate = viewSelectionOfDocument(reply.request.view);
resolveView(candidate, "OPEN"); // fail-closed: unknown pane id / reserved kernel id
sel = candidate;
}
catch (e) {
viewError = msgOf(e);
}
p.emergenceLog?.push({
ordinal: OPEN_ORDINAL,
iteration,
kind: "view-request",
model: p.model,
usage: step.usage,
...latency,
budgetExhausted: exhausted,
panes: sel?.panes.map((x) => x.name) ?? [],
view: reply.request.view,
...(sel?.budget !== undefined ? { viewBudget: sel.budget } : {}),
reason: reply.request.reason,
});
if (exhausted)
return standDown("view-request budget exhausted (bounded authoring loop, ADR-0029)");
if (sel === undefined) {
// A defective View (unknown pane / not a single View) → a repair re-ask carrying the guidance.
repairError = viewError ?? "requestView.view could not be resolved to panes";
view = undefined;
}
else {
view = sel;
repairError = undefined;
}
iteration = nextIteration;
continue;
}
// reply.kind === "invalid" → a repair iteration: surface the guiding error and re-ask.
p.emergenceLog?.push({
ordinal: OPEN_ORDINAL,
iteration,
kind: "repair",
model: p.model,
usage: step.usage,
...latency,
budgetExhausted: exhausted,
error: reply.reason,
});
if (exhausted)
return standDown("authoring budget exhausted during repair (bounded authoring loop, ADR-0029)");
repairError = reply.reason; // a repair re-ask does not re-render — the last materialized lens stays
iteration = nextIteration;
}
}