Skip to content

feat(plugins): fire pre_llm_call, post_llm_call, on_session_start, on_session_end hooks#2930

Closed
crxssrazr93 wants to merge 1 commit into
NousResearch:mainfrom
crxssrazr93:feat/fire-plugin-lifecycle-hooks
Closed

feat(plugins): fire pre_llm_call, post_llm_call, on_session_start, on_session_end hooks#2930
crxssrazr93 wants to merge 1 commit into
NousResearch:mainfrom
crxssrazr93:feat/fire-plugin-lifecycle-hooks

Conversation

@crxssrazr93

Copy link
Copy Markdown

Summary

The plugin system declares 6 lifecycle hooks in VALID_HOOKS (plugins.py:52-59), but only 2 are actually invoked (pre_tool_call, post_tool_call). The remaining 4 are documented in the plugin guide and accepted by register_hook(), but never fired anywhere in the codebase — making them dead code.

This PR wires up all 4 missing hooks:

  • pre_llm_call / post_llm_call — fired in run_agent.py around the main LLM API call (covers both streaming and non-streaming paths)
  • on_session_start — fired in cli.py when the interactive session begins (after banner, before first prompt)
  • on_session_end — fired in cli.py during session cleanup (before _run_cleanup())

Each invocation uses the same try/import/invoke/except pattern as the existing pre_tool_call/post_tool_call hooks in model_tools.py.

Motivation

These hooks unlock important plugin use cases that aren't possible today:

  • pre_llm_call: Auto-manage local LLM servers — when a user switches to a local model via /model custom:write, a plugin can detect the model name and start/swap a llama-server or vLLM instance before the API call goes out. This is the primary motivating use case.
  • post_llm_call: Token usage tracking, response logging, cost monitoring across providers.
  • on_session_start: Initialize plugin state, connect to external services, display plugin status.
  • on_session_end: Clean up resources (kill servers, close connections, flush logs).

Example Plugin: llm-switch

Included in docs/llm-switch-plugin-example/ is a complete working plugin that demonstrates the hooks. It auto-manages a local llama-server when switching models:

  1. User defines models in a models.yaml file (GGUF paths, context sizes, sampling params)
  2. When user does /model custom:write, the pre_llm_call hook detects "write" in the config
  3. Plugin kills any running server and starts the right one
  4. on_session_end kills the server on exit
  5. Also registers a switch_local_llm tool for agent-driven switching
# models.yaml — declarative model config (replaces shell scripts)
server:
  binary: llama-server
  models_dir: ~/llama-models
  port: 8080
  gpu_layers: 99

models:
  write:
    description: "SEO articles and content briefs"
    gguf: qwen3.5-9b/Qwen3.5-9B-UD-Q6_K_XL.gguf
    context: 49152
    kv_cache: { key: q8_0, value: q4_0 }
    sampling: { temp: 0.7, top_p: 0.8 }

Changes

  • run_agent.py: +10 lines — invoke_hook("pre_llm_call", ...) before API call, invoke_hook("post_llm_call", ...) after response
  • cli.py: +10 lines — invoke_hook("on_session_start", ...) in run(), invoke_hook("on_session_end", ...) in cleanup
  • docs/llm-switch-plugin-example/: Complete example plugin (6 files)

Testing

  • Verified the hook invocations don't break existing behavior when no plugins are registered (the try/except pattern ensures graceful degradation)
  • The example plugin is self-contained and can be tested by copying to ~/.hermes/plugins/llm-switch/
  • No existing tests are affected — the hooks are observer-only and wrapped in exception handlers

Platforms tested

  • Linux (Arch)

…_session_end hooks

The plugin system declares 6 lifecycle hooks in VALID_HOOKS but only
pre_tool_call and post_tool_call are actually invoked. The remaining 4
hooks (pre_llm_call, post_llm_call, on_session_start, on_session_end)
are documented in the plugin guide and accepted by register_hook() but
never fired — making them effectively dead code.

This commit wires up all 4 missing hooks:

- pre_llm_call / post_llm_call: fired in run_agent.py around the main
  LLM API call (both streaming and non-streaming paths)
- on_session_start: fired in cli.py when the interactive session begins
- on_session_end: fired in cli.py during session cleanup

Each invocation is wrapped in try/except to match the existing pattern
used by pre_tool_call/post_tool_call, ensuring a misbehaving plugin
cannot break the core agent loop.

Includes an example plugin (docs/llm-switch-plugin-example/) that
demonstrates a practical use case: auto-managing a local llama-server
when switching models via /model. The plugin uses pre_llm_call to
detect local model names and start/swap the server, and on_session_end
to clean up on exit. It also registers a switch_local_llm tool for
agent-driven model switching. Model configurations are defined in a
declarative YAML file rather than shell scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@crxssrazr93

Copy link
Copy Markdown
Author

Superseded — rebased onto latest main (hooks already merged via #3542). New PR incoming with plugin-only changes and updated hook signatures.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant