@sema-agent/core
Version:
Stateless, task-oriented AI agent core
750 lines (749 loc) • 38.5 kB
TypeScript
/**
* design/376 片① — the LLM distillation DRIVER's single source (the "driver half" of design/339
* §1.1: the driver proposes, the engine adjudicates). Everything here is the product code the
* memory benchmark measures — the bench adapters import THESE exports, so every compare run is an
* end-to-end regression of the shipped driver (the true-load-path law: the bytes the rig scores
* are the bytes the npm package ships).
*
* Structure, and why each piece lives here:
* - the PROMPT CONTRACT (frozen, versioned): the prompt is a behavior surface. v1 of the product
* contract is the dl-3 contract BYTE-IDENTICAL — the version whose readings the benchmark
* archived. Any change to these strings is a NEW contract version and must produce bench
* readings before it ships (no-readings prompt edits are unreviewed behavior changes).
* - `parseJsonAnswer` / `sanitizeLlmGroups` / `planParseRepairs`: the structural adapter between
* a model answer and a protocol-shaped proposal, every repair COUNTED (a silent repair lets a
* sloppy plan read as a clean one).
* - `scheduleUnderFuse`: the driver-side plan selection under the engine's supersession fuse
* (next-fit decreasing, no backfill). The ceiling itself is NEVER re-derived here — callers
* compute it with the engine's own exported `supersessionFuseCeiling` (a second implementation
* would be a drift seam).
* - `mintLlmConsolidationPlan`: the live half (grouping + writing passes), archiving everything
* the model was told and answered.
* - `llmPlanDistiller`: a minted plan replayed as a per-cycle distiller (the run-cache form: mint
* once, filter per cycle — never re-mint inside one run).
* - `driveConsolidationToFixpoint`: the host loop over the four engine verbs (snapshot → commit →
* reconcile, repeated), counting exclusively STORE deltas.
*
* The engine holds no distillation power and no model seat (design/339 §1.1 boundary): this module
* calls the engine's HOST verbs and nothing else writes.
*/
import type { MemoryEntry } from "./types.js";
import type { ConsolidationProposal } from "./consolidation.js";
export declare const DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT = 32;
export interface LlmDistillerContract {
/** The contract's registered version id (a NEW id for any byte change, with bench readings). */
version: string;
grouping: {
system: string;
instructionLines: readonly string[];
instructions: string;
};
writing: {
system: string;
instructions: string;
};
sampling: {
temperature: number;
maxTokens: {
grouping: number;
writing: number;
};
};
/** Format-failure retries per call. Frozen INTO the contract: the archived readings were taken
* under this value, so changing it moves the thing under measurement. */
retries: number;
}
export declare const LLM_DISTILLER_CONTRACT: LlmDistillerContract;
/** dl-2: the SAME contract with the retrievability clause disambiguated, nothing else touched. */
export declare const LLM_DISTILLER_CONTRACT_DL2: LlmDistillerContract;
/** dl-3: dl-2 with the SIZE clause substituted, nothing else touched. */
export declare const LLM_DISTILLER_CONTRACT_DL3: LlmDistillerContract;
export declare const LLM_DISTILLER_CONTRACTS: Record<string, LlmDistillerContract>;
/**
* The PRODUCT contract, v1 = dl-3 byte-identical (the version whose benchmark readings are
* archived). The shipping rule this alias exists to make auditable: a change to the product prompt
* is a NEW `version` (pd-2, …) and must produce rig readings BEFORE it ships — the contract-hash
* pin in the test suite reds on any silent byte drift under an unchanged version id.
*
* STANDING OBLIGATION on pd-2 (design/376-C1, ruled — mandatory, not optional): the next contract
* version MUST carry, as its FIRST A/B item, the root-elimination arm for the shared-grouping
* influence channel — the clean arm's writing pass without the grouping-authored `intent` (the
* template's own "(none stated)" arm) and without the `key` name fallback, which empties the one
* byte path an unattributed candidate has into a clean product and retires most of the D-1b guard
* machinery. The A/B must publish an EXPLICIT continuation verdict, one of three states — switch
* (root elimination ships) / hold (guard form stays, with readings) / redirect — it may not be
* left hanging. The exposure-partitioned topology itself deliberately did NOT bump this version
* (the candidate-set composition is topology, not wording; the wording axis stays single-variable
* for exactly this A/B).
*
* WHERE THAT OBLIGATION NOW LIVES IN CODE: the root-elimination arm is the writing-channel option pair on
* {@link mintLlmConsolidationPlan} (`writingIntent: "none-stated"` + `productKeyFallback: false`, recorded as
* `minting.writingChannel`); the A/B readings, verdict and ruling are tracked in `docs/memory-distillation-ga.json`.
*/
export declare const MEMORY_DISTILLER_CONTRACT_V1: LlmDistillerContract;
/**
* The A/B witness: which grouping lines differ between two contracts, by index — published beside
* readings so "only the wording moved" is a checkable statement about the prompts actually sent.
*/
export declare function contractGroupingDiff(a: LlmDistillerContract | undefined, b: LlmDistillerContract | undefined): {
sameLineCount: boolean;
changedClauseIndexes: number[];
before: Array<string | null>;
after: Array<string | null>;
otherClausesIdentical: boolean;
writingPromptIdentical: boolean;
systemPromptsIdentical: boolean;
samplingIdentical: boolean;
};
/** The snapshot row shape the engine's `snapshotConsolidationCandidates` answers. */
export interface DistillerCandidate {
entry: MemoryEntry;
marked: boolean;
}
/** One JSON-only completion request (no streaming, no tools — the contract's whole call shape). */
export interface DistillerChatRequest {
system: string;
user: string;
maxTokens: number;
temperature: number;
}
export interface DistillerChatAnswer {
content: string;
/** The provider's finish reason; `"length"` marks a token-ceiling cut-off (retried, disclosed). */
finishReason?: string | null;
usage?: {
promptTokens?: number;
completionTokens?: number;
} | null;
/** The model id the provider REPORTS having served (audit anchor beside the requested id). */
model?: string | null;
}
export type DistillerChatFn = (req: DistillerChatRequest) => Promise<DistillerChatAnswer>;
/** True when the id is an ALIAS spelling (re-pointable upstream), refused wherever a model id is
* taken. A conservative SCREEN over the common alias grammars, not a full catalog — the honest
* direction is one-way: `true` ⇒ alias, while a `false` only means "not a spelling this screen
* knows". Screened: any id whose FINAL dash segment is `latest`/`default` regardless of how many
* segments the family name has (`chatgpt-4o-latest`, `claude-3-5-sonnet-latest`,
* `gemini-1.5-pro-latest`), plus the single-family `-chat` form (`deepseek-chat`, `qwen-chat` —
* the alias family upstream re-points have actually ridden). Multi-segment `-chat` ids are NOT
* screened on purpose: that suffix also names pinned chat variants of fixed weights
* (`llama-2-7b-chat`), and refusing those would reject explicit ids. */
export declare function isAliasModelId(model: string): boolean;
/**
* An OpenAI-compatible chat seat over an explicit endpoint. Screened at CONSTRUCTION (the
* bad-value loudness law): a missing key, an alias model id or an unservable request root refuses
* with a code before any call is made — an alias can be re-pointed upstream and would move the
* driven model with no trace in the archived plan.
*
* The root is judged by {@link assertServableRequestRoot} — the SAME law the brains apply, because
* emptiness alone let userinfo, a non-http(s) scheme and a non-URL spelling reach `fetch` as a
* TRANSPORT error. Only the ENVELOPE is this seat's; the driver routes config throws apart. */
export declare function openAiCompatChatSeat(seat: {
baseUrl: string;
apiKey: string;
model: string;
}): DistillerChatFn;
export type ParsedJsonAnswer = {
ok: true;
value: unknown;
repaired: boolean;
} | {
ok: false;
error: string;
sample: string;
};
/**
* Parse ONE JSON object out of a model answer. Fenced and prefixed answers are tolerated (a
* formatting failure, not a distillation one) and the tolerance is COUNTED. A truncated answer is
* never salvaged — half a plan scored (or committed) as a whole plan is the failure mode this
* function exists to refuse.
*/
export declare function parseJsonAnswer(text: unknown): ParsedJsonAnswer;
export interface PlanParseRepairs {
grouping: number;
writing: number;
groupingCalls: number;
writingCalls: number;
/** Calls whose answer was cut off at the token ceiling — a HARNESS parameter biting, named per
* call and never folded into a repair count it is not. */
lengthCapped: string[];
retries: number;
}
/**
* Format repairs per PHASE, re-derived from a plan's own archived transcript — one derivation
* produces the number (the grouping stage cannot be counted while the writing stage silently is
* not), and any reader can re-run it against the archive and get the same answer.
*/
export declare function planParseRepairs(plan: {
minting?: {
transcript?: Array<{
label?: unknown;
answer?: unknown;
finishReason?: unknown;
}>;
};
} | undefined | null): PlanParseRepairs;
export interface SanitizedLlmGroups {
groups: Array<{
key: string;
intent: string;
members: Array<{
n: number;
id: string;
}>;
}>;
repairs: {
hallucinatedMembers: number;
duplicateMembers: number;
emptyGroups: number;
oversizeGroups: Array<{
key: string;
members: number;
cap: number;
}>;
nonIntegerMembers: number;
};
claimedIds: string[];
}
/**
* Turn the model's groups into a protocol-shaped product list, repairing only STRUCTURE and
* counting every repair:
*
* hallucinatedMembers — a number that is not in the candidate list at all;
* duplicateMembers — a number claimed by a second group (first claim, in the model's own
* group order, wins; the later claim is dropped);
* emptyGroups — a group left with no members after the two above;
* oversizeGroups — a group over `maxInputsPerProduct`. NOT split: chunking a group is a
* distillation decision, and making it on the model's behalf would put
* this module in the driver's seat. They ride on and the schedule reports
* them as structurally infeasible;
* nonIntegerMembers — a member reference that is not a strict integer. STRICT because
* `parseInt` is a silent truncator and the thing truncated is a ROW
* reference: "2foo"→2 and "1.5"→1 would each fold a DIFFERENT entry than
* the model named while every repair counter stayed at zero.
*/
export declare function sanitizeLlmGroups(groups: unknown, { candidateIds, maxInputsPerProduct }: {
candidateIds: readonly string[];
maxInputsPerProduct?: number;
}): SanitizedLlmGroups;
export interface LlmConsolidationPlanProduct {
key: string;
intent: string;
name: string;
description: string;
type: "project" | "reference";
body: string;
inputIds: string[];
/** The committed rev of each input AT MINT TIME (id → rev). The replay filter refuses to fold an
* input whose committed rev has moved since the model read it — the product's body was written
* over the OLD bytes, and superseding the edited entry under it would suppress a newer value
* behind stale prose. Absent on plans minted before this field existed: those replay by id
* alone, exactly as they always did (the archived-readings compatibility arm). */
inputRevs?: Record<string, string>;
}
/** design/376-C1 §2.2 — one arm's mint accounting inside an exposure-partitioned plan (usage
* "两臂合计+分列": the merged plan's top-level `minting` carries the totals, these rows carry the
* per-arm split PLUS each arm's verbatim grouping prompt — the clean arm's prompt bytes are the
* C1-G5 audit face: any reader can mechanically verify zero marked-entry bytes reached it). */
export interface LlmConsolidationPlanArm {
arm: "clean" | "marked";
candidateCount: number;
calls: number;
promptTokens: number;
completionTokens: number;
retries: number;
truncations: number;
groupingPrompt: string;
}
export interface LlmConsolidationPlan {
kind: "llm-consolidation-plan";
contractVersion: string;
model: string;
/** The endpoint the mint ran against, when the caller supplied one (audit locator only). */
baseUrl?: string;
mintedAt: string;
maxInputsPerProduct: number;
candidateCount: number;
products: LlmConsolidationPlanProduct[];
/** design/376-C1 D-2 — present ⇔ this plan was minted by {@link mintExposurePartitionedPlan}
* (clean/marked candidates partitioned into physically separate call chains). The DRIVE threads
* this onto the committed proposal, where it is the engine's per-product-fold attestation.
* Absent on every plan minted before the field existed — those replay UN-attested (run-level
* fold, the safe default), which is exactly right for prompts composed over the whole library. */
mintExposure?: "partitioned";
/** design/376-C1 D-1b — the mint's served-world roster: every candidate actually RENDERED into
* some arm's grouping call, with the rev and marked flag it was served at. The driver's
* cross-cycle invalidation predicate compares each row against the fresh snapshot
* (offered ∧ same rev ∧ same marked, or departed-with-own-fold-evidence) — any other state
* means the world the prompts were composed over no longer stands. A single withheld marked
* row (markedCount = 1, no marked arm) is deliberately NOT in the roster: no call ever saw it. */
servedCandidates?: Array<{
id: string;
rev: string;
marked: boolean;
}>;
/** design/376-C1 A-5 disclosure — how many marked candidates were withheld from the CLEAN arm's
* prompts (= the marked candidate count when the partition engaged). Present ⇔ partitioned
* mint over a library with at least one marked row. */
withheldFromCleanArm?: number;
minting: {
calls: number;
groupingAttempts: number;
groupingParseRepairs: number;
parseRepairs: PlanParseRepairs;
repairs: SanitizedLlmGroups["repairs"];
writeFailures: Array<{
key: string;
members: number;
reason: string | null;
}>;
usage: {
promptTokens: number;
completionTokens: number;
};
prompts: {
grouping: string;
writingTemplate: string;
systems: {
grouping: string;
writing: string;
};
};
transcript: Array<{
label: string;
ms: number;
usage: DistillerChatAnswer["usage"];
finishReason: string | null;
model: string | null;
answer: string;
}>;
/** Per-arm split of a partitioned mint (see {@link LlmConsolidationPlanArm}); absent on
* single-call-chain plans, INCLUDING the zero-marked degenerate of the partitioned mint
* (whose bytes must stay identical to the plain mint's, modulo the attestation echo). */
arms?: LlmConsolidationPlanArm[];
/** pd-2 root-elimination A/B — the WRITING CHANNEL this plan was minted under, PRESENT ONLY when non-default,
* so a default plan's bytes are unchanged and a reading can attribute its arm from the archive alone. */
writingChannel?: {
intent: "grouping" | "none-stated";
keyFallback: boolean;
};
};
}
export type MintLlmConsolidationPlanResult = {
ok: true;
plan: LlmConsolidationPlan;
} | {
ok: false;
reason: string;
calls: LlmConsolidationPlan["minting"]["transcript"];
};
/**
* Mint a fold plan from a live model over one candidate snapshot. `candidates` is the snapshot
* shape the engine hands the driver (`{ entry, marked }`). Everything the model saw and everything
* it answered is returned inside the plan so the run can be re-read, re-scored and disputed from
* the archive alone.
*/
export declare function mintLlmConsolidationPlan({ candidates, chat, model, baseUrl, maxInputsPerProduct, contract, onProgress, armLabel, writingIntent, productKeyFallback, }: {
candidates: readonly DistillerCandidate[];
chat: DistillerChatFn;
/** The requested model id, recorded into the plan (audit anchor). Alias spellings refuse. */
model: string;
baseUrl?: string;
maxInputsPerProduct?: number;
contract?: LlmDistillerContract;
onProgress?: ((p: {
phase: "grouping";
groups: number;
claimed: number;
of: number;
} | {
phase: "writing";
done: number;
of: number;
}) => void) | null;
/** design/376-C1 — the partitioned mint's TRANSCRIPT arm tag (`grouping.clean#1` /
* `writing.marked#1:key#1` label forms). Prompt bytes are untouched — this names calls in the
* archive only, and stays compatible with `planParseRepairs`' `startsWith("grouping")` phase
* split and its `#N` retry grammar. Absent ⇒ the historical label forms, byte-identical. */
armLabel?: "clean" | "marked";
/**
* pd-2 root-elimination A/B (the standing obligation on {@link MEMORY_DISTILLER_CONTRACT_V1}) — the WRITING
* CHANNEL: which grouping-authored bytes reach the writing pass. `"grouping"` (absent ≡ this, the shipped form)
* fills `{{INTENT}}` with the grouping pass's own `intent`; `"none-stated"` fills it with `(none stated)` for
* EVERY group, so the clean arm's authoring prompt carries no grouping-authored intent bytes. Mint TOPOLOGY, not
* wording: the contract's frozen text is untouched and no version moves. Recorded on the plan
* (`minting.writingChannel`) whenever non-default.
*/
writingIntent?: "grouping" | "none-stated";
/**
* The channel's second byte path: `true` (absent ≡ this) names a product after the grouping-authored `key` when
* the writing answer carries no `name`; `false` treats a missing or empty `name` as a FORMAT failure (retried,
* then a counted write failure — R3), so nothing grouping-authored ever becomes a product's name on this arm.
*/
productKeyFallback?: boolean;
}): Promise<MintLlmConsolidationPlanResult>;
/** ONE rev sentinel for every face that RECORDS or RE-CHECKS a candidate's rev (the mint roster
* below, the driver's cross-cycle roster re-check, the replay's offered map). The snapshot
* interface is structural — a host-supplied engine can hand entries whose rev is not a string at
* runtime — and the recording and re-checking sides MUST agree on the fallback, or a rev-less
* candidate compares unequal to its own roster row and self-invalidates the mint it was just
* served into (a paid mint discarded on its own cycle, re-minted and re-discarded forever). */
export declare const normalizedCandidateRev: (rev: unknown) => string;
/**
* design/376-C1 §2.2 — mint an EXPOSURE-PARTITIONED fold plan: candidates are bisected on the
* snapshot's per-candidate `marked` flag and each subset is minted through its own physically
* separate call chain, so no clean product's authoring chain (grouping intent included) is ever
* served a marked entry's bytes. This is what makes the engine's per-product fold SOUND — the
* attestation the plan carries (`mintExposure: "partitioned"`) is the freeze's gate, and this
* function is the single source the product driver and every bench adapter share.
*
* - CLEAN arm: `mintLlmConsolidationPlan` over the clean subset — contract, templates, retries,
* sampling all byte-identical to the plain mint (dl-3 恒等; only the candidate-set composition
* moves, which is topology, not wording — no prompt-contract version bump, D-10/A-10).
* - MARKED arm: the same contract over the marked subset, minted only at `markedCount ≥ 2`
* (design C1-B): a single marked row has no group to fold and simply stays where it is — an
* index handle row loses no discoverability by standing still. Withheld rows are disclosed
* (`withheldFromCleanArm`) and kept OUT of the served roster (no call saw them).
* - ZERO-marked degenerate: exactly ONE arm over exactly the full set with the HISTORICAL
* transcript labels — byte-identical to the plain mint modulo the attestation echo and the
* roster (C1-G1's identity claim).
* - Either arm failing fails the WHOLE mint (one mint, one verdict; both arms' calls ride the
* failure transcript). This widens nothing: under the run-level mint an adversarial marked row
* already sat inside the single grouping call and could fail it whole.
* - Cross-arm group structure is impossible by construction: each arm's `sanitizeLlmGroups`
* valid map is built over that arm's own 1..N numbering.
*/
export declare function mintExposurePartitionedPlan(args: Parameters<typeof mintLlmConsolidationPlan>[0]): Promise<MintLlmConsolidationPlanResult>;
/**
* The detector's frozen algorithm parameters — a CONTRACT SIBLING, published beside the readings
* so a run's verdict can be re-derived from the archive alone, and deliberately NOT a knob: this
* is a safety gate, and a safety gate with an off switch is a safety gate that gets switched off
* under deadline pressure. A new parameter set is a NEW `version`, exactly like the prompt
* contract's own rule.
*/
export interface MemoryDistillerPurityContract {
version: string;
/** Unicode normalization applied to BOTH sides before comparison (so a decomposed copy of a
* composed original still compares equal). */
normalization: "NFC";
/** Case is PRESERVED. Case-folding would widen the detector toward paraphrase matching and start
* minting false positives on ordinary prose — this gate answers "were these bytes copied", and
* a case change is already outside "verbatim". */
caseFold: false;
/** Shingle width, in whitespace-separated words: the comparison unit on every side. */
shingleWords: number;
/** A shingle shorter than this many characters is dropped before comparison (the floor that
* keeps short, high-frequency phrases out of the evidence set). */
minShingleChars: number;
}
export declare const MEMORY_DISTILLER_PURITY_CONTRACT_V1: MemoryDistillerPurityContract;
/** One witness: a marked-side-unique shingle found verbatim in a clean product's own bytes. */
export interface CleanArmLeakFinding {
/** The product's INDEX in `plan.products` — the locator safe to put in a message. Unlike
* {@link productKey} it is harness-assigned, so it cannot itself carry a payload. */
productIndex: number;
/** The product's key AS THE MODEL WROTE IT (`sanitizeLlmGroups` passes `String(g.key)` through
* unbounded). Useful when reading the archive; NEVER safe to interpolate into a notice, an
* announcement or a log line that reaches a model context — the key is model-authored text and
* can carry the same payload the shingle does. Use {@link productIndex} there. */
productKey: string;
field: "name" | "description" | "body";
/** The offending shingle VERBATIM — the evidence, not a description of it. Same rule as
* {@link productKey}: this is for the archive and an operator's own eyes, never for a message
* that lands in front of a model. */
shingle: string;
}
export interface CleanArmLeakVerdict {
/** False ⇔ the gate had nothing to check (see {@link notEngagedReason}); never a pass claim. */
engaged: boolean;
notEngagedReason?: string;
/** Size of the marked-side-unique evidence set (clean-arm bytes already subtracted). */
markedUniqueShingles: number;
/** How many products were scanned (the all-clean-input products of a partitioned plan). */
scannedProducts: number;
findings: CleanArmLeakFinding[];
}
/**
* design/383 §1.3 (S-1) — a VERBATIM-LEAK DETECTOR over a minted exposure-partitioned plan. Pure
* bytes, zero model calls, run in the driver between the mint and the plan cache.
*
* WHAT IT ASKS: did any byte sequence that ONLY the marked arm was shown come back out inside a
* product that will commit UNMARKED? The prompt-side audit (C1-G5) proves the clean arm's prompt
* carried no marked bytes; this proves the partition machine itself was not bypassed, mis-wired or
* side-channelled — a different failure surface, which is why both gates ship.
*
* HOW:
* 1. evidence source = the MARKED arm's verbatim grouping prompt (`minting.arms[marked]`), which
* is the exact rendered form the model saw — `name — description\n body`. Reading only entry
* bodies would miss a payload parked in a `description`;
* 2. minus the CLEAN arm's rendered bytes ⇒ the MARKED-SIDE-UNIQUE set. Text both sides legitimately
* carry (the shared instruction template, a description a clean sibling also holds) proves
* nothing and is subtracted here — this step is what keeps the gate off false positives;
* 3. scanned surface = the all-clean-input products' `name` / `description` / `body`. "Clean" is
* read off the plan's own served roster (no marked input id), which is the same judgment that
* decides the product commits without an origin marker. A plan that claims the attestation but
* carries NO roster (no plan this driver mints, but a foreign minter could stamp one) has every
* product read as clean — including the marked arm's own, which legitimately holds marked
* bytes — so it refuses. That is the intended direction: an attestation with no evidence behind
* it should not buy a plan the per-product fold;
* 4. any intersection ⇒ the caller fails the mint WHOLE. A leak falsifies the clean arm's
* isolation premise, and the other products of the same mint rest on that same premise.
*
* HONEST BOUNDARY (do not let this JSDoc be read as more than it is): this catches VERBATIM
* copying. It does not catch paraphrase, translation, or a summary of marked content — a complete
* detector would need a semantic judge, i.e. a model judging a model, which this design forbids.
* A leak shorter than one shingle (see the contract's `shingleWords`/`minShingleChars`) is below
* the floor and will not be seen. It is a leak DETECTOR, not a purity PROOF; `engaged: false` is
* "nothing to check here", never "checked and clean".
*/
export declare function detectCleanArmVerbatimLeak(plan: LlmConsolidationPlan, contract?: MemoryDistillerPurityContract): CleanArmLeakVerdict;
/** The distiller signature the drive consumes: `(candidates, snapshot, scope) -> proposal`.
* At runtime only `entry.id` is load-bearing for the replay filter — test/bench stubs that hand
* partial entry shapes are tolerated by the reads, the type states the engine's real snapshot.
* The drive consumes the proposal's FOLD face only (`products`; `scope` is the drive's own): a
* proposal carrying directed `intents` is REFUSED by the drive rather than silently narrowed —
* directed update/delete intents go through `commitConsolidationPlan` directly (see the drive's
* own doc for why a per-cycle forward is structurally impossible). */
export type ConsolidationDistillFn = (candidates: readonly DistillerCandidate[], snapshot: unknown, scope: string) => ConsolidationProposal | Promise<ConsolidationProposal>;
/**
* A minted plan replayed as a per-cycle distiller. Per cycle it filters each product's inputs down
* to the ids the snapshot is still offering — a product whose inputs have all been folded is empty
* and is the drive's termination signal. `supersede: true` rides on every input: an input
* attributed without the edge is provenance only, the row would stay active, and the run would
* read as "consolidation did nothing" for a reason on this side of the seam.
*
* This is the RUN-CACHE form the cost model depends on: one mint per run, this filter per cycle —
* re-minting per cycle would multiply the model bill by the cycle count for the same answer.
*/
export declare function llmPlanDistiller(plan: {
products?: ReadonlyArray<Pick<LlmConsolidationPlanProduct, "name" | "description" | "type" | "body" | "inputIds"> & {
inputRevs?: Record<string, string>;
}>;
mintExposure?: "partitioned";
} | undefined | null): ConsolidationDistillFn;
export interface FuseSchedule<P> {
schedule: "next-fit-decreasing";
picked: P[];
targets: number;
ceiling: number;
deferred: number;
oversize: Array<{
name: string;
inputs: number;
cap: number;
}>;
}
/**
* One cycle's plan selection under the engine's supersession fuse.
*
* NEXT-FIT DECREASING: products in descending target count, stop at the first that does not fit.
* The missing backfill is the point, not an oversight: the ceiling is a FRACTION of the active set
* and every fold shrinks the active set, so a cycle that spends leftover budget on a small fold —
* one that would still fit later, at a lower ceiling — permanently lowers the ceiling every
* remaining large fold has to clear. Measured with the shipped 0.25 cap: a backfilling schedule
* dead-ends with two large digests permanently unfoldable while the same products converge when
* the large folds simply go first.
*
* `maxProductsPerRun` is honoured here too; a product whose input count exceeds
* `maxInputsPerProduct` can never be frozen at all and is reported structurally infeasible rather
* than retried forever.
*/
export declare function scheduleUnderFuse<P extends {
name?: string;
inputs?: Array<{
id: string;
supersede?: boolean;
}>;
}>(products: readonly P[] | undefined, ceiling: number, { maxProductsPerRun, maxInputsPerProduct }?: {
maxProductsPerRun?: number;
maxInputsPerProduct?: number;
}): FuseSchedule<P>;
/** The engine face the drive consumes (MemoryEngine satisfies it structurally; tests may stub). */
export interface ConsolidationDriveEngine {
snapshotConsolidationCandidates(scope: string, opts?: {
force?: {
requestId: string;
};
full?: boolean;
}): Promise<{
cycleToken: string;
watermark?: unknown;
candidates: Array<{
entry: MemoryEntry;
marked: boolean;
}>;
}>;
commitConsolidationPlan(cycleToken: string, proposal: ConsolidationProposal, opts: {
requestId: string;
}): Promise<ConsolidationDriveReceipt>;
reconcileConsolidation(scope: string): Promise<{
replayed?: {
planId: string;
state: string;
} | null;
} | undefined | void>;
}
/** The commit receipt fields the drive reads (the engine's receipt carries more). */
export interface ConsolidationDriveReceipt {
planId?: string;
state?: string;
products?: {
frozen?: number;
applied?: number | null;
conflicts?: number | null;
refused?: Array<{
index: number;
reason: string;
}>;
};
}
export interface ConsolidationFoldState {
active: number;
superseded: number;
products: number;
}
export interface ConsolidationDriveCycleRow {
cycle: number;
activeSetSize: number;
ceiling: number;
candidates?: number;
/** How many of the cycle's served candidates carried a committed external-origin marker — the
* engine-served fact the fold law and the instruction hard gate key on (design/376 §2.1: a
* reading about the marked treatment must show the snapshot actually saw marked content, and a
* zero-marked run must show the zero). Absent only on the row of a snapshot that refused. */
markedCandidates?: number;
deferred?: number;
planId?: string;
state?: string;
applied?: number | null;
conflicts?: number | null;
refusedProducts?: Array<{
index: number;
reason: string;
}>;
reconciled?: {
planId: string;
state: string;
} | null;
note?: string;
refused?: string;
recovery?: {
attempted: boolean;
ok: boolean;
replayed: {
planId: string;
state: string;
} | null;
summary: string;
report?: unknown;
};
attempted: {
products: number;
targets: number;
};
committed: {
products: number;
superseded: number;
};
/** `committed`'s two flat mirrors — the drive-report projections read the row under these names
* as well as under `committed.*`; all three spellings are one store-delta fact. */
products: number;
targets: number;
}
export interface ConsolidationDriveResult {
/** A STORE fact: did anything actually fold. A refused first cycle answers false. */
ran: boolean;
schedule: "next-fit-decreasing";
cycles: ConsolidationDriveCycleRow[];
cycleCount: number;
productsCommitted: number;
entriesSuperseded: number;
productsAttempted: number;
engineRefusedProducts: Array<{
index: number;
reason: string;
}>;
converged: boolean;
foldState: {
before: ConsolidationFoldState;
after: ConsolidationFoldState;
};
stop?: {
reason: string;
refused?: boolean;
stalled?: boolean;
parked?: boolean;
exhausted?: boolean;
planId?: string;
fuseBlocked?: Array<{
name: string;
targets: number;
}>;
oversize?: Array<{
name: string;
inputs: number;
cap: number;
}>;
recovery?: unknown;
};
}
/** The engine's refusals carry a `code` and a `reasons` array; a bare `.message` throws half away. */
export declare function codedReason(err: unknown): string;
export interface DriveConsolidationOptions {
distill: ConsolidationDistillFn;
/** The fuse denominator AND every published count are store facts, never driver assumptions. */
readFoldState: () => Promise<ConsolidationFoldState> | ConsolidationFoldState;
options: {
supersedeRatioCap: number;
supersedeAbsoluteFloor: number;
maxProductsPerRun: number;
maxInputsPerProduct: number;
};
/** The ceiling arithmetic — pass the engine's `supersessionFuseCeiling` (single source). */
fuseCeilingOf: (activeSetSize: number, opts: {
supersedeRatioCap: number;
supersedeAbsoluteFloor: number;
}) => number;
requestIdPrefix?: string;
maxCycles?: number;
/**
* The snapshot step, injectable so a HOST scheduling policy (throttle waits, run-scoped force,
* busy recovery) wraps the verb without forking the loop. Default = the plain full snapshot.
* Anything it throws takes the loop's refusal arm exactly as the plain call would.
*/
takeSnapshot?: (cycle: number) => ReturnType<ConsolidationDriveEngine["snapshotConsolidationCandidates"]>;
/** Observation hook after each cycle row closes against the store (run persistence seat). */
onCycle?: (row: ConsolidationDriveCycleRow) => void | Promise<void>;
}
/**
* Drive consolidation to its FIXPOINT: repeat snapshot → commit → reconcile until nothing is left
* to fold, the fuse can no longer admit the smallest remaining product, or the engine refuses.
*
* Why a loop is the honest shape: the supersession fuse caps ONE plan at a fraction of the active
* set (0.25 shipped), so a library that accumulated for many sessions cannot be folded by a single
* plan — the engine refuses that by design, and a driver that fired one plan and stopped would be
* converting a safety property into a failure report.
*
* Every snapshot asks `full: true`: snapshots are CHANGE-DRIVEN by default, so cycle 2 of a stable
* library would answer an EMPTY candidate set and strand most of the library unfolded. `full` is
* the protocol's own name for "re-distill what is already there", which is exactly what every
* cycle of a first consolidation is.
*
* WHAT WAS PROPOSED IS NEVER WHAT IS COUNTED: every per-cycle figure is a STORE delta —
* `readFoldState()` before and after each cycle, `committed` is the difference. Receipts are not
* enough (a `completed` receipt can carry per-product freeze refusals; a `conflict` receipt says
* how many applied but not which); the store is the only witness that cannot be off by a refusal.
*/
export declare function driveConsolidationToFixpoint(engine: ConsolidationDriveEngine, scope: string, opts: DriveConsolidationOptions): Promise<ConsolidationDriveResult>;
/** A host snapshot policy (the injectable `takeSnapshot`) may classify its own stop as PARKED by
* throwing an error carrying this marker — e.g. a busy seat whose reconcile answered a conflict
* plan awaiting the host valve. The drive maps it onto the parked stop family. */
export declare const CONSOLIDATION_SNAPSHOT_PARKED: unique symbol;
/**
* A host ABORT marker: an error the injectable snapshot policy tags with this symbol is NOT an
* engine refusal — it is the host tearing the run down (a cancelled wait, a shutdown hook). The
* drive RETHROWS it instead of composing a refusal stop, so the caller's run account stays
* PENDING and the paid mint survives to the resume — folding a host abort into a terminal
* `refused` would silently discard a whole mint over a shutdown.
*/
export declare const CONSOLIDATION_DRIVE_ABORT: unique symbol;