Skip to content

feat(gateway): fire agent_loop_stopped plugin hook on interrupt#27208

Open
francip wants to merge 1 commit into
NousResearch:mainfrom
kortexa-ai:kortexa/agent-loop-stopped-hook
Open

feat(gateway): fire agent_loop_stopped plugin hook on interrupt#27208
francip wants to merge 1 commit into
NousResearch:mainfrom
kortexa-ai:kortexa/agent-loop-stopped-hook

Conversation

@francip

@francip francip commented May 17, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds an agent_loop_stopped plugin hook fired from gateway/run.py::_interrupt_and_clear_session right after running_agent.interrupt(). Lets plugins drop per-turn external resources (e.g. an outbound RPC call the agent loop was awaiting a result from) when the user interrupts the run with /stop or the fast-path of /new.

Today plugins can react to on_session_finalize for the /new slow path, but /stop and the /new fast-path inside _dispatch_message bypass that hook entirely — outbound work just sits until it times out, while the agent has already moved on.

Related Issue

Fixes #27206

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)

Changes Made

  • gateway/run.py — emit agent_loop_stopped via invoke_hook right after running_agent.interrupt() in _interrupt_and_clear_session. Wrapped in try/except so a misbehaving handler can't block the interrupt. Same dispatch shape as on_session_finalize at gateway/run.py:8310.
  • hermes_cli/plugins.py — add agent_loop_stopped to VALID_HOOKS with inline docstring covering kwargs (session_key, platform, reason, invalidation_reason).
  • tests/gateway/test_agent_loop_stopped_hook.py — three pytest cases covering hook presence in VALID_HOOKS, dispatch with expected kwargs on interrupt, and fault tolerance when a registered handler raises.

How to Test

./venv/bin/python -m pytest tests/gateway/test_agent_loop_stopped_hook.py -v

All three cases pass. End-to-end smoke against hermes-livekit (the motivating consumer) is pending v0.3.0 release of that plugin; the plugin's hook subscription is wired but is currently a no-op against an unpatched core.

Motivating use case

hermes-livekit v0.3.0 exposes client-registered remote tools the agent can call over a WebRTC data channel:

# adapter.py — the proxy hermes invokes
async def proxy(args, **_kwargs):
    ...
    return await asyncio.wait_for(future, timeout=TOOL_CALL_TIMEOUT)

When the user hits /stop, the agent's interrupt flag flips but the proxy keeps waiting; the client stays in "owe a result" mode for 30s and the agent:tool-call-cancelled notification never goes out. This hook gives the plugin one signal to fail the future and notify the client immediately.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (feat(gateway):)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this feature
  • I've run pytest tests/gateway/test_agent_loop_stopped_hook.py -v and all 3 tests pass
  • I've added tests for my changes
  • I've tested on my platform: macOS 25.4.1, Python 3.11

Documentation & Housekeeping

  • I've updated relevant documentation — VALID_HOOKS in hermes_cli/plugins.py has an inline docstring describing the new hook's kwargs. No separate hooks documentation file exists; the inline comments next to VALID_HOOKS are the canonical source today.
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (no config keys)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A (additive plugin hook, no workflow change)
  • I've considered cross-platform impact — N/A (pure Python, no platform-specific code)
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A (no tools touched)

francip added a commit to kortexa-ai/hermes-livekit that referenced this pull request May 17, 2026
Connected clients can register tools the agent's LLM can invoke. The
agent calls them with a targeted agent:tool-call JSON message; the
client runs the tool locally and replies with client:tool-result. New
inbound types client:tool-register / -unregister / -result on the
hermes-control topic; matching outbound agent:tool-registered /
-unregistered / agent:tool-call / -cancelled / -timeout events. Flat
JSON envelope (no payload wrapper) to keep the protocol terse.

Tools register into the hermes registry under toolset
'hermes-livekit-tools'. Operators must add that toolset to their
livekit platform_toolsets list in ~/.hermes/config.yaml -- the plugin
does not auto-activate it.

Cleanup paths:
- participant_disconnected: deregister that client's tools, fail their
  pending calls
