Skip to content

fix(memory): entry-point + project-dir discovery parity for out-of-tree providers (salvage of #18842, #40644, #76567) - #80493

Closed
gigabyte22 wants to merge 5 commits into
NousResearch:mainfrom
gigabyte22:fix/memory-provider-discovery-parity
Closed

fix(memory): entry-point + project-dir discovery parity for out-of-tree providers (salvage of #18842, #40644, #76567)#80493
gigabyte22 wants to merge 5 commits into
NousResearch:mainfrom
gigabyte22:fix/memory-provider-discovery-parity

Conversation

@gigabyte22

Copy link
Copy Markdown
Contributor

Closes #40101.

Problem

plugins/memory/ is closed to new providers, so every new memory backend now ships out of tree. But the out-of-tree path is measurably weaker than the in-tree one, and the docs already promise otherwise.

plugins/memory/__init__.py:_iter_provider_dirs() walks directories only — bundled and $HERMES_HOME/plugins/. The general PluginManager (hermes_cli/plugins.py:1371-1391) scans four sources including pip entry points. CONTRIBUTING.md:78 and AGENTS.md both state memory discovery "picks them up from user/project plugin directories and pip entry points". It does not.

The user-visible symptom is #40101: a correctly-registered pip-installed provider reports Plugin: NOT installed. The workaround in the wild is a bespoke second install step — note that Memori, documented in memory-providers.md, ships pip install hermes-memori plus hermes-memori install for exactly this reason.

This is a salvage, not a fourth attempt

Three PRs already address parts of this and none have merged. Rather than add another competing implementation, this rebases them onto current main with authorship preserved and adds what none of them cover — the same approach as #79239.

PR Author Taken
#18842 @smarzola Entry-point discovery via hermes_agent.memory_providers, ctx.register_skill() forwarding, tests, developer-guide docs
#76567 @mlsmith Classify entry-point provider plugins without importing them, including the dotted-name parent-import fix
#40644 @AxDSan Not taken — its discovery half duplicates #18842, and its pipx switch was correctly rejected in review (pipx venvs are invisible to both importlib.metadata and the agent runtime) and reverted by the author. Its Mnemosyne docs belong in a docs PR.

#18842 and #76567 are complementary rather than competing: one adds memory entry-point discovery, the other stops the general PluginManager from eagerly importing such packages in every hermes process.

What this adds on top

Project-local providers (./.hermes/plugins/<name>/), gated on HERMES_ENABLE_PROJECT_PLUGINS exactly as PluginManager gates its own project scan. Completes the four sources.

find_provider_dir() resolves package entry points. This is load-bearing rather than cosmetic: config_schema.py (the dashboard config panel) and cli.py (the hermes <provider> subcommands) are read from disk, not imported — plugins/memory/config_schema.py:14-17 does this deliberately so the web server never pulls in the agent runtime. Without a directory, a pip-installed provider silently loses both.

list_memory_provider_names() includes entry-point providers, so they appear in the dashboard's memory.provider dropdown.

Resolution stays import-free. resolve_module_origin() is extracted from #76567's _resolve_module_source() and shared, so discovery walks a module's file layout instead of importing it. find_provider_dir() is called from the dashboard and from argparse setup — long before the operator has chosen a provider — so importing every installed candidate would execute third-party code on the strength of a package merely being present. A test asserts resolution leaves no side effect and no sys.modules entry.

PluginContext.register_memory_provider(). Memory was the only provider category without one — context engine, image gen, video gen, web search, browser, TTS, transcription, secret source, dashboard auth and platform all have one.

_ProviderCollector delegates unknown register_* calls to a real PluginContext instead of carrying three hand-written no-ops. It silently dropped register_tool / register_hook, and had no register_auxiliary_task at all — despite PluginContext.register_auxiliary_task's own docstring using a memory provider (hindsight's pre-retain dedup) as its worked example. It can no longer drift behind PluginContext.

A raise after register_memory_provider() no longer costs the provider. The loader caught it into a logger.debug, discarded the registered instance, and fell through to "instantiate any MemoryProvider subclass" — returning a different, unconfigured provider. A silent downgrade that looked like success, and the exact outcome of calling register_auxiliary_task.

Precedence

bundled > user > project > entrypoint — deliberately the reverse of the general PluginManager's later-wins order, and documented in the module docstring. A memory provider is activated by name, so letting a directory dropped into a working tree shadow a shipped provider would silently redirect the agent's memory. Existing behaviour is unchanged; the new sources sit below it.

Safety

Activation is still gated on memory.provider naming the plugin. Discovery enumerates; it does not load. Using a real PluginContext must not start also requiring the plugin in plugins.enabled — that would break every existing user-installed provider — and there is a test pinning it.

Testing

406 passed across tests/plugins/memory/, tests/agent/test_memory_provider.py, tests/hermes_cli/test_plugins.py, tests/test_plugin_skills.py, tests/hermes_cli/test_web_server.py. Six failures in test_hindsight_provider.py are a missing optional dependency (hindsight_client_api) and fail identically on main.

New coverage in tests/plugins/memory/test_discovery_sources.py: project-dir discovery on and off, entry-point discovery, precedence, import-free resolution, bare-module entry points, the secondary-registration fix, and the plugins.enabled invariant.

Verified end to end against a real third-party provider (kainappsinc/elephant) installed by pip alone with no directory copy — it appears in the dropdown, resolves its directory, loads with its 34 tools, and renders its dashboard config panel.

Follow-ups, deliberately not bundled

  • hermes_cli/config_defaults.py:3792 hardcodes env-var metadata per bundled provider; external plugins cannot join. Largely served already by get_config_schema() / config_schema.py, and a much larger refactor.
  • hermes_cli/main.py:9210 still hardcodes "honcho" in _SUBCOMMANDS. Cosmetic — it only affects unquoted multi-word session names — but it is the same class of thing PR fix(honcho): plugin drift overhaul + feat(plugins): CLI registration system #5295 removed.

@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard comp/plugins Plugin system and bundled plugins tool/memory Memory tool and memory providers tool/skills Skills system (list, view, manage) P3 Low — cosmetic, nice to have labels Aug 6, 2026
smarzola and others added 5 commits August 13, 2026 13:29
…ting

Entry-point (pip-installed) plugins exposing register_memory_provider()
or register_provider() + ProviderProfile were treated as plain
standalone plugins and eagerly imported by the general PluginManager,
even though memory and model providers have their own discovery
systems and the module has no register() for the general manager to
call. The import registered nothing and paid the module's full import
cost in every Hermes process (a pip memory provider pulls fastembed ->
onnxruntime, ~60 MB RSS).

Entry-point manifests now get the same source-scan classification as
directory plugins via a shared _detect_kind_from_source() helper: the
module is resolved with importlib.util.find_spec (no import) and its
first 8192 chars are scanned for provider markers. Memory providers ->
kind=exclusive, model providers -> kind=model-provider; both are
recorded for introspection and skipped by the general loader.
Unresolvable or non-Python modules stay standalone (default behavior
unchanged).

Tests: an enabled pip entry-point memory provider is never imported;
a pip entry-point model provider routes to providers/ discovery.
find_spec() on a dotted module name imports the parent package first,
executing its __init__.py — which is exactly where a provider's heavy
imports typically live (fastembed -> onnxruntime and friends). The
previous classifier only preserved the no-import property for
top-level entry points.

_resolve_module_source() now resolves only the top-level name with
find_spec() (import-free for top-level names) and walks the remaining
dotted segments through submodule_search_locations by hand, mirroring
PathFinder's file conventions (part.py module / part/__init__.py
package). Namespace packages, zipped modules, extension modules, and
anything else unexpected fall back to standalone (the safe default).
.pyc origins map back to source via source_from_cache.

