Skip to content

feat(plugins): propagate session context to plugin hooks - #42416

Closed
Gerkinfeltser wants to merge 2 commits into
NousResearch:mainfrom
Gerkinfeltser:feat/plugin-session-context
Closed

feat(plugins): propagate session context to plugin hooks#42416
Gerkinfeltser wants to merge 2 commits into
NousResearch:mainfrom
Gerkinfeltser:feat/plugin-session-context

Conversation

@Gerkinfeltser

@Gerkinfeltser Gerkinfeltser commented Jun 8, 2026

Copy link
Copy Markdown

feat(plugins): propagate session_id to command handlers and hooks

Summary

Plugin command handlers and pre_tool_call hooks now receive correct session context, enabling per-session stateful plugins.

Problem

Two gaps in Hermes plugin session propagation:

  1. Command handlers never received session_id. All four dispatch sites (CLI, Gateway/Discord, TUI×2) called plugin_handler(raw_args) with only the raw argument string. kwargs.get("session_id") was always None, so every session fell back to "_default" — making per-session state impossible.

  2. pre_tool_call hooks received session_id="". Three call sites in tool_executor.py and agent_runtime_helpers.py passed an empty string. This is partially addressed by upstream PR fix(plugins): propagate session_id to pre_tool_call hook from all 3 callsites #34622 (open since May 29) which covers the hook side, but that PR does not cover command dispatch.

  3. Gateway key ≠ agent session_id. Gateway command dispatch resolves to a Discord session key (e.g. agent:main:discord:group:...) when no agent is running yet. Hooks later receive the agent's UUID (20260608_122644_7ab1b0c3). Plugins tracking per-session state see two different keys for the same conversation.

Changes

Commit 1: Propagate session_id to command handlers and pre_tool_call hooks

  • hermes_cli/plugins.py — New call_plugin_command_handler() helper that uses inspect.signature() to pass session_id (and future context kwargs) to handlers that accept them, while legacy handlers that only take raw_args keep working. Broad TypeError fallback is intentionally avoided to avoid masking real plugin bugs.
  • cli.py — Already passes session_id in v0.15.2 (no change needed).
  • gateway/run.py — Command dispatch now resolves agent.session_id with fallback to _quick_key, passes it via call_plugin_command_handler().
  • tui_gateway/server.py — Both dispatch sites use _session_context_id() helper to resolve the live agent's session_id, falling back to session key.
  • agent/tool_executor.py — Both get_pre_tool_call_block_message() call sites now pass session_id=agent.session_id.
  • agent/agent_runtime_helpers.py — Same for its single call site.

Commit 2: Pass gateway_session_key to hooks for session bridging

  • agent/conversation_loop.pypre_llm_call hook invocation now passes gateway_session_key=getattr(agent, "_gateway_session_key", "").
  • hermes_cli/plugins.pyget_pre_tool_call_block_message() accepts and forwards gateway_session_key to invoke_hook("pre_tool_call", ...).
  • agent/tool_executor.py — Both callers pass gateway_session_key.
  • agent/agent_runtime_helpers.py — Same.

This lets plugins bridge the Discord/gateway key (used at command dispatch time when no agent exists yet) to the agent UUID (used at hook invocation time).

Commit 3: Add focused session-context tests

  • tests/hermes_cli/test_plugins.py — Adds coverage for call_plugin_command_handler() preserving legacy raw-args handlers, forwarding accepted context kwargs, and forwarding all context to **kwargs handlers.
  • tests/hermes_cli/test_plugins.py — Adds coverage that get_pre_tool_call_block_message() forwards both session_id and gateway_session_key into invoke_hook().
  • tui_gateway/server.py — Initializes call_plugin_command_handler = None alongside the other optional plugin dispatch imports for cleaner fallback handling.

Relationship to upstream PR #34622

Upstream PR #34622 (fix(plugins): propagate session_id to pre_tool_call hook from all 3 callsites) covers the hook-side session_id propagation. This PR:

The two PRs are complementary. If #34622 merges first, this PR's hook-side changes can be rebased out, leaving only the dispatch and bridging work.

Files changed

File Changes
hermes_cli/plugins.py +41 call_plugin_command_handler() helper + gateway_session_key in get_pre_tool_call_block_message()
cli.py +7/-1 Already correct in v0.15.2
gateway/run.py +13/-2 Agent session_id resolution + dispatch
tui_gateway/server.py +29/-3 _session_context_id() helper, both dispatch sites
agent/conversation_loop.py +1 gateway_session_key in pre_llm_call
agent/tool_executor.py +4 session_id + gateway_session_key in both call sites
agent/agent_runtime_helpers.py +2 session_id + gateway_session_key
tests/hermes_cli/test_plugins.py Focused tests for command handler context and hook context forwarding

8 files, +155/-6

Testing

  • Static validation: py_compile passes on all changed runtime files and focused tests
  • Full test suite: 74 passed, 2 pre-existing failures (same 2 TestPluginDiscovery tests fail on base v0.15.2 — unrelated to this PR)
  • Targeted session-context tests: 8 passed covering legacy handlers, context kwargs, **kwargs handlers, and hook context forwarding
  • Runtime tested in Discord gateway session: per-session command dispatch and tool blocking work correctly with gateway key → agent session_id bridging

Breaking changes

None. Legacy plugin handlers that only accept raw_args continue to work — the call_plugin_command_handler() helper inspects the signature and calls accordingly.

Update after review feedback

After review feedback, this PR also fixes indentation fallout from the initial patch application in agent/conversation_loop.py and agent/tool_executor.py.

I added regression coverage for the executor plugin-block call sites to verify _gateway_session_key is passed through in both sequential and concurrent tool execution paths. The new tests also caught and fixed a missing gateway_session_key argument in the sequential path.

Additional targeted checks run locally:

  • python -m pytest tests/run_agent/test_tool_call_guardrail_runtime.py -q --no-header
  • python -m pytest tests/hermes_cli/test_plugins.py -q --no-header -k "block or gateway or session"
  • python -m py_compile agent/tool_executor.py agent/conversation_loop.py tests/run_agent/test_tool_call_guardrail_runtime.py
  • git diff --check -- agent/tool_executor.py agent/conversation_loop.py tests/run_agent/test_tool_call_guardrail_runtime.py

@Gerkinfeltser
Gerkinfeltser requested a review from a team June 8, 2026 22:30
@alt-glitch alt-glitch added type/feature New feature or request comp/plugins Plugin system and bundled plugins comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have labels Jun 8, 2026
@Gerkinfeltser
Gerkinfeltser force-pushed the feat/plugin-session-context branch 2 times, most recently from 360d8e7 to 04f2b0e Compare June 9, 2026 00:39
@Gerkinfeltser Gerkinfeltser reopened this Jun 9, 2026
…sion_key to plugin hooks

Resolved conflicts with upstream/main (_ts_scope_block feature,
expanded _emit_terminal_post_tool_call params, conversation_loop additions).

8 files, +365/-11
@benegessarit

Copy link
Copy Markdown
Contributor

Ran into the same gap building a session-scoped plugin command and arrived at essentially the same helper design before finding this PR — so +1 on the approach, especially routing everything through one signature-inspecting call site instead of an env bridge.

Heads-up though: the branch head currently doesn't parse, which is probably why CI hasn't said anything useful. Two files have flush-left lines that look like a patch-tool dedent artifact:

  • agent/conversation_loop.py:424user_message = _ctx.user_message at column 0 inside the function
  • agent/tool_executor.py:329if _ts_scope_block is not None: at column 0, which orphans the indented block below it (SyntaxError: '(' was never closed at line 332)

python -m py_compile agent/conversation_loop.py agent/tool_executor.py reproduces both. Everything else I checked compiles fine.

I opened #44563 scoped to just the TUI-gateway command-handler half (the path with no env fallback at all), with gateway-level regression tests for both dispatch sites — happy to close it in favor of this once the hooks half is green, or to lift those tests onto your branch if that's useful.

@Gerkinfeltser

Copy link
Copy Markdown
Author

Thanks for catching that. You were right — the patch had indentation fallout in `agent/conversation_loop.py` and `agent/tool_executor.py`.

I’ve fixed those issues and added regression tests that exercise both sequential and concurrent executor paths, verifying `_gateway_session_key` reaches `get_pre_tool_call_block_message`.

The tests also caught one extra issue in the sequential path, where `gateway_session_key` still wasn’t passed through the real plugin-block call. That’s fixed as well.

Targeted local checks are passing; I’ll push shortly for CI.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for identifying the session-context gap; the command-handler premise is still present on current main.

Problems

  • agent/conversation_loop.py:436 adds a second per-turn prologue after build_turn_context() has already done that work. Current main performs message initialization, user-turn accounting, and pre_llm_call in agent/turn_context.py:271-318,478-529; retaining the copied block would run those effects twice.
  • The hook changes target the former get_pre_tool_call_block_message() route. Current main dispatches through resolve_pre_tool_block() at agent/tool_executor.py:449,1107, agent/agent_runtime_helpers.py:2193, and model_tools.py:1185, so the gateway-key plumbing needs to be ported through that central helper instead.

Suggested changes

  • Remove the copied conversation-loop prologue and reapply only the intended hook argument at the current agent/turn_context.py boundary.
  • Port the command helper to the four current raw handler calls (cli.py:8980, gateway/run.py:10067, and both TUI paths), with integration coverage. The related TUI-focused PR #44563 is useful context.

Automated hermes-sweeper review.

_ext_prefetch_cache = _ctx.ext_prefetch_cache

# Main conversation loop counters (pure locals consumed by the loop below).
# Initialize conversation (copy to avoid mutating the caller's list)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build_turn_context() has already completed this prologue immediately above. Repeating the old body rebuilds messages, increments turn counters, and fires pre_llm_call a second time; please remove this copied block and port the hook change to the current turn-context boundary.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:blast-massive Sweeper blast radius: massive — everyone, every turn (invariant surface) labels Jul 14, 2026
@Gerkinfeltser

Copy link
Copy Markdown
Author

Superseded by the current-main draft port: #65188

This replacement carries the session-context work through the current turn_context.py / resolve_pre_tool_block() architecture and is based on current upstream main.

@teknium1 teknium1 added the area/sessions Session lifecycle, resume, persistence, history label Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:blast-massive Sweeper blast radius: massive — everyone, every turn (invariant surface) sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants