fix(plugins): await async handlers in CLI and TUI dispatch
This commit is contained in:
parent
79cffa9232
commit
ca9a61ae38
9
cli.py
9
cli.py
@ -6582,12 +6582,17 @@ class HermesCLI:
|
||||
self._console_print(f"[bold red]Quick command '{base_cmd}' has unsupported type (supported: 'exec', 'alias')[/]")
|
||||
# Check for plugin-registered slash commands
|
||||
elif base_cmd.lstrip("/") in _get_plugin_cmd_handler_names():
|
||||
from hermes_cli.plugins import get_plugin_command_handler
|
||||
from hermes_cli.plugins import (
|
||||
get_plugin_command_handler,
|
||||
resolve_plugin_command_result,
|
||||
)
|
||||
plugin_handler = get_plugin_command_handler(base_cmd.lstrip("/"))
|
||||
if plugin_handler:
|
||||
user_args = cmd_original[len(base_cmd):].strip()
|
||||
try:
|
||||
result = plugin_handler(user_args)
|
||||
result = resolve_plugin_command_result(
|
||||
plugin_handler(user_args)
|
||||
)
|
||||
if result:
|
||||
_cprint(str(result))
|
||||
except Exception as e:
|
||||
|
||||
@ -33,12 +33,15 @@ so plugin-defined tools appear alongside the built-in tools.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import importlib
|
||||
import importlib.metadata
|
||||
import importlib.util
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import types
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@ -1226,6 +1229,46 @@ def get_plugin_command_handler(name: str) -> Optional[Callable]:
|
||||
return entry["handler"] if entry else None
|
||||
|
||||
|
||||
def resolve_plugin_command_result(result: Any) -> Any:
|
||||
"""Resolve a plugin command return value, awaiting async handlers when needed.
|
||||
|
||||
Sync CLI/TUI dispatch sites call plugin handlers from plain functions.
|
||||
If a handler is async, await it directly when no loop is running; if
|
||||
we're already inside an active loop, run it in a helper thread with its
|
||||
own loop so the caller still gets a concrete result synchronously.
|
||||
"""
|
||||
if not inspect.isawaitable(result):
|
||||
return result
|
||||
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
return asyncio.run(result)
|
||||
|
||||
outcome: Dict[str, Any] = {}
|
||||
failure: Dict[str, BaseException] = {}
|
||||
done = threading.Event()
|
||||
|
||||
def _runner() -> None:
|
||||
try:
|
||||
outcome["value"] = asyncio.run(result)
|
||||
except BaseException as exc: # pragma: no cover - re-raised below
|
||||
failure["exc"] = exc
|
||||
finally:
|
||||
done.set()
|
||||
|
||||
thread = threading.Thread(
|
||||
target=_runner,
|
||||
name="hermes-plugin-command-await",
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
done.wait()
|
||||
if "exc" in failure:
|
||||
raise failure["exc"]
|
||||
return outcome.get("value")
|
||||
|
||||
|
||||
def get_plugin_commands() -> Dict[str, dict]:
|
||||
"""Return the full plugin commands dict (name → {handler, description, plugin}).
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ from hermes_cli.plugins import (
|
||||
get_plugin_command_handler,
|
||||
get_plugin_commands,
|
||||
get_pre_tool_call_block_message,
|
||||
resolve_plugin_command_result,
|
||||
discover_plugins,
|
||||
invoke_hook,
|
||||
)
|
||||
@ -1061,6 +1062,27 @@ class TestPluginCommands:
|
||||
assert mgr._plugin_commands["cmd-b"]["plugin"] == "plugin-b"
|
||||
|
||||
|
||||
class TestPluginCommandResultResolution:
|
||||
def test_returns_sync_values_unchanged(self):
|
||||
assert resolve_plugin_command_result("ok") == "ok"
|
||||
|
||||
def test_awaits_async_result_without_running_loop(self):
|
||||
async def _handler():
|
||||
return "async-ok"
|
||||
|
||||
assert resolve_plugin_command_result(_handler()) == "async-ok"
|
||||
|
||||
def test_awaits_async_result_with_running_loop(self, monkeypatch):
|
||||
class _Loop:
|
||||
pass
|
||||
|
||||
async def _handler():
|
||||
return "threaded-ok"
|
||||
|
||||
monkeypatch.setattr("hermes_cli.plugins.asyncio.get_running_loop", lambda: _Loop())
|
||||
assert resolve_plugin_command_result(_handler()) == "threaded-ok"
|
||||
|
||||
|
||||
# ── TestPluginDispatchTool ────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@ -594,6 +594,24 @@ def test_command_dispatch_returns_skill_payload(server):
|
||||
assert result["name"] == "hermes-agent-dev"
|
||||
|
||||
|
||||
def test_command_dispatch_awaits_async_plugin_handler(server):
|
||||
async def _handler(arg):
|
||||
return f"async:{arg}"
|
||||
|
||||
with patch(
|
||||
"hermes_cli.plugins.get_plugin_command_handler",
|
||||
lambda name: _handler if name == "async-cmd" else None,
|
||||
):
|
||||
resp = server.handle_request({
|
||||
"id": "r-plugin",
|
||||
"method": "command.dispatch",
|
||||
"params": {"name": "async-cmd", "arg": "hello"},
|
||||
})
|
||||
|
||||
assert "error" not in resp
|
||||
assert resp["result"] == {"type": "plugin", "output": "async:hello"}
|
||||
|
||||
|
||||
# ── dispatch(): pool routing for long handlers (#12546) ──────────────
|
||||
|
||||
|
||||
|
||||
@ -4115,11 +4115,15 @@ def _(rid, params: dict) -> dict:
|
||||
return _ok(rid, {"type": "alias", "target": qc.get("target", "")})
|
||||
|
||||
try:
|
||||
from hermes_cli.plugins import get_plugin_command_handler
|
||||
from hermes_cli.plugins import (
|
||||
get_plugin_command_handler,
|
||||
resolve_plugin_command_result,
|
||||
)
|
||||
|
||||
handler = get_plugin_command_handler(name)
|
||||
if handler:
|
||||
return _ok(rid, {"type": "plugin", "output": str(handler(arg) or "")})
|
||||
result = resolve_plugin_command_result(handler(arg))
|
||||
return _ok(rid, {"type": "plugin", "output": str(result or "")})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user