/** * L2 — the ReAct loop. The kernel's only loop; everything else is harness. * * An async generator that yields every event as it happens (never buffers a * turn into a list — the reference implementation's failure), and converges * on exactly one `terminal` event per run (ADR-0004). * * SINGLE TRUTH (Phase B): the loop holds ONE EventLog. Messages are never * stored alongside it — every adapter call derives them via * `projectMessages(log.all)` (kernel/project.ts). A fresh log encodes the * seed `messages` into events first, so even a one-shot call replays * exactly. Compaction is recorded as a `microcompacted` boundary (old * sessions: a `compacted` event) and re-applied by the projection, keeping * the replay identical to the live run. * * Per iteration: * assemble (onUserMessage / onPreLlm) * → adapter.stream(): events yielded straight through; every validated * and policy-allowed tool call LAUNCHES its execution immediately * (streaming execution) — the executions run concurrently under a window of 4 * (0.1.26, ADR-0024 Amd), their events queued and drained between * stream events (completion order; the projection re-orders the * results by call order — the byte discipline) * → the turn settles: the launched executions finish (receipts land * before any terminal), the ask-gated successors follow the human's * verdict (the conservative order) * no tool calls / maxTurns / abort / max_tokens → terminal event, return * * Retry lives HERE and only here (ADR-0005): a retryable StructuredError * from the adapter is retried with backoff inside the generator frame — and * ONLY before anything streamed (Phase B): once a text delta or tool call * left the adapter, a failure is an `error` terminal, never a silent * re-stream that duplicates output or tool calls. */ import { type Adapter, type AbortSignalLike } from "../protocol/adapter.js"; import type { ContinuationScope, Event, StructuredError } from "../protocol/events.js"; import type { ApprovalChain } from "../protocol/extension.js"; import { EventLog } from "./event-log.js"; import type { EventInput } from "./event-log.js"; import type { AssistantBlock, AssistantMessage, Message, ToolResultMessage } from "../protocol/messages.js"; import { ToolRegistry } from "../tools/registry.js"; import type { HookHost } from "./hooks.js"; import { type PermissionDecision } from "./permission.js"; export interface LoopConfig { readonly adapter: Adapter; readonly model: string; readonly systemPrompt?: string; readonly registry: ToolRegistry; readonly hooks?: HookHost; readonly maxTurns?: number; readonly maxRetries?: number; /** * MG-1 (ADR-0051 Amendment 5): the run's continuation scope — the * kernel stamps it onto a committed stop's envelope (adapters cannot * forge scope). Absent = an unscoped run (SDK-injected or faux * adapters): adapter-emitted continuation is STRIPPED at the commit. */ readonly continuationScope?: ContinuationScope; /** XP-1: the RESOLVED reasoning wire values — passed to the adapter * verbatim; absent = provider defaults (the byte anchor). */ readonly reasoning?: { readonly thinking?: "adaptive" | "enabled" | "disabled"; readonly effort?: string; }; /** * Seed history. When a `log` is provided, the log IS the truth and this * is only used if the log is empty. See ADR-0002 / kernel/project.ts. */ readonly messages?: readonly Message[]; /** The run's event log. Pass the session's log to make this run durable. */ readonly log?: EventLog; /** * ADR-0055 (A1b): the COMPACTION POINT — called before every request with * the log and the messages about to be sent. It returns the durable facts * to append (a `summarized` boundary, a `microcompacted` prune), or none. * The kernel appends and yields them and re-derives; it keeps no policy. * `why` is "overflow" once, after the provider refused the context: a * run that gets nothing back then ends on that refusal. */ readonly compact?: (events: readonly Event[], messages: readonly Message[], why: "request" | "overflow") => Promise; readonly signal?: AbortSignalLike; readonly temperature?: number; readonly maxTokens?: number; /** * Phase D: the channel that resolves a `defer` permission. When the * onPreTool hook defers, the loop persists a `permission_requested` * event, yields it, and AWAITS this promise — the same run resumes when * a human decides. Absent, a defer degrades to an honest denial. */ readonly resolveApproval?: (decisionId: string) => Promise; /** * round 4 (adversarial): a verdict the human ALREADY gave before an abort landed. * The abort path consults this BEFORE yielding the aborted terminal: a * consumed verdict must be recorded (exactly once), never lost — the * human's decision outranks the abort. */ readonly approvalVerdict?: (decisionId: string) => boolean | undefined; /** * E1: the COMPOSED approval chain — the runtime composes the * extensions' policies (deny > allow > ask, the R3 ruling) into ONE * decide; the kernel's gate calls it BEFORE the human flow. Allow/deny * are recorded durably with decidedBy = the deciding extension, never * pausing for a human; an ask falls into the human flow (its speaker * names the first non-abstain — the panel's why-asked line). A * throwing chain counts as ask. Absent, no chain runs. A durable * decision already recorded (resume) takes effect and the chain never * re-runs. */ readonly approvalPolicy?: ApprovalChain; /** P3: the session's id — carried to tools via ToolContext.sessionId. */ readonly sessionId?: string; } /** * R3e (owner ruling, 2026-08-28) — there is NO default turn limit. * * `maxTurns` stays: a caller that wants a bound sets one, and the * `max_turns` terminal is unchanged. What is retired is the DEFAULT. * * The reasoning, from the incident that found it: a real session read a * doc, listed a directory, read four files and ran four commands — 43 * calls — and stopped at 20 turns, mid-task, saying nothing. A guardrail * for "you set it running and walked away" was firing on someone sitting * at the keyboard, where esc, the context window and the balance are * already the bounds. Both reference implementations agree: neither puts * a turn limit on an interactive session, and the one that has the * mechanism spends it on forked subagents and non-interactive SDK calls, * where nobody is watching. * * `Number.POSITIVE_INFINITY` rather than deleting the check: the * comparison, the terminal and every test that sets a limit stay exactly * as they were, and one line says what changed. */ export declare const DEFAULT_MAX_TURNS: number; /** ADR-0005 Amendment 2: ten, about 2.7 minutes of backoff before a turn * gives up. It was 2 — three attempts inside 750 ms, which is no budget * at all against a gateway that drops a stream and comes back. */ export declare const DEFAULT_MAX_RETRIES = 10; /** CX-1 F8: the longest Retry-After the kernel will honor; beyond it the * run stops with an explicit error rather than waiting or retrying early. */ export declare const RETRY_AFTER_MAX_MS = 60000; /** * ADR-0005 Amendment 2 — the n-th retry's delay: exponential from 500 ms, * capped at 32 s, plus up to 25% of that as jitter, and never shorter than * the provider's Retry-After. 0.5 / 1 / 2 / 4 / 8 / 16 / 32 … seconds. * * It replaced `max(n × 250 ms, Retry-After)` (CX-1 F8), under which the * whole default budget was spent in 750 ms. The jitter keeps many sessions * behind one gateway from retrying in lockstep. `random` is a parameter so * the curve can be asserted; the loop uses the real one. */ export declare function retryDelayMs(n: number, retryAfterMs?: number, random?: () => number): number; export declare function loop(config: LoopConfig): AsyncGenerator; /** * Adapter exceptions → StructuredError. Anything already shaped like one * passes through; everything else is `unknown` — never a regex over error * text (ADR-0005). */ export declare function toStructuredError(err: unknown): StructuredError; export type { EventInput, AssistantBlock, AssistantMessage, Message, ToolResultMessage };