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

  1. Audit your middleware: the dispatch callback must be invoked on exactly one control-flow path — guard it with a local 'called' flag if unsure.
  2. Implement retries above the middleware layer (wrap the whole tool call), not by re-invoking the callback inside middleware.
  3. Remove or fix the offending middleware and reproduce with a minimal chain of one custom middleware plus defaults.
  4. 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

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


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/da167d5e1c707f3b. Report an issue: GitHub.