mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: deduplicate memory provider tools to prevent 400 on strict providers (#10511)
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'
This commit is contained in:
parent
861efe274b
commit
91980e3518
2 changed files with 58 additions and 2 deletions
17
run_agent.py
17
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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue