/** * The harness runtime — build contract §1.6. * * It builds the `Turn`, runs any `Harness`, converts the closed `HarnessEvent` * vocabulary plus mirrored tool calls into the EXISTING ai-SDK UIMessage stream, * persists the transcript one row per message, and enforces the frozen routing * table. Hot-path views ride the injected `wrapWorkspace` slot — the runtime * carries no app knowledge of its own. * * It decides nothing. Orchestration is thinking, and thinking is the harness's. */ import { type AuditEvent, type Guard, type Harness, type HarnessEvent, type Principal, type RunContext, type SeatModels, type ThreadId, type ToolRegistry, type TurnId, type TurnSkills, type TurnTools, type WorkspaceFs } from "@vendoai/core"; import { type CapabilityMissConfig } from "./capability-miss.js"; import { type ToolBridgeOptions } from "./tool-bridge.js"; import { type LanguageModel, type UIMessage } from "ai"; import { type HarnessStateStore } from "./harness-state.js"; /** Which phase of a turn a duration belongs to — `agent_run`'s flat breakdown, * and the whole vocabulary. */ export type TurnTimingKey = "ttft" | "store" | "prompt" | "tools" | "guard"; /** * ONE turn's measurements, collected by whoever is standing where the time is * spent: composition marks its store reads and its prompt assembly, the runtime * marks the first output and the steps, the guard and the tool bridge mark * theirs. DURATIONS AND COUNTS ONLY — a mark can never carry a prompt, an * argument, or a name. * * `add` accumulates, because tool time and guard time are many calls and one * number each. `elapsed` is ms since the turn began, which is what the * time-to-first-output mark and the run's own `durationMs` are read from. */ export interface TurnTimings { add(key: TurnTimingKey, ms: number): void; /** One more model call. */ step(): void; elapsed(): number; readonly ms: Readonly>>; readonly steps: number; } /** A collector for the turn starting NOW. */ export declare function createTurnTimings(): TurnTimings; /** Build contract §6 — lane D's `threadMessageStore(store)` return value. Typed * structurally so this package never imports @vendoai/store: the store handle * arrives as a composed value. */ export interface TranscriptStore { /** One row per message; per-row CAS on `revision` for edits. */ upsert(principal: Principal, threadId: ThreadId, message: UIMessage, seq: number): Promise; /** The whole turn's changed messages in ONE call, in array order — the store * assigns positions itself, under the row it writes. Optional the way every * capability in this codebase is optional: a store that cannot batch omits * it and the per-message loop below still runs. */ upsertMany?(principal: Principal, threadId: ThreadId, messages: ReadonlyArray): Promise; /** Reassembled by seq, oldest → newest. */ list(principal: Principal, threadId: ThreadId): Promise; } export interface HarnessRuntimeDeps { /** The GUARD-BOUND registry (`VendoGuard.bind(hostTools)`) — the one choke * point every harness's calls pass through, whatever the dialect. */ tools: ToolRegistry; guard: Guard; skills: TurnSkills; transcript: TranscriptStore; /** Defaults to process-lifetime memory: a session id is disposable by contract. */ harnessState?: HarnessStateStore; /** * Wrap the turn's workspace before the harness sees it — the one injection * point for a commit-intercepting façade (the render seam is composition's * implementation; see `wrapWorkspaceForRender` in `@vendoai/apps`). `emit` * writes a data part on the wire's view channel for this turn; `turnId` is * the turn being wrapped. Unset, the harness runs on the workspace as given — * the runtime itself knows nothing about apps. */ wrapWorkspace?: (workspace: WorkspaceFs, opts: { emit: (streamId: string, part: unknown) => void; turnId: TurnId; }) => WorkspaceFs; /** The shipped tool-bridge rails composition owns: `toolOutputCap`, the * `preflight` connect gate, and the capability-miss `onCall` hook. The writer * and the per-turn connect-card set are the runtime's to supply. */ bridge?: Omit; /** This turn's bound on an interactive approval wait; unset uses the frozen * APPROVAL_WAIT_MS. */ approvalWaitMs?: number; /** * Publish the turn now in flight to the host process's own doors, and retract * it at turn end (the returned disposer). * * The one consumer today is the MCP door's turn credential (10-mcp §3b): a * `claudeCode()` box reaches its host's tools over native remote MCP, and the * door has to answer with THIS turn's ctx, THIS turn's equipped tools and THIS * turn's approval card — the same `turn.tools` the harness holds, not a * reconstruction of it. Publishing is not a grant: nothing can be reached * without a credential the harness minted, and the credential's whole * authority is the window between this call and its disposer. */ liveTurn?: (published: { threadId: ThreadId; ctx: RunContext; tools: TurnTools; /** * Hand the user's words to THIS turn while it runs (§10.2), and answer * whether they landed. Published here rather than through a second hook * because "the turn now in flight, reachable by the process's own doors" is * exactly what this hook already means. */ steer: (text: string, messageId: string) => Promise; }) => () => void; /** This turn's measurements ({@link TurnTimings}), for whoever reports them. * The runtime fills the marks only it can see — the first output on the wire * and the model calls. Unset, nothing is measured. */ timings?: TurnTimings; /** * Land this turn's three closing writes — the messages it produced, the * harness state to carry into the next one, and the run's audit row — in ONE * call, when composition has a store that serves it. * * The runtime decides WHAT closes a turn either way: the same message diff, * the same dirty check, the same "no spend and no failure, no row" rule feed * this and the three separate writes below. Unset — the mount does not serve * a batched commit — those three run exactly as they always have, retry and * per-write isolation included, which is why this is a slot and not a switch. */ commitTurn?: (turn: { messages: UIMessage[]; /** Present only when the harness's state CHANGED and has a value to keep; * clearing a slot is not something a commit can express, so it stays with * the state door. */ state?: string; audit?: AuditEvent; }) => Promise; } export interface TurnRunInput { harness: Harness; threadId: ThreadId; /** The canonical transcript for this turn, INCLUDING the new user message. */ messages: UIMessage[]; ctx: RunContext; workspace: WorkspaceFs; /** The seats `Turn.models` carries (contract §4, relaxed): any subset — only * a seat the harness actually reads matters. Unset = no seats, which is the * whole truth for a harness like `claudeCode()` that brings its own brain. */ models?: SeatModels; options?: Options; /** §1.4 — did the caller prove presence (a click/message/submit)? */ interactive: boolean; /** The assembled system prompt for THIS turn (`Turn.system`). Composition's to * assemble — it is venue-gated and carries the guard's directions, so it needs * the ctx — and the runtime's to deliver, which is what puts a NAMED harness on * the same brief as the default one. */ system?: string; /** The user's live screen snapshot (`Turn.situation`), ready-formatted. * Delivered beside `system` but never folded into it: the system prompt is * the stable prefix and this changes every message, so the harness places it * behind the history where it cannot cost the prefix its cache. */ situation?: string; /** The capability-miss rail for THIS turn: the honest-refusal reporter, listed * beside the projected tools, plus the repeated-failure detector on the * bridge. Per turn, not per runtime, because the intent is the user's latest * ask (`latestUserIntent(messages)`). */ capabilityMiss?: { config: CapabilityMissConfig; intent: string; threadId?: ThreadId; }; signal?: AbortSignal; /** Every event the harness yields, as the runtime routes it. Observation * only: routing, filtering and the wire are unchanged by it. */ observe?: (event: HarnessEvent) => void; } export interface HarnessRuntime { run(input: TurnRunInput): Promise; } /** The metering figures an audit row carries — the `usage` HarnessEvent's own * shape, which is why a harness can hand one straight over. */ export interface UsageTotals { inputTokens: number; outputTokens: number; cacheReadTokens?: number; cacheWriteTokens?: number; model?: string; } export declare function addUsage(totals: UsageTotals | undefined, event: Extract): UsageTotals; export declare function createHarnessRuntime(deps: HarnessRuntimeDeps): HarnessRuntime; //# sourceMappingURL=runtime.d.ts.map