Skip to content

[Bug]: post_tool_call hook is not invoked for built-in tools (memory, todo, session_search, clarify, delegate_task) #12922

@wpsl5168

Description

@wpsl5168

Bug Description

The post_tool_call plugin hook is not invoked for several built-in tools (memory, todo, session_search, clarify, delegate_task) and for memory-provider tools, because run_agent.py dispatches these directly without going through model_tools.handle_function_call(), which is where invoke_hook("post_tool_call", ...) lives.

Result: any plugin that registers a post_tool_call hook to observe these tools (e.g. an external memory bridge that wants to be notified when the user updates their MEMORY.md) silently never receives callbacks, even though pre_tool_call works fine for the same tools.

Steps to Reproduce

  1. Create a plugin that registers a post_tool_call hook:
# ~/.hermes/plugins/test-observer/__init__.py
def _on_post(tool_name, args, result, **kwargs):
    print(f"[post_tool_call] {tool_name} args={args}")

def register(ctx):
    ctx.register_hook("post_tool_call", _on_post)
  1. Start Hermes (gateway) and trigger:

    • a memory action (add/replace/remove)
    • a todo write
    • a session_search
    • a clarify
    • a delegate_task
  2. Expected: [post_tool_call] memory args=... printed for each call.

  3. Actual: Nothing printed for any of the above. The hook fires only for tools dispatched through the registry (web_search, terminal, read_file, etc.).

Verified with grep — pre_tool_call is invoked in _invoke_tool (concurrent path) via get_pre_tool_call_block_message, but no corresponding post_tool_call invocation exists for the elif branches.

Expected Behavior

post_tool_call should fire for every tool the agent successfully executes, regardless of whether it's dispatched through handle_function_call() (registry) or short-circuited inline by run_agent (built-in tools). The plugin contract documented in website/docs/guides/build-a-hermes-plugin.md says:

This hook fires for ALL tool calls, not just ours

That is currently false for ~5 tool names.

Actual Behavior

post_tool_call only fires for tools routed through model_tools.handle_function_call() at model_tools.py:516. The shortcuts in run_agent.py skip this path entirely.

Root Cause

Two dispatch paths in run_agent.py bypass handle_function_call():

Concurrent pathRunAgent._invoke_tool() (run_agent.py ~line 8008):

if function_name == "todo":
    return _todo_tool(...)               # ← bypasses post_tool_call
elif function_name == "session_search":
    return _session_search(...)          # ← bypasses
elif function_name == "memory":
    result = _memory_tool(...)
    ...
    return result                        # ← bypasses
elif self._memory_manager and self._memory_manager.has_tool(...):
    return self._memory_manager.handle_tool_call(...)  # ← bypasses
elif function_name == "clarify":
    return _clarify_tool(...)            # ← bypasses
elif function_name == "delegate_task":
    return _delegate_task(...)           # ← bypasses
else:
    return handle_function_call(..., skip_pre_tool_call_hook=True)  # ✅ fires post hook

Sequential path_execute_tool_calls() has the same structural pattern with the same 5 elif branches that skip the registry.

The hook is only fired by model_tools.py:514-526:

try:
    from hermes_cli.plugins import invoke_hook
    invoke_hook("post_tool_call", tool_name=..., args=..., result=..., ...)
except Exception:
    pass

So tools that never enter handle_function_call() never trigger the hook.

Impact

  • Plugins documented to observe all tools silently miss critical events.
  • External memory bridges (e.g. plugins that mirror Hermes MEMORY.md to a vector DB) cannot reliably stay in sync with built-in memory writes.
  • Audit/observability plugins under-report by ~5 tool names.
  • Asymmetric with pre_tool_call, which is invoked correctly for all the same built-in tools (block-message check at _invoke_tool line 8019 and the equivalent sequential check). Plugins reasonably expect symmetry.

Environment

  • OS: Ubuntu 24.04 (x86_64)
  • Python: 3.12
  • Hermes: main @ 8a6aa58 (post v0.x)

Proposed Fix

PR incoming. Approach: extend the existing skip_pre_tool_call_hook pattern with a symmetric skip_post_tool_call_hook parameter on handle_function_call(), add a private _fire_post_tool_call_hook() helper on the agent, and call it from each bypass branch in both dispatch paths. Minimal surface area, mirrors existing code style.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High — major feature broken, no workaroundcomp/agentCore agent loop, run_agent.py, prompt buildercomp/pluginsPlugin system and bundled pluginstool/memoryMemory tool and memory providerstype/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions