feat(hermes): package the adapter as an installable hermes-elephant plugin - #32
Merged
Conversation
…lugin The adapter was a directory you were told to copy into a hermes checkout. This makes it a real distribution, and fixes two things that stopped the documented setup from working at all. Packaging - Distribution `hermes-elephant`, import package `hermes_elephant`. The import package is deliberately not `elephant`: that name belongs to the Electrophysiology Analysis Toolkit on PyPI and a top-level collision would break anyone with both installed. The provider is still `elephant` everywhere a user sees it — `memory.provider`, `hermes elephant`, and the installed plugin directory. - `hermes-elephant install` copies the provider into `$HERMES_HOME/plugins/elephant/`. hermes discovers memory providers by directory scan only, never pip entry points (upstream #40101), so pip alone leaves it invisible. Same two-step shape the Memori provider ships. - Declares the `hermes_agent.memory_providers` entry point so the install step becomes redundant once upstream discovery lands. Deliberately does NOT join `hermes_agent.plugins`: the general PluginManager eagerly imports that group in every hermes process and then calls register(ctx) on a context with no register_memory_provider. - Stdlib-only, asserted in the tests — installing must never add a package to the hermes runtime. Fixes - `hermes elephant ...` was dead for every directory install. hermes registers a user-installed provider's package as a synthetic shell with no __file__, so cli.py's `from . import DEFAULT_URL` raised ImportError and discover_plugin_cli_commands() returned nothing — while plugin.yaml and the README advertised the subcommands. Shared names moved to `_shared.py`; submodule imports resolve through the shell's __path__. Regression test drives the real loader shape and fails against the old import. - Episodes now carry `origin`. Without it the dreamer treats every transcript as human conversation, so a subagent's machine-written task text was mined for facts *about the operator* — elephant's extraction prompt keys off this field to suppress exactly that. Integration - `on_delegation` records a subagent's task and result against the parent session, the last unimplemented write hook. - `config_schema.py` gives the provider a native hermes dashboard panel; validated against hermes's real ProviderField/ProviderConfigSchema. - `plugin.yaml` states `kind: exclusive` rather than relying on hermes's source-text auto-coercion. - README documents wiring elephant's pull-only `:Intention` nodes to hermes cron — elephant never fires them, and hermes is the scheduler.
test_procedure_round_trip re-read the ORIGINAL id after a body update and expected the new content. That stopped being true when #17 replaced in-place procedure updates with supersession: a body change now creates a NEW node at v+1 and retires the old one, leaving its body intact and walkable via :SUPERSEDES. The adapter already reports the new id back to the model, so nothing was broken except the test's expectation. Now asserts the actual invariant — the edit is at the new id, the old id still answers with v1, and lookup by name follows the live version so an agent holding only the name is not stranded. Full live suite against a real service: 127 passed, 1 skipped.
…list Three review findings, all about the installer promising more than it verified. Shipped set is now a directory boundary. The provider moved to hermes_elephant/provider/ and install copies that tree wholesale, so copy mode, --link mode and the built wheel are the same files by construction. The hand-maintained PLUGIN_FILES allowlist and the test holding it in agreement are both gone — the test could only ever compare the allowlist against the source checkout, never against the wheel it was meant to guard. --link also stops exposing install.py to hermes's submodule exec, which was an untested asymmetry between dev and ship. Install verifies the discovery contract. hermes classifies a provider directory by scanning the first 8KB of its __init__.py for MemoryProvider / register_memory_provider. A packaging gap used to print a warning and then report success, leaving the user to reconcile "Installed" here with "Plugin: NOT installed" there. Now it fails, cleans up, and says why — and pins an upstream constraint that is otherwise invisible. status detects a stale copy. Copying rather than symlinking means `pip install -U hermes-elephant` leaves the profile running the old version with nothing to notice. The marker now records the version and status reports STALE. Also: - hermes_home() defers to hermes_constants.get_hermes_home() when importable, and otherwise honours %LOCALAPPDATA%\hermes on Windows. It previously hardcoded ~/.hermes, which on Windows installs where nothing scans and then reports success. - _is_ours() only claims a symlink that resolves to our own package, so uninstall cannot unlink one the user made pointing at their own tree. - __pycache__ and dotfiles are never copied into the plugin directory. Verified against a real elephant on a throwaway Neo4j testcontainer: 129 passed, 1 skipped. Entry-point discovery re-checked end to end against a hermes checkout — pip-only install still resolves its directory, config panel, CLI, and 34 tools.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Turns
adapters/hermesfrom a directory you are told to copy into a hermes checkout into a real pip distribution,hermes-elephant.pip install hermes-elephant hermes-elephant install hermes memory setup # select 'elephant'Why the two-step install
hermes-agent discovers memory providers by scanning directories only — the bundled
plugins/memory/tree and$HERMES_HOME/plugins/. It does not scan pip entry points (upstream #40101, still open and labelledneeds-decision). Sopip installalone leaves the provider invisible andhermes memory statusreportsPlugin: NOT installed.hermes-elephant installcopies the provider into$HERMES_HOME/plugins/elephant/, the path stock hermes already supports — the same shape the Memori provider ships. The package also declares ahermes_agent.memory_providersentry point, so the install step becomes redundant the moment upstream discovery lands. It deliberately does not joinhermes_agent.plugins: the general PluginManager eagerly imports that group in every hermes process and then callsregister(ctx)on a context with noregister_memory_provider.Two real bugs fixed
hermes elephant …was dead for every directory install. hermes registers a user-installed provider's package as a synthetic shell with no__file__, socli.py'sfrom . import DEFAULT_URLraisedImportError: ... (unknown location),discover_plugin_cli_commands()returned nothing, and the subcommands silently did not exist — whileplugin.yamland the README advertised them. Shared names moved to_shared.py; submodule imports resolve through the shell's__path__. The regression test drives the real loader shape and fails against the old import.Episodes carried no
origin. elephant's extraction prompt keys off that field to append "no human user is present; any USER-labeled trigger text is machine-generated". Without it, a subagent's machine-written task text was dreamed as human conversation and mined for facts about the operator.Integration
on_delegation— records a subagent's task and result against the parent session. The last unimplemented write hook; subagents run withskip_memory=Trueand have no provider session of their own.config_schema.py— a native hermes dashboard config panel, validated against hermes's realProviderField/ProviderConfigSchema.plugin.yaml— stateskind: exclusiveinstead of relying on hermes's source-text auto-coercion.:Intentionnodes tohermes cron. elephant never fires them by design; hermes is the scheduler.Packaging notes
The import package is
hermes_elephant, notelephant— that name belongs to the Electrophysiology Analysis Toolkit on PyPI and a top-level collision would break anyone with both installed. The provider is stillelephanteverywhere a user sees it:memory.provider,hermes elephant, and the installed plugin directory.The shipped set is a directory boundary (
hermes_elephant/provider/) rather than a filename allowlist, so copy mode,--linkmode and the built wheel cannot disagree. Install verifies the installed tree against hermes's discovery contract and fails loudly rather than reporting success on a packaging gap.statusreports a stale copy afterpip install -U.Stdlib-only, asserted in the tests — installing this must never add a package to the hermes runtime.
Testing
58 passedon the fake-transport suite (no Docker, no network).129 passed, 1 skippedon the live suite against a real elephant on a throwaway Neo4j testcontainer (pnpm test:hermes-live).Also fixes
test_procedure_round_trip, which re-read the original id after a body update. That stopped holding when #17 replaced in-place procedure updates with supersession; the adapter was already correct in reporting the new id.