From 5a95fb2e14b6e77e23d56bca31927dfa63f7a2c4 Mon Sep 17 00:00:00 2001 From: Dave Heritage Date: Fri, 29 May 2026 02:10:06 +0530 Subject: [PATCH 1/2] feat: expose completed-turn message context to memory providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an optional `messages` keyword to the `MemoryProvider.sync_turn` contract so external/community memory plugins can receive the OpenAI-style conversation message list for the completed turn — including assistant tool calls and tool result content — not just the final assistant text. Dispatch uses signature inspection (`_provider_sync_accepts_messages`): only providers that declare a `messages` parameter (or `**kwargs`) receive it; all existing in-tree providers keep their legacy text-only signature and are called unchanged. No structured-trace envelope is added to core — providers reconstruct whatever they need from the standard message list. Also documents Memori as a standalone community memory provider. Salvaged from #28065 — rebased onto current main. Co-authored-by: Dave Heritage --- agent/conversation_loop.py | 1 + agent/memory_manager.py | 35 ++++++++++++++++- agent/memory_provider.py | 13 ++++++- run_agent.py | 9 ++++- tests/agent/test_memory_provider.py | 29 ++++++++++++++ .../run_agent/test_memory_sync_interrupted.py | 39 +++++++++++++++++++ .../developer-guide/memory-provider-plugin.md | 14 ++++++- .../user-guide/features/memory-providers.md | 22 +++++++++++ 8 files changed, 155 insertions(+), 7 deletions(-) diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index 7e7ee26431f..92796b65c8e 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -4561,6 +4561,7 @@ def run_conversation( original_user_message=original_user_message, final_response=final_response, interrupted=interrupted, + messages=messages, ) # Background memory/skill review — runs AFTER the response is delivered diff --git a/agent/memory_manager.py b/agent/memory_manager.py index 79547139086..fc5d96da4fe 100644 --- a/agent/memory_manager.py +++ b/agent/memory_manager.py @@ -368,11 +368,42 @@ class MemoryManager: # -- Sync ---------------------------------------------------------------- - def sync_all(self, user_content: str, assistant_content: str, *, session_id: str = "") -> None: + @staticmethod + def _provider_sync_accepts_messages(provider: MemoryProvider) -> bool: + """Return whether sync_turn accepts a messages keyword.""" + try: + signature = inspect.signature(provider.sync_turn) + except (TypeError, ValueError): + return True + params = list(signature.parameters.values()) + if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params): + return True + return "messages" in signature.parameters + + def sync_all( + self, + user_content: str, + assistant_content: str, + *, + session_id: str = "", + messages: Optional[List[Dict[str, Any]]] = None, + ) -> None: """Sync a completed turn to all providers.""" for provider in self._providers: try: - provider.sync_turn(user_content, assistant_content, session_id=session_id) + if messages is not None and self._provider_sync_accepts_messages(provider): + provider.sync_turn( + user_content, + assistant_content, + session_id=session_id, + messages=messages, + ) + else: + provider.sync_turn( + user_content, + assistant_content, + session_id=session_id, + ) except Exception as e: logger.warning( "Memory provider '%s' sync_turn failed: %s", diff --git a/agent/memory_provider.py b/agent/memory_provider.py index d801d856a04..116ceff406f 100644 --- a/agent/memory_provider.py +++ b/agent/memory_provider.py @@ -112,11 +112,22 @@ class MemoryProvider(ABC): that do background prefetching should override this. """ - def sync_turn(self, user_content: str, assistant_content: str, *, session_id: str = "") -> None: + def sync_turn( + self, + user_content: str, + assistant_content: str, + *, + session_id: str = "", + messages: Optional[List[Dict[str, Any]]] = None, + ) -> None: """Persist a completed turn to the backend. Called after each turn. Should be non-blocking — queue for background processing if the backend has latency. + + ``messages`` is the OpenAI-style conversation message list as of the + completed turn, including any assistant tool calls and tool results. + Providers that do not need raw turn context can ignore it. """ @abstractmethod diff --git a/run_agent.py b/run_agent.py index 6d3af390b6d..a5c9f63e993 100644 --- a/run_agent.py +++ b/run_agent.py @@ -2302,6 +2302,7 @@ class AIAgent: original_user_message: Any, final_response: Any, interrupted: bool, + messages: list | None = None, ) -> None: """Mirror a completed turn into external memory providers. @@ -2334,9 +2335,13 @@ class AIAgent: if not (self._memory_manager and final_response and original_user_message): return try: + sync_kwargs = {"session_id": self.session_id or ""} + if messages is not None: + sync_kwargs["messages"] = messages self._memory_manager.sync_all( - original_user_message, final_response, - session_id=self.session_id or "", + original_user_message, + final_response, + **sync_kwargs, ) self._memory_manager.queue_prefetch_all( original_user_message, diff --git a/tests/agent/test_memory_provider.py b/tests/agent/test_memory_provider.py index 6f8cfc8a93d..3c54f78d5be 100644 --- a/tests/agent/test_memory_provider.py +++ b/tests/agent/test_memory_provider.py @@ -84,6 +84,13 @@ class MetadataMemoryProvider(FakeMemoryProvider): self.memory_writes.append((action, target, content, metadata or {})) +class MessagesMemoryProvider(FakeMemoryProvider): + """Provider that opts into completed-turn message context.""" + + def sync_turn(self, user_content, assistant_content, *, session_id="", messages=None): + self.synced_turns.append((user_content, assistant_content, session_id, messages)) + + # --------------------------------------------------------------------------- # MemoryProvider ABC tests # --------------------------------------------------------------------------- @@ -236,6 +243,28 @@ class TestMemoryManager: assert p1.synced_turns == [("user msg", "assistant msg")] assert p2.synced_turns == [("user msg", "assistant msg")] + def test_sync_all_passes_messages_to_opted_in_provider(self): + mgr = MemoryManager() + p = MessagesMemoryProvider("external") + mgr.add_provider(p) + messages = [ + {"role": "assistant", "tool_calls": [{"id": "call-1"}]}, + {"role": "tool", "tool_call_id": "call-1", "content": "ok"}, + ] + + mgr.sync_all("user msg", "assistant msg", session_id="sess-1", messages=messages) + + assert p.synced_turns == [("user msg", "assistant msg", "sess-1", messages)] + + def test_sync_all_omits_messages_for_legacy_provider(self): + mgr = MemoryManager() + p = FakeMemoryProvider("external") + mgr.add_provider(p) + + mgr.sync_all("user msg", "assistant msg", messages=[{"role": "tool"}]) + + assert p.synced_turns == [("user msg", "assistant msg")] + def test_sync_failure_doesnt_block_others(self): """If one provider's sync fails, others still run.""" mgr = MemoryManager() diff --git a/tests/run_agent/test_memory_sync_interrupted.py b/tests/run_agent/test_memory_sync_interrupted.py index feeb028927b..3a118002e2b 100644 --- a/tests/run_agent/test_memory_sync_interrupted.py +++ b/tests/run_agent/test_memory_sync_interrupted.py @@ -91,6 +91,45 @@ class TestSyncExternalMemoryForTurn: session_id="test_session_001", ) + def test_completed_turn_syncs_messages_when_present(self): + agent = _bare_agent() + messages = [ + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "type": "function", + "function": { + "name": "terminal", + "arguments": "{\"command\":\"pytest\"}", + }, + } + ], + }, + { + "role": "tool", + "name": "terminal", + "tool_call_id": "call-1", + "content": "final Hermes-processed output", + } + ] + + agent._sync_external_memory_for_turn( + original_user_message="run tests", + final_response="tests passed", + interrupted=False, + messages=messages, + ) + + agent._memory_manager.sync_all.assert_called_once_with( + "run tests", + "tests passed", + session_id="test_session_001", + messages=messages, + ) + # --- Edge cases (pre-existing behaviour preserved) ------------------ def test_no_final_response_skips(self): diff --git a/website/docs/developer-guide/memory-provider-plugin.md b/website/docs/developer-guide/memory-provider-plugin.md index fa1a79791a8..c490fb2153f 100644 --- a/website/docs/developer-guide/memory-provider-plugin.md +++ b/website/docs/developer-guide/memory-provider-plugin.md @@ -154,10 +154,10 @@ hooks: **`sync_turn()` MUST be non-blocking.** If your backend has latency (API calls, LLM processing), run the work in a daemon thread: ```python -def sync_turn(self, user_content, assistant_content): +def sync_turn(self, user_content, assistant_content, *, session_id="", messages=None): def _sync(): try: - self._api.ingest(user_content, assistant_content) + self._api.ingest(user_content, assistant_content, session_id=session_id, messages=messages) except Exception as e: logger.warning("Sync failed: %s", e) @@ -167,6 +167,16 @@ def sync_turn(self, user_content, assistant_content): self._sync_thread.start() ``` +`messages` is optional OpenAI-style conversation context as of the completed +turn. When present, it includes user/assistant messages, assistant tool calls, +and tool result messages. Providers that do not need raw turn context can omit +the `messages` parameter; Hermes will continue calling them with the legacy +signature. + +Cloud providers should document what parts of `messages` are sent off-device. +Tool calls and tool results may contain file paths, command output, or other +workspace data. + ## Profile Isolation All storage paths **must** use the `hermes_home` kwarg from `initialize()`, not hardcoded `~/.hermes`: diff --git a/website/docs/user-guide/features/memory-providers.md b/website/docs/user-guide/features/memory-providers.md index 91d4f5bba60..a655bd42dc3 100644 --- a/website/docs/user-guide/features/memory-providers.md +++ b/website/docs/user-guide/features/memory-providers.md @@ -520,6 +520,27 @@ echo 'SUPERMEMORY_API_KEY=***' >> ~/.hermes/.env **Support:** [Discord](https://supermemory.link/discord) · [support@supermemory.com](mailto:support@supermemory.com) +### Memori + +Structured long-term memory using Memori Cloud, with background completed-turn capture, tool-aware turn context, and explicit recall tools for facts, summaries, quota, signup, and feedback. + +| | | +|---|---| +| **Best for** | Agent-controlled recall with structured project and session attribution | +| **Requires** | `pip install hermes-memori` + `hermes-memori install` + [Memori API key](https://app.memorilabs.ai/signup) | +| **Data storage** | Memori Cloud | +| **Cost** | Memori pricing | + +**Tools:** `memori_recall` (search long-term memory), `memori_recall_summary` (summarized context), `memori_quota` (usage/quota), `memori_signup` (request signup email), `memori_feedback` (send integration feedback) + +**Setup:** +```bash +pip install hermes-memori +hermes-memori install +hermes config set memory.provider memori +hermes memory setup +``` + --- ## Provider Comparison @@ -534,6 +555,7 @@ echo 'SUPERMEMORY_API_KEY=***' >> ~/.hermes/.env | **RetainDB** | Cloud | $20/mo | 5 | `requests` | Delta compression | | **ByteRover** | Local/Cloud | Free/Paid | 3 | `brv` CLI | Pre-compression extraction | | **Supermemory** | Cloud | Paid | 4 | `supermemory` | Context fencing + session graph ingest + multi-container | +| **Memori** | Cloud | Free/Paid | 5 | `hermes-memori` | Tool-aware memory + structured recall | ## Profile Isolation From d464d08a5f7c689704d9d348c9d5bae9f3baa5ba Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 29 May 2026 02:10:12 +0530 Subject: [PATCH 2/2] chore: add devwdave to AUTHOR_MAP Maps both commit emails (david@memorilabs.ai, dave@devwdave.com) used on #28065 to the devwdave GitHub account so the contributor audit in scripts/release.py passes. --- scripts/release.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/release.py b/scripts/release.py index 779d4341dbb..209a0552657 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -101,6 +101,8 @@ AUTHOR_MAP = { "kronexoi13@gmail.com": "kronexoi", "hua.zhong@kingsmith.com": "vgocoder", "hermes@marian.local": "Schrotti77", + "david@memorilabs.ai": "devwdave", + "dave@devwdave.com": "devwdave", "1920071390@campus.ouj.ac.jp": "zapabob", "gaia@gaia.local": "jfuenmayor", "jiahuigu@users.noreply.github.com": "Jiahui-Gu",