diff --git a/examples/hermes-plugin/CONFIGURATION.md b/examples/hermes-plugin/CONFIGURATION.md index 05ed556fcd..3586deca05 100644 --- a/examples/hermes-plugin/CONFIGURATION.md +++ b/examples/hermes-plugin/CONFIGURATION.md @@ -23,6 +23,20 @@ OPENVIKING_ENDPOINT=http://127.0.0.1:1933 The default profile uses `~/.hermes/.env`. A named profile uses `~/.hermes/profiles//.env`. Start a new Hermes session after changing it. +## Capture and explicit retrieval without automatic recall + +To keep conversation capture and explicit `viking_*` tools without injecting +historical context automatically, run this in the profile you want to change: + +```bash +hermes config set memory.openviking.auto_recall false +``` + +The profile-scoped environment override is `OPENVIKING_AUTO_RECALL=false`. +This suppresses both session-start memory context and per-turn query recall +before recall-related client or network I/O. Turn capture, memory commits, +and explicit tools keep their existing behavior. The default is `true`. + ## Quick Local model support Quick Local copies Hermes's language model settings into its own server config. diff --git a/examples/hermes-plugin/__init__.py b/examples/hermes-plugin/__init__.py index 2e338061cb..8440db5390 100644 --- a/examples/hermes-plugin/__init__.py +++ b/examples/hermes-plugin/__init__.py @@ -144,6 +144,7 @@ def _cfg_field(key: str, description: str, **extra) -> dict: _cfg_field("account", "Advanced local identity override (leave blank for user API keys)"), _cfg_field("user", "Advanced local user override (leave blank for user API keys)"), _cfg_field("agent", "Optional peer ID for separate assistant context. Uses user memory when no peer is configured.", default=_DEFAULT_AGENT), + _cfg_field("auto_recall", "Inject historical context automatically; capture and explicit tools remain available when disabled", type="boolean", default=True), _cfg_field("recall_scope", "Automatic recall: all peers (shared) or current sender (peer). Unset preserves existing behavior.", type="string", choices=["shared", "peer"], default=None), _cfg_field( @@ -2001,6 +2002,9 @@ def system_prompt_block(self) -> str: def prefetch(self, query: str, *, session_id: str = "") -> str: """Session-start memory block (once per session) + query recall.""" + cfg, env = self._profile_config_and_env() + if not self._setting("auto_recall", cfg, env=env): + return "" query_text = _derive_openviking_user_text(query).strip() if not self._ensure_client(): return "" diff --git a/examples/hermes-plugin/tests/test_standalone.py b/examples/hermes-plugin/tests/test_standalone.py index 07450e44e4..610fa3b4e8 100644 --- a/examples/hermes-plugin/tests/test_standalone.py +++ b/examples/hermes-plugin/tests/test_standalone.py @@ -4,6 +4,7 @@ import threading from contextlib import contextmanager from http.server import BaseHTTPRequestHandler, HTTPServer +from unittest.mock import Mock import pytest @@ -1354,3 +1355,51 @@ def test_runtime_spawn_uses_provider_home_under_another_profile(external_provide assert spawned[0]["OPENAI_API_KEY"] == "owner-key" assert (home_a / "logs/openviking-server.log").exists() assert not (home_b / "logs/openviking-server.log").exists() + + +@pytest.mark.parametrize("query", ["", "Recall my preferences"]) +@pytest.mark.parametrize("source", ["config", "env"]) +def test_auto_recall_off_skips_all_prefetch_io(external_provider, monkeypatch, query, source): + home, provider, _, _ = external_provider("recall-off") + provider._hermes_home = str(home) + provider._hermes_home_bound = True + if source == "config": + provider.save_config({"auto_recall": False}, str(home)) + else: + (home / ".env").write_text("OPENVIKING_AUTO_RECALL=false\n") + ensure_client = Mock(side_effect=AssertionError("automatic recall performed client I/O")) + monkeypatch.setattr(provider, "_ensure_client", ensure_client) + assert provider.prefetch(query, session_id="new-session") == "" + ensure_client.assert_not_called() + + +def test_auto_recall_default_preserves_both_context_blocks(external_provider, monkeypatch): + home, provider, _, _ = external_provider("recall-default") + provider._hermes_home = str(home) + provider._hermes_home_bound = True + monkeypatch.setattr(provider, "_ensure_client", lambda: True) + startup = Mock(return_value="profile context") + recall = Mock(return_value="query context") + monkeypatch.setattr(provider, "_session_start_memory_context", startup) + monkeypatch.setattr(provider, "_search_prefetch_context", recall) + assert provider.prefetch("Recall preferences", session_id="session") == ( + "## OpenViking Context\nprofile context\n\nquery context" + ) + startup.assert_called_once_with("session") + recall.assert_called_once_with("Recall preferences", session_id="session") + + +def test_auto_recall_off_preserves_explicit_tools_and_capture(external_provider, monkeypatch): + home, provider, _ = _live_provider(external_provider, monkeypatch, "capture-only") + schemas = provider.get_tool_schemas() + provider.save_config({"auto_recall": False}, str(home)) + assert provider.prefetch("Remember preferences", session_id="live-sid") == "" + assert provider.get_tool_schemas() == schemas + provider._client.post.return_value = {"result": {"memories": [{"uri": "viking://user/test/memories/preference.md", "abstract": "A preference"}]}} + result = provider.handle_tool_call("viking_search", {"query": "preferences", "mode": "deep"}) + assert json.loads(result)["results"][0]["abstract"] == "A preference" + assert any(c.args[0].endswith("/search") for c in provider._client.post.call_args_list) + _finish_turn(provider, user="A new preference") + captured = [c for c in provider._client.post.call_args_list if c.args[0].endswith("/messages/batch")] + assert captured + assert [m["role"] for m in captured[0].args[1]["messages"]] == ["user", "assistant"]