mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-24 10:52:21 +00:00
The success/staged gating and op-expansion for mirroring built-in memory writes to external providers lived in a standalone agent/memory_write_bridge.py helper called inline from two core call sites (tool_executor.py, agent_runtime_helpers.py). That left the mirror decision-making in the agent loop, outside the memory-provider interface. Fold it into a new MemoryManager.notify_memory_tool_write() entry point: the loop now hands over the raw tool result + args and a metadata callback, and the manager decides whether/what to mirror. Both core call sites collapse to a single call; the orphan module is removed. No MemoryProvider ABC change. Tests rewritten as behavior tests against the manager method.
145 lines
4.4 KiB
Python
145 lines
4.4 KiB
Python
"""Behavior tests for the built-in memory → external provider bridge.
|
|
|
|
The bridge lives behind the MemoryManager interface
|
|
(``MemoryManager.notify_memory_tool_write``): the agent loop hands over the raw
|
|
built-in memory tool result + args, and the manager decides whether/what to
|
|
mirror to external providers. These tests drive that method with a fake
|
|
external provider and assert which ``on_memory_write`` calls land.
|
|
"""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from agent.memory_manager import MemoryManager
|
|
from agent.memory_provider import MemoryProvider
|
|
|
|
|
|
class _RecordingProvider(MemoryProvider):
|
|
"""Minimal external provider that records on_memory_write calls."""
|
|
|
|
def __init__(self) -> None:
|
|
self.calls = []
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return "recording"
|
|
|
|
def is_available(self) -> bool:
|
|
return True
|
|
|
|
def initialize(self, session_id: str, **kwargs) -> None:
|
|
pass
|
|
|
|
def get_tool_schemas(self):
|
|
return []
|
|
|
|
def shutdown(self) -> None:
|
|
pass
|
|
|
|
def on_memory_write(self, action, target, content, metadata=None):
|
|
self.calls.append({
|
|
"action": action,
|
|
"target": target,
|
|
"content": content,
|
|
"metadata": dict(metadata or {}),
|
|
})
|
|
|
|
|
|
def _manager_with_provider():
|
|
mgr = MemoryManager()
|
|
provider = _RecordingProvider()
|
|
mgr.add_provider(provider)
|
|
return mgr, provider
|
|
|
|
|
|
def test_notifies_remove_with_old_text_after_success():
|
|
mgr, provider = _manager_with_provider()
|
|
mgr.notify_memory_tool_write(
|
|
json.dumps({"success": True}),
|
|
{"action": "remove", "target": "memory", "old_text": "stale preference entry"},
|
|
)
|
|
assert provider.calls == [
|
|
{
|
|
"action": "remove",
|
|
"target": "memory",
|
|
"content": "",
|
|
"metadata": {"old_text": "stale preference entry"},
|
|
}
|
|
]
|
|
|
|
|
|
def test_skips_failed_memory_write():
|
|
mgr, provider = _manager_with_provider()
|
|
mgr.notify_memory_tool_write(
|
|
json.dumps({"success": False, "error": "No entry matched"}),
|
|
{"action": "remove", "target": "memory", "old_text": "stale preference entry"},
|
|
)
|
|
assert provider.calls == []
|
|
|
|
|
|
def test_skips_staged_memory_write():
|
|
mgr, provider = _manager_with_provider()
|
|
mgr.notify_memory_tool_write(
|
|
json.dumps({"success": True, "staged": True, "pending_id": "abc123"}),
|
|
{"action": "remove", "target": "memory", "old_text": "stale preference entry"},
|
|
)
|
|
assert provider.calls == []
|
|
|
|
|
|
@pytest.mark.parametrize("tool_result", [None, [], object(), "not-json"])
|
|
def test_skips_unrecognized_tool_result_shape(tool_result):
|
|
mgr, provider = _manager_with_provider()
|
|
mgr.notify_memory_tool_write(
|
|
tool_result,
|
|
{"action": "add", "target": "memory", "content": "new fact"},
|
|
)
|
|
assert provider.calls == []
|
|
|
|
|
|
def test_preserves_old_text_for_replace_and_remove_batch():
|
|
mgr, provider = _manager_with_provider()
|
|
mgr.notify_memory_tool_write(
|
|
json.dumps({"success": True}),
|
|
{
|
|
"target": "user",
|
|
"operations": [
|
|
{"action": "replace", "old_text": "old preference", "content": "updated"},
|
|
{"action": "remove", "old_text": "obsolete preference"},
|
|
{"action": "add", "content": "new fact"},
|
|
],
|
|
},
|
|
)
|
|
assert provider.calls == [
|
|
{"action": "replace", "target": "user", "content": "updated",
|
|
"metadata": {"old_text": "old preference"}},
|
|
{"action": "remove", "target": "user", "content": "",
|
|
"metadata": {"old_text": "obsolete preference"}},
|
|
{"action": "add", "target": "user", "content": "new fact", "metadata": {}},
|
|
]
|
|
|
|
|
|
def test_non_mutating_actions_are_not_mirrored():
|
|
mgr, provider = _manager_with_provider()
|
|
mgr.notify_memory_tool_write(
|
|
json.dumps({"success": True}),
|
|
{"action": "read", "target": "memory"},
|
|
)
|
|
assert provider.calls == []
|
|
|
|
|
|
def test_build_metadata_callback_is_merged_per_op():
|
|
mgr, provider = _manager_with_provider()
|
|
mgr.notify_memory_tool_write(
|
|
json.dumps({"success": True}),
|
|
{"action": "add", "target": "memory", "content": "fact"},
|
|
build_metadata=lambda: {"session_id": "s1", "tool_name": "memory"},
|
|
)
|
|
assert provider.calls == [
|
|
{
|
|
"action": "add",
|
|
"target": "memory",
|
|
"content": "fact",
|
|
"metadata": {"session_id": "s1", "tool_name": "memory"},
|
|
}
|
|
]
|