@sema-agent/core
Version:
Stateless, task-oriented AI agent core
97 lines (96 loc) • 6.02 kB
TypeScript
import type { MemoryEntry, ScoredMemoryEntry } from "./types.js";
/**
* design/142 §3 — the per-replica per-scope sync cursor: the COMMON BASELINE a reconciliation run
* judges divergence against (`id → rev` as of the last completed sync round). This is a NEW
* persistence face — deliberately NOT revs.json (that is the local disk-vs-committed ledger): File
* plane = a control-plane sidecar, DB plane = a service-owned table (PG+TiDB dual-dialect).
* Serializable by construction.
*/
export interface MemorySyncCursor {
scope: string;
/** The PEER this cursor tracks (e.g. "central", a repo remote name) — one cursor per peer. */
peer: string;
/** id → rev at the last completed sync round with this peer. */
baseRevs: Record<string, string>;
/** Caller-injected clock (never Date.now inside the engine — clock-injection precedent). */
updatedAtMs: number;
}
export interface MemorySyncConflict {
id: string;
/** Present when the side still has the entry (absent = deleted on that side). */
local?: MemoryEntry;
remote?: MemoryEntry;
baseRev?: string;
}
export interface MemorySyncPlan {
/** Entries the LOCAL side should send to the peer (new here or changed-here-only). 独立轨 F8 —
* CAS discipline for the peer write: a push item exists ONLY when the peer is still at the
* baseline, so the peer-side conditional write uses `baseRevs[id]` as its baseRev (absent from
* the baseline ⇒ the peer has no such entry ⇒ plain add). Never write the peer unconditionally. */
push: MemoryEntry[];
/** Entries the LOCAL side should adopt from the peer (new there or changed-there-only). Same CAS
* discipline as {@link push}, mirrored: local conditional write with `baseRevs[id]`. */
pull: MemoryEntry[];
/** DELETE propagation (without these the removal never crosses and every round
* re-plans the same state = livelock): the peer removed an entry the local side left UNCHANGED
* since baseline ⇒ delete locally (CAS on baseRev — a local edit racing in makes the delete a
* conflict on the next round instead of silent loss). */
deleteLocal: Array<{
id: string;
baseRev: string;
}>;
/** Symmetric: locally removed, peer unchanged since baseline ⇒ delete on the peer. */
deleteRemote: Array<{
id: string;
baseRev: string;
}>;
/** Both sides diverged from the baseline (or delete-vs-edit races): the caller resolves via the
* §3 ladder — winner by the caller's policy, LOSER MINTED AS A SIBLING entry (never dropped),
* the pair recorded so a three-way exchange cannot rebuild the same conflict forever. */
conflicts: MemorySyncConflict[];
/** Ids deleted on BOTH sides since the baseline (pure cursor cleanup, no data movement). */
cleared: string[];
}
/**
* Pure three-way set reconciliation over one scope. `baseRevs` = the persisted common baseline
* (empty object = first sync round: everything present is "new" on its side; identical revs on
* both sides collapse to no-op).
*
* Judgment per id (rev lineage, mtime never consulted):
* - both sides at baseline rev → no-op; identical revs (any lineage) → no-op + baseline advance;
* - changed on exactly ONE side → push/pull;
* - changed on BOTH sides, or delete-on-one × edit-on-the-other → conflict (caller's ladder);
* - deleted on one side × UNCHANGED on the other → the delete propagates (deleteLocal/deleteRemote,
* CAS on baseRev: without this leg removals never cross and the plan livelocks);
* - present only on one side with NO baseline → new entry → propagate;
* - absent on both but present in baseline → cleared (cursor cleanup).
*/
export declare function reconcileMemoryEntries(baseRevs: Readonly<Record<string, string>>, local: readonly MemoryEntry[], remote: readonly MemoryEntry[]): MemorySyncPlan;
/** The baseline to persist AFTER a sync round completes: every id both sides now agree on. The
* caller passes the POST-round entry sets (after applying push/pull and resolving conflicts). */
export declare function nextSyncBaseline(local: readonly MemoryEntry[], remote: readonly MemoryEntry[]): Record<string, string>;
/**
* A third-party semantic recall side-channel (Mem0/Zep/Cognee/… adapters — research 2026-07-12:
* all five surveyed engines fit THIS seat; none fits the authority seat). STRICTLY READ-ONLY by
* construction: a RecallSource feeds candidate hits into injection-time recall; it never sees the
* write path, never participates in harvest, and the system runs identically without it.
*
* Score contract: DISTANCE in [0, 2] (0 = identical, 2 = orthogonal — the vector-rung convention);
* adapters normalize their similarity/relevance scores into this scale. Returned entries may be
* the third party's RE-STATEMENTS (extractive engines rewrite) — that is acceptable HERE precisely
* because the seat is advisory: the authoritative body is re-read from the real backend by id/slug
* when a hit is followed. Mount point: a deployment composes its RecallSource(s) inside its recall
* select hook (the design/65 selective-recall trust seat) or pre-queries before prepare; core adds
* no RunnerDeps seat until a real integration signals the need (YAGNI two-lane ruling).
*/
export interface RecallSource {
/** Stable adapter name (diagnostics/attribution: hits are labeled, never silently mixed). */
name: string;
search(query: string, scopes: readonly string[], opts?: {
limit?: number;
}): Promise<ScoredMemoryEntry[]>;
}
/** Merge recall hits from multiple sources with the authority backend's own results: id-deduped
* (FIRST occurrence wins — callers order sources by trust, authority first), ascending by score
* (distance), capped at `limit`. Pure — no I/O, no source calls. */
export declare function mergeRecallHits(hitLists: ReadonlyArray<readonly ScoredMemoryEntry[]>, limit: number): ScoredMemoryEntry[];