import { z } from "zod"; import * as smoltalk from "smoltalk"; import type { SmolConfig } from "smoltalk"; import { type MemoryConfig, type MemoryStore as MemoryStoreType } from "./types.js"; import { MemoryGraph } from "./graph.js"; import type { ExtractionResult } from "./extraction.js"; import { type LogLevel } from "../../logger.js"; import type { StatelogClient } from "../../statelogClient.js"; import type { LLMClient } from "../llmClient.js"; /** * A pluggable reference to the active memoryId. * * Per resolved decision #1, in production this is backed by * `stateStack.other.memoryId` so it survives interrupt/resume. For tests * an in-memory ref is used. */ export type MemoryIdRef = { get(): string; set(id: string): void; }; export type MemoryManagerOptions = { store: MemoryStoreType; config: MemoryConfig; /** The runtime-wide LLMClient (`SmoltalkClient`, `DeterministicClient`, * or whatever the user registered via `setLLMClient`). MemoryManager * uses it directly so registering a custom client takes effect for * memory's text + embed calls too. */ llmClient: LLMClient; /** Default `SmolConfig` overrides (api keys, provider, etc.). Merged * into every text and embed call. */ smoltalkDefaults?: Partial; source?: string; memoryIdRef?: MemoryIdRef; /** Statelog client used to emit `llmCall`/`embedding` spans and * `promptCompletion`/`embedCompletion` events for memory's * internal LLM/embed calls. Optional so tests can construct * managers without one; production wires the per-execCtx * client through `context.ts`. When absent, every statelog hook * is a no-op. */ statelogClient?: StatelogClient; /** Threshold for the manager's internal logger. The manager * creates one logger per instance via `createLogger(logLevel)`; * every line is `[memory]`-prefixed so users can grep/filter. */ logLevel?: LogLevel; }; /** * Plan describing how the caller should reshape its message thread * after compaction. The MemoryManager returns indices, not message * instances, so the caller can rebuild the thread from its own * smoltalk.Message instances and preserve identity (===). That keeps * any caller-side maps keyed on message identity (e.g. trace * correlations) valid across compaction. */ export type CompactionPlan = { /** Indices in the original messages array to keep at the head, in order. * Excludes any prior summary message — that gets replaced, not stacked. */ systemPrefixIndices: number[]; /** Indices in the original messages array to keep verbatim at the tail, * in order. These are the most recent messages preserved across the split. */ tailIndices: number[]; /** Full content for the new summary system message (already includes the * SUMMARY_MESSAGE_PREFIX so callers can pass it straight to systemMessage). */ summaryMessageContent: string; }; export declare class MemoryManager { private store; private config; private llmClient; private smoltalkDefaults; private source; private memoryIdRef; /** Optional — every call site uses `?.` so the absence of a * statelog client (e.g. in unit tests) is a clean no-op. */ private statelogClient?; /** Built once in the constructor from `options.logLevel`. Each line * emitted by the manager is prefixed `[memory]` so users can * filter on the prefix from stderr. */ private logger; private cache; constructor(options: MemoryManagerOptions); /** * Run a single-prompt text completion through the registered * LLMClient. We always go through `llmClient.text` (not directly * to smoltalk) so a custom client registered via `setLLMClient` * — including the `DeterministicClient` used in tests — controls * memory's text calls too. * * Wrapped in an `llmCall` span + `promptCompletion` event so the * call shows up in the trace viewer alongside agency-driven LLM * calls. Errors emit a statelog `error` event before being * re-thrown so a failed memory call is visible even though the * post-turn hook in `prompt.ts` swallows it to keep the agent * running. */ private _text; /** Same routing as _text(), but for embeddings. Returns a single * vector (the LLMClient.embed protocol accepts string|string[]; we * always pass one string here so the response is one vector). * * Wrapped in an `embedding` span + `embedCompletion` event. The * span type is distinct from `llmCall` so the viewer can render * cost/latency for embeddings separately. */ private _embed; /** One-shot guard so the "Tier-2 disabled" notice fires once per manager, * not on every recall. */ private _embeddingDisabledLogged; /** The active LLM provider, used to derive the embedding model. Reads the * active branch stack's `llmDefaults` (set by setModel/setLlmOptions — e.g. * the agent's `--local-model`), falling back to the baked smoltalk defaults * and finally to deriving the provider from the model name. Returns * undefined when no provider can be determined. */ private activeEmbeddingProvider; /** Resolve the embedding model + provider to use, or null when Tier-2 should * be disabled. An explicit `embeddings.model` wins; otherwise the model is * derived from the active provider via `EMBED_MODEL_BY_PROVIDER`. Providers * with no embedding endpoint (anthropic, llama-cpp, custom) yield null. */ resolveEmbedding(): { model: string; provider?: string; } | null; /** Embed `text`, or return null (logging once) when Tier-2 is disabled * because the active provider has no embedding endpoint. Callers treat null * as "skip semantic embedding for this item" — no remote call is made. */ private embedOrSkip; /** Emit a single notice (logger + statelog) the first time Tier-2 is * disabled, so the user sees semantic recall is off (e.g. a local model * with no embedding endpoint) without per-recall warn spam. */ private noteEmbeddingDisabled; getMemoryId(): string; setMemoryId(id: string): void; isInitialized(): boolean; /** * Returns the graph for the active memoryId. Throws if the cache * has not been loaded yet (call init() or any async operation first). */ getGraph(): MemoryGraph; init(): Promise; private getEntry; private model; /** * Build the extraction prompt for a single user message. Stdlib * agency code calls this, hands the result to `llm()` with a typed * `ExtractionResult`, and then calls `applyExtractionFromLLM` — * keeping the LLM call itself in the agency runPrompt pipeline so * tracing, cost/token accounting, and the structured-output Zod * schema all flow through the standard path. */ buildExtractionPromptFor(content: string): Promise; /** * Apply a typed extraction result to the active graph. Used by the * agency-side `remember` after it gets a structured result from * `llm(...)`. * * Wrapped in a `memoryRemember` umbrella span so the agency-side * call shape (`buildExtractionPromptFor` → `llm` → `applyExtractionFromLLM`) * still nests its embedding work under one parent in the viewer. */ applyExtractionFromLLM(result: ExtractionResult): Promise; /** * Convenience wrapper used by tests that want the whole extraction * round-trip in one call. The agency runtime path goes through * `buildExtractionPromptFor` + `applyExtractionFromLLM` instead. * * Opens its own `memoryRemember` span — the nested * `applyExtractionFromLLM` opens a child of the same type, which * is acceptable: the viewer will see two adjacent rows for the * same logical operation. We could short-circuit the inner call, * but the duplication is harmless and avoids coupling the two * methods through a flag. */ remember(content: string): Promise; private tier1And2; recall(query: string, options?: { model?: string; }): Promise; recallForInjection(query: string): Promise; /** * Slice the recall result down to `DEFAULT_RECALL_K`, resolve each * id to its entity (dropping ids that no longer exist), and render * the final user-visible string. Shared tail for `recall` (Tier * 1+2+3) and `recallForInjection` (Tier 1+2 only). */ private formatTopK; /** * Build the forget prompt for a query against the active graph. * Same split-pattern as extraction (see `buildExtractionPromptFor`): * stdlib agency code calls this, hands it to `llm()` with a typed * `ForgetResult`, then calls `applyForgetFromLLM`. */ buildForgetPromptFor(query: string): Promise; /** * Apply a typed forget result to the active graph. Substring match, * case-insensitive (resolved decision #7). * * Wrapped in a `memoryForget` umbrella span so the agency-side call * shape (`buildForgetPromptFor` → `llm` → `applyForgetFromLLM`) * groups its writes under one parent in the viewer. */ applyForgetFromLLM(parsed: ForgetResult): Promise; /** * Convenience wrapper used by tests that want the whole forget * round-trip in one call. The agency runtime path goes through * `buildForgetPromptFor` + `applyForgetFromLLM` instead. */ forget(query: string): Promise; onTurn(messages: smoltalk.Message[]): Promise; compactIfNeeded(messages: smoltalk.Message[]): Promise; /** Persist all cached memoryIds to disk. */ save(): Promise; private autoExtract; private generateEmbeddings; /** * Build the per-line "id: name (type) — facts" candidate index * fed to the Tier-3 filter prompt. Including the entity's current * observations lets the LLM judge relevance without us having to * dump the full graph; using stable ids (not names) makes the * response unambiguous and lets us reject hallucinated ids. */ private buildCandidateIndex; private embeddingRecallEntityIds; /** * Tier 3: ask the LLM to pick the relevant entities from a fixed * candidate list (the union of Tier 1 + Tier 2 hits, or the whole * graph on the small-graph fallback). Returns ids in the order the * LLM emitted, filtered to the offered set so a hallucinated id is * silently dropped instead of crashing the recall. */ private llmFilterCandidates; } declare const ForgetResultSchema: z.ZodObject<{ observations: z.ZodArray>; relations: z.ZodArray>; }, z.core.$strip>; export type ForgetResult = z.infer; export {};