feat(hindsight): default recall_types to observation only

Auto-recall used to surface every fact type Hindsight had on the
session — `world`, `experience`, and `observation`. That triple-ships
the same underlying signal in three different framings: observations
are the concrete events the user said/did/asked, while world and
experience facts are aggregate summaries Hindsight derives from those
exact observations. Including all three burns most of
`recall_max_tokens` on rephrasings, crowds out events the model
actually needs to see, and produces effective duplicates in the
prompt — observations themselves are deduplicated by construction
so observation-only recall is denser per token and closer to
conversational ground truth.

Change
------
- Default `_recall_types = ["observation"]` (was `None`, which
  delegated to server-side "return everything").
- `initialize()` now treats a missing `recall_types` config the same
  way; also accepts comma-separated strings for parity with `recall_tags`.
- An explicit `recall_types=[]` config falls back to the default rather
  than disabling the filter (would silently widen recall vs. the new
  default).
- Added to `get_config_schema()` so it's discoverable via `hermes config`.

Per-call `hindsight_recall` tool invocations are unaffected — they
already only forward `types` when the caller passes the argument.

Docs / migration
----------------
plugins/memory/hindsight/README.md grows a "Behavior change" callout
explaining the why (no-duplicates, information-efficient) and how to
restore the legacy broad recall:

    "recall_types": "observation,world,experience"   # or a JSON list

in `~/.hermes/hindsight/config.json`.

Tests
-----
- `test_default_values` updated for the new default.
- New cases: explicit list override, CSV string accepted, empty list
  falls back to default (not "wider than default").
This commit is contained in:
Nicolò Boschi 2026-05-28 18:35:39 +02:00 committed by kshitij
parent 321ce94e25
commit 490b3e76b1
3 changed files with 52 additions and 2 deletions

View file

@ -197,10 +197,32 @@ class TestConfig:
assert provider._recall_max_input_chars == 800
assert provider._tags is None
assert provider._recall_tags is None
# Default recall narrowed to observation-only; world/experience are
# aggregate facts that often crowd out concrete-event signal during
# auto-recall. Users opt back in via the recall_types config key.
assert provider._recall_types == ["observation"]
assert provider._bank_mission == ""
assert provider._bank_retain_mission is None
assert provider._retain_context == "conversation between Hermes Agent and the User"
def test_recall_types_default_is_observation_only(self, provider):
"""Auto-recall must filter to observation by default."""
assert provider._recall_types == ["observation"]
def test_recall_types_explicit_list_overrides_default(self, provider_with_config):
p = provider_with_config(recall_types=["world", "experience", "observation"])
assert p._recall_types == ["world", "experience", "observation"]
def test_recall_types_csv_string_accepted(self, provider_with_config):
"""For parity with recall_tags, comma-separated strings work too."""
p = provider_with_config(recall_types="observation, world")
assert p._recall_types == ["observation", "world"]
def test_recall_types_empty_list_falls_back_to_default(self, provider_with_config):
"""An empty list shouldn't disable the filter (would be wider than default)."""
p = provider_with_config(recall_types=[])
assert p._recall_types == ["observation"]
def test_custom_config_values(self, provider_with_config):
p = provider_with_config(
retain_tags=["tag1", "tag2"],