- full adapter teardown / room rejoin: wipe all tool state
- on_session_finalize (existing hook, fires on /new): cancel pending
  remote tool calls, notify owners with agent:tool-call-cancelled
- agent_loop_stopped (proposed hook, fires on /stop):
  no-op until upstream PR NousResearch/hermes-agent#27208 lands; once
  merged, /stop mid-call cancellation comes online with no plugin
  change
- per-call timeout (default 30s, override via
  HERMES_LIVEKIT_TOOL_TIMEOUT_SEC)

Full design including v0.4 RPC pivot and v0.5 byte-streams roadmap in
docs/remote-tools-design.md. Execution status + cross-session
breadcrumbs (notably the pending upstream PR) in PLAN.md.

Adds examples/test_client.py: an interactive + oneshot Python client
that registers desktop_notify (pops a macOS notification via osascript)
and lets you drive the agent over the data channel without the voice
path. Single file, no extra deps beyond what the plugin already pins.
Verified end-to-end against Avery (the hermes-agent gateway).
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins labels May 17, 2026
@francip francip force-pushed the kortexa/agent-loop-stopped-hook branch from 01686e2 to 7202a84 Compare May 21, 2026 15:08
Plugins that hold per-turn external resources (e.g. an outbound RPC call
the agent loop is currently awaiting a result from) have no way to learn
that the user interrupted the run with /stop. Today they can react to
on_session_finalize for /new, but /stop and the /new fast-path inside
_dispatch_message bypass that hook entirely — the resources keep
waiting until they time out.

Add an agent_loop_stopped invoke_hook dispatch right after
running_agent.interrupt() in _interrupt_and_clear_session, so every
abort path (/stop, the /new fast-path, the pending-sentinel and
hung-agent stop paths) gives plugins one consistent signal. Kwargs are
session_key, platform, reason, invalidation_reason. Dispatch is wrapped
in try/except so a misbehaving plugin can't block the interrupt.

Motivating use case: hermes-livekit (https://github.com/kortexa-ai/hermes-livekit)
v0.3.0 exposes client-registered remote tools that the agent calls over
a WebRTC data channel. When the user runs /stop mid-call, the plugin's
proxy is blocked on asyncio.wait_for(future, timeout=30s) and the
client stays in "we owe a result" mode for that whole window. This
hook lets the plugin fail the future and notify the client
immediately.

Adds agent_loop_stopped to VALID_HOOKS with comments documenting the
kwargs. Adds tests covering hook presence, dispatch on interrupt, and
fault tolerance when a plugin handler raises.
@francip francip force-pushed the kortexa/agent-loop-stopped-hook branch from 7202a84 to e26d927 Compare May 29, 2026 22:27
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused hook addition and the concrete hermes-livekit use case. I verified the premise still holds on current main: gateway/run.py:12475 interrupts the running agent and then clears session state without a plugin signal, and /stop//new reach that helper from gateway/run.py:6724 and gateway/run.py:6741.

Problems

  • The PR says inline VALID_HOOKS comments are the canonical docs, but current main has user-facing plugin-hook docs: website/docs/user-guide/features/plugins.md:188 and website/docs/user-guide/features/hooks.md:378. The new hook should be listed there with kwargs and gateway-only semantics.
  • The new test does not exercise an actual interrupted agent. In the PR diff, tests/gateway/test_agent_loop_stopped_hook.py initializes runner._running_agents = {}, so the hook assertion would pass even if no running_agent.interrupt() happened.
  • The hook is inserted outside the real-agent branch in gateway/run.py; gateway/slash_commands.py:613 sends the pending-sentinel /stop path through the same helper. Please either make pending/no-active-run emission intentional and tested, or only emit after a real agent is interrupted.

Suggested changes

  • Add docs entries in both hook tables/details pages.
  • Update the regression test to install a mock running agent, assert interrupt() is called, and cover the intended sentinel behavior.

This is an automated hermes-sweeper review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(gateway): plugin hook for /stop interrupt — agent_loop_stopped

3 participants