/** * The turn loop — ONE implementation, every caller in this package. * * This is the `streamText` call the `vendo()` harness drives — and the same loop * serves its hired subagents and the screen agent, so every rail here — the step * cap, `buildFailedStop`, the history window, the cache breakpoints, the * abandoned-approval provider rewrite, the `activeTools` gate — is shared: a * rail can only drift by being changed for every caller at once. * * What is deliberately NOT here: how output reaches a consumer. The harness * reads `result.fullStream` and yields the closed event vocabulary; the wire is * the runtime's business. */ import { type TurnId, type VendoStepLimitPart } from "@vendoai/core"; import { streamText, type LanguageModel, type ModelMessage, type StopCondition, type ToolSet, type UIMessage } from "ai"; import { type CompactionConfig, type CompactionState } from "./compaction.js"; import { type ResolvedModel } from "./failover.js"; import { type WorkbenchAgent, type WorkbenchEvent } from "../workbench.js"; export declare const DEFAULT_MAX_STEPS = 20; /** §4.1 item 3 — the per-turn provider retry budget, STATED. It used to be unset, * so the loop inherited whatever the SDK's default happened to be: a posture * nobody chose, that no reader of this file could see, and that a minor version * bump could change under us. The value matches the SDK's own default, so making * it explicit changed no behaviour — only who owns it. */ export declare const DEFAULT_MAX_RETRIES = 2; /** * §4.1 item 4 — a token ceiling for one turn, as one more StopCondition. The * caller closes over whose ceiling it is (a tenant, a seat, a plan), because the * loop has no business knowing. * * A StopCondition is consulted AFTER a step, so crossing the ceiling always costs * the step that crossed it. That is what makes this a budget rather than a hard * cap, and it is the only honest shape available: token spend is not knowable * until the provider reports it. */ export declare function tokenBudgetStop(maxTotalTokens: number): StopCondition; /** An approval the conversation abandoned reaches the PROVIDER as a denied tool * call, not as our internal `approval-responded` state. */ export declare function providerHistory(messages: UIMessage[]): UIMessage[]; /** * What the loop is asked to do about a window it now knows the size of. * * `contextWindowTokens` and the two ratios come from {@link CompactionConfig}; * the rest is the turn's own: which seat summarizes, what the thread already * remembers, and whether the caller is past asking. */ export interface TurnCompaction extends CompactionConfig { model: LanguageModel; state?: CompactionState; /** Compact whatever the estimate says — the overflow retry's re-entry. */ force?: boolean; } /** * One turn's prompt inputs. This was four positionals; the shipment's window * table, compaction and overflow retry add three more, and a seventh positional * is unreadable at the call site — so the shape is declared once, whole, before * three slices fill it. * * BREAKING: `turnModelMessages` is public (`vendo/index.ts`). */ export interface TurnPromptInput { messages: UIMessage[]; system: string; /** The live toolset, so the trigger can count the tools block. */ tools?: ToolSet; historyWindow?: number; tokenBudget?: number; compaction?: TurnCompaction; /** Model messages this turn ALREADY produced: appended after the projection * and never summarized, so a retry CONTINUES the turn instead of re-running * its tool calls — each one a real guarded effect. */ resume?: readonly ModelMessage[]; /** Volatile context for THIS call only — the user's live screen snapshot. * Appended after the cache breakpoints are placed, so it can never sit * inside a cached prefix: it changes every message, and volatile bytes * ahead of stable ones are what kept the prompt cache at 0%. Never * persisted, never summarized, never shed — and small enough (≤2k tokens, * the client caps the snapshot) that the compaction estimate not counting * it stays honest. */ trailing?: readonly ModelMessage[]; /** The turn's own signal. Building a projection is normally pure, but the * summarizer pass is a provider call, and a caller that hung up before the * first token must not keep paying for one (AGENT-3). */ signal?: AbortSignal; /** The workbench's ear, already bound to the turn and the agent (dev-only; see * `../workbench.ts`). Unset — every caller but `startTurn` — is silence. */ workbench?: (event: WorkbenchEvent) => void; } export interface TurnPrompt { messages: ModelMessage[]; /** Carried out as DATA, because the loop does not know where the caller's * state slot is. Written by the summarizer, and carrying the boundary the next * turn rebuilds this same projection from. */ compacted?: CompactionState; } /** * The provider messages for one turn: the system prompt, the summary standing in * for the band it absorbed, the verbatim tail that follows it, and the cache * breakpoints that keep a growing thread from re-billing. * * The ORDER is the mechanism. The candidate prompt is REBUILT first — summary * plus the messages the summary never read — and only then measured. Measuring the * stored transcript instead is what made a thread whose bulk is one huge paste pay * a summarizer pass on every single turn: that number is permanently over the * trigger, so the trigger fired forever and the summarizer re-read the whole paste * each time, for about what simply sending it would have cost. */ export declare function turnModelMessages(input: TurnPromptInput): Promise; export interface TurnLoopOptions { model: LanguageModel; /** §4.1 item 3 — the rungs BELOW `model`, tried in order when a provider fails * before producing any output. Unset (the normal case) means no ladder is built * and the model reaches `streamText` exactly as it does today. See * {@link failoverModel} for why the boundary is the first byte. */ fallbacks?: readonly ResolvedModel[]; system: string; messages: UIMessage[]; /** Already built and guard-bound by the caller (the harness runtime's * delegating set). */ tools: ToolSet; signal?: AbortSignal; /** §3.5 — the turn this loop is running, for anything downstream that has to * name it. Optional only because a caller may drive the loop outside a * composed turn; every composed caller mints one. */ turnId?: TurnId; context?: TurnContext; /** Extra stop conditions, COMPOSED with the loop's own three rather than * replacing them. The array used to be a literal, so a caller who needed a * fourth condition had nowhere to put it and would have had to grow a second * stop mechanism beside this one. */ stopWhen?: readonly StopCondition[]; /** Which tools the model may PICK this step — gates choice only; execution is * always the guard-bound path. Re-read each step via `prepareStep`, so a tool * the caller equips mid-turn is choosable on the very next step. */ activeTools?: () => string[]; /** The window this turn has, and what the thread already remembers about * filling it. Unset means no window awareness at all — the loop's behaviour * before this shipment. */ compaction?: TurnCompaction; /** Model messages this turn already produced, for a retry that continues it. */ resume?: readonly ModelMessage[]; /** Volatile per-call context, appended behind the history — see TurnPromptInput. */ trailing?: readonly ModelMessage[]; /** Which of the turn's loops this drive is, for the workbench's diagnostics * only (dev-only; see `../workbench.ts`). Defaults to the resident, because a * caller that has never heard of the workbench is the turn's own thinker. */ workbenchAgent?: WorkbenchAgent; } /** The per-turn knobs, one shape for every drive of the loop so no caller can * carry half of them (`vendo()` used to pass `maxSteps` alone, which made every * other knob structurally unreachable from the default harness). */ export interface TurnContext { maxOutputTokens?: number; /** Bound the messages re-sent per turn to the last N whole messages. */ historyWindow?: number; /** §4.1 item 2 — bound the PROMPT instead of the message count: reasoning and * old tool payloads are shed before any message is dropped. Estimated, not * tokenized (see {@link CHARS_PER_TOKEN}). */ contextTokenBudget?: number; maxSteps?: number; /** How many times the SDK re-issues a failed provider call. Defaults to * {@link DEFAULT_MAX_RETRIES}; 0 spends nothing. */ maxRetries?: number; } export interface TurnLoop { result: ReturnType; maxSteps: number; /** * AGENT-7: exhausting the step cap is VISIBLE. Call after the stream drains — * a run that still wanted tool calls after its final permitted step ended * because of the cap, not because the model finished. */ stepLimitPart(): Promise; /** What this turn compacted, as DATA for whoever owns the state slot — the * loop does not know where that is. Written by the summarizer. */ compacted?: CompactionState; } export declare function startTurn(options: TurnLoopOptions): Promise; //# sourceMappingURL=loop.d.ts.map