Conversation
Auxiliary LLM tasks (vision, compression, web_extract, etc.) currently
require modifications to core files for any plugin that needs its own
task slot — specifically the _AUX_TASKS list in hermes_cli/main.py and
the hardcoded env-var bridging dict in gateway/run.py. This violates
the 'plugins must not modify core files' rule and forces every memory
or context plugin that wants its own auxiliary task to either fork
core or open a coupled core+plugin PR.
This change adds a generic plugin surface for auxiliary task
registration:
ctx.register_auxiliary_task(
key='memory_retain_filter',
display_name='Memory retain filter',
description='hindsight pre-retain dedup/extract',
defaults={'timeout': 30, 'extra_body': {'reasoning_effort': 'low'}},
)
After registration, the task automatically:
- Appears in 'hermes model → Configure auxiliary models' picker via
a new _all_aux_tasks() merge of built-in + plugin tasks
- Has its provider/model/base_url/api_key bridged from config.yaml
to AUXILIARY_<KEY_UPPER>_* env vars at gateway startup
(gateway/run.py now uses a dynamic bridged-keys set instead of
a hardcoded per-task dict)
- Gets plugin-declared defaults (timeout, extra_body, etc.) layered
underneath user config so unconfigured plugin tasks still work
(agent/auxiliary_client._get_auxiliary_task_config)
- Resets to auto via 'Reset all to auto' alongside built-ins
Validation:
- Rejects shadowing of built-in keys (vision, compression, etc.)
- Rejects invalid key shapes (must match [A-Za-z0-9_]+)
- Rejects cross-plugin collisions (clear error)
- Allows same-plugin re-registration (idempotent updates)
Plugin discovery failures (rare) fall back gracefully — the aux
config UI still shows built-in tasks if get_plugin_auxiliary_tasks()
raises, and gateway env-var bridging keeps working for built-ins.
Built-in tasks remain hardcoded in _AUX_TASKS for stability — they're
the baseline UX, and DEFAULT_CONFIG already ships their defaults.
Plugin tasks layer on top.
Tests: 15 new tests in test_plugin_auxiliary_tasks.py covering API
validation, manager state lifecycle, helper sort order, _all_aux_tasks
merge semantics, _reset_aux_to_auto inclusion of plugin tasks, and
default-layering in auxiliary_client.
Updates the gateway-bridge code-parity test (test_auxiliary_config_bridge)
to assert the new dynamic shape rather than the hardcoded literal env
var names which no longer appear post-refactor.
Motivation: this unblocks PR #20262 (hindsight smart retain pipeline)
and similar plugins that need a dedicated aux task slot. The change
is non-breaking — built-in env vars (AUXILIARY_VISION_PROVIDER, etc.)
keep working since they're produced by the same f-string template
that built the hardcoded names.
… circuit breaker)
Adds a configurable client-side pipeline around hindsight_retain to reduce
write amplification and improve recall quality at scale.
Pipeline components:
- Pre-retain dedup via recall + auxiliary LLM
- Client-side extraction (retain_extract) before retain
- Delta retain mode for linear-cost memory writes
- Auxiliary circuit breaker with aux_fallback_to_main knob
- Smart pipeline applied to tool retains AND flush-on-switch path
- Dynamic scope filtering on recall + retain
- Bank config fetch from API for richer pre-filter prompts
- Warning on incomplete bank config when smart pipeline is active
Auxiliary task registration:
The pipeline routes its dedup/extract calls through a dedicated auxiliary
task (memory_retain_filter) so users can pin a cheap classifier model
(gpt-oss-20b, gemma-3-flash) without affecting the main chat model.
Per the plugins-must-not-modify-core rule, this PR depends on the
register_auxiliary_task() PluginContext API
(feat/plugin-aux-task-registration). The plugin's register(ctx) function
calls:
ctx.register_auxiliary_task(
key='memory_retain_filter',
display_name='Memory retain filter',
description='pre-retain content classification (Hindsight smart pipeline)',
defaults={'timeout': 30, ...},
)
After the parent PR lands, this declares Hindsight's task slot through
the plugin surface — no core file modifications required.
Defaults / fixes:
- retain_every_n_turns default 5 → 1 (opt-in only)
- Filter recall types in dedup check
- Fetch bank config based on api_url presence only
- Test fixture restoration for retain pipeline attributes
|
Thanks for the work here, @McClean-Edison! The parent PR #29817 has landed on main (via salvage #31177), so this is unblocked. Before we proceed, this needs @nicolo-esposito's review first — could you ping Nicolo in the Nous Discord and ask him to take a look at this PR? Once he approves the design, Teknium will do the final review and we'll salvage it onto current main with your authorship preserved. (FYI the cherry-pick onto current main is clean, and the targeted memory-plugin tests pass — just waiting on Nicolo's signoff on the architecture before merging.) |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the plugin-scoped implementation; the parent auxiliary-task registration API is now on main, and current main still has no equivalent smart-retain pipeline.
Problems
aux_fallback_to_mainis not a main-model fallback: PRplugins/memory/hindsight/__init__.py:1592callsget_text_auxiliary_client(""), while currentagent/auxiliary_client.py:5148-5156resolves a task-less auxiliary client with no activemain_runtime.- The registered
timeout: 30at PRplugins/memory/hindsight/__init__.py:2652-2658is never passed to the directclient.chat.completions.create()calls (for example lines1620-1625). The same bypass skips the task'sextra_bodyhandling. - The documented
plugins.memory_provider.hindsight.smart_pipelineconfig is not read by the provider loader (plugins/memory/hindsight/__init__.py:349-392); the implementation instead adds flat provider-config fields. - The new pipeline paths have no behavioral tests; the provider-test change only disables bank-config fetches.
Suggested changes
- Use the auxiliary execution path that applies per-task routing/timeout/extra-body and pass an explicit active-main runtime for the fallback case.
- Align documentation with the actual configuration surface and add hermetic coverage for classifier/dedup/extraction/breaker/scope/flush paths.
- Preserve current append-mode retention behavior added in
09d66037when salvaging.
This is an automated hermes-sweeper review.
|
|
||
| task = "memory_retain_filter" | ||
| if use_main: | ||
| client, model = get_text_auxiliary_client("") # "" = main model |
There was a problem hiding this comment.
get_text_auxiliary_client("") does not select the active main-model runtime: it resolves a task-less auxiliary client and receives no main_runtime. Please pass an explicit main runtime into this provider path, or remove the aux_fallback_to_main guarantee.
| f"Reply with one word: SKIP, GENERAL, or SCOPED" | ||
| ) | ||
|
|
||
| response = client.chat.completions.create( |
There was a problem hiding this comment.
The registered memory_retain_filter timeout and extra_body are not applied here: this direct SDK call has neither. Route this through the task-aware auxiliary execution helper (and use it at the other smart-pipeline call sites) so the advertised per-task configuration takes effect.
What does this PR do?
Adds a configurable client-side smart retain pipeline to the Hindsight memory plugin to reduce write amplification and improve recall quality at scale. Without this, every
hindsight_retaincall hits the bank — duplicates, near-duplicates, and noisy raw-text retains all land as separate units, the bank grows linearly with chatter, and recall quality degrades. With this, retains are deduped, extracted, and delta-merged client-side before they reach the API, and the whole pipeline runs through a dedicated auxiliary task slot so the classifier model can be pinned cheaply (gpt-oss-20b / gemma-3-flash) without dragging the main chat model into pre-retain work.The pipeline is opt-in via plugin config and degrades cleanly: if the auxiliary call fails repeatedly, the circuit breaker opens and (per
aux_fallback_to_mainknob) either falls back to the main model or skips the smart step and writes raw.Depends on #29817 (
feat(plugins): add register_auxiliary_task()). That PR adds thePluginContext.register_auxiliary_task()API, which this plugin uses to declare itsmemory_retain_filterslot without modifying core files. Don't merge this until the parent lands; once it does, this branch rebases cleanly onto the merge commit, and the dependency commit drops out.Related Issue
N/A — companion PR to #29817.
Fixes #
Type of Change
Changes Made
plugins/memory/hindsight/__init__.py— adds the smart retain pipeline:recall+ auxiliary LLM classifier (skips if a near-duplicate already exists)retain_extract) — auxiliary LLM rewrites raw retain text into a normalised memory unit before it hits the bankaux_fallback_to_mainknob — opens after N consecutive aux failures, optionally re-routes to mainapi_urlis configured)register(ctx)callsctx.register_auxiliary_task(key="memory_retain_filter", display_name="Memory retain filter", description="pre-retain content classification (Hindsight smart pipeline)", defaults={"timeout": 30, ...})— declares the aux task slot through the plugin surface, no core editsretain_every_n_turns5 → 1 (smart pipeline is opt-in via the smart-pipeline config block; sampling default tightened so the pipeline sees every turn when it's on)api_urlpresence only (was previously coupled to other flags)tests/agent/test_memory_session_switch.py— adds 11 lines covering test fixture restoration for the new retain-pipeline attributes on the memory provider, so cross-session switch tests don't leak state between runstests/plugins/memory/test_hindsight_provider.py— adds 8 lines covering the new pipeline knobsHow to Test
git rebase main— the dependency commit2d94abe9awill drop out cleanly)pytest tests/plugins/memory/test_hindsight_provider.py tests/agent/test_memory_session_switch.py -q— all pass~/.hermes/config.yaml:yaml
plugins:
memory_provider:
hindsight:
smart_pipeline:
enabled: true
dedup: true
extract: true
delta: true
aux_fallback_to_main: false
hermes model→Configure auxiliary modelsand confirmmemory_retain_filterappears in the picker (provided by the registration API in the parent PR). Pin it to a cheap classifier modeldedup: skippedin the agent log)aux_fallback_to_mainis honouredChecklist
Code
feat(hindsight):)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — pipeline knobs are documented inline in the plugin's config schema and docstringscli-config.yaml.exampleif I added/changed config keys — N/A (plugin-owned config block underplugins.memory_provider.hindsight.smart_pipeline, schema documented in the plugin)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A (additive, plugin-internal)hindsight_retain/hindsight_recalltool surface is unchanged; the pipeline is a client-side wrapper)Backwards Compatibility
Non-breaking and opt-in. The smart pipeline is gated behind
plugins.memory_provider.hindsight.smart_pipeline.enabled(default off). With it off, the plugin's retain/recall paths are byte-identical to the current behaviour. Theretain_every_n_turnsdefault change (5 → 1) only affects users who already have the smart pipeline on, since the pipeline's whole point is to make every-turn retains cheap.The
memory_retain_filteraux task is declared via the parent PR's plugin registration API — no core files are modified; the slot exists only when the Hindsight plugin is loaded.Screenshots / Logs
N/A — internal pipeline, no UI surface beyond
hermes model(wherememory_retain_filterappears as another row in the auxiliary picker, contributed by the parent PR).