mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-31 06:51:29 +00:00
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:
parent
321ce94e25
commit
490b3e76b1
3 changed files with 52 additions and 2 deletions
|
|
@ -75,8 +75,17 @@ Config file: `~/.hermes/hindsight/config.json`
|
|||
| `recall_prompt_preamble` | — | Custom preamble for recalled memories in context |
|
||||
| `recall_tags` | — | Tags to filter when searching memories |
|
||||
| `recall_tags_match` | `any` | Tag matching mode: `any` / `all` / `any_strict` / `all_strict` |
|
||||
| `recall_types` | `observation` | Fact types surfaced by auto-recall. Comma-separated string or JSON list. **Default narrowed to `observation` only** (see "Behavior change" below). Set to `observation,world,experience` to also include raw facts. |
|
||||
| `auto_recall` | `true` | Automatically recall memories before each turn |
|
||||
|
||||
> **Behavior change — `recall_types` defaults to `observation` only.**
|
||||
>
|
||||
> Previously auto-recall returned all three fact types. It now returns only observations.
|
||||
>
|
||||
> Per [Hindsight's docs](https://hindsight.vectorize.io/developer/observations), observations are the **consolidated** knowledge layer Hindsight builds on top of raw facts: deduplicated beliefs grounded in evidence, refined as new facts arrive, with proof counts and freshness signals. Raw `world` / `experience` facts are the individual supporting evidence that feeds them. For per-turn context injection, observations are denser per token and avoid feeding the model multiple raw facts that one observation already summarizes.
|
||||
>
|
||||
> Restore the broad recall with `"recall_types": "observation,world,experience"` (string or JSON list) in `~/.hermes/hindsight/config.json`. Per-turn `hindsight_recall` tool calls are unaffected — they only filter by `types` when the tool argument is passed explicitly.
|
||||
|
||||
### Retain
|
||||
|
||||
| Key | Default | Description |
|
||||
|
|
|
|||
|
|
@ -579,7 +579,15 @@ class HindsightMemoryProvider(MemoryProvider):
|
|||
# Recall controls
|
||||
self._auto_recall = True
|
||||
self._recall_max_tokens = 4096
|
||||
self._recall_types: list[str] | None = None
|
||||
# Default to observation-only recall. Observations are Hindsight's
|
||||
# consolidated knowledge layer — deduplicated, evidence-grounded
|
||||
# beliefs built from many raw facts, with proof counts and
|
||||
# freshness signals (see hindsight.vectorize.io/developer/observations).
|
||||
# Including raw world/experience facts re-ships the supporting
|
||||
# evidence that observations already summarize, burning the
|
||||
# `recall_max_tokens` budget. Users can restore the broader
|
||||
# recall via the `recall_types` config key.
|
||||
self._recall_types: list[str] = ["observation"]
|
||||
self._recall_prompt_preamble = ""
|
||||
self._recall_max_input_chars = 800
|
||||
|
||||
|
|
@ -856,6 +864,7 @@ class HindsightMemoryProvider(MemoryProvider):
|
|||
{"key": "retain_assistant_prefix", "description": "Label used before assistant turns in retained transcripts", "default": "Assistant"},
|
||||
{"key": "recall_tags", "description": "Tags to filter when searching memories (comma-separated)", "default": ""},
|
||||
{"key": "recall_tags_match", "description": "Tag matching mode for recall", "default": "any", "choices": ["any", "all", "any_strict", "all_strict"]},
|
||||
{"key": "recall_types", "description": "Fact types to surface on auto-recall (comma-separated or list). Defaults to observation-only — observations are Hindsight's consolidated, deduplicated, evidence-grounded knowledge layer; raw world/experience facts are the supporting evidence observations already summarize. Set to e.g. 'observation,world,experience' to also include raw facts.", "default": "observation"},
|
||||
{"key": "auto_recall", "description": "Automatically recall memories before each turn", "default": True},
|
||||
{"key": "auto_retain", "description": "Automatically retain conversation turns", "default": True},
|
||||
{"key": "retain_every_n_turns", "description": "Retain every N turns (1 = every turn)", "default": 1},
|
||||
|
|
@ -1187,7 +1196,17 @@ class HindsightMemoryProvider(MemoryProvider):
|
|||
# Recall controls
|
||||
self._auto_recall = self._config.get("auto_recall", True)
|
||||
self._recall_max_tokens = int(self._config.get("recall_max_tokens", 4096))
|
||||
self._recall_types = self._config.get("recall_types") or None
|
||||
# Default narrows recall to observation-only; pass an explicit
|
||||
# `recall_types` list in config.json to broaden (e.g. include
|
||||
# "world" / "experience") or to disable the filter entirely.
|
||||
configured_types = self._config.get("recall_types")
|
||||
if configured_types is None:
|
||||
self._recall_types = ["observation"]
|
||||
elif isinstance(configured_types, str):
|
||||
# Allow comma-separated strings for parity with recall_tags.
|
||||
self._recall_types = [t.strip() for t in configured_types.split(",") if t.strip()]
|
||||
else:
|
||||
self._recall_types = list(configured_types) or ["observation"]
|
||||
self._recall_prompt_preamble = self._config.get("recall_prompt_preamble", "")
|
||||
self._recall_max_input_chars = int(self._config.get("recall_max_input_chars", 800))
|
||||
self._retain_async = self._config.get("retain_async", True)
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue