Installing into Hermes Agent¶
Hermes funnels every provider call through one function, so this is a seam, not a fork. Two files change, six lines in total. That matters for the reason below.
Why not fork Hermes¶
An earlier version of this work was a fork — the whole upstream tree carried along to add about 670 lines. It worked, and it was the wrong shape:
- Every upstream release had to be merged rather than installed alongside, with conflict resolution in files the integration never touched.
- The decision layer could not be tested without standing up all of Hermes.
- Nobody running Pydantic AI or anything else could use any of it.
A standalone package with a seven-method host protocol lets the two sides be versioned, tested
and upgraded independently. You pip install this, you keep tracking Hermes main normally,
and the only thing you maintain is the seam below.
Step 1 — install¶
pip install "jevqueue @ git+https://github.com/iamvazghen/jevqueue.git"
pip install "jevqueue[static] @ git+https://github.com/iamvazghen/jevqueue.git" # only if you want static_tools
The package is not on PyPI, so the install names the repository.
Step 2 — the seam in agent/turn_api_call.py¶
Find _perform_api_call inside perform_api_call. Rename it to _generate_api_call and put a
new _perform_api_call in front of it:
def _perform_api_call(next_api_kwargs):
governor = getattr(agent, "_jevqueue", None)
if governor is not None:
return governor.call(agent, next_api_kwargs, _generate_api_call)
return _generate_api_call(next_api_kwargs)
def _generate_api_call(next_api_kwargs):
# ... the original body, unchanged ...
Nothing else in that file moves. When the governor is absent — not configured, or disabled for this profile — the second function runs directly and the call path is byte-for-byte what it was.
Step 3 — the wiring in agent/agent_init.py¶
In init_agent, after _agent_cfg is loaded:
from jevqueue.adapters.hermes import install
install(agent, _agent_cfg)
install reads the jev section. Absent, or enabled: false, and it sets
agent._jevqueue = None — so switching the feature off is one line of YAML rather than an
uninstall, and the seam above falls back to plain generation.
Step 4 — configure per profile¶
jev:
enabled: true
confidence_threshold: 0.80 # below this, delegate rather than route
completion_threshold: 0.95 # below this, refuse to stop
state_chars: 24000
timeout: 5
on_error: delegate # or: stop
goal_hint: "You operate a home lab and a small business."
static_tools:
docker_ps: {}
An unknown key here is an error, not a warning — a typo that is silently ignored leaves the gate at its default while you believe you tightened it.
Step 5 — the credential¶
install reads TYPESAFE_API_KEY from Hermes's own secret scope
(agent.secret_scope) when it is available, and falls back to the process environment. It is
never read from this package's config, because a credential should have exactly one home and
Hermes already provides one.
Step 6 — verify¶
pytest # Hermes's own suite: unchanged
python -m jevqueue.selfcheck # not a thing; use the script below
python scripts/verify_live.py # from this repo, with your key
Then run a real turn and look at the events:
for event in agent._jevqueue.events[-5:]:
print(event.path, event.action, round(event.confidence or 0, 3), event.decision_ms)
You want to see a mix. All delegated means the thresholds are too tight or the tool
descriptions are too thin to route on. All tool_arguments with no delegated at all is worth
a second look — a decision layer that is never unsure is usually one that is not being asked
hard questions.
What stays upstream¶
Tools, execution sandboxes, session persistence, memory and FTS/vector retrieval, skills, context compression, and every provider transport remain untouched Hermes code. This package reads the toolset and the transcript and constrains one provider call. That is the entire contact surface.
Keeping current with upstream¶
Because this is an installed dependency rather than a fork, the normal flow works:
git fetch upstream && git merge upstream/main
The only file that can conflict is turn_api_call.py, and only if upstream restructures
_perform_api_call itself. If that happens, the fix is to re-apply the same two-function
pattern around whatever the new call site is called — the governor does not care what the
surrounding function is named, only that it is handed the request and a way to send it.
Limitations¶
api_modemust bechat_completions.codex_responsesis a different request shape with different tool semantics;HermesHostraises rather than mis-parsing it.- Streaming is not governed. Check how
_should_streamresolves for your profile. - This adapter is not verified in CI against a live Hermes checkout — the seam is small enough to read, but verify it in your own fork and please report breakage.