UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

66 lines (65 loc) • 4.64 kB
/** * The landing-admission protocol: a caller that holds authority over WHERE notes may land (the consolidation run's write * judge) and a backend that names the landings (it alone knows the final spelling — a collision suffix is its decision, taken * under its transaction lock against the files as they are at that moment). * * A LANDING is every entry file a patch writes or removes: an add's final spelling, an update's target, the file a delete * removes, and the old file a rename leaves. The backend plans a batch under its lock, finds every landing, and — before it * stages anything — leaves the lock to put the ones the caller has not yet been told of to `admit`; it then plans again and * writes only when every landing is one `admit` resolved. A spelling that moved in between (a rival writer took the name) is * asked about again; it is never renamed after it was judged. The judgment is awaited outside the lock: it may wait on a rule store. * * An admitted batch is asked whole and committed one landing at a time ({@link cutPerLanding}): a backend's commit record is * replayed by its crash recovery without anyone to ask, so it never carries a landing behind another one. A crash between two * landings leaves the earlier landed and the later uncommitted, to be asked again by whoever applies it next; recovery only * ever completes the one landing whose commit had begun. */ import type { MemoryBackend, NotePatch, PatchReport } from "./types.js"; /** One landing: the entry's id and the scope directory and slug of the file it writes or removes. */ export interface PatchLanding { readonly id: string; readonly scope: string; readonly slug: string; } /** Resolves ⇒ the landings may be written; rejects ⇒ nothing is written and the rejection propagates unchanged. */ export type AdmitLandings = (landings: readonly PatchLanding[]) => Promise<void>; /** Rounds of "plan, ask, plan again" a batch gets before the backend gives up on a name that keeps moving. */ export declare const MAX_LANDING_ADMISSION_ROUNDS = 4; /** Thrown from inside the lock, before anything is staged: these landings have not been admitted yet. */ export declare class LandingsNeedAdmission extends Error { readonly landings: readonly PatchLanding[]; constructor(landings: readonly PatchLanding[]); } /** * Inside the lock, once the batch is planned with its `landings`: throw {@link LandingsNeedAdmission} naming the ones not in * `admitted`. Nothing is on disk yet, so the throw is a clean exit. */ export declare function requireLandingsAdmitted(admitted: ReadonlySet<string>, landings: readonly PatchLanding[]): void; /** One planned patch's share of an admitted batch: where its operations and evidence end in the batch's lists, and — iff it has * a landing — the account as it stands after it. */ export interface PatchShare<R> { readonly opsEnd: number; readonly evidenceEnd: number; readonly landed?: R; } /** One transaction of an admitted batch: the operations and evidence it commits and the account it leaves. */ export interface LandingCut<O, E, R> { ops: O[]; evidence: E[]; account: R; } /** * The commit cuts of an admitted batch, in order: a cut closes at each patch that has a landing, so no cut carries two patches' * landings (a rename's two files are one patch's, committed together; a patch with operations but no landing — a conflict's restore * of committed bytes — rides with the next). A cut leaves the account as it stood after its landing; the last cut leaves * `finalAccount`, the batch's (whatever the patches after the last landing changed rides with it). */ export declare function cutPerLanding<O, E, R>(ops: readonly O[], evidence: readonly E[], shares: readonly PatchShare<R>[], finalAccount: R): Array<LandingCut<O, E, R>>; /** A backend that cannot admit is handed the landings its caller admitted one call each, so none of its commits carries a landing * behind another whatever its own commit unit is; `before` (the caller's holder check) runs ahead of each call. */ export declare function applyEachAlone(backend: Pick<MemoryBackend, "applyPatches">, patches: readonly NotePatch[], before: () => void): Promise<PatchReport>; /** * The backend's side of the loop. `run(admitted)` plans and applies one locked attempt (it throws * {@link LandingsNeedAdmission} when the plan names a spelling not in `admitted`); with no `admit` it runs once, unconditioned. */ export declare function admittingLandings<T>(admit: AdmitLandings | undefined, run: (admitted: ReadonlySet<string> | undefined) => Promise<T>): Promise<T>;