fix(plugins): wire-plugin-lifecycle-hooks - #2820
Conversation
…session_end hooks Four lifecycle hooks were listed in VALID_HOOKS and accepted by register_hook() but invoke_hook() was never called for them, so plugin callbacks registered against these hooks never fired. - on_session_start: fired at end of AIAgent.__init__ after session_id is set - pre_llm_call / post_llm_call: fired in both _interruptible_api_call and _interruptible_streaming_api_call around the API request thread - on_session_end: fired in run_conversation after _persist_session Fixes NousResearch#2817
…start, on_session_end hooks Covers: - correct kwargs delivered to each callback - exceptions in callbacks do not propagate - all six hooks present in VALID_HOOKS
|
Minor edge case: on streaming fallback, pre/post_llm_call may fire twice. |
Plugin discovery only happened inside handle_function_call (first tool use), so on_session_start and early pre_llm_call invocations fired before any plugins were loaded. Explicitly calling discover_plugins() (idempotent) before on_session_start ensures hooks have registered listeners at session init time.
|
Tested this locally - all six hooks are firing correctly now. A test tool_logger plugin is receiving callbacks and logging llm_call events with model, tokens, duration, along with session lifecycle events. Verified:
Thanks for the fix. |
|
@MillionthOdin16 Glad it helped! Thanks for the verification. |
|
Just for information, I was trying to add a post llm call hook to fix issues with local inference services, and the hook wasn't able to run because the Debugging a Hermes Plugin Hook That Never FiresThe SymptomA plugin registered for Phase 1: Verify the Plugin LoadsThe first step was confirming the plugin wasn't the problem. Checking the plugin's own log file showed the
Phase 2: Trace the Hook Invocation in the Agent CoreThe next step was reading
Then the critical search: where does the agent actually call grep -n 'invoke_hook.*post_llm_call' run_agent.pyThis found exactly one call site, deep in the main agent loop, inside an if final_response and not interrupted:
invoke_hook("post_llm_call", ...)Phase 3: Trace the Failure Code PathThe next question: does the retry-exhaustion path reach that Searching for the error message text ( # Line ~7825 — retry exhaustion
return {
"final_response": _final_response,
"messages": messages,
"completed": False,
"failed": True,
"error": _final_summary,
}
# ... 750+ lines later ...
# Line ~8578 — hook invocation (never reached on failure)
if final_response and not interrupted:
invoke_hook("post_llm_call", ...)Root cause: The hook was placed at the end of the "happy path" only. Every early-return failure path bypassed it. Phase 4: The Fix PatternThe fix was structural, not plugin-specific: In the agent core ( # Before the early return
try:
invoke_hook(
"post_llm_call",
session_id=self.session_id,
user_message=original_user_message,
assistant_response=_final_response,
conversation_history=list(messages),
model=self.model,
platform=getattr(self, "platform", None) or "",
failed=True, # new field
error=_final_summary, # new field
)
except Exception as exc:
logger.warning("post_llm_call hook failed: %s", exc)
return { ... }In the plugin: Use the structured kwargs as a fast path, with the original text-scan as a fallback for forward compatibility: def _is_retry_exhaustion(failed: bool, error: str, text: str) -> bool:
# Fast path: structured kwargs
if failed and error:
return True
# Fallback: text scan
has_failure = any(pat in text for pat in FAILURE_PATTERNS)
has_error = any(pat in text for pat in ERROR_PATTERNS)
return has_failure and has_errorPhase 5: Debug Logging GotchaThe initial debug line logged raw _log(f"DEBUG kwargs: {kwargs}")Since summary = {
k: (str(v)[:120] + "..." if len(str(v)) > 120 else v)
for k, v in kwargs.items()
if k != "conversation_history"
}Takeaways for Hermes Plugin Authors
|
|
Thanks for this PR — the hooks you wired here are now live on This is an automated hermes-sweeper review. Commit
The commit message notes it was "Salvaged from PR #2823", a related PR. Your work directly informed that merge. One related gap worth a follow-up issue: as @cbonilla20 notes, |
Fixes #2817
Summary
Four lifecycle hooks (
pre_llm_call,post_llm_call,on_session_start,on_session_end) were listed inVALID_HOOKSand accepted byregister_hook()butinvoke_hook()was never called for them - plugincallbacks silently never fired.
on_session_start: fired at end ofAIAgent.__init__aftersession_idis setpre_llm_call/post_llm_call: fired in both_interruptible_api_calland_interruptible_streaming_api_callaround the API request threadon_session_end: fired inrun_conversationafter_persist_sessionTest plan
pre_llm_callreceives correctmessagesandmodelkwargspost_llm_callreceives correctmessages,response, andmodelkwargson_session_start/on_session_endreceive correctsession_idandplatformkwargsVALID_HOOKS