Agent Memory provides memory_search and conversation_search tools that each employ three strategies with automatic degradation: hybrid (FTS5 + vector embedding merged via Reciprocal Rank Fusion), embedding-only, and FTS5-only, gracefully falling back when indexing services are unavailable. Both tools apply filtering and over-retrieval to candidate results before merging ranked lists, then render formatted responses that report which strategy produced results and any configuration guidance needed. Reciprocal Rank Fusion (RRF) is a rank-merging algorithm that combines independently ranked result lists into a single ordering without requiring score scales to be aligned, enabling hybrid search to meaningfully merge FTS5 keyword ranks and vector similarity scores.
The memory_search tool, implemented in src/core/tools/memory-search.ts, supports three search strategies with automatic degradation: hybrid (FTS5 keyword + vector embedding in parallel, merged via Reciprocal Rank Fusion — the default), embedding (pure vector similarity, used when FTS5 is unavailable), and fts (pure FTS5 keyword search, used when embedding is unavailable).[1] Tool registration is handled via api.registerTool() in index.ts; src/core/tools/memory-search.ts contains only the search logic and response formatting.[1]
Hybrid search runs FTS5 and vector lookups in parallel via Promise.all, each over-retrieving limit × 3 candidates before merging.[1] rrfMergeL1 merges the ranked result lists using Reciprocal Rank Fusion with the standard RRF constant k = 60; items appearing in multiple lists accumulate scores of 1 / (60 + rank + 1), and the score field of each returned item is replaced with the computed RRF score.[1] FTS5 and vector search failures inside executeMemorySearch are non-fatal: each branch catches errors, logs a warning, and returns an empty list so the surviving strategy can still produce results.[1]
Scene filtering in executeMemorySearch is applied after merging using case-insensitive substring matching (r.scene_name.toLowerCase().includes(normalizedScene)) rather than exact equality.[1] formatSearchResponse renders each memory item with its type, priority, scene name, and score; items with priority < 0 are labeled (global instruction) instead of showing the numeric priority.[1] The MemorySearchResult interface exposes a strategy field reporting which path was used ("hybrid", "embedding", "fts", or "none") and an optional message field for error or advisory text.[1] When neither an embedding service nor FTS5 is available, executeMemorySearch returns { results: [], total: 0, strategy: "none" } with a message advising the caller to configure an embedding provider (e.g. openai_compatible) via the embedding.provider setting.[1]
The conversation_search tool, implemented in src/core/tools/conversation-search.ts, mirrors the same three-strategy pattern with automatic degradation: hybrid (FTS5 + vector in parallel, merged via RRF), embedding (pure vector similarity), and fts (pure FTS5 keyword search).[2] Like memory_search, the tool is registered via api.registerTool() in index.ts, with core logic confined to src/core/tools/conversation-search.ts.[2] ConversationSearchResultItem represents a single L0 message with fields: id, session_key, role ("user" or "assistant"), content, score, and recorded_at.[2]
executeConversationSearch returns { results: [], total: 0, strategy: "none" } immediately for empty or whitespace-only queries, without touching any store.[2] If vectorStore is not provided, executeConversationSearch also returns { results: [], total: 0, strategy: "none" } regardless of other parameters.[2]
Over-retrieval in conversation-search.ts uses candidateK = limit × 4 when a sessionKey filter is present, and limit × 3 otherwise, to compensate for post-merge session filtering.[2] FTS5 and vector searches are executed in parallel via Promise.all; failures in either branch are non-fatal and return an empty array so the other strategy can still produce results.[2] Reciprocal Rank Fusion in conversation-search.ts uses the standard RRF constant K = 60; items appearing in multiple ranked lists have their scores summed, and the score field of each returned item is replaced by its RRF score.[2] Session-key filtering is applied after RRF merging: the merged result set is filtered to r.session_key === sessionFilter, then trimmed to limit.[2]
The effective strategy reported in ConversationSearchResult.strategy reflects which branches actually returned results: both → "hybrid", only vector → "embedding", only FTS5 → "fts"; when neither returns results, the value falls back to "embedding" or "fts" depending on availability.[2] The ConversationSearchResult interface carries an optional message field for configuration advice; when that field is present, formatConversationSearchResponse renders it as its entire output.[2] When neither the embedding service nor FTS5 is available, executeConversationSearch populates message advising the caller to configure an embedding provider (e.g. openai_compatible) via the embedding.provider setting.[2] formatConversationSearchResponse formats each result as **[role]** Session: <session_key> [recorded_at] (score: X.XXX) followed by the message content, with results separated by --- dividers.[2]
Sources