import { jsonResult, readNumberParam, readStringParam, type AnyAgentTool, type OpenClawConfig, } from "openclaw/plugin-sdk/memory-core-host-runtime-core"; import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-core-host-runtime-files"; import { resolveMemoryCorePluginConfig, resolveMemoryDeepDreamingConfig, } from "openclaw/plugin-sdk/memory-core-host-status"; import { recordShortTermRecalls } from "./short-term-promotion.js"; import { clampResultsByInjectedChars, decorateCitations, resolveMemoryCitationsMode, shouldIncludeCitations, } from "./tools.citations.js"; import { buildMemorySearchUnavailableResult, createMemoryTool, getMemoryCorpusSupplementResult, getMemoryManagerContext, getMemoryManagerContextWithPurpose, loadMemoryToolRuntime, MemoryGetSchema, MemorySearchSchema, searchMemoryCorpusSupplements, } from "./tools.shared.js"; function buildRecallKey( result: Pick, ): string { return `${result.source}:${result.path}:${result.startLine}:${result.endLine}`; } function resolveRecallTrackingResults( rawResults: MemorySearchResult[], surfacedResults: MemorySearchResult[], ): MemorySearchResult[] { if (surfacedResults.length === 0 || rawResults.length === 0) { return surfacedResults; } const rawByKey = new Map(); for (const raw of rawResults) { const key = buildRecallKey(raw); if (!rawByKey.has(key)) { rawByKey.set(key, raw); } } return surfacedResults.map((surfaced) => rawByKey.get(buildRecallKey(surfaced)) ?? surfaced); } function queueShortTermRecallTracking(params: { workspaceDir?: string; query: string; rawResults: MemorySearchResult[]; surfacedResults: MemorySearchResult[]; timezone?: string; }): void { const trackingResults = resolveRecallTrackingResults(params.rawResults, params.surfacedResults); void recordShortTermRecalls({ workspaceDir: params.workspaceDir, query: params.query, results: trackingResults, timezone: params.timezone, }).catch(() => { // Recall tracking is best-effort and must never block memory recall. }); } async function getSupplementMemoryReadResult(params: { relPath: string; from?: number; lines?: number; agentSessionKey?: string; corpus?: "memory" | "wiki" | "all"; }) { const supplement = await getMemoryCorpusSupplementResult({ lookup: params.relPath, fromLine: params.from, lineCount: params.lines, agentSessionKey: params.agentSessionKey, corpus: params.corpus, }); if (!supplement) { return null; } const { content, ...rest } = supplement; return { ...rest, text: content, }; } export function createMemorySearchTool(options: { config?: OpenClawConfig; agentSessionKey?: string; }): AnyAgentTool | null { return createMemoryTool({ options, label: "Memory Search", name: "memory_search", description: "Mandatory recall step: semantically search MEMORY.md + memory/*.md (and optional session transcripts) before answering questions about prior work, decisions, dates, people, preferences, or todos. Optional `corpus=wiki` or `corpus=all` also searches registered compiled-wiki supplements. If response has disabled=true, memory retrieval is unavailable and should be surfaced to the user.", parameters: MemorySearchSchema, execute: ({ cfg, agentId }) => async (_toolCallId, params) => { const query = readStringParam(params, "query", { required: true }); const maxResults = readNumberParam(params, "maxResults"); const minScore = readNumberParam(params, "minScore"); const requestedCorpus = readStringParam(params, "corpus") as | "memory" | "wiki" | "all" | undefined; const { resolveMemoryBackendConfig } = await loadMemoryToolRuntime(); const shouldQueryMemory = requestedCorpus !== "wiki"; const shouldQuerySupplements = requestedCorpus === "wiki" || requestedCorpus === "all"; const memory = shouldQueryMemory ? await getMemoryManagerContext({ cfg, agentId }) : null; if (shouldQueryMemory && memory && "error" in memory && !shouldQuerySupplements) { return jsonResult(buildMemorySearchUnavailableResult(memory.error)); } try { const citationsMode = resolveMemoryCitationsMode(cfg); const includeCitations = shouldIncludeCitations({ mode: citationsMode, sessionKey: options.agentSessionKey, }); let rawResults: MemorySearchResult[] = []; let surfacedMemoryResults: Array = []; let provider: string | undefined; let model: string | undefined; let fallback: unknown; let searchMode: string | undefined; if (shouldQueryMemory && memory && !("error" in memory)) { rawResults = await memory.manager.search(query, { maxResults, minScore, sessionKey: options.agentSessionKey, }); const status = memory.manager.status(); const decorated = decorateCitations(rawResults, includeCitations); const resolved = resolveMemoryBackendConfig({ cfg, agentId }); const memoryResults = status.backend === "qmd" ? clampResultsByInjectedChars(decorated, resolved.qmd?.limits.maxInjectedChars) : decorated; surfacedMemoryResults = memoryResults.map((result) => ({ ...result, corpus: "memory" as const, })); const sleepTimezone = resolveMemoryDeepDreamingConfig({ pluginConfig: resolveMemoryCorePluginConfig(cfg), cfg, }).timezone; queueShortTermRecallTracking({ workspaceDir: status.workspaceDir, query, rawResults, surfacedResults: memoryResults, timezone: sleepTimezone, }); provider = status.provider; model = status.model; fallback = status.fallback; searchMode = (status.custom as { searchMode?: string } | undefined)?.searchMode; } const supplementResults = shouldQuerySupplements ? await searchMemoryCorpusSupplements({ query, maxResults, agentSessionKey: options.agentSessionKey, corpus: requestedCorpus, }) : []; const results = [...surfacedMemoryResults, ...supplementResults] .toSorted((left, right) => { if (left.score !== right.score) { return right.score - left.score; } return left.path.localeCompare(right.path); }) .slice(0, Math.max(1, maxResults ?? 10)); return jsonResult({ results, provider, model, fallback, citations: citationsMode, mode: searchMode, }); } catch (err) { const message = err instanceof Error ? err.message : String(err); return jsonResult(buildMemorySearchUnavailableResult(message)); } }, }); } export function createMemoryGetTool(options: { config?: OpenClawConfig; agentSessionKey?: string; }): AnyAgentTool | null { return createMemoryTool({ options, label: "Memory Get", name: "memory_get", description: "Safe snippet read from MEMORY.md or memory/*.md with optional from/lines; `corpus=wiki` reads from registered compiled-wiki supplements. Use after search to pull only the needed lines and keep context small.", parameters: MemoryGetSchema, execute: ({ cfg, agentId }) => async (_toolCallId, params) => { const relPath = readStringParam(params, "path", { required: true }); const from = readNumberParam(params, "from", { integer: true }); const lines = readNumberParam(params, "lines", { integer: true }); const requestedCorpus = readStringParam(params, "corpus") as | "memory" | "wiki" | "all" | undefined; const { readAgentMemoryFile, resolveMemoryBackendConfig } = await loadMemoryToolRuntime(); if (requestedCorpus === "wiki") { const supplement = await getSupplementMemoryReadResult({ relPath, from: from ?? undefined, lines: lines ?? undefined, agentSessionKey: options.agentSessionKey, corpus: requestedCorpus, }); return jsonResult( supplement ?? { path: relPath, text: "", disabled: true, error: "wiki corpus result not found", }, ); } const resolved = resolveMemoryBackendConfig({ cfg, agentId }); if (resolved.backend === "builtin") { try { const result = await readAgentMemoryFile({ cfg, agentId, relPath, from: from ?? undefined, lines: lines ?? undefined, }); return jsonResult(result); } catch (err) { if (requestedCorpus === "all") { const supplement = await getSupplementMemoryReadResult({ relPath, from: from ?? undefined, lines: lines ?? undefined, agentSessionKey: options.agentSessionKey, corpus: requestedCorpus, }); if (supplement) { return jsonResult(supplement); } } const message = err instanceof Error ? err.message : String(err); return jsonResult({ path: relPath, text: "", disabled: true, error: message }); } } const memory = await getMemoryManagerContextWithPurpose({ cfg, agentId, purpose: "status", }); if ("error" in memory) { return jsonResult({ path: relPath, text: "", disabled: true, error: memory.error }); } try { const result = await memory.manager.readFile({ relPath, from: from ?? undefined, lines: lines ?? undefined, }); return jsonResult(result); } catch (err) { if (requestedCorpus === "all") { const supplement = await getSupplementMemoryReadResult({ relPath, from: from ?? undefined, lines: lines ?? undefined, agentSessionKey: options.agentSessionKey, corpus: requestedCorpus, }); if (supplement) { return jsonResult(supplement); } } const message = err instanceof Error ? err.message : String(err); return jsonResult({ path: relPath, text: "", disabled: true, error: message }); } }, }); }