import type { AgentTool } from "../internal/harness.js"; /** * A scored memory candidate returned by {@link MemoryStore.searchScored} (design/41). Carries a * **stable id** (for {@link MemoryStore.update}/{@link MemoryStore.delete}) and a similarity `score`. * * `score` is a **cosine distance ∈ [0, 2]** where **0 = identical** and larger = less similar — pinned * by contract so the consolidation band (`{lo, hi}`) is calibrated the same way across stores (a store * that returned a *similarity* instead of a *distance* would invert the band). The reference * {@link InMemoryMemoryStore} has no embeddings, so it returns a lexical distance in [0, 1] (a subset * of the range) as a stand-in for tests/dev; a production store returns a real cosine distance. */ export interface ScoredMemory { id: string; text: string; score: number; /** design/84 Seam B (前置 BLOCKER): true when this note was CREATED by a consolidation ADD (not the * `remember` tool / a caller). Periodic/incremental consolidation EXCLUDES these from its candidate set so * it never re-consolidates its own output (infinite re-merge). Stores that don't track it leave it absent * (treated as not-generated — back-compat: the inline task-end path already excludes this-task notes by id). */ consolidationGenerated?: boolean; } /** * The vector-ranking capability RUNG a {@link MemoryStore} actually achieves (design/81 Slice 5). It is a * runtime OBSERVABLE (it depends on whether an embedder was injected AND whether the backend has native vector * ops), NOT a static class trait — a deployment reads `store.vectorMode` to decide whether to build a native * index / for metrics, and to answer "does this backend do neural recall?". * - `lexical` — no embedder: `1 - Jaccard` everywhere (the default; byte-identical to the pre-Slice-5 floor). * - `portable` — embedder + IN-PROCESS cosine over stored vectors (File jsonl / Pg jsonb / **MySQL JSON** — * any backend that can store a float array; O(n), no index). MySQL sits HERE, not "unsupported". * - `native` — embedder + a backend index/operator (pgvector `<=>` / TiDB `VEC_COSINE_DISTANCE`). */ export type MemoryVectorMode = "lexical" | "portable" | "native"; /** * Produces an embedding for one note/query (design/81 Slice 3 — the OPTIONAL vector seam). **Byte-identical * to {@link import("../stores/pg.js").PgEmbedder}** so the same caller-injected embedder satisfies both the * Pg and File backends. It is an OPTIONAL injection: a store WITHOUT one falls back to the lexical * cosine-distance (the TOC default — no model file, no native binary in core's default path). When one IS * injected the File backend uses it for vector ranking (Slice 5) via a rebuildable sidecar index; injecting * none keeps the lexical floor unchanged. Core bundles NO embedder; a cloud/TOB consumer injects its own. */ export interface Embedder { embed(text: string): Promise; dimensions: number; } /** * Long-term (L2) memory backend — pluggable, scoped, durable across sessions. * * Design adapted (MIT) from CodeWhale's `memory.rs` (a persistent note file injected into the * system prompt) and Anthropic's memory tool (the model manages its own memory via a tool; the * backend lives in your infrastructure). The default is in-memory; a deployment provides a durable * backend (e.g. TiDB) implementing this interface. * * `scope` partitions memory by tenant — e.g. `user:42`, `org:7`, `agent:main`, or `global`. */ export interface MemoryStore { /** Full memory content for a scope (timestamped bullets), or null when empty. */ read(scope: string): Promise | string | null; /** * Append one durable note to a scope. Implementations timestamp it. A store that supports * consolidation (implements {@link searchScored}/{@link update}/{@link delete}) **MUST** return the new * note's **stable id**: memory consolidation (design/41) uses it to exclude a just-appended note from * its own reconcile candidates, so an id-addressable store that returns `void` would let a note * self-match and trigger a wasteful no-op every cycle. Returning `void` is fine only for stores that * do NOT support consolidation. */ append(scope: string, note: string): Promise | string | void; /** Clear a scope's memory. */ clear(scope: string): Promise | void; /** * Optional just-in-time (L3) retrieval: return notes in `scope` relevant to `query`. * When implemented, the Runner exposes a `recall` tool so the model can query memory on demand * instead of relying only on the injected block (useful when a scope's memory is large). */ search?(scope: string, query: string, limit?: number): Promise | string[]; /** * Optional id-addressable similarity retrieval (design/41): return up to `limit` notes in `scope` * most similar to `query`, each with a **stable id** and a cosine-distance `score` (see * {@link ScoredMemory}). Required (together with {@link update}/{@link delete}) for memory * consolidation; when any of the three is absent, consolidation is a safe no-op. * * **Concurrency contract:** memory consolidation keys on the task's memory `scope`, NOT its session, * and the Runner's per-session lock does not serialize across sessions. So `searchScored`/`update`/ * `delete` MAY be called concurrently for the SAME scope from different sessions. A durable async store * must therefore either linearize per-scope writes (e.g. optimistic lock by id) or accept eventual * consistency — at worst a near-duplicate survives one extra consolidation pass (fail-open, no loss). */ searchScored?(scope: string, query: string, limit?: number): Promise | ScoredMemory[]; /** The vector-ranking rung this store achieves (design/81 Slice 5). Absent ⇒ treated as `"lexical"` * (back-compat). A runtime observable — a deployment reads it to decide index/metrics, NOT to decide * whether to inject an embedder (injection is config-driven; this is the RESULT). See {@link MemoryVectorMode}. */ readonly vectorMode?: MemoryVectorMode; /** Optional: replace a note's text by id (consolidation UPDATE). The id stays stable. Must be safe * under concurrent same-scope calls — see {@link searchScored}'s concurrency contract. */ update?(scope: string, id: string, text: string): Promise | void; /** Optional: remove a note by id (consolidation DELETE). The id is never reused afterwards. Must be * safe under concurrent same-scope calls — see {@link searchScored}'s concurrency contract. */ delete?(scope: string, id: string): Promise | void; /** * Optional **manifest** for selective recall (design/65 §2.2/§8#6): a per-note HEADER (id + a short * description + mtime [+ name/type when structured]) — **NOT the body** (mirrors CC reading only the * frontmatter, `memoryScan.ts FRONTMATTER_MAX_LINES=30`). The Runner injects this index for a large * scope and side-queries which ids are relevant, then fetches only those via {@link getByIds}. */ listStructuredNotes?(scope: string): Promise | MemoryNoteHeader[]; /** Optional: fetch the FULL records (with body) for the selected ids (design/65 §2.2). Unknown ids are * silently skipped. Order is not guaranteed. Pairs with {@link listStructuredNotes}. */ getByIds?(scope: string, ids: string[]): Promise | MemoryNoteRecord[]; /** * Optional: append a **structured/typed** note (design/65 §2.1, P2). `type` defaults to `"project"` and * `description` to the body's first sentence (derived at write time — store responsibility, §8#6). Returns * the new note's stable id (like {@link append}). A store that supports this surfaces `name`/`type` in * {@link listStructuredNotes} so the selective-recall side-query gets a `[type]` relevance signal. */ appendStructured?(scope: string, note: StructuredNoteInput): Promise | string; /** * Optional **legitimate door** for a *promotable* artifact write (design/77 §2 Gate-2). A promotable * note (a self-evolution artifact — skill/playbook/outcome-stats; classified by NOTE STRUCTURE, not a * caller flag, via {@link classifyPromotable}) may ONLY reach the backend through this method, which * carries the required `utilityVerdict`. The store-boundary gate ({@link guardedMemoryStore}) routes a * present+positive promotable write here; a promotable note WITHOUT a present+positive verdict is * fail-closed REJECTED at the boundary (never reaches any store method). `utilityVerdict` is opaque * `unknown` in core — the *definition* of "positive" stays in the profile (taxonomy never enters core). * Stores that do not implement this can still serve caller-authored writes; promotable writes simply * have nowhere legitimate to land and are rejected (fail-closed). */ appendPromotable?(scope: string, note: StructuredNoteInput, utilityVerdict: unknown): Promise | string; /** * **INTERNAL consolidation-only door** for the design/84 Seam B marker. Memory consolidation (and ONLY * it — `runMemoryConsolidation`) calls this to materialize a merged "add" decision tagged * `consolidationGenerated:true`, so a later periodic/incremental pass EXCLUDES its own output from the * candidate set (no infinite re-merge). It is the COUNTERPART to the {@link guardedMemoryStore} boundary * defense: the public {@link appendStructured} STRIPS a caller-supplied `consolidationGenerated` (an * external caller must never be able to mark a note "consolidation-generated" and silently exile it from * consolidation forever), while THIS method is the single legitimate inlet that may set it. The guarded * store wires it to set the marker then forward to the inner `appendStructured`; a raw (un-wrapped) store * need not implement it — consolidation then falls back to `appendStructured` (which on an un-wrapped * store still honors the marker, preserving the old behavior for direct-store tests). NOT a public caller * surface: it carries no extra capability beyond `appendStructured` + the one internal flag. */ appendConsolidationGenerated?(scope: string, note: StructuredNoteInput): Promise | string; /** * design/84 Seam B — read the periodic/incremental consolidation cursor for `scope` (decision 3). The * cursor is an **opaque ordering marker** the STORE defines (this core never inspects it): a value such * that every note whose ordering marker is `<= cursor` has ALREADY been folded into a prior consolidation * pass and MUST NOT be re-fed (no infinite re-merge), while every note `> cursor` is still pending. In the * reference stores it is a note **id** (uuidv7 — lexicographically time-sortable), so "after the cursor" * is a simple string `>` comparison. Returns `undefined` when no pass has run yet (⇒ the WHOLE scope is * pending — the first pass sweeps it all). PAIR with {@link setConsolidationCursor}: a store implements * BOTH or NEITHER ({@link supportsPeriodicConsolidation}); a store with neither makes {@link consolidateScope} * a safe NO-OP (it never silently degrades to a full re-consolidation). */ getConsolidationCursor?(scope: string): Promise | string | undefined; /** * design/84 Seam B — persist the periodic/incremental consolidation cursor for `scope` (decision 3). Called * by {@link consolidateScope} ONLY after a pass succeeds in full, advancing the cursor to the max ordering * marker the pass observed (seen note ids ∪ added ids). Must be durable (a later pass reads it back). PAIR * with {@link getConsolidationCursor}. */ setConsolidationCursor?(scope: string, cursor: string): Promise | void; } /** Standard structured-note types (design/65 §2.1, CC `memoryTypes.ts`). Any other string is accepted and * degrades gracefully (stored + shown as-is) — `type` is widened to `string` everywhere for that reason. */ export type MemoryNoteType = "user" | "feedback" | "project" | "reference"; /** Input to {@link MemoryStore.appendStructured}. Only `body` is required. */ export interface StructuredNoteInput { body: string; /** A short slug for `[[name]]` cross-references (design/65 §2.1). Optional. */ name?: string; /** One of {@link MemoryNoteType}, or any string (unknown types degrade). Default `"project"`. */ type?: string; /** One-line summary for the manifest. Default = first sentence of `body`. */ description?: string; /** `[[name]]` cross-reference slugs (design/65 §2.1; v1 stores/displays, no link-following). */ links?: string[]; /** * design/77 §2 Gate-2 structural marker. An opaque (`unknown`) outcome-statistics blob attached to a * self-evolution artifact (held-out pass-rate, usage count, …). Its PRESENCE makes a note *promotable* * even when `type` is in the caller-authored closed set — the structural marker WINS over the type * (see {@link classifyPromotable}). Core never inspects its shape; the taxonomy/threshold stays in the * profile (`RunnerDeps.utilityGate`). */ outcomeStats?: unknown; /** * design/84 Seam B (前置 BLOCKER): mark a note as CREATED BY A CONSOLIDATION ADD. Set ONLY by * {@link import("./runner/memory-consolidation.js").runMemoryConsolidation} when it materializes a merged * "add" decision — never by the `remember` tool or a normal caller. A store persists it (and surfaces it on * {@link ScoredMemory}/{@link MemoryNoteHeader}) so periodic/incremental consolidation can EXCLUDE these * notes from its candidate set and not re-consolidate its own output forever. It does NOT affect the * promotable/secret gates (a consolidation ADD is `caller_authored` by construction). */ consolidationGenerated?: boolean; } /** A manifest entry: enough to judge relevance, never the body (design/65 §2.2). */ export interface MemoryNoteHeader { /** Store contract: ids must be SINGLE-LINE whitespace-free tokens (uuid-like). The manifest renders * `- {id}: …` lines and the side-query matches selected ids by string equality — an id containing * a newline could forge manifest lines, and a renderer-side rewrite would break the equality match, * so the contract sits on the store (search 1.95.1 review #2). */ id: string; /** A one-line summary used as the relevance signal (derived from the note's first sentence when untyped). */ description: string; /** Append/update time as ms since epoch — drives the freshness caveat (design/65 §8#8). */ mtimeMs: number; /** Present once notes are structured (design/65 §2.1, P2); absent for legacy flat notes. */ name?: string; /** One of {@link MemoryNoteType} or any other string (unknown degrades gracefully). */ type?: string; /** design/84 Seam B: true when this note was created by a consolidation ADD (excluded from periodic * consolidation candidates). Absent for `remember`/caller notes. */ consolidationGenerated?: boolean; } /** A full note record (header + body) returned by {@link MemoryStore.getByIds}. */ export interface MemoryNoteRecord extends MemoryNoteHeader { text: string; } /** * True when a store implements the full id-addressable trio required for memory consolidation * (design/41): {@link MemoryStore.searchScored}/{@link MemoryStore.update}/{@link MemoryStore.delete}. * When false, the Runner skips consolidation entirely (graceful no-op). */ export declare function supportsConsolidation(store: MemoryStore): boolean; /** * True when a store implements the design/84 Seam B cursor PAIR * ({@link MemoryStore.getConsolidationCursor}/{@link MemoryStore.setConsolidationCursor}). Periodic/incremental * consolidation ({@link import("./consolidate-scope.js").consolidateScope}) GATES on this: a store missing * either method makes a periodic pass a safe **NO-OP** (never a silent full re-consolidation of the whole * scope, which would re-merge already-consolidated notes forever). Both-or-neither — a store implementing * only one is treated as unsupported. Independent of {@link supportsConsolidation} (the id-addressable trio), * which a periodic pass ALSO needs to actually mutate. */ export declare function supportsPeriodicConsolidation(store: MemoryStore): boolean; /** The closed set of caller-authored note types (design/65 {@link MemoryNoteType}, memory.ts). A note * whose `type` is in this set is caller-authored; a `type` *outside* it (e.g. `"skill"`) is promotable. * Exported so a caller-authored inlet (e.g. consolidation ADD) can NORMALIZE a model-suggested type to * the closed set before the store boundary — a legitimate caller_authored write must never trip the * promotable gate just because the model invented a non-standard `type` slug (MINOR-3). */ export declare const CALLER_AUTHORED_TYPES: ReadonlySet; /** The default caller-authored note type (matches {@link StructuredNoteInput.type}'s documented default). */ export declare const DEFAULT_CALLER_AUTHORED_TYPE: MemoryNoteType; /** * Profile-injected predicate that judges whether a promotable note's `outcomeStats` clear the promotion bar * (design/77 §2). Core only guarantees a verdict is PRESENT for a promotable write; this predicate — wired * by the deployment — receives the note's (opaque) `outcomeStats` blob and returns whether it clears the * bar. (It is NOT handed the `utilityVerdict` token; the verdict's PRESENCE is checked separately by * {@link enforcePromotableWriteGate}, then the gate judges the outcome statistics.) When UNWIRED, a * promotable write fails closed. */ export type UtilityGate = (stats: unknown) => boolean; /** Typed, fail-closed error thrown when a promotable artifact write reaches the store boundary without a * present-and-positive `utilityVerdict` (design/77 §2). The `code` is stable for callers to switch on; * the write NEVER reaches the backend. */ export declare class MemoryGateError extends Error { readonly code: string; constructor(code: string, detail?: string); } /** * Scan `text` for a high-confidence credential (CC-parity P0-2). Returns the matched pattern's stable * `label` (NEVER the matched secret value — labels are safe to log/surface), or `null` when clean. * Deliberately CONSERVATIVE: only provider-prefixed keys + PEM private-key headers match, so ordinary * prose that merely mentions tokens/keys/passwords passes through untouched (false-positives are the head * risk — a wrongly-rejected normal note silently breaks legitimate memory). */ export declare function detectSecret(text: string): string | null; /** * Store-boundary secret-scan invariant (CC-parity P0-2). Fail-closed: if `body` carries a high-confidence * credential, throw {@link MemoryGateError}`("memory.secret_detected")` — the write NEVER reaches the * backend. The error detail names the pattern LABEL (e.g. `aws_access_key_id`), never the matched value, * so the secret is not re-leaked into a log/error sink. A clean body is a no-op. Applied by * {@link guardedMemoryStore} on EVERY durable write inlet (`append`, `appendStructured`, the * `appendPromotable` door) so both the `remember` tool and consolidation ADD are covered at one chokepoint. */ export declare function enforceSecretWriteGate(body: string): void; /** * Structured-note secret-scan invariant (CC-parity P0-2). Scans EVERY text field of a {@link StructuredNoteInput} * that becomes DURABLE and/or enters the manifest — not just `body`. The body alone is insufficient: `name` * (rendered as `[[name]]` cross-references) and `description` (rendered into the recall manifest + injected * into the side-query sub-prompt) are ALSO persisted and surfaced, so a secret hidden there * (`appendStructured({ body: "safe", description: "sk_live_…" })`) would otherwise be durably stored and * leaked into every later same-scope manifest — a real bypass of the body-only scan. `links` slugs are NOT * scanned: they are short `[[name]]`-style reference tokens (no credential body shape clears the * conservative detector), and scanning them adds FP surface for no realistic gain. Fail-closed: the first * field that carries a high-confidence credential throws {@link MemoryGateError}`("memory.secret_detected")`. */ export declare function enforceStructuredNoteSecretGate(note: StructuredNoteInput): void; /** * Classify a structured-note write as `caller_authored` (normal memory — ungated) or `promotable` (a * self-evolution artifact — gated) — design/77 §2. PURE, taxonomy-free CORE logic. * * `promotable` iff the note's `type` is defined AND outside the caller-authored closed set, OR the note * carries a structural promotable marker. **The structural marker WINS over the closed-set type** — a * `type:"reference"` note WITH an `outcomeStats` marker classifies `promotable`. */ export declare function classifyPromotable(note: StructuredNoteInput): "caller_authored" | "promotable"; /** * The store-boundary Gate-2 check (design/77 §2). For a `promotable` note, require a present-and-positive * `utilityVerdict`; otherwise throw {@link MemoryGateError}`("memory.promotable_ungated")` fail-closed — * the write NEVER reaches the backend. A `caller_authored` note is unchanged (no verdict needed). * * **Gate-presence invariant (CRITICAL):** "positive" is decided by the profile `utilityGate`. When the * gate is UNWIRED, a promotable note STILL fails closed (an absent gate never auto-passes). When wired, * the verdict must be present (`!== undefined`) and the gate must accept its `outcomeStats`. */ export declare function enforcePromotableWriteGate(note: StructuredNoteInput, utilityVerdict: unknown, utilityGate?: UtilityGate): void; /** * Wrap a {@link MemoryStore} so EVERY durable write path runs the Gate-2 chokepoint (design/77 §2). One * wrapper at the store boundary covers BOTH inlets (the `remember` tool and consolidation ADD) — no * per-inlet edits. It intercepts BOTH write methods (§2 polish): * - `appendStructured` — classify the structured note; a `promotable` one needs a present+positive verdict. * Since `appendStructured` carries NO verdict, a promotable structured write is ALWAYS rejected here; * the legitimate way to land one is {@link MemoryStore.appendPromotable} (which carries the verdict). * - `append` — the legacy/typeless fallback CANNOT carry structure, so any append-path write is * classified `caller_authored` and passes through unchanged (documents the chokepoint's exhaustiveness * across store methods — a promotable artifact structurally cannot flow out of this path). * * `appendPromotable` is the gated legitimate door: it runs the gate WITH the supplied verdict, then (on * pass) writes via the inner store's `appendStructured` (or `append` fallback). The id-addressed `update` * (consolidation's UPDATE op) replaces a note's text with new model-generated content, so it is ALSO * secret-scanned (a promotable artifact structurally cannot flow out of it — only existing-note text is * rewritten). `delete` and the read/search/manifest surface are straight pass-throughs (they carry no new * content and never WRITE a promotable artifact). */ export declare function guardedMemoryStore(inner: MemoryStore, utilityGate?: UtilityGate): MemoryStore; /** * Default in-memory MemoryStore (lost on restart). Use a durable backend in production. * * Id-addressable (design/41): each note gets a UUID stable across `update` and never reused after * `delete`, so {@link searchScored}/{@link update}/{@link delete} work and memory consolidation can run. * It has no embeddings, so `searchScored` returns a **lexical** (Jaccard) distance as a stand-in for a * real cosine distance — fine for dev/tests, but a production store should back this with vectors. */ export declare class InMemoryMemoryStore implements MemoryStore { private byScope; /** design/84 Seam B: per-scope periodic-consolidation cursor (lost on restart, like the rest of this store). */ private cursorByScope; /** design/81 Slice 5: the in-memory store has no embedder — always the lexical floor. */ readonly vectorMode: "lexical"; read(scope: string): string | null; append(scope: string, note: string): string | void; appendStructured(scope: string, note: StructuredNoteInput): string; clear(scope: string): void; /** * Keyword search backing the `recall` tool. design/81 Slice 2b — uses the SHARED synonym/stem-aware * {@link lexicalSearchMatch} (the SAME matcher as {@link import("../stores/file/memory-store.js").FileMemoryStore.search}), * so the `recall` tool returns IDENTICAL results across the InMemory and File backends. With no synonym/stem * hit it degrades to the plain substring filter (byte-identical to the pre-Slice-2b behavior). */ search(scope: string, query: string, limit?: number): string[]; /** * Lexical-distance retrieval (consolidation candidate source). Distance = `1 - Jaccard(terms)` so it * lives in [0, 1] ⊂ the [0, 2] cosine-distance contract (0 = identical term set). Returns the top * `limit` nearest entries (excludes zero-overlap ones). Deterministic — handy for tests. */ searchScored(scope: string, query: string, limit?: number): ScoredMemory[]; update(scope: string, id: string, text: string): void; delete(scope: string, id: string): void; /** Manifest (design/65 §2.2): a header per note, body excluded. description = the note's first sentence * (legacy flat notes have no frontmatter; this is the council #10 P1 fallback). */ listStructuredNotes(scope: string): MemoryNoteHeader[]; /** design/84 Seam B: read the periodic-consolidation cursor (opaque note-id high-water mark). */ getConsolidationCursor(scope: string): string | undefined; /** design/84 Seam B: persist the periodic-consolidation cursor. */ setConsolidationCursor(scope: string, cursor: string): void; getByIds(scope: string, ids: string[]): MemoryNoteRecord[]; } /** * design/81 Slice 2b — a ZERO-MODEL lexical term expander closing the only real lexical recall weakness the * red-team raised (the `recall`-tool `search` misses paraphrases like "auth token" vs "credentials/bearer"). * Pure TS, NO embedder/native/model dep — same posture as the Jaccard stand-in. For a lower-cased query * term it returns that term PLUS: * - its members of a small bidirectional **synonym set** (auth↔credentials↔bearer, token↔key↔secret, …), * looked up for BOTH the original term AND its plural stem (so an inflected query like "tokens" — whose * singular "token" is the synonym-group member — still expands to {bearer, apikey, secret, key}), and * - a light **plural** stem (`-ies→-y`, `-es`, `-s`, with a ≥3-char guard) so "tokens" reaches "token". * (The `-ing`/`-ed` rules were removed — they over-truncated and produced wrong stems.) * Always includes the original term, so a non-expandable term degrades to the existing substring match * (byte-identical to the prior behavior when no synonym/stem applies). The caller (`lexicalSearchMatch`) * treats the returned variants as an OR within one query term (still AND across terms). */ export declare function expandLexicalTerms(term: string): string[]; /** * Shared SYNONYM/STEM-aware lexical match (design/81 Slice 2b) — the ONE matcher used by BOTH * {@link InMemoryMemoryStore.search} and {@link import("../stores/file/memory-store.js").FileMemoryStore.search}, * so the `recall` tool returns IDENTICAL results across backends (the "byte-identical to InMemory" contract). * It is a recall-QUALITY behavior, correct to apply everywhere. (Pg's `search` is a separate SQL path — * a known follow-on, unchanged in this pass.) * * Matching rule: tokenize `text` on word boundaries, then for each whitespace-separated `query` term the * text must match AT LEAST ONE of that term's {@link expandLexicalTerms} variants (OR within a term), and * EVERY query term must match (AND across terms). The ORIGINAL query term keeps its pre-Slice-2b SUBSTRING * semantics (back-compat); the EXPANDED synonym/stem variants are matched on WORD BOUNDARIES (set membership * over the text's token set) so a short injected synonym like "key"/"db" cannot substring-match an unrelated * word ("keyboard"/"double"). An empty/whitespace-only query never matches. */ export declare function lexicalSearchMatch(query: string, text: string): boolean; /** First sentence (or ~120-char head) of a note — the manifest description fallback for untyped notes. */ export declare function firstSentence(text: string): string; /** The canonical, normalized shape of {@link import("./types.js").TaskSpec.memory} — the ONE form every * consumer reads (prepare-task, recall, consolidation, the remember tool), so the union never narrows * downstream. `scopes` is the ordered read-side layering (first = stable prefix, last = volatile tail = * highest priority); `writeScope` is the single layer ALL writes land in (`null` = read-only, no writes). */ export interface NormalizedMemorySpec { /** Ordered, deduped, non-empty opaque scopes for read-side layered injection (list order = inject order). */ scopes: string[]; /** The single scope all writes route to (decision 3), or `null` for a read-only (inherited) layering. */ writeScope: string | null; /** Mirrors `memory.enabled` (default true) — false ⇒ the whole memory feature is off for this task. */ enabled: boolean; /** design/142 §1.2 — present iff the task opted into the v2 scope identity contract (keys validated). */ scopeContract?: "v2"; } /** The raw, backward-compatible union accepted on `TaskSpec.memory` (design/84 Seam A). */ export type MemorySpecInput = { scope?: string; scopes?: ReadonlyArray; writeScope?: string | null; enabled?: boolean; /** * design/142 §1.2 — EXPLICIT opt-in to the v2 scope identity contract (`user:` / `org:` / * `proj:/` / `userproj:` typed keys). Absent ⇒ every scope stays byte-identically * OPAQUE, including keys that happen to look like `user:42` (the documented legacy shape above — the * contract is NEVER activated by prefix sniffing, 复审 F1 double-confirmed). Under `"v2"`, keys * carrying a reserved prefix are validated fail-loud (`config.memory_scope_key`); unprefixed keys * remain legal opaque legacy scopes. */ scopeContract?: "v2"; }; /** * Normalize the backward-compatible {@link import("./types.js").TaskSpec.memory} union into ONE canonical * {@link NormalizedMemorySpec} (design/84 Seam A decision 1). The consumer side reads ONLY this result — no * narrowing of the union anywhere downstream: * - `scopes` wins over `scope` when both are present; otherwise `scope` → `[scope]`. * - blank/whitespace scopes are dropped and duplicates collapsed (FIRST occurrence wins — keeps the * caller's stable→volatile order); list order is the inject order (stable prefix first, volatile tail last). * - `writeScope` defaults to the LAST scope (the highest-priority layer); an EXPLICIT `null` means read-only * (no `remember` tool, no consolidation); an explicit non-null string is taken as-is (it need NOT be one * of `scopes` — a deployment may write a layer it does not inject, though normally it is the last scope). * ⚠️ Layered READ seeding (service [646]①, 142-S4): when `scopes` comes from a registry's defaultScopes * (shared read layers), ALWAYS pin `writeScope` explicitly — the last-scope default would land every * harvest in whichever shared layer happens to be listed last (a cross-tenant write surface). * - returns `undefined` when there are no usable scopes (the caller treats it like "no memory configured"). * * Backward compatibility: `{ scope: "user:42" }` → `{ scopes: ["user:42"], writeScope: "user:42", enabled: true }`, * so every single-scope path is byte-identical to the pre-design/84 behavior. */ export declare function normalizeMemorySpec(input: MemorySpecInput | undefined): NormalizedMemorySpec | undefined; /** Max bytes of memory injected into the prompt; larger is truncated with a marker. (CodeWhale: 100 KiB) * Exported so the inject-all de-dup seeding (prepare-task.ts) can detect truncation and NOT seed dropped * (truncated-out) note ids as "already surfaced" — see the MINOR-1 audit fix there. */ export declare const MAX_MEMORY_BYTES: number; /** The wrapper-tag family the memory injection fences neutralize (`` is ALWAYS * included by {@link sanitizeUntrustedText} itself). Single source (design/138 S2-C, C-F10/O-F11): * the memory write-time scan (`memory-engine/scan.ts`) detects break-out markup by diffing * `sanitizeUntrustedText(text, MEMORY_WRAPPER_TAGS)` against the raw text, so the scan rules and * the ACTUAL injection boundary (this file's compose* fences) can never drift apart. */ export declare const MEMORY_WRAPPER_TAGS: readonly string[]; /** * Wrap memory content in a `` block. This is the **variable tail** of the system prompt * (per-user, scoped, timestamped) — the default prompt provider places it LAST so the stable base * prompt before it stays a cacheable prefix (see `defaultPromptProvider`). * Truncates over-cap content with a `` marker (mirrors CodeWhale `as_system_block`). * Returns undefined for empty content. */ export declare function composeMemoryBlock(content: string | null | undefined, scope: string): string | undefined; /** * Compose the LAYERED inject-all memory block for multiple scopes (design/84 Seam A decision 2). One * `` block with a `` subsection per scope, **in the given list order** — the * caller passes stable layers FIRST (cacheable prefix) and volatile layers LAST (re-computed tail), and the * highest-priority layer is therefore at the END of the block (closest to the model's attention, matching the * single-scope tail placement). Empty layers are omitted. Each layer's content is byte-capped + fence-sanitized * exactly like {@link composeMemoryBlock} (the same A-7 escape class). Returns `undefined` when every layer is * empty (no block to inject). * * **Cache-stability guarantee (§5):** the subsection order is EXACTLY the caller's list order — never sorted * or reordered here — and the per-scope wrapper/format is fixed, so a stable layer at the front renders * byte-identically across turns (the 90/98% prefix-cache hit measured in §5.1/§5.2 depends on this). The * VOLATILE layers the caller places last are the only part that changes, so only the tail re-computes. */ export declare function composeLayeredMemoryBlock(layers: ReadonlyArray<{ scope: string; content: string | null | undefined; }>): string | undefined; /** Build a `recall` tool that searches long-term memory on demand (just-in-time L3 retrieval). * @deprecated design/138 S4 — the Runner no longer mounts this (legacy memoryStore path retired; * new model-facing `Recall` calls get the TOMBSTONED_TOOLS migration text). Kept exported one major * for deployments that mount it as an explicit custom tool; removal is the next major. */ export declare function createRecallTool(store: MemoryStore, scope: string): AgentTool; /** * Build the `remember` tool bound to a store + scope. Auto-applies (writes are scoped to memory). * * `onRemembered` (optional) is called after each successful append with the new note's `text` and its * store id (when the store returned one) — the Runner used it to collect this task's notes for * end-of-task consolidation (design/41) without re-reading the store. * * @deprecated design/138 S4 — the Runner no longer mounts this (legacy memoryStore path retired; new * model-facing `Remember` calls get the TOMBSTONED_TOOLS migration text). Kept exported one major for * deployments that mount it as an explicit custom tool; removal is the next major. */ export declare function createRememberTool(store: MemoryStore, scope: string, onRemembered?: (note: { id?: string; text: string; }) => void): AgentTool; //# sourceMappingURL=memory.d.ts.map