Regression: a dotted entry point whose parent __init__.py writes an
execution marker and imports the child — asserts the parent never
executed and neither module enters sys.modules during classification.
Fails against the previous implementation (marker written), passes now.
…ation

Documents and tests the routing contract the sweeper review asked about:
classification records the manifest but does not activate anything.

- model-provider test now exercises providers.get_provider_profile() against
  the pip-only name (None today — providers discovery is directory-based)
  and asserts the module never leaks into sys.modules via that path.
- new test for the mnemosyne shape: a pip entry point duplicating a
  same-name directory provider. The pip copy is classified exclusive and
  never imported; the directory copy still activates through
  plugins.memory discovery, exactly once.
- _classify_entrypoint_kind docstring now states the activation contract
  explicitly: pip-only providers were equally unactivatable pre-change
  (both destination systems are directory-only; the
  hermes_agent.memory_providers entry-point group has no consumers), so
  classification only removes the wasted import. Entry-point activation
  is tracked upstream (NousResearch#40644 for memory); this change is its
  prerequisite, preventing double import once it lands.
…ee providers

Builds on the three salvaged commits: adds the sources and integration points
they leave out, so a pip-installed memory provider is not a second-class
citizen next to a directory install.

Discovery
- Project-local providers (./.hermes/plugins/<name>/), gated on
  HERMES_ENABLE_PROJECT_PLUGINS exactly as PluginManager gates its own project
  scan. Completes the four sources CONTRIBUTING.md and AGENTS.md already
  promised; memory was the only discovery system missing two of them.
- find_provider_dir() now resolves a package entry point to its directory.
  This is load-bearing: config_schema.py (the dashboard panel) and cli.py (the
  `hermes <provider>` subcommands) are read from disk rather than imported, so
  without a directory a pip-installed provider silently lost both.
- list_memory_provider_names() includes entry-point providers, so they appear
  in the dashboard's memory.provider dropdown.

Resolution stays import-free. hermes_cli.plugins.resolve_module_origin() is
extracted from _resolve_module_source() (added by the salvaged NousResearch#76567) and
shared, so discovery walks a module's file layout instead of importing it.
find_provider_dir() is called from the dashboard and from argparse setup, long
before the operator has chosen a provider — importing every installed candidate
would execute third-party code on the strength of a package being present.
A test asserts the resolution leaves no side effects and no sys.modules entry.

Registration
- PluginContext gains register_memory_provider(). Memory was the only provider
  category without one; context engine, image gen, video gen, web search,
  browser, TTS, transcription, secret source, dashboard auth and platform all
  have one.
- _ProviderCollector delegates unknown register_* calls to a real
  PluginContext instead of carrying three hand-written no-ops. It silently
  dropped register_tool/register_hook, and had no register_auxiliary_task at
  all — despite PluginContext.register_auxiliary_task documenting a memory
  provider (hindsight's pre-retain dedup) as its worked example. It can no
  longer drift behind PluginContext.
- A raise after register_memory_provider() no longer costs the provider. The
  loader caught it into a debug log, discarded the registered instance, and
  fell through to "instantiate any MemoryProvider subclass" — returning a
  different, unconfigured provider. A silent downgrade that looked like
  success, and the exact outcome of calling register_auxiliary_task.

Activation is unchanged: still gated on memory.provider naming the plugin, and
covered by a test so the real PluginContext cannot start requiring
plugins.enabled — that would break every existing user-installed provider.

Verified end to end against a real third-party provider (kainappsinc/elephant)
installed by pip alone, with no directory copy: it appears in the dropdown,
resolves its directory, loads with its tools, and renders its dashboard panel.

Closes NousResearch#40101.
@gigabyte22
gigabyte22 force-pushed the fix/memory-provider-discovery-parity branch from 720d7c3 to 7af6e84 Compare August 13, 2026 13:32
@gigabyte22

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (post–manifest v2 #64165 / ownership ledger). The only conflict was an insert-insert collision with the new manifest helpers in hermes_cli/plugins.py; resolved keeping both. _detect_kind_from_source now also backs the manifest parser's directory coercion that landed on main, so the heuristic stays in one place. register_memory_provider deliberately records no ownership-ledger handle — it mutates no global registry, and its docstring says so.

Full suites re-run on the rebased branch: 574 passed, 1 skipped across tests/plugins/memory/, tests/agent/test_memory_provider.py, tests/hermes_cli/test_plugins.py, tests/test_plugin_skills.py, tests/hermes_cli/test_web_server.py. The six test_hindsight_provider.py failures are the pre-existing optional hindsight-client dependency (security.allow_lazy_installs=false), identical on main — this PR touches no hindsight file.

One triage note: this is labelled P3 ("cosmetic, nice to have"), but it's a type/bug closing #40101, where users are still paying the cost — most recently on Aug 11, when a user resorted to hand-symlinking the package into ~/.hermes/plugins/ to get past Plugin: NOT installed. The sweeper's verdict on #18842 (one of the PRs salvaged here) was salvageability=high, and its two suggested changes — the combined entry-point + register_skill() regression and the developer-guide docs — are both included in this PR. Happy to adjust anything review turns up.

teknium1 added a commit that referenced this pull request Aug 13, 2026
Sibling test pinned the old zero-arg constructor; salvaged #80493 gave
_ProviderCollector a required provider name (used for skill registration
and PluginContext delegation).
teknium1 added a commit that referenced this pull request Aug 13, 2026
Sibling test pinned the old zero-arg constructor; salvaged #80493 gave
_ProviderCollector a required provider name (used for skill registration
and PluginContext delegation).
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #85527 — all five commits cherry-picked onto current main with authorship preserved in git history (your parity commit, @m1k3s0's two classification commits + activation-contract tests, and @smarzola's packaged-skills commit). One follow-up on top from us: a sibling test (test_plugin_cli_registration.py) pinned the old zero-arg _ProviderCollector() constructor and needed updating for the new required name arg.

Thanks for doing this as a proper salvage rather than a fourth competing implementation — the authorship table in your PR body made the credit trail trivial to preserve.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have tool/memory Memory tool and memory providers tool/skills Skills system (list, view, manage) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mnemosyne-hermes Plugin: NOT installed despite correct entry point registration (Python 3.11 / Hermes venv)

4 participants