Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,10 @@ HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
mkdir -p "$HERMES_HOME"
echo "MEMORI_API_KEY=YOUR_MEMORI_API_KEY" >> "$HERMES_HOME/.env"
echo "MEMORI_ENTITY_ID=your-app-user-id" >> "$HERMES_HOME/.env"
echo "MEMORI_PROJECT_ID=hermes" >> "$HERMES_HOME/.env"
```

`MEMORI_PROJECT_ID` is optional; when omitted, the provider uses Hermes' active project context for scoping.

For setup and configuration, see the [Hermes Quickstart](docs/memori-cloud/hermes/quickstart.mdx). For architecture and lifecycle details, see the [Hermes Overview](docs/memori-cloud/hermes/overview.mdx).

## MCP (Connect Your Agent in One Command)
Expand Down
3 changes: 2 additions & 1 deletion docs/memori-cloud/hermes/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Give Hermes Agent structured, persistent memory with the Memori mem

# Hermes Overview

Memori gives Hermes Agent a structured long-term memory provider. It plugs into Hermes' memory-provider interface, captures completed turns after responses, and exposes explicit tools for memory search, summaries, quota checks, signup, and feedback.
Memori gives Hermes Agent a structured long-term memory provider. It plugs into Hermes' memory-provider interface, captures completed agent activity after responses, and exposes explicit tools for memory search, summaries, quota checks, signup, and feedback.

## How It Works

Expand All @@ -14,6 +14,7 @@ The Hermes integration is a Python memory provider named `memori`.
- **Agent-controlled recall** is available through `memori_recall` and `memori_recall_summary`.
- **Advanced augmentation** captures completed turns in the background after responses.
- **Account helpers** are available through `memori_signup`, `memori_quota`, and `memori_feedback`.
- **Project scoping** uses `MEMORI_PROJECT_ID` when configured, or Hermes' active project context when omitted.

Hermes' built-in `MEMORY.md` and `USER.md` files remain active. Memori is additive and provides durable, structured, cross-session memory in Memori Cloud. It does not mirror edit, replace, or remove operations from those built-in files.

Expand Down
3 changes: 2 additions & 1 deletion docs/memori-cloud/hermes/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
mkdir -p "$HERMES_HOME"
echo "MEMORI_API_KEY=your-key" >> "$HERMES_HOME/.env"
echo "MEMORI_ENTITY_ID=your-user-or-workspace-id" >> "$HERMES_HOME/.env"
echo "MEMORI_PROJECT_ID=hermes" >> "$HERMES_HOME/.env"
```

`MEMORI_PROJECT_ID` is optional. If omitted, the provider uses Hermes' active workspace, agent identity, user ID, session title, or session ID as the project scope.

## Verify

```bash
Expand Down
6 changes: 5 additions & 1 deletion integrations/hermes/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Memori for Hermes Agent

Memori gives Hermes Agent structured long-term memory. It captures completed user/assistant exchanges after each turn and exposes explicit tools for memory search, summaries, quota checks, signup, and feedback.
Official Memori Labs provider for Hermes Agent, enabling structured long-term memory from agent trace and execution.

Memori gives Hermes Agent structured, long-term memory from agent trace and execution. It captures completed agent activity — including user goals, assistant decisions, tool usage, workflow steps, outcomes, constraints, failures, and feedback — and structures that activity into durable memory primitives for future recall. This allows Hermes agents to learn from prior execution, preserve workflow context, avoid repeated mistakes, and become more efficient over time. The provider exposes explicit tools for memory recall, summaries, quota checks, signup, and feedback.

## Requirements

Expand Down Expand Up @@ -58,6 +60,8 @@ Environment variables override file config:
- `MEMORI_PROJECT_ID`
- `MEMORI_PROCESS_ID`

`MEMORI_PROJECT_ID` is optional. When it is not configured, Hermes-provided project context such as the active workspace, agent identity, user ID, session title, or session ID is used as the Memori project scope.

## Tools

- `memori_recall`
Expand Down
2 changes: 2 additions & 0 deletions integrations/hermes/plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
name: memori
version: 0.1.0
description: Structured long-term memory for Hermes Agent using Memori Cloud.
pip_dependencies:
- memori
hooks:
- on_session_end
2 changes: 1 addition & 1 deletion integrations/hermes/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "hermes-memori"
version = "0.1.0"
version = "0.1.1"
description = "Official Memori Labs long-term memory provider for Hermes Agent"
authors = [{name = "Memori Labs Team", email = "noc@memorilabs.ai"}]
license = "Apache-2.0"
Expand Down
55 changes: 36 additions & 19 deletions integrations/hermes/src/memori_hermes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ class MemoryProvider: # type: ignore[no-redef]
logger = logging.getLogger(__name__)

PLUGIN_NAME = "memori"
DEFAULT_PROJECT_ID = "hermes"
SYNC_JOIN_TIMEOUT_SECS = 5.0
HERMES_PLATFORM = "hermes"


@dataclass
class MemoriConfig:
api_key: str
entity_id: str
project_id: str = DEFAULT_PROJECT_ID
project_id: str | None = None
process_id: str | None = None
base_url: str | None = None

Expand Down Expand Up @@ -72,9 +72,7 @@ def _load_config(hermes_home: str | Path | None = None) -> MemoriConfig | None:
api_key = _env("MEMORI_API_KEY") or str(file_config.get("apiKey") or "")
entity_id = _env("MEMORI_ENTITY_ID") or str(file_config.get("entityId") or "")
project_id = (
_env("MEMORI_PROJECT_ID")
or str(file_config.get("projectId") or "")
or DEFAULT_PROJECT_ID
_env("MEMORI_PROJECT_ID") or str(file_config.get("projectId") or "") or None
)
process_id = _env("MEMORI_PROCESS_ID") or file_config.get("processId")
base_url = _env("MEMORI_API_URL_BASE") or file_config.get("baseUrl")
Expand All @@ -98,7 +96,7 @@ def __init__(self, client: MemoriAgentClient | None = None) -> None:
self._client = client
self._config: MemoriConfig | None = None
self._session_id = ""
self._platform = ""
self._project_id = ""
self._agent_identity = ""
self._sync_thread: threading.Thread | None = None

Expand All @@ -120,14 +118,15 @@ def initialize(self, session_id: str, **kwargs: Any) -> None:

self._config = config
self._session_id = str(session_id)
self._platform = str(kwargs.get("platform") or "hermes")
self._agent_identity = str(kwargs.get("agent_identity") or "")

process_id = config.process_id or self._agent_identity or self._platform or None
project_id = config.project_id or self._project_id_from_agent(kwargs)
self._project_id = project_id
process_id = config.process_id or self._agent_identity or HERMES_PLATFORM
self._client = self._client or MemoriAgentClient(
api_key=config.api_key,
entity_id=config.entity_id,
project_id=config.project_id,
project_id=project_id,
process_id=process_id,
base_url=config.base_url,
)
Expand Down Expand Up @@ -200,7 +199,7 @@ def _sync_turn_background(
user_content=user_content,
assistant_content=assistant_content,
session_id=session_id,
platform=self._platform or "hermes",
platform=HERMES_PLATFORM,
)
except MemoriApiError as exc:
logger.warning("Memori sync_turn failed: %s", exc)
Expand Down Expand Up @@ -283,29 +282,47 @@ def get_config_schema(self) -> list[dict[str, Any]]:
{
"key": "project_id",
"description": "Project scope for Memori recall and summaries",
"default": DEFAULT_PROJECT_ID,
},
]

def save_config(self, values: dict[str, Any], hermes_home: str) -> None:
path = _config_path(hermes_home)
path.parent.mkdir(parents=True, exist_ok=True)
config = {
"entityId": values.get("entity_id") or values.get("entityId"),
"projectId": values.get("project_id")
or values.get("projectId")
or DEFAULT_PROJECT_ID,
}
config = _read_file_config(hermes_home)

entity_id = values.get("entity_id") or values.get("entityId")
if entity_id:
config["entityId"] = entity_id

project_id = values.get("project_id") or values.get("projectId")
if project_id:
config["projectId"] = project_id

path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")

def _project_id_from_agent(self, kwargs: dict[str, Any]) -> str:
project_id = str(
kwargs.get("agent_workspace")
or kwargs.get("agent_identity")
or kwargs.get("user_id")
or kwargs.get("session_title")
or self._session_id
).strip()
if not project_id:
raise RuntimeError(
"Memori project_id is not configured and Hermes did not provide "
"an agent project scope."
)
return project_id

def _with_project_defaults(self, args: dict[str, Any]) -> dict[str, Any]:
params = {k: v for k, v in args.items() if v not in (None, "")}
if (
self._config
self._project_id
and not params.get("projectId")
and not params.get("project_id")
):
params["projectId"] = self._config.project_id
params["projectId"] = self._project_id
return params


Expand Down
18 changes: 15 additions & 3 deletions integrations/hermes/src/memori_hermes/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@

from typing import Any

from memori import Memori

DEFAULT_TIMEOUT_SECS = 30
MEMORI_PLATFORM = "hermes"


class MemoriApiError(RuntimeError):
Expand All @@ -26,6 +25,18 @@ def __init__(
base_url: str | None = None,
timeout: int = DEFAULT_TIMEOUT_SECS,
) -> None:
try:
from memori import Memori
except ModuleNotFoundError as exc:
missing = exc.name or "memori"
raise RuntimeError(
f"Memori SDK dependency missing: {missing}. Run: pip install memori"
) from exc
except ImportError as exc:
raise RuntimeError(
"Memori SDK could not be imported. Run: pip install memori"
) from exc

self.entity_id = entity_id
self.project_id = project_id
self.memori = Memori(api_key=api_key, base_url=base_url).attribution(
Expand All @@ -42,13 +53,14 @@ def capture_turn(
session_id: str,
platform: str,
) -> None:
del platform
try:
self.memori.capture_agent_turn(
user_content=user_content,
assistant_content=assistant_content,
project_id=self.project_id,
session_id=session_id,
platform=platform or "hermes",
platform=MEMORI_PLATFORM,
)
except Exception as exc: # noqa: BLE001
raise MemoriApiError(str(exc)) from exc
Expand Down
2 changes: 2 additions & 0 deletions integrations/hermes/src/memori_hermes/plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
name: memori
version: 0.1.0
description: Structured long-term memory for Hermes Agent using Memori Cloud.
pip_dependencies:
- memori
hooks:
- on_session_end
19 changes: 18 additions & 1 deletion integrations/hermes/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import sys
from pathlib import Path
from unittest.mock import patch

import pytest

Expand All @@ -10,6 +11,22 @@
from memori_hermes.client import MemoriAgentClient, MemoriApiError # noqa: E402


def test_client_reports_missing_memori_dependency() -> None:
def fake_import(name, *args, **kwargs):
if name == "memori":
raise ModuleNotFoundError("No module named 'memori'", name="memori")
return original_import(name, *args, **kwargs)

original_import = __import__
with patch("builtins.__import__", side_effect=fake_import):
with pytest.raises(RuntimeError, match="pip install memori"):
MemoriAgentClient(
api_key="key",
entity_id="entity",
project_id="project",
)


def make_client() -> MemoriAgentClient:
return MemoriAgentClient(
api_key="key",
Expand Down Expand Up @@ -74,7 +91,7 @@ def fake_capture_agent_turn(**kwargs):
"assistant_content": "a",
"project_id": "project",
"session_id": "session",
"platform": "cli",
"platform": "hermes",
}


Expand Down
6 changes: 3 additions & 3 deletions integrations/hermes/tests/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,17 @@ def test_sync_turn_runs_background_capture() -> None:
client = FakeClient()
provider = MemoriMemoryProvider(client=client)
provider._session_id = "session-1"
provider._platform = "cli"

provider.sync_turn("hello", "hi")
provider.shutdown()

assert client.captured == [("hello", "hi", "session-1", "cli")]
assert client.captured == [("hello", "hi", "session-1", "hermes")]


def test_handle_recall_adds_project_default() -> None:
client = FakeClient()
provider = MemoriMemoryProvider(client=client)
provider._config = type("Config", (), {"project_id": "project-1"})()
provider._project_id = "project-1"

result = json.loads(provider.handle_tool_call("memori_recall", {"query": "prefs"}))

Expand All @@ -103,3 +102,4 @@ def test_config_schema_contains_required_setup_fields() -> None:
keys = {field["key"] for field in schema}
assert {"api_key", "entity_id", "project_id"} <= keys
assert schema[0]["env_var"] == "MEMORI_API_KEY"
assert "default" not in schema[2]
Loading