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