NousResearch/hermes-agent · error · RuntimeError
Hermes tool execution callback invoked more than once
Error message
Hermes tool execution callback invoked more than once
What it means
Raised in agent/tool_executor.py's authorized-dispatch closure: the middleware pipeline may invoke the final execution callback exactly once, enforced under a dispatch lock with a state['dispatched'] flag. A second invocation — typically a middleware calling next()/callback twice, or an authorization middleware both dispatching and re-dispatching after post-processing — trips this RuntimeError to surface the double-execution bug.
Source
Thrown at agent/tool_executor.py:516
from agent import relay_tools
from hermes_cli.middleware import (
apply_tool_request_middleware,
run_tool_execution_middleware,
)
trace = middleware_trace if middleware_trace is not None else []
state = {
"args": function_args,
"middleware_trace": trace,
"blocked": False,
"dispatched": False,
}
dispatch_lock = threading.Lock()
def _authorized_dispatch(final_args: dict[str, Any]) -> Any:
with dispatch_lock:
if state["dispatched"]:
raise RuntimeError(
"Hermes tool execution callback invoked more than once"
)
state["dispatched"] = True
state["blocked"] = False
state["args"] = final_args
def _begin() -> None:
_begin_tool_execution(
agent,
function_name=function_name,
function_args=final_args,
effective_task_id=effective_task_id,
tool_call_id=tool_call_id,
display_index=display_index,
)
def _advance_start_order(callback=None) -> None:
if begin_execution is None:View on GitHub (pinned to c896c09c42)
Solutions
- Audit your middleware: the dispatch callback must be invoked on exactly one control-flow path — guard it with a local 'called' flag if unsure.
- Implement retries above the middleware layer (wrap the whole tool call), not by re-invoking the callback inside middleware.
- Remove or fix the offending middleware and reproduce with a minimal chain of one custom middleware plus defaults.
- If you ship no custom middleware and still hit this, report it as a Hermes core bug with the middleware stack dump (middleware_trace is captured in state).
Example fix
# before (middleware re-dispatching)
def my_middleware(args, next):
result = next(args)
if result_needs_retry(result):
return next(args) # second call -> RuntimeError
return result
# after
def my_middleware(args, next):
return next(args) # retry belongs outside; wrap the outer call instead Defensive patterns
Strategy: type-guard
Validate before calling
def make_safe_middleware(mw):
called = False
def wrapper(args, next):
nonlocal called
if called:
raise RuntimeError("double dispatch detected before Hermes does")
called = True
return mw(args, next)
return wrapper
# register wrapper-wrapped middleware so double-invocation fails fast in YOUR code Try / catch
try:
result = execute_tool_with_middleware(...)
except RuntimeError as exc:
if "invoked more than once" in str(exc):
disable_offending_middleware_and_retry_without_it()
else:
raise Prevention
- Invoke the dispatch callback on exactly one control-flow path; guard with a local called flag.
- Implement retries by wrapping the entire tool call from outside, never by re-calling next()/callback inside middleware.
- Test each custom middleware in isolation before stacking it on the default chain.
- On upgrades, re-read the middleware contract — arity and single-invocation rules are enforced strictly.
When it happens
Trigger: A custom tool-execution middleware that calls the dispatch callback again after inspecting results (e.g. retry-on-failure implemented at the wrong layer); a middleware chain where two branches both forward the callback; looping middleware that re-enters the continuation on timeout; SDK upgrades changing callback arity such that a wrapper forwards twice.
Common situations: Adding a custom middleware (logging, audit, retry) around Hermes tool execution; porting Express/Koa-style 'next()' habits where double-next is tolerated; a bug where an exception path falls through into a second call.
Related errors
- First-run remote setup completed without a saved remote back
- Hermes failed to assign a child identity.
- tool args must be a mapping, got {type(args).__name__}
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/da167d5e1c707f3b.
Report an issue: GitHub.