@sema-agent/core
Version:
Stateless, task-oriented AI agent core
244 lines (243 loc) • 15.3 kB
TypeScript
/**
* 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>;