mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
Merge pull request #34097 from kshitijk4poor/salvage/memori-trace-messages
feat: expose completed-turn message context to memory providers (salvage #28065)
This commit is contained in:
commit
11d93096b3
9 changed files with 157 additions and 7 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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`:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue