fix(plugins): await async hook callbacks instead of collecting bare coroutines

Slash-command handlers gained loop-safe awaiting in ca9a61ae38, but
`PluginManager.invoke_hook` still called `async def` hook callbacks directly:
the coroutine object was appended to the results (so `pre_llm_call` context
injection silently did nothing) and Python warned "coroutine was never
awaited". `_invoke_hook_callback` now routes every return through
`resolve_plugin_command_result`, which covers both the direct and the
timeout-bounded paths and is safe under the gateway's running loop.

Fixes #12449 (remaining hook half). Salvage of #63240 by @Bartok9, applied
one layer down so the bounded-worker path is covered too.

Co-authored-by: Bartok9 <Bartok9@users.noreply.github.com>
This commit is contained in:
teknium1
2026-09-12 06:58:54 -07:00
committed by Teknium
parent 1664e12fc7
commit 24444e52eb

View File

@@ -150,18 +150,24 @@ def _hook_uses_callback_timeout(hook_name: str, timeout: float) -> bool:
class PluginDispatchMixin:
@staticmethod
def _invoke_hook_callback(callback: Callable, payload: Dict[str, Any]) -> Any:
"""Invoke a hook while withholding additive fields from narrow legacy callbacks."""
"""Invoke a hook while withholding additive fields from narrow legacy callbacks.
An ``async def`` callback returns a coroutine; resolve it the way plugin slash commands
are (loop-safe), otherwise the bare coroutine object is appended to the results and the
plugin's body never runs (#12449).
"""
from hermes_cli.plugins import resolve_plugin_command_result
try:
parameters = inspect.signature(callback).parameters
except (TypeError, ValueError):
return callback(**payload) # no introspectable signature: historical behavior
return resolve_plugin_command_result(callback(**payload)) # no introspectable signature
if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in parameters.values()):
return callback(**payload)
return resolve_plugin_command_result(callback(**payload))
keyword_kinds = {inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY}
return callback(**{
return resolve_plugin_command_result(callback(**{
name: value for name, value in payload.items()
if name in parameters and parameters[name].kind in keyword_kinds
})
}))
def invoke_hook(self, hook_name: str, **kwargs: Any) -> List[Any]:
"""Call all callbacks for *hook_name*; return their non-``None`` results.