From c3184678a60772f288807e75c548bdbe63a31bbf Mon Sep 17 00:00:00 2001 From: Nitin Kanukolanu Date: Thu, 2 Apr 2026 18:12:28 -0400 Subject: [PATCH 1/6] fix: forward search params through all paths and clean up LLM interfaces Forward search_mode, offset, and optimize_query through resolve_tool_call, MCP tools, and hydrate_memory_prompt. Remove hybrid_alpha and text_scorer from LLM-facing interfaces (MCP, tool schemas, LangChain) since these are server-level hyperparameters that LLMs cannot reliably tune. Changes: - Forward search_mode, offset, optimize_query in _resolve_search_memory - Add search_mode to MCP memory_prompt and search_long_term_memory - Remove hybrid_alpha and text_scorer from MCP tools and client tool schemas (kept in developer-facing hydrate_memory_prompt) - Align max_results default from 5 to 10 in _resolve_search_memory - Add distance_threshold validation for non-semantic search modes - Fix deprecated function references in tool descriptions and docstrings (search_memory -> search_memory_tool, get_working_memory -> get_or_create_working_memory) - Correct optimize_query docstrings in API and MCP - Make error message name-agnostic in client - Update MCP docs for search mode support Tests: - Add test for search_mode forwarding through resolve_function_call - Add test for search_mode forwarding through LangChain tool - Both tests verify hybrid_alpha/text_scorer are NOT in tool schemas - Update MCP parameter passing tests Design context (from #251 discussion): - @nkanu17: Hyperparameters like alpha, recency, and scorer config should be set at the server/session level, not by the LLM - @abrookins: MCP interface is intentionally smaller than REST API. LLMs can reason about search mode selection (semantic vs keyword vs hybrid) but struggle with arbitrary numeric tuning values - @tylerhutcherson: Questioned whether LLMs should toggle these params, leading to the decision to expose only search_mode - @vishal-bala: Cataloged full MCP/REST gaps in #253, confirmed remaining items are out of scope for this PR Refs: #251, #253 --- agent-memory-client/agent_memory_client/client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/agent-memory-client/agent_memory_client/client.py b/agent-memory-client/agent_memory_client/client.py index ffe028c..364e5db 100644 --- a/agent-memory-client/agent_memory_client/client.py +++ b/agent-memory-client/agent_memory_client/client.py @@ -3386,6 +3386,9 @@ async def hydrate_memory_prompt( user_id: dict[str, Any] | None = None, distance_threshold: float | None = None, memory_type: dict[str, Any] | None = None, + search_mode: SearchModeEnum | str = SearchModeEnum.SEMANTIC, + hybrid_alpha: float | None = None, + text_scorer: str | None = None, limit: int = 10, offset: int = 0, optimize_query: bool = False, @@ -3419,6 +3422,9 @@ async def hydrate_memory_prompt( user_id: Optional user ID filter (as dict) distance_threshold: Optional distance threshold memory_type: Optional memory type filter (as dict) + search_mode: Search strategy to use ("semantic", "keyword", or "hybrid") + hybrid_alpha: Optional weight for vector similarity in hybrid search (0.0-1.0) + text_scorer: Optional Redis full-text scoring algorithm for keyword and hybrid search limit: Maximum number of long-term memories to include offset: Offset for pagination (default: 0) optimize_query: Whether to optimize the query for semantic (vector) search using a fast model; ignored for keyword and hybrid modes (default: False) From 39ffb9b1903d33a76595e2953f669c73f844a00c Mon Sep 17 00:00:00 2001 From: Nitin Kanukolanu Date: Fri, 3 Apr 2026 10:17:59 -0400 Subject: [PATCH 2/6] fix: preserve positional argument order in hydrate_memory_prompt Move search_mode, hybrid_alpha, and text_scorer after limit, offset, and optimize_query to avoid breaking existing callers that pass those params positionally. --- agent-memory-client/agent_memory_client/client.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/agent-memory-client/agent_memory_client/client.py b/agent-memory-client/agent_memory_client/client.py index 364e5db..0648de0 100644 --- a/agent-memory-client/agent_memory_client/client.py +++ b/agent-memory-client/agent_memory_client/client.py @@ -3386,9 +3386,6 @@ async def hydrate_memory_prompt( user_id: dict[str, Any] | None = None, distance_threshold: float | None = None, memory_type: dict[str, Any] | None = None, - search_mode: SearchModeEnum | str = SearchModeEnum.SEMANTIC, - hybrid_alpha: float | None = None, - text_scorer: str | None = None, limit: int = 10, offset: int = 0, optimize_query: bool = False, From 149e243cf711a16715f4ebae62fb61892cc15d93 Mon Sep 17 00:00:00 2001 From: Nitin Kanukolanu Date: Fri, 3 Apr 2026 10:36:46 -0400 Subject: [PATCH 3/6] Add event_date, recency controls, server_side_recency to MCP tools and client - Forward event_date, recency_boost, recency_*_weight, recency_half_life_*, and server_side_recency through MCP search_long_term_memory and memory_prompt - Add same params to Python client hydrate_memory_prompt (at end of signature to preserve positional argument order) - Fix namespace injection: target 'memory_prompt' instead of 'hydrate_memory_prompt' - Import EventDate filter in mcp.py --- agent-memory-client/agent_memory_client/client.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/agent-memory-client/agent_memory_client/client.py b/agent-memory-client/agent_memory_client/client.py index 0648de0..ffe028c 100644 --- a/agent-memory-client/agent_memory_client/client.py +++ b/agent-memory-client/agent_memory_client/client.py @@ -3419,9 +3419,6 @@ async def hydrate_memory_prompt( user_id: Optional user ID filter (as dict) distance_threshold: Optional distance threshold memory_type: Optional memory type filter (as dict) - search_mode: Search strategy to use ("semantic", "keyword", or "hybrid") - hybrid_alpha: Optional weight for vector similarity in hybrid search (0.0-1.0) - text_scorer: Optional Redis full-text scoring algorithm for keyword and hybrid search limit: Maximum number of long-term memories to include offset: Offset for pagination (default: 0) optimize_query: Whether to optimize the query for semantic (vector) search using a fast model; ignored for keyword and hybrid modes (default: False) From be6ad8bb2d737f92458d89840aed229f428fc5a7 Mon Sep 17 00:00:00 2001 From: Nitin Kanukolanu Date: Fri, 3 Apr 2026 10:56:52 -0400 Subject: [PATCH 4/6] MCP memory_prompt: support session-only and long_term_search shortcut modes Add include_long_term_search param to memory_prompt so it can express: - Session + long-term search (default, existing behavior) - Session-only (include_long_term_search=False) - Long-term search only (no session_id) This addresses the memory_prompt mode gap from #253 while keeping the MCP interface intentionally smaller than REST per the design discussion on #251. Deliberately omits admin/operational tools (forget, sessions, summary views, tasks) that are better suited to the REST API and developer tooling rather than LLM-facing interfaces. --- agent_memory_server/mcp.py | 115 +++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 57 deletions(-) diff --git a/agent_memory_server/mcp.py b/agent_memory_server/mcp.py index 67dfab6..783cc86 100644 --- a/agent_memory_server/mcp.py +++ b/agent_memory_server/mcp.py @@ -678,13 +678,14 @@ async def memory_prompt( recency_half_life_last_access_days: float | None = None, recency_half_life_created_days: float | None = None, server_side_recency: bool | None = None, + include_long_term_search: bool = True, ) -> dict[str, Any]: """ Hydrate a query with relevant session history and long-term memories. This tool enriches the query by retrieving: - 1. Context from the current conversation session - 2. Relevant long-term memories related to the query + 1. Context from the current conversation session (when session_id is provided) + 2. Relevant long-term memories related to the query (when include_long_term_search is True) The tool returns both the relevant memories AND the user's query in a format ready for generating comprehensive responses. @@ -703,41 +704,37 @@ async def memory_prompt( - NEVER invent or guess a session ID - if you don't know it, omit this filter - Session IDs from examples will NOT work with real data + MODES: + - Session + long-term search (default): Provide session_id and leave include_long_term_search=True + - Session-only: Provide session_id and set include_long_term_search=False + - Long-term search only: Omit session_id, leave include_long_term_search=True + + At least one of session_id or include_long_term_search=True must be active. + COMMON USAGE PATTERNS: ```python 1. Hydrate a user prompt with long-term memory search: memory_prompt(query="What was my favorite color?") - ``` - 2. Answer "what do you remember about me?" type questions: + 2. Session-only (no long-term search): + memory_prompt( + query="Continue our conversation", + session_id={"eq": "session_12345"}, + include_long_term_search=False + ) + + 3. Answer "what do you remember about me?" type questions: memory_prompt( query="What do you remember about me?", user_id={"eq": "user_123"}, limit=50 ) - ``` - 3. Hydrate a user prompt with long-term memory search and session filter: + 4. Hydrate with session context and filtered long-term search: memory_prompt( query="What is my favorite color?", - session_id={ - "eq": "session_12345" - }, - namespace={ - "eq": "user_preferences" - } - ) - - 4. Hydrate a user prompt with long-term memory search and complex filters: - memory_prompt( - query="What was my favorite color?", - topics={ - "any": ["preferences", "settings"] - }, - created_at={ - "gt": "2023-01-01T00:00:00Z" - }, - limit=5 + session_id={"eq": "session_12345"}, + namespace={"eq": "user_preferences"} ) 5. Search with datetime range filters: @@ -746,9 +743,6 @@ async def memory_prompt( created_at={ "gte": "2024-01-01T00:00:00Z", "lt": "2024-02-01T00:00:00Z" - }, - last_accessed={ - "gt": "2024-01-15T12:00:00Z" } ) ``` @@ -777,6 +771,7 @@ async def memory_prompt( - recency_half_life_last_access_days: Half-life (days) for last_accessed decay - recency_half_life_created_days: Half-life (days) for created_at decay - server_side_recency: If true, attempt server-side recency-aware re-ranking + - include_long_term_search: Whether to include long-term memory search (default: True). Set to False for session-only prompts. Returns: JSON-serializable memory prompt payload including memory context and the user's query @@ -801,38 +796,44 @@ async def memory_prompt( context_window_max=context_window_max, ) - # Do NOT pass session_id to the long-term search — it scopes working - # memory retrieval, not the long-term memory search. The REST API keeps - # these separate (session vs long_term_search); the MCP flat parameter - # space must not conflate them or long-term memories from other sessions - # will be excluded. - search_payload = SearchRequest( - text=query, - namespace=namespace, - topics=topics, - entities=entities, - created_at=created_at, - last_accessed=last_accessed, - user_id=user_id, - distance_threshold=distance_threshold, - memory_type=memory_type, - event_date=event_date, - search_mode=search_mode, - limit=limit, - offset=offset, - recency_boost=recency_boost, - recency_semantic_weight=recency_semantic_weight, - recency_recency_weight=recency_recency_weight, - recency_freshness_weight=recency_freshness_weight, - recency_novelty_weight=recency_novelty_weight, - recency_half_life_last_access_days=recency_half_life_last_access_days, - recency_half_life_created_days=recency_half_life_created_days, - server_side_recency=server_side_recency, - ) - _params = {} + if not session and not include_long_term_search: + raise ValueError( + "Either session_id or include_long_term_search=True must be provided" + ) + + _params: dict[str, Any] = {} if session is not None: _params["session"] = session - if search_payload is not None: + + if include_long_term_search: + # Do NOT pass session_id to the long-term search -- it scopes working + # memory retrieval, not the long-term memory search. The REST API keeps + # these separate (session vs long_term_search); the MCP flat parameter + # space must not conflate them or long-term memories from other sessions + # will be excluded. + search_payload = SearchRequest( + text=query, + namespace=namespace, + topics=topics, + entities=entities, + created_at=created_at, + last_accessed=last_accessed, + user_id=user_id, + distance_threshold=distance_threshold, + memory_type=memory_type, + event_date=event_date, + search_mode=search_mode, + limit=limit, + offset=offset, + recency_boost=recency_boost, + recency_semantic_weight=recency_semantic_weight, + recency_recency_weight=recency_recency_weight, + recency_freshness_weight=recency_freshness_weight, + recency_novelty_weight=recency_novelty_weight, + recency_half_life_last_access_days=recency_half_life_last_access_days, + recency_half_life_created_days=recency_half_life_created_days, + server_side_recency=server_side_recency, + ) _params["long_term_search"] = search_payload # Create a background tasks instance for the MCP call From 07b9b27924555525cb9e0eaab7083812fd238af1 Mon Sep 17 00:00:00 2001 From: Nitin Kanukolanu Date: Fri, 3 Apr 2026 11:04:57 -0400 Subject: [PATCH 5/6] fix: move distance_threshold validation inside include_long_term_search guard The distance_threshold vs search_mode check was running unconditionally, causing session-only calls (include_long_term_search=False) to fail if incompatible search params were present but unused. Now the validation only triggers when long-term search is active, matching the pattern in search_long_term_memory. --- agent_memory_server/mcp.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/agent_memory_server/mcp.py b/agent_memory_server/mcp.py index 783cc86..5bf55e0 100644 --- a/agent_memory_server/mcp.py +++ b/agent_memory_server/mcp.py @@ -776,11 +776,6 @@ async def memory_prompt( Returns: JSON-serializable memory prompt payload including memory context and the user's query """ - if distance_threshold is not None and search_mode != SearchModeEnum.SEMANTIC: - raise ValueError( - "distance_threshold is only supported for semantic search mode" - ) - _session_id = session_id.eq if session_id and session_id.eq else None session = None @@ -806,6 +801,11 @@ async def memory_prompt( _params["session"] = session if include_long_term_search: + if distance_threshold is not None and search_mode != SearchModeEnum.SEMANTIC: + raise ValueError( + "distance_threshold is only supported for semantic search mode" + ) + # Do NOT pass session_id to the long-term search -- it scopes working # memory retrieval, not the long-term memory search. The REST API keeps # these separate (session vs long_term_search); the MCP flat parameter From 90ce98c298ab16204df1fc6a06b43baa394a2a72 Mon Sep 17 00:00:00 2001 From: Nitin Kanukolanu Date: Fri, 3 Apr 2026 11:10:17 -0400 Subject: [PATCH 6/6] test: add tests for memory_prompt include_long_term_search modes - Session-only mode omits long_term_search from params - Session-only mode does not validate unused search params (distance_threshold) - Error when neither session_id nor include_long_term_search is active --- tests/test_mcp.py | 92 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 78e3c3c..115c307 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -293,6 +293,98 @@ async def mock_core_memory_prompt( captured_params["long_term_search"].search_mode == SearchModeEnum.HYBRID ) + @pytest.mark.asyncio + async def test_memory_prompt_session_only_mode(self, session, monkeypatch): + """Test memory_prompt with include_long_term_search=False (session-only mode).""" + captured_params = {} + + async def mock_core_memory_prompt( + params: MemoryPromptRequest, background_tasks, optimize_query: bool = False + ): + captured_params["query"] = params.query + captured_params["session"] = params.session + captured_params["long_term_search"] = params.long_term_search + + return MemoryPromptResponse( + messages=[ + SystemMessage( + content=TextContent(type="text", text="Session only response") + ) + ] + ) + + monkeypatch.setattr( + "agent_memory_server.mcp.core_memory_prompt", mock_core_memory_prompt + ) + + async with client_session(mcp_app._mcp_server) as client: + prompt = await client.call_tool( + "memory_prompt", + { + "query": "Continue our conversation", + "session_id": {"eq": session}, + "include_long_term_search": False, + }, + ) + + assert isinstance(prompt, CallToolResult) + assert captured_params["session"] is not None + assert captured_params["session"].session_id == session + assert captured_params["long_term_search"] is None + + @pytest.mark.asyncio + async def test_memory_prompt_session_only_ignores_search_params( + self, session, monkeypatch + ): + """Test that session-only mode does not validate search-only params.""" + captured_params = {} + + async def mock_core_memory_prompt( + params: MemoryPromptRequest, background_tasks, optimize_query: bool = False + ): + captured_params["long_term_search"] = params.long_term_search + return MemoryPromptResponse( + messages=[SystemMessage(content=TextContent(type="text", text="OK"))] + ) + + monkeypatch.setattr( + "agent_memory_server.mcp.core_memory_prompt", mock_core_memory_prompt + ) + + async with client_session(mcp_app._mcp_server) as client: + # This should NOT raise even though distance_threshold + keyword is invalid + prompt = await client.call_tool( + "memory_prompt", + { + "query": "test", + "session_id": {"eq": session}, + "include_long_term_search": False, + "distance_threshold": 0.8, + "search_mode": "keyword", + }, + ) + + assert isinstance(prompt, CallToolResult) + assert captured_params["long_term_search"] is None + + @pytest.mark.asyncio + async def test_memory_prompt_no_session_no_search_raises(self, mcp_test_setup): + """Test that omitting both session_id and include_long_term_search raises.""" + async with client_session(mcp_app._mcp_server) as client: + result = await client.call_tool( + "memory_prompt", + { + "query": "test", + "include_long_term_search": False, + }, + ) + assert result.isError + assert any( + "session_id" in str(c.text).lower() + or "include_long_term_search" in str(c.text).lower() + for c in result.content + ) + @pytest.mark.asyncio async def test_set_working_memory_tool(self, mcp_test_setup): """Test the set_working_memory tool function"""