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()