/** * msgs → neutral units. Bundles each assistant-tool-call message with the contiguous tool result(s) * that answer its ids into ONE atomic unit (so pairing can never be split). system + first user turn * are pinned. * @param {Array>} msgs * @returns {Array>} */ export function toUnits(msgs: Array>): Array>; /** * units → msgs. Honors drop (absent units), reorder (order of the returned array), recall-inject * (units with no backing → one synthesised message), and COMPRESS (a unit whose `content` was rewritten * is reconstructed from the new content). Atomic units keep their assistant tool-call message verbatim * so pairing holds; a content rewrite lands on the tool RESULT. A multi-result atomic bundle whose * content was rewritten is kept VERBATIM — a flat string can't be faithfully split back into N * results, and splitting is grammar (bareagent's), not litectx's to attempt. This isn't a special * case: litectx's compress() is a pure text→text render that returns verbatim when handed no single * parseable format (compress.js — "never returns less than the body losslessly"), so a flattened * multi-result unit round-trips unchanged on both sides. RATIFIED by litectx (2026-06-12). The pairing * seatbelt is the final guard. * @param {Array>} units * @returns {Array>} */ export function fromUnits(units: Array>): Array>; /** * Wrap litectx's `assemble(units, ctx)` verb into the Loop's msgs-level `assemble(msgs, ctx)` seam. * litectx ships the **`AssembleResult` envelope** `{ units, dropped, tokens }` (CE-PRD §8.2: `dropped[]` * is load-bearing — it ships in the same slice, never silently truncated). This wrapper accepts that * envelope (uses `.units`) OR a bare `units` array (a simpler consumer). `dropped`/`tokens` are litectx's * accounting; the Loop's seam is msgs-in/msgs-out, so they're not threaded onward here (the canonical * transcript already holds every dropped unit by id — restorable on demand). * Fail-OPEN at this layer too: any other return shape → the original msgs are sent unchanged. A thrown * error (incl. HaltError) is left to the Loop's own fail-open / HaltError handling — not swallowed here. * @param {(units: Array>, ctx: any) => (any | Promise)} assembleUnits * @returns {(msgs: Array>, ctx: any) => Promise>>} * @when you want to adapt a litectx-style neutral-unit assemble(units, ctx) verb into the Loop's assemble(msgs, ctx) context-assembly seam * @fails fail-open — any unexpected return shape sends the original msgs unchanged; a thrown error (incl. HaltError) is left to the Loop's own handling. * @example * const loop = new Loop({ provider, assemble: unitAssembler(litectx.assemble) }); */ export function unitAssembler(assembleUnits: (units: Array>, ctx: any) => (any | Promise)): (msgs: Array>, ctx: any) => Promise>>; /** * Wrap litectx's `trim(units, policy)` verb (R-C5) into the Loop's destructive `trim(msgs, ctx)` seam — * the RT-2 harvest-before-evict interlock. Unlike {@link unitAssembler} (a non-destructive per-round VIEW), * this is EVICTION: the Loop replaces its canonical transcript with the returned (smaller) msgs, so old * turns are permanently dropped — which is only safe because every dropped turn is harvested FIRST. * * The returned function `(msgs, ctx) => keptMsgs`: * 1. `toUnits(msgs)` → `trim(units, policy)` → `{ units (kept), dropped, harvest }`. * 2. **Interlock — harvest BEFORE evict:** `await onHarvest({ key, content, unit })` for every harvest * unit. If `onHarvest` throws (e.g. a write-gate `deny` → HaltError, or a store fault), this throws * BEFORE returning the evicted view → the Loop fail-opens (no eviction that round) → nothing is lost; * the next round retries and the idempotent key upserts the already-persisted ones. You cannot drop * history you have not persisted. * 3. `fromUnits(kept)` is the bounded transcript. Fail-OPEN: an unrecognised `trim` return shape → the * original msgs unchanged (no eviction). A throw propagates (HaltError → governance halt). * * `.flush(msgs, ctx)` — the F2 residual harvest: `trim` only hands back EVICTED turns, so the final * keepLastN window is never harvested mid-run and a clean run would diverge from an end-of-task batch. * Call `.flush` on completion to harvest the surviving non-pinned turns (no eviction); the idempotent key * means it never duplicates what eviction already harvested. The Loop invokes it only on **clean * completion** — on a halt (e.g. bareguard `maxTurns`), `stop()`, or error exit the survivors stay intact * in `result.msgs` but are NOT auto-flushed (auto-flushing a content-halt could re-trigger the deny that * caused it); harvest `result.msgs` yourself on those exits if you need the final window persisted. * * `onHarvest` is the consumer's harvest POLICY point (litectx CE-PRD §6/R-W*: bareagent owns the trigger * + what's worth keeping, litectx owns the mechanism + worklist). It is REQUIRED — the adapter structurally * enforces harvest-before-evict; pass `async () => {}` to opt INTO lossy bounding. * * @param {{ trim?: Function, onHarvest?: Function, policy?: any }} [opts] * `trim` — litectx's `(units, policy) => { units, harvest }` verb (REQUIRED). `onHarvest` — * `({ key, content, unit }) => void|Promise` (REQUIRED; the harvest policy point). `policy` — litectx * TrimPolicy: `{ keepLastN }` or `{ maxTokens }` (maxTokens wins). Both verbs are runtime-checked. * @returns {((msgs: Array>, ctx?: any) => Promise>>) & { flush: (msgs: Array>, ctx?: any) => Promise }} * @when you want to adapt litectx's trim(units, policy) verb into the Loop's destructive trim(msgs, ctx) seam — harvest-before-evict with an F2 residual .flush * @fails throws if the trim/onHarvest verbs are missing (runtime-checked); the fold is fail-open and a HaltError propagates. `.flush` drains the residual harvest. * @example * const loop = new Loop({ provider, trim: unitTrimmer({ trim: litectx.trim, onHarvest, policy: { keepLastN: 20 } }) }); */ export function unitTrimmer(opts?: { trim?: Function; onHarvest?: Function; policy?: any; }): ((msgs: Array>, ctx?: any) => Promise>>) & { flush: (msgs: Array>, ctx?: any) => Promise; }; /** * Stable harvest key for a unit — the dedup id for harvest-before-evict (RT-2). MUST come from a durable * property of the TURN, never from `unit.id`: `toUnits` mints ids from a module counter, so the same turn * is `u1` on one call and `u3` on the next (poc/rt2-trim-interlock.mjs F1) — keying on it double-writes. * Derived here from the unit's verbatim `_msgs` backing: the joined `tool_call_id`s for a tool turn (stable * by construction), else a hash of `[role, content]` for a plain turn. The consumer namespaces it (e.g. * `${taskId}:${key}`) and feeds it to `remember(id, …)`, which upserts → a replayed hop overwrites instead * of duplicating. NOT a content search (litectx FTS can't match ids; sealed meta is unsearchable) — a * deterministic key passed to the keyed write. The consumer's `taskId` prefix is the isolation boundary; * treat the returned key as opaque (don't re-parse it). * * Two collision hardenings (grounded in poc/rt2-audit-grounding.mjs): ids are `encodeURIComponent`-escaped * before the `,` join, so `['a','b']` and `['a,b']` can no longer alias the same key; and the plain-turn * hash is **two near-independent streams** (FNV-1a + DJB2) → ~64-bit, so a long-running agent's harvest * can't silently overwrite a turn via a 32-bit birthday collision (a single 32-bit FNV exhausted in ~5e5 * distinct turns). Normal provider ids (`call_…`) round-trip unchanged through the escape. * @param {Record} unit - a unit from {@link toUnits} (its `_msgs` backing is read). * @returns {string} * @when you need the stable content-address for a transcript unit (the key harvest-before-evict writes under) — a collision-resistant 64-bit id * @fails never throws; normal provider ids round-trip unchanged, and two near-independent hash streams avoid 32-bit birthday collisions. * @example * const key = harvestKey(unit); // stable id for this turn's harvest */ export function harvestKey(unit: Record): string; /** chars/4 token estimate over a list of messages (matches poc2 / the Loop's own heuristic). */ export function approxTokens(msgs: any): number; /** * Drop any tool-result whose tool_call_id has no open assistant tool-call before it, and any assistant * tool-call message left with zero surviving results. The final grammar guard: even if litectx hands * back something that would orphan a pair, the wire is always valid. Returns a fresh array. */ export function pairingSeatbelt(msgs: any): any[];