UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

244 lines (243 loc) • 15.3 kB
/** * design/376 片② — the host CONSOLIDATION DRIVER verb: one call that takes a long-accumulated, * never-consolidated library to its fold fixpoint through the four engine verbs, as a PERSISTENT * MULTI-CYCLE RUN. * * Why a run is a first-class object: under factory defaults a cold-start consolidation is a * dozen-plus cycles with a hard 60-second throttle floor between them — a persistent job measured * in minutes, not a call. The engine deliberately keeps NO driver state (its gate row settles per * plan and clears the session count), so without a durable run account a process exit between two * cycles would strand the work: no open plan to reconcile, no session edge to re-arm, and a naive * re-entry would re-mint the whole plan at full model cost. The run row under the control plane is * the resume seat: `runMemoryConsolidation` first looks for a PENDING run of the scope and * CONTINUES it (same plan cache, same force requestId, zero new mint) — the idempotent re-entry * GD-15 pins. * * Boundary reminders (design/339 §1.1, unchanged by this module): * - the engine adjudicates and commits; this driver only proposes and schedules — every write * goes through `commitConsolidationPlan`, nothing here touches the store; * - the engine holds no model seat; the model reaches this module as an explicit chat function; * - core still owns no timer: the throttle waits here run INSIDE a host's explicit call (or the * auto lane's task-boundary tick), never on a background schedule of core's own. */ import type { EngineNotice } from "../types.js"; import { type ConsolidationGateRow, type ConsolidationRunStopReason, type MemoryConsolidationOptions } from "./consolidation.js"; import { type ConsolidationPlanFoldEvidence, type ConsolidationPlanSummary } from "./engine.js"; import { mintLlmConsolidationPlan, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationFoldState, type DistillerChatFn, type LlmConsolidationPlan, type LlmConsolidationPlanProduct, type LlmDistillerContract } from "./distiller.js"; export declare const CONSOLIDATION_DRIVER_RUNS_FILE = "distiller-runs.json"; /** §1.6-4 — the mint archive: what was shown to the model and what it answered, verbatim, per run * (the fold law's audit other-half; strict-family placement beside the plan workbook). */ export declare const CONSOLIDATION_DRIVER_PLANS_DIR = "distiller-plans"; export interface ConsolidationDriverRunRow { runId: string; scope: string; /** The WRITER claim: every open/resume bumps this under the run * store's lock, and every later write of the row is conditional on (runId, attempt) still * standing — of two invocations that raced onto one scope, exactly one keeps writing; the * superseded one stops with a coded error instead of silently clobbering the winner's counters * or terminal state. (Model spend under a genuine concurrent double-call is still at-least-once * — the claim serializes the ACCOUNT; the engine's plan-seat CAS serializes the store, and * cross-node cost suppression is the engine lease seat's job.) */ attempt: number; /** The run-scoped force credential: ONE requestId across every forced snapshot of the run (the * engine dedups the FORCED audit announcement on it — one line per run). */ requestId: string; /** The contract version of the run's surviving MINT — stamped at open, RE-stamped (with * {@link model}) by a resume that re-mints. */ contractVersion: string; /** The id that minted the run's surviving plan: stamped at open; RE-stamped on a resume that * re-mints (the plan cache absent — nothing of the opening mint survived, so the current seat * mints every byte this run commits). The per-attempt plan archive is the byte-level anchor * either way. */ model: string; startedAt: number; state: "pending" | "settled"; /** Terminal only; the ONE closed set the receipt and the `_incomplete` notice share. */ outcome?: ConsolidationRunStopReason; settledAt?: number; stopDetail?: string; /** The minted plan's replayable half (mint ONCE per run; each cycle filters, never re-mints). * design/376-C1: `mintExposure` + `servedCandidates` ride the cache — the attestation threads * onto every cycle's proposal, the roster feeds the cross-cycle invalidation predicate. Rows * cached before the fields existed replay UN-attested with no roster check (run-level fold — * exactly the treatment their whole-library prompts earned). */ planCache?: { maxInputsPerProduct: number; products: LlmConsolidationPlanProduct[]; mintExposure?: "partitioned"; servedCandidates?: Array<{ id: string; rev: string; marked: boolean; }>; }; /** design/376-C1 A-5 disclosure — marked candidates withheld from the clean arm's prompts at * mint time (present ⇔ the partition engaged over a marked library). */ withheldFromCleanArm?: number; /** design/383 §1.5 ③ — of {@link withheldFromCleanArm}, how many rows were withheld from EVERY * call, i.e. never sent to the provider at all (a lone marked row has no group to fold, so no * arm renders it). Recorded at mint time rather than derived from the roster later: the roster * dies with a discarded cache, and "your data never left this machine" must not become * unanswerable because an unrelated guard fired. */ withheldNeverSent?: number; /** The archived mint transcript's file name under {@link CONSOLIDATION_DRIVER_PLANS_DIR}. */ planArchive?: string; /** COMMITTED cycles only (the store moved): a refused/fuse-blocked/zero-delta cycle row does * not count — the receipt's "committed cycle(s)" label and the incomplete notice's zero-cycle * branch both read this number as writes. */ cyclesDone: number; /** Store-delta superseded total across EVERY invocation of this run (the conservation wall's * evidence half — see the convergence check in the verb). Accumulated per cycle AFTER the * commit, so a crash between a cycle's commit and its row write loses that cycle's delta — * {@link foldBaseline} is the wall's immune second witness for exactly that window. */ entriesSuperseded: number; /** The scope's fold-state `superseded` count at run OPEN (fresh claim; absent on rows opened * before the field existed). The conservation wall's STORE anchor: `final.superseded − this` * attests the run's folds even when a crash lands between a cycle's commit and its account * write — the seat serializes this scope's folds, so the since-open delta is this run's. */ foldBaseline?: { superseded: number; }; planIds: string[]; usage: { calls: number; promptTokens: number; completionTokens: number; retries: number; truncations: number; }; repairs: { hallucinatedMembers: number; duplicateMembers: number; nonIntegerMembers: number; emptyGroups: number; oversizeGroups: number; }; writeFailures: Array<{ key: string; members: number; reason: string | null; }>; residue?: Array<{ name: string; targets: number; }>; /** The next moment a snapshot can pass the hard throttle (resume scheduling hint). */ nextEligibleAt?: number; } /** Host read face: the scope's latest driver run row (pending = resumable; settled = audit). */ export declare function readConsolidationDriverRun(controlDir: string, scope: string): ConsolidationDriverRunRow | undefined; /** Archive one mint (prompts + transcript verbatim) beside the run account. The name carries the * CLAIM (`attempt`): two invocations that raced onto one run each archive * under their own name, so a superseded worker can never overwrite the winning transcript — the * run row's `planArchive` (written under the attempt guard) names the one that counts. */ export declare function distillerPlanArchiveName(runId: string, attempt: number): string; export declare function archiveDistillerPlan(controlDir: string, runId: string, plan: LlmConsolidationPlan, attempt?: number): string; /** The engine face this verb consumes — `MemoryEngine` satisfies it structurally. * (`readConsolidationPlanFoldEvidence` joined in design/376 slice ③, #436: the conservation * wall's terminal witness is member-level, so the face is REQUIRED — an engine without it could * only offer the store-delta counts an interleaved fold can numerically offset.) */ export interface ConsolidationDriverEngine extends ConsolidationDriveEngine { readConsolidationGate(scope: string): ConsolidationGateRow | undefined; listConsolidationPlans(): ConsolidationPlanSummary[]; readConsolidationPlanFoldEvidence(planId: string): ConsolidationPlanFoldEvidence; readConsolidationFoldState(scope: string): Promise<ConsolidationFoldState>; readonly controlPlaneRoot: string; } export interface RunMemoryConsolidationOptions { /** The explicit model seat (design/339 §1.1: the ENGINE holds none). Build one with * `openAiCompatChatSeat` or hand any JSON-only completion function. */ chat: DistillerChatFn; /** The explicit model id, recorded into the run and the archive (alias spellings refuse). */ model: string; /** Audit locator recorded into the archive when the seat is endpoint-shaped. */ baseUrl?: string; /** The frozen prompt contract; default = the product v1 (dl-3 byte-identical). A different * version here is a deployment's explicit measured choice. */ contract?: LlmDistillerContract; /** The PROTOCOL knobs — pass the same object the engine was constructed with * (`MemoryEngineOptions.consolidation`); the driver derives its throttle window and per-plan * caps from it. Defaults = the factory defaults. */ consolidation?: MemoryConsolidationOptions; /** Abort the run after this many cycles in THIS call (the run row stays pending only on crash; * a reached bound settles `max_cycles`). Positive integer; default 64. */ maxCycles?: number; /** R2 — the structural-repair budget over the mint's member references: the ratio of member * claims the sanitiser had to DROP (hallucinated + duplicate + non-integer) to all member * claims the model made. Above this, the plan does not represent the model's intent and the * run aborts with zero writes rather than committing a harness-repaired fold. In [0, 1]; * default 0.10. (Empty groups are the same drops counted at group grain; oversize groups are * not repairs — they ride on, named, and the schedule reports them infeasible. Format repairs * and retries are disclosed separately and bounded by the contract's own retry count.) */ mintRepairBudget?: number; /** The run's force credential; defaults to a minted id. A RESUMED run keeps its original. */ requestId?: string; /** Host seams for the throttle waits (tests and hosts with their own clocks). */ now?: () => number; sleep?: (ms: number) => Promise<void>; /** Delivery sink for the run's advisory notices, when the caller wires one: delivered at the mint, once, * through the engine's notice throat, and still on the receipt. Absent ⇒ the receipt alone carries them. */ onNotice?: (notice: EngineNotice) => void; onProgress?: Parameters<typeof mintLlmConsolidationPlan>[0]["onProgress"]; } export interface ConsolidationRunReceipt { runId: string; scope: string; /** Whether this call CONTINUED a pending run (no new mint) instead of starting one. */ resumed: boolean; outcome: ConsolidationRunStopReason; converged: boolean; stopDetail?: string; /** This call's cycles (store-delta accounted); `cyclesDone` is the run total across resumes. */ cycles: ConsolidationDriveCycleRow[]; cyclesDone: number; productsCommitted: number; entriesSuperseded: number; planIds: string[]; model: string; contractVersion: string; usage: ConsolidationDriverRunRow["usage"]; repairs: ConsolidationDriverRunRow["repairs"]; writeFailures: ConsolidationDriverRunRow["writeFailures"]; residue: Array<{ name: string; targets: number; }>; /** design/376-C1 A-5 disclosure — marked candidates withheld from the clean arm's prompts * (present ⇔ the partitioned mint engaged over a marked library). A host reading a clean * product's silence about some fact key should know the current value may sit in a withheld * (marked, handle-faced) row. */ withheldFromCleanArm?: number; /** design/383 §1.5 ③ — of {@link withheldFromCleanArm}, the rows sent to NO call at all (never * left this process). "Withheld from the clean arm" and "never sent to the provider" are * different facts about a user's data; the receipt reports them separately so a host's wording * can too. */ withheldNeverSent?: number; planArchive?: string; /** Advisory notices minted for this run (the `_incomplete` disclosure when not converged). */ notices: EngineNotice[]; /** A failed announcement enqueue is disclosed, never allowed to fail the settled run. */ announceFailures?: string[]; } /** * Run (or RESUME) one scope's consolidation as a persistent multi-cycle job. See the module note * for the shape; the per-refusal behavior implements the design/376 §1.2 failure table: * * - `_disabled` / a corrupt gate / an unreadable governance account BEFORE any commit: the coded * error is rethrown (a configuration/store fault the host must repair — a pending run row, if * one exists, stays resumable after the repair); * - `_gated` (time/session gate): retried under the run's force requestId — the run-level force * the design names; the FORCED audit line is engine-deduplicated to once per run; * - `_gated` (hard throttle): the driver WAITS the window out (`min(minRunIntervalMs, 60s)`) and * continues — a throttled cycle is scheduling, not a terminal; * - `_busy`: never a blind snapshot retry (the seat refuses snapshots too — a blind loop spins * forever): `reconcileConsolidation` first; a conflict/corrupt plan parks the run for the host * valve; a live competing worker is waited out briefly, then reported; * - model failures / a mint over the repair budget: the run settles `driver_failed` with ZERO * writes — the mint sits between snapshot and commit, so no seat was taken, `lastRunAt` never * moved and the time gate is not burned (the failure is free to retry); * - a conflict plan mid-run parks (`parked`); fuse residue and the cycle bound settle * `fuse_residue` / `max_cycles` with the residue named — the next run's FULL snapshot * re-distills the shrunken library (cross-run = re-mint; in-run = plan cache). */ export declare function runMemoryConsolidationDriver(engine: ConsolidationDriverEngine, scope: string, opts: RunMemoryConsolidationOptions): Promise<ConsolidationRunReceipt>;