From 91980e35183017998e30e23a9d9abadc02274d17 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:09:32 -0700 Subject: [PATCH] fix: deduplicate memory provider tools to prevent 400 on strict providers (#10511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Memory provider plugins (e.g. Mnemosyne) can register tools via two paths: 1. Plugin system (ctx.register_tool) → tool registry → get_tool_definitions() 2. Memory manager → get_all_tool_schemas() → direct append in AIAgent.__init__ Path 2 blindly appended without checking if path 1 already added the same tool names. This created duplicate function names in the tools array sent to the API. Most providers silently handle duplicates, but Xiaomi MiMo (via Nous Portal) strictly rejects them with a 400 Bad Request. Fix: build a set of existing tool names before memory manager injection and skip any tool whose name is already present. Confirmed via live testing against Nous Portal: - Unique tool names → 200 OK - Duplicate tool names → 400 'Provider returned error' --- run_agent.py | 17 ++++++++++-- tests/agent/test_memory_provider.py | 43 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/run_agent.py b/run_agent.py index 359a92185..244fea6b2 100644 --- a/run_agent.py +++ b/run_agent.py @@ -1224,14 +1224,27 @@ class AIAgent: logger.warning("Memory provider plugin init failed: %s", _mpe) self._memory_manager = None - # Inject memory provider tool schemas into the tool surface + # Inject memory provider tool schemas into the tool surface. + # Skip tools whose names already exist (plugins may register the + # same tools via ctx.register_tool(), which lands in self.tools + # through get_tool_definitions()). Duplicate function names cause + # 400 errors on providers that enforce unique names (e.g. Xiaomi + # MiMo via Nous Portal). if self._memory_manager and self.tools is not None: + _existing_tool_names = { + t.get("function", {}).get("name") + for t in self.tools + if isinstance(t, dict) + } for _schema in self._memory_manager.get_all_tool_schemas(): + _tname = _schema.get("name", "") + if _tname and _tname in _existing_tool_names: + continue # already registered via plugin path _wrapped = {"type": "function", "function": _schema} self.tools.append(_wrapped) - _tname = _schema.get("name", "") if _tname: self.valid_tool_names.add(_tname) + _existing_tool_names.add(_tname) # Skills config: nudge interval for skill creation reminders self._skill_nudge_interval = 10 diff --git a/tests/agent/test_memory_provider.py b/tests/agent/test_memory_provider.py index eba772a04..7e07d2f33 100644 --- a/tests/agent/test_memory_provider.py +++ b/tests/agent/test_memory_provider.py @@ -782,6 +782,49 @@ class TestOnMemoryWriteBridge: mgr.on_memory_write("remove", "memory", "old fact") assert p.memory_writes == [("remove", "memory", "old fact")] + def test_memory_manager_tool_injection_deduplicates(self): + """Memory manager tools already in self.tools (from plugin registry) + must not be appended again. Duplicate function names cause 400 errors + on providers that enforce unique names (e.g. Xiaomi MiMo via Nous Portal). + + Regression test for: duplicate mnemosyne_recall / mnemosyne_remember / + mnemosyne_stats in tools array → 400 from Nous Portal. + """ + mgr = MemoryManager() + p = FakeMemoryProvider("ext", tools=[ + {"name": "ext_recall", "description": "Recall", "parameters": {}}, + {"name": "ext_remember", "description": "Remember", "parameters": {}}, + ]) + mgr.add_provider(p) + + # Simulate self.tools already containing one of the plugin tools + # (as if it was registered via ctx.register_tool → get_tool_definitions) + existing_tools = [ + {"type": "function", "function": {"name": "ext_recall", "description": "Recall (from registry)", "parameters": {}}}, + {"type": "function", "function": {"name": "web_search", "description": "Search", "parameters": {}}}, + ] + + # Apply the same dedup logic from run_agent.py __init__ + _existing_names = { + t.get("function", {}).get("name") + for t in existing_tools + if isinstance(t, dict) + } + for _schema in mgr.get_all_tool_schemas(): + _tname = _schema.get("name", "") + if _tname and _tname in _existing_names: + continue + existing_tools.append({"type": "function", "function": _schema}) + if _tname: + _existing_names.add(_tname) + + # ext_recall should NOT be duplicated; ext_remember should be added + tool_names = [t["function"]["name"] for t in existing_tools] + assert tool_names.count("ext_recall") == 1, f"ext_recall duplicated: {tool_names}" + assert tool_names.count("ext_remember") == 1 + assert tool_names.count("web_search") == 1 + assert len(existing_tools) == 3 # web_search + ext_recall + ext_remember + def test_on_memory_write_tolerates_provider_failure(self): """If a provider's on_memory_write raises, others still get notified.""" mgr = MemoryManager()