/** * The per-run runtime blob — the wiring, threaded down the whole recursion tree (and forwarded to the worker * Loop's `policy`/governance via `options.ctx`). Distinct from `opts` (the policy knobs). */ export type RecurseCtx = { /** * - The model the workers call. Required (here or on `opts.provider`). */ provider?: import("../types").Provider | undefined; /** * - bareguard `policy(tool, args, ctx)` — the gate. Sees `ctx.depth` so it can * enforce `limits.maxDepth`/budget/calls. recurse adds NO second guard layer (§6). */ policy?: Function | undefined; /** * - Budget hook forwarded to every worker Loop AND the verifier — judge * and worker tokens are all real spend (BA1: never invisible). */ onLlmResult?: Function | undefined; /** * - The current recursion depth (0 at the top). Incremented on each self-call; * threaded into `policy`. Callers normally omit it (defaults to 0). */ depth?: number | undefined; /** * - Optional event stream forwarded to each worker Loop (receipts substrate). This * is the observability channel for worker activity (relayfact F15/BA-5): recurse intentionally does NOT take * `onToolCall`/`onText` Loop callbacks — instead every worker Loop emits `loop:tool_call` / `loop:tool_result` * (and `loop:text`/`loop:done`) to THIS stream (loop.js), so a consumer observes worker tool calls by reading * the stream, not via per-call callbacks. The full audit trail is stream + the RC-10 receipts tree + (if a * gate is wired) the bareguard audit. */ stream?: object; /** * - Optional litectx handle (RC-5, §10 step 7). Backs the `search` * retrieval mode (`recall`). NOT used by `scan` — litectx has no exhaustive enumerate verb today; scan reads * the generic array slice-source `opts.corpus` instead (the litectx-resident scan case waits on the litectx * `enumerate` verb and drops in behind the same socket). */ litectx?: { recall: Function; } | undefined; }; export type RecurseOptions = { /** * - Fallback provider if `ctx.provider` is absent. */ provider?: import("../types").Provider | undefined; /** * - Open topology ceiling (§1): the depth past which the `spawn_child` tool is * no longer offered (`maxDepth=1` ⇒ flat fan-out, no nesting). NOT the safety halt — that is bareguard's, * and actual depth is always ≤ `limits.maxDepth`. */ maxDepth?: number | undefined; /** * - (Gap 3 / 0.21.0) An optional caller stance PREPENDED to every Family-A worker's * system prompt (e.g. "You are a senior security engineer; …"). It AUGMENTS the built-in decomposition policy + * depth-scrub, never replaces them (that text drives the spawn mechanics), and CARRIES DOWN the whole tree * (preserved by `forChild` — a durable worker stance, unlike the top-only `contract`/`evaluate`). Deliberately * NOT applied to the isolated verifier (would defeat the anti-sycophancy isolation, A1) nor the deterministic * scan judge. Absent/blank ⇒ the worker prompt is byte-identical to pre-0.21 (backward-compatible). * **SECURITY:** this is a PRIVILEGED system-prompt seam — treat `persona` like a system prompt. Do NOT pass * untrusted / end-user-controlled text here; a hostile persona is prepended ahead of the decomposition policy * and can override it (and any safety framing) for every worker in the tree. Caller-trusted input only. */ persona?: string | undefined; /** * - (BA-9 / relayfact F19) An optional caller-supplied READ-ONLY working-context * blob (e.g. "project root: /abs/path\nfiles are relative to it") prepended to EVERY worker's task message as * a `Working context:` block, so a sliced child can LOCATE its artifact — the concrete context (absolute * paths / cwd) the Planner strips when it paraphrases the parent goal into child subtasks. CARRIES DOWN the * tree (preserved by `forChild`, like `persona`) and, when forced fan-out plans, is forwarded as the Planner's * `info` so the slices themselves are path-aware. Also shown to the verifier (neutral FACTS, not a stance, so * no anti-sycophancy concern — and an agentic critic needs the path to exercise the artifact). Distinct from * `persona`: persona is a privileged SYSTEM-prompt stance; context is run-state facts on the USER message. * Absent/blank ⇒ byte-identical to pre-BA-9 (backward-compatible). Validated live (`poc/ba9-context-thread.mjs`: * a weak model went 0/3 → 3/3 at locating an unguessable file once the root was threaded). * **SECURITY:** this becomes part of every worker's prompt (and the verifier's) — lower-privilege than * `persona` (the USER message, not the SYSTEM prompt) but still a prompt-injection surface. Intended for * TRUSTED run-state (paths/cwd); do NOT pass untrusted / end-user-controlled text here. */ context?: string | undefined; /** * - Handle tools offered to EVERY worker (RC-5 pull-default: litectx * `recall`/`get`, wired at build step 7). Workers query on demand; never the whole corpus. */ tools?: import("../types").ToolDef[] | undefined; /** * (Opt-in, BA-8 / relayfact F17) Turn a DEFINITE LEAF (a node that is offered no `spawn_child` — `simple` * tier or at `maxDepth`) into a bounded generate→sense→regenerate loop instead of a single pass, so a failed * slice can self-correct. `sensor` is a DETERMINISTIC close (test/compile/lint — NOT a model judge, R-S8) that * returns a `Verdict`; on a non-pass its `critique` (the GAP, not the transcript) is fed FRESH into the next * attempt (D6/A1 anti-anchoring) and, on models that ACCEPT `temperature`, the **retry temperature ESCALATES** * (`temperatures`, default `[0.2,0.7,1.0]`) — the live-validated lever that lets a weak model escape a * repeat-the-same-mistake rut (`poc/ba8-leaf-refine.mjs`: 0/5 → 2-3/5; flat temp recovers 0/5). On a * temperature-fixed model (BA-10) the provider drops the param, `receipts.refineLeaf.temperatures` records * `null`, and the fed-back gap critique carries recovery alone — UNLESS the rejected-attempt buffer engages * (below). `maxIterations` defaults to * `temperatures.length`; the REAL bound is bareguard (each attempt is gate-checked + metered). CARRIES DOWN the * tree (preserved by `forChild`), so it engages at the leaves of a Family-A decomposition. Recovery is PARTIAL * (a stubborn blind spot may persist) — `receipts.refineLeaf.passed` reports honestly. Does NOT apply to a node * that delegates (its children + the tree verify own quality), nor to the scan/fanout/partition dispatch paths. * Absent ⇒ a leaf is a single pass (byte-identical to pre-BA-8). An error-keyed `recall` is the CALLER's tool * (`opts.tools`) keyed off the fed-back critique — bareagent stays litectx-agnostic. * **Sensor integrity (RSI-learnings #1/#5, "audit the close"):** the `sensor` MUST judge the RETURNED result * (tamper-proof — e.g. build/run the returned string in an isolated context, as `poc/ba8-leaf-refine.mjs` does), * NEVER a worker side-effect a worker with edit tools could GAME (writing a passing file then returning junk, or * editing the failing test itself). A gameable close is the reward-hacking surface every RSI system in the field * got bitten by; the loop optimizes against WHATEVER the sensor reads, so keep it outside what the worker can write. * **Broken sensor ≠ failing model (BA-15):** a sensor that THROWS (non-Halt) or returns a MALFORMED verdict * (anything but `{pass: boolean}` or a valid tri-state `status`) is a faulty ARBITER — the loop stops at the * FIRST broken close (never retries against it) and returns a labeled `{incomplete, blocker:'broken-sensor'}` * (+ `receipts.blockerDetail`), with `best` preserving the model's last attempt. A `HaltError` thrown by the * sensor stays a clean governance halt. The sensor's EXECUTION environment is the caller's: run untrusted / * model-generated checks in an isolated child process WITH A TIMEOUT — a sensor that hangs forever hangs the * leaf (no bareguard checkpoint fires between sensor start and return; confirmed by `poc/rlmplans-hung-sensor.mjs`). * **`rejectedBuffer` (BA-14):** a SkillOpt-shaped rejected-attempt buffer — instead of only the LATEST critique, * surface the model's OWN prior failed attempts verbatim ("you wrote these, they failed X — write something * STRUCTURALLY DIFFERENT"). This is DIRECTED diversity (attack the specific repeated mistake), where escalation * is RANDOM diversity; the two are ANTAGONISTIC (`poc/ba14b-temp-with-buffer.mjs`: temperature monotonically * degrades the buffer, 100%→70%→50% across 0.2/0.7/1.0), so when the buffer engages the retry temperature is * HELD at `temperatures[0]` (flat-low), never escalated. `true` = force on (also on temperature-accepting * models); `false` = force off (pure BA-8 escalation); UNSET = ADAPTIVE — engage only once a prior attempt's * temperature was dropped (a temperature-fixed model, BA-10, where escalation is inert and the buffer is the * sole lever — `poc/ba14-rejected-buffer.mjs`: flat-temp 50%→100%). `receipts.refineLeaf.rejectedBuffer` * reports whether any iteration injected it. Bounded by `maxIterations` (the buffer never outgrows it). */ refineLeaf?: { sensor: (result: any, ctx: { task: string; context: string | undefined; contract: string | null; }) => (Verdict | Promise); maxIterations?: number; temperatures?: number[]; rejectedBuffer?: boolean; } | undefined; /** * - Definition of done (A3). When present, the verifier grades against THIS, * not the loose task, and verification always runs. */ contract?: string | undefined; /** * Override the verifier (fills `recurse()`'s verify slot, §7.1). Default = an `Evaluator` rubric pass. */ evaluate?: ((result: any, ctx: { contract: string | null; task: string; }) => (Verdict | Promise)) | undefined; /** * Override synthesis/reduce (NB-3). A FUNCTION is a deterministic code-reduce over the child `results` — the * §9.1 aggregation path (LLM arithmetic over partials carried ~10–15% error). A STRATEGY string runs the * built-in reducer: `'concat'` (lossless no-LLM join) or `'merge'` (an isolated Loop-driven subjective * merge); a string is ignored when no child ran. Default (unset) = the worker's own final text (Family A: * the parent model already combined the children's results in its closing turn). */ synthesize?: "concat" | "merge" | ((args: { task: string; text: string | null; results: any[]; children: object[]; ctx: RecurseCtx; }) => any) | undefined; /** * - (Opt-in, NB-2 / Family B) FORCED fan-out: decompose into exactly this many * independent parallel workers via `Planner`→`runPlan`, then reduce. A positive integer here is the count; * it OVERRIDES the tier→count map. Setting it (or `mode:'fanout'`) takes the deterministic-parallelism path * instead of the model-driven Family-A default. For known-parallel tasks where the caller wants guaranteed * fan-out, not the model's adaptive choice. */ count?: number | undefined; /** * - (Opt-in, NB-2 / Family B) `'fanout'` = forced semantic fan-out * WITHOUT a fixed count — derived from `assessComplexity`'s tier via the calibrated map (medium/complex/ * critical → 2/4/6; simple → 1); `opts.count` takes precedence. `'partition'` = the DATA-DRIVEN WIDTH path * (§11): measure `opts.corpus` and partition it into `max(opts.count floor, ⌈size/workerBudget⌉)` parallel * scan-workers (capped by the guards), CODE-reducing the per-chunk counts. Distinct from `'fanout'`: a data * partition, not a `Planner` semantic split. */ mode?: "fanout" | "partition" | undefined; /** * - (`mode:'partition'`) items per worker; width = `⌈corpus.length / * workerBudget⌉` (default 100). A calibratable knob (the §9.1 algorithm), not a discovered constant. */ workerBudget?: number | undefined; /** * - (Family B) max workers run at once per wave (default 4). The wave * structure is `runPlan`'s; bareguard still bounds the family rate independently. */ concurrency?: number | undefined; /** * - (§10 step 7) the retrieval shape for a task OVER A * CORPUS, routed by question shape (§9.2.1). `'scan'` (the default WHEN `opts.corpus` is present) = process * every slice + LLM-judge + CODE-count — the only COMPLETE path (for "how many / all"). `'search'` = litectx * `recall` handle tool offered to the worker (needle; CANNOT count; requires `ctx.litectx`). `'exact'` = a * deterministic code-side AND-term filter tool over `opts.corpus`. `'tools'` = the PER-QUERY Family-A face: * offer the worker `scan_count` (over `opts.corpus`) + `search_memory` (when `ctx.litectx`) + `exact_match` * (array corpus) ALL AT ONCE, and let it pick the shape PER SUB-QUERY — the routing lives in the tool * descriptions (scan says "use for how many / all / count"; search says "never count"), so a mixed task gets * needle-search AND complete-count without per-sub-query adopter declaration. The completeness guard upgrades a * `'search'` on a "how many / all" ask to `'scan'` (UPGRADE-only, never a silent downgrade); it does NOT fire * for `'tools'` (the complete `scan_count` is always offered there, so a mixed task keeps its search tool). * Absent `corpus` AND `retrieval`, behaviour is unchanged (Family A / single-shot) — fully backward-compatible. */ retrieval?: "tools" | "search" | "scan" | "exact" | undefined; /** * - (§10 step 7) the generic slice-source scan/partition * reads: an in-hand `{id, text}[]` array, OR an async `() => Promise` (e.g. `litectxCorpus(litectx, * {kind})` materializing a litectx-resident corpus via `enumerate`). recurse depends on this SHAPE, never on * litectx. Malformed entries are dropped, never miscounted. */ corpus?: Slice[] | (() => Promise) | undefined; /** * - (scan) items per judge window. Default 8 (§9.2.1 recall knee — the one * calibrated number; per-model). */ window?: number | undefined; /** * - (scan) shuffled-boundary passes unioned for recall. Default 2 (~0.91 recall). */ passes?: number | undefined; }; /** * One audit/receipts node (RC-10) — the recursion tree reconstructs from these alone: parent→child lineage * (`spawned`), each subgoal (`task`), each gap report (`verdict`), cost per node (`tokens`). */ export type RecurseNode = { task: string; depth: number; complexity: { level: string; score: number; }; critical: boolean; /** * - Child nodes (lineage). */ spawned: RecurseNode[]; verdict: Verdict | null; incomplete: boolean; halted: boolean; /** * - Set when this node stopped for a specific non-model reason (mirrors * `RecurseResult.blocker`): `'governance-deny'` (BA-11) — its Loop short-circuited a consecutive-policy-deny * spin; `'broken-sensor'` (BA-15) — the caller's `refineLeaf.sensor` threw or returned a malformed verdict; * `'broken-verifier'` (BA-15) — the caller's `opts.evaluate` did (the default Evaluator path is never labeled). */ blocker?: string | undefined; /** * - (BA-15) with a `broken-*` blocker: what the arbiter did (threw with * which message, or which malformed shape it returned) — the actionable half of the label. */ blockerDetail?: string | undefined; /** * - (BA-15) a * DESCENDANT's blocker, surfaced here so an aggregating node still reports the fault upward. Deliberately * SEPARATE from this node's own `blocker` (which means "THIS node's arbiter/Loop broke"): stamping a * descendant's label onto every ancestor made the receipts tree accuse nodes whose sensor never ran, and * re-labelled a parent `governance-deny` when only one child was denied. `blockerTask` names the culprit. */ blockerFrom?: { blocker: string; blockerDetail?: string; blockerTask?: string; } | undefined; /** * - The worker Loop's `metrics.tokens`. */ tokens: object | null; /** * - (BA-8) when * this leaf ran as a bounded refine loop: how many attempts it took and whether the deterministic sensor finally * passed (false = honest non-recovery, not a faked success). `temperatures` are the EFFECTIVE per-attempt temps * (BA-10): a `null` marks an attempt the model ran at its DEFAULT because it rejected the requested temperature. * `rejectedBuffer` (BA-14): whether any iteration injected the rejected-attempt buffer (prior failed attempts * surfaced verbatim); when it engages the REQUESTED retry temperature is held flat at `temperatures[0]`, never * escalated (ba14b antagonism) — but a temperature-fixed model still drops it, so the EFFECTIVE temp recorded * above is `null`, not `temperatures[0]`. */ refineLeaf?: { iterations: number; passed: boolean; temperatures: (number | null)[]; rejectedBuffer: boolean; } | undefined; model: string | null; /** * - (§10 step 7) the retrieval mode this node ran (`scan`/`search`/`exact`), * or null/absent for a plain reasoning node. */ retrieval?: string | null | undefined; /** * - set when the completeness guard upgraded the mode (e.g. * `'search→scan (completeness)'`) — the audit trail for RC-9-applied-to-retrieval. */ retrievalUpgraded?: string | undefined; /** * - (scan) the scan * shape: window/passes used, slices scanned, ids matched (CODE-counted). */ scan?: { window: number; passes: number; scanned: number; matched: number; } | undefined; /** * - (`mode:'partition'`) the data-driven width audit: corpus size, the budget knob, the count floor, the * data-derived width `⌈size/budget⌉`, the chosen `width = max(floor, dataWidth)`, and matched count. */ partition?: { size: number; workerBudget: number; floor: number; dataWidth: number; width: number; matched?: number; } | undefined; }; export type RecurseResult = { /** * - The synthesized answer (on convergence). */ result?: any; /** * - The verifier's gap report (null when verification did not run). */ verdict?: import("./evaluator").Verdict | null | undefined; /** * - true on guard exhaustion / a dead worker / an incomplete child (RC-9) — * never a faked pass. */ incomplete?: boolean | undefined; /** * - The best partial answer when `incomplete` (RC-9). */ best?: any; /** * - When `incomplete` because a child failed: the sub-task(s) that came * back incomplete (§9 scenario 1) — the anti-survivor-sum signal, not a quiet undercount. */ missingSlices?: string[] | undefined; /** * - Present when `incomplete` for a specific, actionable reason. `'governance-deny'` * (BA-11): the worker's Loop short-circuited after N consecutive policy denials rather than burn to the * budget cap — the caller can widen scope / re-gate / escalate instead of reading it as a model failure. * `'broken-sensor'` (BA-15): the caller's `refineLeaf.sensor` threw or returned a malformed verdict — the * ARBITER is faulty, not the model; fix the sensor and re-run (`receipts.blockerDetail` says what it did). * `'broken-verifier'` (BA-15): same fault class at the verify slot — the caller's `opts.evaluate` threw * (non-Halt) or returned a malformed verdict; the default Evaluator path is never labeled (its failures are * provider-class faults). For both `broken-*` blockers `best` preserves the model's last non-empty output * (BA-5) — the arbiter broke, so treat it as best-effort work rather than a graded pass. */ blocker?: string | undefined; /** * - (BA-15) with a `broken-*` blocker: what the arbiter did — the * ACTIONABLE half of the label, surfaced on the result (not only in `receipts`) so a caller branching on * `blocker` can report the cause without walking the receipts tree. */ blockerDetail?: string | undefined; /** * - (BA-15) when the blocker was INHERITED from a descendant in a nested run: * which sub-task actually broke. Without it a nested failure reports "a sensor broke" with no way to find * which one. */ blockerTask?: string | undefined; /** * - The audit node for this call (RC-10). */ receipts: RecurseNode; }; export type Provider = import("../types").Provider; export type ToolDef = import("../types").ToolDef; export type Verdict = import("./evaluator").Verdict; export type Slice = { id: string; text: string; }; /** * @typedef {object} RecurseCtx * The per-run runtime blob — the wiring, threaded down the whole recursion tree (and forwarded to the worker * Loop's `policy`/governance via `options.ctx`). Distinct from `opts` (the policy knobs). * @property {Provider} [provider] - The model the workers call. Required (here or on `opts.provider`). * @property {Function} [policy] - bareguard `policy(tool, args, ctx)` — the gate. Sees `ctx.depth` so it can * enforce `limits.maxDepth`/budget/calls. recurse adds NO second guard layer (§6). * @property {Function} [onLlmResult] - Budget hook forwarded to every worker Loop AND the verifier — judge * and worker tokens are all real spend (BA1: never invisible). * @property {number} [depth] - The current recursion depth (0 at the top). Incremented on each self-call; * threaded into `policy`. Callers normally omit it (defaults to 0). * @property {object} [stream] - Optional event stream forwarded to each worker Loop (receipts substrate). This * is the observability channel for worker activity (relayfact F15/BA-5): recurse intentionally does NOT take * `onToolCall`/`onText` Loop callbacks — instead every worker Loop emits `loop:tool_call` / `loop:tool_result` * (and `loop:text`/`loop:done`) to THIS stream (loop.js), so a consumer observes worker tool calls by reading * the stream, not via per-call callbacks. The full audit trail is stream + the RC-10 receipts tree + (if a * gate is wired) the bareguard audit. * @property {{recall: Function}} [litectx] - Optional litectx handle (RC-5, §10 step 7). Backs the `search` * retrieval mode (`recall`). NOT used by `scan` — litectx has no exhaustive enumerate verb today; scan reads * the generic array slice-source `opts.corpus` instead (the litectx-resident scan case waits on the litectx * `enumerate` verb and drops in behind the same socket). */ /** * @typedef {object} RecurseOptions * @property {Provider} [provider] - Fallback provider if `ctx.provider` is absent. * @property {number} [maxDepth=3] - Open topology ceiling (§1): the depth past which the `spawn_child` tool is * no longer offered (`maxDepth=1` ⇒ flat fan-out, no nesting). NOT the safety halt — that is bareguard's, * and actual depth is always ≤ `limits.maxDepth`. * @property {string} [persona] - (Gap 3 / 0.21.0) An optional caller stance PREPENDED to every Family-A worker's * system prompt (e.g. "You are a senior security engineer; …"). It AUGMENTS the built-in decomposition policy + * depth-scrub, never replaces them (that text drives the spawn mechanics), and CARRIES DOWN the whole tree * (preserved by `forChild` — a durable worker stance, unlike the top-only `contract`/`evaluate`). Deliberately * NOT applied to the isolated verifier (would defeat the anti-sycophancy isolation, A1) nor the deterministic * scan judge. Absent/blank ⇒ the worker prompt is byte-identical to pre-0.21 (backward-compatible). * **SECURITY:** this is a PRIVILEGED system-prompt seam — treat `persona` like a system prompt. Do NOT pass * untrusted / end-user-controlled text here; a hostile persona is prepended ahead of the decomposition policy * and can override it (and any safety framing) for every worker in the tree. Caller-trusted input only. * @property {string} [context] - (BA-9 / relayfact F19) An optional caller-supplied READ-ONLY working-context * blob (e.g. "project root: /abs/path\nfiles are relative to it") prepended to EVERY worker's task message as * a `Working context:` block, so a sliced child can LOCATE its artifact — the concrete context (absolute * paths / cwd) the Planner strips when it paraphrases the parent goal into child subtasks. CARRIES DOWN the * tree (preserved by `forChild`, like `persona`) and, when forced fan-out plans, is forwarded as the Planner's * `info` so the slices themselves are path-aware. Also shown to the verifier (neutral FACTS, not a stance, so * no anti-sycophancy concern — and an agentic critic needs the path to exercise the artifact). Distinct from * `persona`: persona is a privileged SYSTEM-prompt stance; context is run-state facts on the USER message. * Absent/blank ⇒ byte-identical to pre-BA-9 (backward-compatible). Validated live (`poc/ba9-context-thread.mjs`: * a weak model went 0/3 → 3/3 at locating an unguessable file once the root was threaded). * **SECURITY:** this becomes part of every worker's prompt (and the verifier's) — lower-privilege than * `persona` (the USER message, not the SYSTEM prompt) but still a prompt-injection surface. Intended for * TRUSTED run-state (paths/cwd); do NOT pass untrusted / end-user-controlled text here. * @property {ToolDef[]} [tools] - Handle tools offered to EVERY worker (RC-5 pull-default: litectx * `recall`/`get`, wired at build step 7). Workers query on demand; never the whole corpus. * @property {{sensor: (result: any, ctx: {task: string, context: string|undefined, contract: string|null}) => (Verdict|Promise), maxIterations?: number, temperatures?: number[], rejectedBuffer?: boolean}} [refineLeaf] * (Opt-in, BA-8 / relayfact F17) Turn a DEFINITE LEAF (a node that is offered no `spawn_child` — `simple` * tier or at `maxDepth`) into a bounded generate→sense→regenerate loop instead of a single pass, so a failed * slice can self-correct. `sensor` is a DETERMINISTIC close (test/compile/lint — NOT a model judge, R-S8) that * returns a `Verdict`; on a non-pass its `critique` (the GAP, not the transcript) is fed FRESH into the next * attempt (D6/A1 anti-anchoring) and, on models that ACCEPT `temperature`, the **retry temperature ESCALATES** * (`temperatures`, default `[0.2,0.7,1.0]`) — the live-validated lever that lets a weak model escape a * repeat-the-same-mistake rut (`poc/ba8-leaf-refine.mjs`: 0/5 → 2-3/5; flat temp recovers 0/5). On a * temperature-fixed model (BA-10) the provider drops the param, `receipts.refineLeaf.temperatures` records * `null`, and the fed-back gap critique carries recovery alone — UNLESS the rejected-attempt buffer engages * (below). `maxIterations` defaults to * `temperatures.length`; the REAL bound is bareguard (each attempt is gate-checked + metered). CARRIES DOWN the * tree (preserved by `forChild`), so it engages at the leaves of a Family-A decomposition. Recovery is PARTIAL * (a stubborn blind spot may persist) — `receipts.refineLeaf.passed` reports honestly. Does NOT apply to a node * that delegates (its children + the tree verify own quality), nor to the scan/fanout/partition dispatch paths. * Absent ⇒ a leaf is a single pass (byte-identical to pre-BA-8). An error-keyed `recall` is the CALLER's tool * (`opts.tools`) keyed off the fed-back critique — bareagent stays litectx-agnostic. * **Sensor integrity (RSI-learnings #1/#5, "audit the close"):** the `sensor` MUST judge the RETURNED result * (tamper-proof — e.g. build/run the returned string in an isolated context, as `poc/ba8-leaf-refine.mjs` does), * NEVER a worker side-effect a worker with edit tools could GAME (writing a passing file then returning junk, or * editing the failing test itself). A gameable close is the reward-hacking surface every RSI system in the field * got bitten by; the loop optimizes against WHATEVER the sensor reads, so keep it outside what the worker can write. * **Broken sensor ≠ failing model (BA-15):** a sensor that THROWS (non-Halt) or returns a MALFORMED verdict * (anything but `{pass: boolean}` or a valid tri-state `status`) is a faulty ARBITER — the loop stops at the * FIRST broken close (never retries against it) and returns a labeled `{incomplete, blocker:'broken-sensor'}` * (+ `receipts.blockerDetail`), with `best` preserving the model's last attempt. A `HaltError` thrown by the * sensor stays a clean governance halt. The sensor's EXECUTION environment is the caller's: run untrusted / * model-generated checks in an isolated child process WITH A TIMEOUT — a sensor that hangs forever hangs the * leaf (no bareguard checkpoint fires between sensor start and return; confirmed by `poc/rlmplans-hung-sensor.mjs`). * **`rejectedBuffer` (BA-14):** a SkillOpt-shaped rejected-attempt buffer — instead of only the LATEST critique, * surface the model's OWN prior failed attempts verbatim ("you wrote these, they failed X — write something * STRUCTURALLY DIFFERENT"). This is DIRECTED diversity (attack the specific repeated mistake), where escalation * is RANDOM diversity; the two are ANTAGONISTIC (`poc/ba14b-temp-with-buffer.mjs`: temperature monotonically * degrades the buffer, 100%→70%→50% across 0.2/0.7/1.0), so when the buffer engages the retry temperature is * HELD at `temperatures[0]` (flat-low), never escalated. `true` = force on (also on temperature-accepting * models); `false` = force off (pure BA-8 escalation); UNSET = ADAPTIVE — engage only once a prior attempt's * temperature was dropped (a temperature-fixed model, BA-10, where escalation is inert and the buffer is the * sole lever — `poc/ba14-rejected-buffer.mjs`: flat-temp 50%→100%). `receipts.refineLeaf.rejectedBuffer` * reports whether any iteration injected it. Bounded by `maxIterations` (the buffer never outgrows it). * @property {string} [contract] - Definition of done (A3). When present, the verifier grades against THIS, * not the loose task, and verification always runs. * @property {(result: any, ctx: {contract: string|null, task: string}) => (Verdict|Promise)} [evaluate] * Override the verifier (fills `recurse()`'s verify slot, §7.1). Default = an `Evaluator` rubric pass. * @property {((args: {task: string, text: string|null, results: any[], children: object[], ctx: RecurseCtx}) => any) | 'concat' | 'merge'} [synthesize] * Override synthesis/reduce (NB-3). A FUNCTION is a deterministic code-reduce over the child `results` — the * §9.1 aggregation path (LLM arithmetic over partials carried ~10–15% error). A STRATEGY string runs the * built-in reducer: `'concat'` (lossless no-LLM join) or `'merge'` (an isolated Loop-driven subjective * merge); a string is ignored when no child ran. Default (unset) = the worker's own final text (Family A: * the parent model already combined the children's results in its closing turn). * @property {number} [count] - (Opt-in, NB-2 / Family B) FORCED fan-out: decompose into exactly this many * independent parallel workers via `Planner`→`runPlan`, then reduce. A positive integer here is the count; * it OVERRIDES the tier→count map. Setting it (or `mode:'fanout'`) takes the deterministic-parallelism path * instead of the model-driven Family-A default. For known-parallel tasks where the caller wants guaranteed * fan-out, not the model's adaptive choice. * @property {'fanout'|'partition'} [mode] - (Opt-in, NB-2 / Family B) `'fanout'` = forced semantic fan-out * WITHOUT a fixed count — derived from `assessComplexity`'s tier via the calibrated map (medium/complex/ * critical → 2/4/6; simple → 1); `opts.count` takes precedence. `'partition'` = the DATA-DRIVEN WIDTH path * (§11): measure `opts.corpus` and partition it into `max(opts.count floor, ⌈size/workerBudget⌉)` parallel * scan-workers (capped by the guards), CODE-reducing the per-chunk counts. Distinct from `'fanout'`: a data * partition, not a `Planner` semantic split. * @property {number} [workerBudget] - (`mode:'partition'`) items per worker; width = `⌈corpus.length / * workerBudget⌉` (default 100). A calibratable knob (the §9.1 algorithm), not a discovered constant. * @property {number} [concurrency] - (Family B) max workers run at once per wave (default 4). The wave * structure is `runPlan`'s; bareguard still bounds the family rate independently. * @property {'scan'|'search'|'exact'|'tools'} [retrieval] - (§10 step 7) the retrieval shape for a task OVER A * CORPUS, routed by question shape (§9.2.1). `'scan'` (the default WHEN `opts.corpus` is present) = process * every slice + LLM-judge + CODE-count — the only COMPLETE path (for "how many / all"). `'search'` = litectx * `recall` handle tool offered to the worker (needle; CANNOT count; requires `ctx.litectx`). `'exact'` = a * deterministic code-side AND-term filter tool over `opts.corpus`. `'tools'` = the PER-QUERY Family-A face: * offer the worker `scan_count` (over `opts.corpus`) + `search_memory` (when `ctx.litectx`) + `exact_match` * (array corpus) ALL AT ONCE, and let it pick the shape PER SUB-QUERY — the routing lives in the tool * descriptions (scan says "use for how many / all / count"; search says "never count"), so a mixed task gets * needle-search AND complete-count without per-sub-query adopter declaration. The completeness guard upgrades a * `'search'` on a "how many / all" ask to `'scan'` (UPGRADE-only, never a silent downgrade); it does NOT fire * for `'tools'` (the complete `scan_count` is always offered there, so a mixed task keeps its search tool). * Absent `corpus` AND `retrieval`, behaviour is unchanged (Family A / single-shot) — fully backward-compatible. * @property {Slice[] | (() => Promise)} [corpus] - (§10 step 7) the generic slice-source scan/partition * reads: an in-hand `{id, text}[]` array, OR an async `() => Promise` (e.g. `litectxCorpus(litectx, * {kind})` materializing a litectx-resident corpus via `enumerate`). recurse depends on this SHAPE, never on * litectx. Malformed entries are dropped, never miscounted. * @property {number} [window] - (scan) items per judge window. Default 8 (§9.2.1 recall knee — the one * calibrated number; per-model). * @property {number} [passes] - (scan) shuffled-boundary passes unioned for recall. Default 2 (~0.91 recall). */ /** * @typedef {object} RecurseNode * One audit/receipts node (RC-10) — the recursion tree reconstructs from these alone: parent→child lineage * (`spawned`), each subgoal (`task`), each gap report (`verdict`), cost per node (`tokens`). * @property {string} task * @property {number} depth * @property {{level: string, score: number}} complexity * @property {boolean} critical * @property {RecurseNode[]} spawned - Child nodes (lineage). * @property {Verdict|null} verdict * @property {boolean} incomplete * @property {boolean} halted * @property {string} [blocker] - Set when this node stopped for a specific non-model reason (mirrors * `RecurseResult.blocker`): `'governance-deny'` (BA-11) — its Loop short-circuited a consecutive-policy-deny * spin; `'broken-sensor'` (BA-15) — the caller's `refineLeaf.sensor` threw or returned a malformed verdict; * `'broken-verifier'` (BA-15) — the caller's `opts.evaluate` did (the default Evaluator path is never labeled). * @property {string} [blockerDetail] - (BA-15) with a `broken-*` blocker: what the arbiter did (threw with * which message, or which malformed shape it returned) — the actionable half of the label. * @property {{blocker: string, blockerDetail?: string, blockerTask?: string}} [blockerFrom] - (BA-15) a * DESCENDANT's blocker, surfaced here so an aggregating node still reports the fault upward. Deliberately * SEPARATE from this node's own `blocker` (which means "THIS node's arbiter/Loop broke"): stamping a * descendant's label onto every ancestor made the receipts tree accuse nodes whose sensor never ran, and * re-labelled a parent `governance-deny` when only one child was denied. `blockerTask` names the culprit. * @property {object|null} tokens - The worker Loop's `metrics.tokens`. * @property {{iterations: number, passed: boolean, temperatures: (number|null)[], rejectedBuffer: boolean}} [refineLeaf] - (BA-8) when * this leaf ran as a bounded refine loop: how many attempts it took and whether the deterministic sensor finally * passed (false = honest non-recovery, not a faked success). `temperatures` are the EFFECTIVE per-attempt temps * (BA-10): a `null` marks an attempt the model ran at its DEFAULT because it rejected the requested temperature. * `rejectedBuffer` (BA-14): whether any iteration injected the rejected-attempt buffer (prior failed attempts * surfaced verbatim); when it engages the REQUESTED retry temperature is held flat at `temperatures[0]`, never * escalated (ba14b antagonism) — but a temperature-fixed model still drops it, so the EFFECTIVE temp recorded * above is `null`, not `temperatures[0]`. * @property {string|null} model * @property {string|null} [retrieval] - (§10 step 7) the retrieval mode this node ran (`scan`/`search`/`exact`), * or null/absent for a plain reasoning node. * @property {string} [retrievalUpgraded] - set when the completeness guard upgraded the mode (e.g. * `'search→scan (completeness)'`) — the audit trail for RC-9-applied-to-retrieval. * @property {{window: number, passes: number, scanned: number, matched: number}} [scan] - (scan) the scan * shape: window/passes used, slices scanned, ids matched (CODE-counted). * @property {{size: number, workerBudget: number, floor: number, dataWidth: number, width: number, matched?: number}} [partition] * - (`mode:'partition'`) the data-driven width audit: corpus size, the budget knob, the count floor, the * data-derived width `⌈size/budget⌉`, the chosen `width = max(floor, dataWidth)`, and matched count. */ /** * @typedef {object} RecurseResult * @property {any} [result] - The synthesized answer (on convergence). * @property {Verdict|null} [verdict] - The verifier's gap report (null when verification did not run). * @property {boolean} [incomplete] - true on guard exhaustion / a dead worker / an incomplete child (RC-9) — * never a faked pass. * @property {any} [best] - The best partial answer when `incomplete` (RC-9). * @property {string[]} [missingSlices] - When `incomplete` because a child failed: the sub-task(s) that came * back incomplete (§9 scenario 1) — the anti-survivor-sum signal, not a quiet undercount. * @property {string} [blocker] - Present when `incomplete` for a specific, actionable reason. `'governance-deny'` * (BA-11): the worker's Loop short-circuited after N consecutive policy denials rather than burn to the * budget cap — the caller can widen scope / re-gate / escalate instead of reading it as a model failure. * `'broken-sensor'` (BA-15): the caller's `refineLeaf.sensor` threw or returned a malformed verdict — the * ARBITER is faulty, not the model; fix the sensor and re-run (`receipts.blockerDetail` says what it did). * `'broken-verifier'` (BA-15): same fault class at the verify slot — the caller's `opts.evaluate` threw * (non-Halt) or returned a malformed verdict; the default Evaluator path is never labeled (its failures are * provider-class faults). For both `broken-*` blockers `best` preserves the model's last non-empty output * (BA-5) — the arbiter broke, so treat it as best-effort work rather than a graded pass. * @property {string} [blockerDetail] - (BA-15) with a `broken-*` blocker: what the arbiter did — the * ACTIONABLE half of the label, surfaced on the result (not only in `receipts`) so a caller branching on * `blocker` can report the cause without walking the receipts tree. * @property {string} [blockerTask] - (BA-15) when the blocker was INHERITED from a descendant in a nested run: * which sub-task actually broke. Without it a nested failure reports "a sensor broke" with no way to find * which one. * @property {RecurseNode} receipts - The audit node for this call (RC-10). */ /** * Decompose a task into fresh-context workers, verify against a setpoint, and synthesize one result — * assembled from existing primitives, not reimplemented (G1/G6). * * ⚠️ RESOURCE BOUNDS ARE bareguard's, not recurse()'s — OPEN BY DESIGN (§6, "no second guard layer"), and the * one thing to know before running it. `recurse()` adds NO intrinsic total-work cap. The **Family-A default** * (model-driven `spawn_child`) lets a node spawn UP TO each Loop's `HARD_ROUND_LIMIT` (100) children PER LEVEL, * each recursing to `opts.maxDepth` (default 3) — so node count, and therefore TOKEN + $ SPEND, compounds * multiplicatively and is **not capped by recurse itself**. (The forced paths — `mode:'fanout'`/`'partition'` — * ARE bounded: a deterministic `count` + a `concurrency` cap. The uncapped path is the model-driven default.) * This is real, not theoretical: a live POC (`poc/rlm-defer2-history-overflow.mjs`) showed a weak model * over-decomposing into 40–117 calls on a single run. **So: running WITHOUT bareguard — or without ANY * token/cost cap — CAN BURN TOKENS / $ unboundedly (up to ~100×depth nodes).** **WIRE bareguard** * (`ctx.policy` via `wireGate`) for any non-trivial or untrusted run — it enforces depth/budget/call caps and * the pre-wave fan-out checkpoint, turning a runaway into a clean `{incomplete}`. With no gate available, the * only local brakes are `opts.maxDepth: 1` (flat — no nesting) and the provider/key's own usage limits. * * @param {string} task - The goal. * @param {RecurseCtx} [ctx] - The runtime wiring (provider, policy, depth, …). Threaded down the tree. * @param {RecurseOptions} [opts] - The policy knobs. * @returns {Promise} `{ result, verdict, receipts }` on convergence; `{ incomplete, best, * receipts }` on guard exhaustion. NEVER a fabricated success (RC-9). * @throws {Error} no provider supplied (on neither `ctx.provider` nor `opts.provider`). * @when a task is too big for one model pass and you want it split, fanned out, verified, and merged — with total cost capped by a gate * @fails returns `{incomplete, best}` on guard exhaustion or a dead worker (never a faked pass); a gate HaltError exits clean. Cost is open by design — run under a budget gate. * @example * const ctx = wireGate(gate); * const { result, incomplete } = await recurse('audit 400 logs', ctx, { provider, corpus }); * if (incomplete) retryOrEscalate(result); */ export function recurse(task: string, ctx?: RecurseCtx, opts?: RecurseOptions): Promise;