/** * `memory_search` — the mandatory-recall tool. * * Port of upstream `createMemorySearchTool` at * `upstream reference`. The tool description * is verbatim "Mandatory recall step..." from upstream — this is the load- * bearing line that turns "the agent may recall" into "the agent will recall". */ import { tool } from '@langchain/core/tools'; import { MEMORY_SEARCH_DESCRIPTION, MEMORY_SEARCH_TOOL_NAME, } from '@/memory/constants'; import { buildMemorySearchUnavailableResult, clampResultsByInjectedChars, MemorySearchSchema, type MemorySearchToolResult, type MemoryToolBinding, } from './shared'; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function createMemorySearchTool(binding: MemoryToolBinding) { return tool( async (args): Promise => { try { const entries = await binding.backend.search( binding.scope, args.query, { maxResults: args.maxResults, minScore: args.minScore, mmr: binding.searchOptions?.mmr, temporalDecay: binding.searchOptions?.temporalDecay, citations: binding.searchOptions?.citations, } ); const clamped = clampResultsByInjectedChars( entries, binding.maxInjectedChars ); // [phase2-recall-tracking] debug: fire-and-forget best-effort record if (binding.recallTracker && clamped.length > 0) { void binding.recallTracker .record({ agentId: binding.scope.agentId, query: args.query, hits: clamped.map((e) => ({ id: e.id, path: e.path, score: e.score, })), }) .catch(() => undefined); } const payload: MemorySearchToolResult = { results: clamped.map((entry) => ({ id: entry.id, path: entry.path, content: entry.content, score: entry.score, createdAt: entry.createdAt, citation: entry.citation, })), }; return JSON.stringify(payload); } catch (err) { const message = err instanceof Error ? err.message : String(err); return JSON.stringify(buildMemorySearchUnavailableResult(message)); } }, { name: MEMORY_SEARCH_TOOL_NAME, description: MEMORY_SEARCH_DESCRIPTION, schema: MemorySearchSchema, } ); }