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, type LogSink } from "../../logger.js"; import { type LLMClient } from "../llmClient.js"; import type { Run } from "../asyncContext.js"; /** * The part of a run memory reads: the context it accounts into, the * branch stack it charges, the calling module, and the branch's logger. * Callers pass their whole `Run`. */ export type MemoryRun = Pick; /** * 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. * * Both take the run of the call they serve. One manager is shared by every * fork branch, and each branch keeps its own id on its own stack. */ export type MemoryIdRef = { get(run: MemoryRun): string; set(run: MemoryRun, 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; /** 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; /** Where the logger writes: the settings part of the run's host. */ logSink?: LogSink; }; /** * 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 of the expiring messages in the compacted range, in order. They * stay in the thread right after the summary and are left out of it, so a * message the runtime will remove later does not live on in the summary. * See docs/dev/runtime/expiring-messages.md. */ keptIndices: 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; /** 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 * `run`'s 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(run: MemoryRun): { model: string; provider?: string; } | null; /** Resolved once; every embed awaits the same promise. `null` is a * failed resolution, kept so it is not retried on every embed. */ private _embeddingTarget; /** resolveEmbedding, with a local model name turned into what smoltalk * needs: a .gguf path for llama-cpp, the served name for mlx. */ resolveEmbeddingTarget(run: MemoryRun): Promise<{ model: string; provider?: string; } | null>; private resolveEmbeddingTargetOnce; /** 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; /** The providers that can embed but only through a model the user names: * a local server has no catalog to derive from. */ private static readonly LOCAL_PROVIDERS; private embeddingDisabledMessage; /** 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(run: MemoryRun): string; setMemoryId(run: MemoryRun, id: string): void; isInitialized(run: MemoryRun): 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(run: MemoryRun): MemoryGraph; init(run: MemoryRun): 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(run: MemoryRun, 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(run: MemoryRun, 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(run: MemoryRun, content: string): Promise; private tier1And2; recall(run: MemoryRun, query: string, options?: { model?: string; }): Promise; recallForInjection(run: MemoryRun, 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(run: MemoryRun, 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(run: MemoryRun, 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(run: MemoryRun, query: string): Promise; onTurn(run: MemoryRun, messages: smoltalk.Message[]): Promise; compactIfNeeded(run: MemoryRun, 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 {};