mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
Before the one-gateway runtime each classic CLI loaded its own plugins, so ctx.inject_message(text) without a session key meant that CLI's own chat. On a gateway session the classic CLI now loads no plugin code (PLG-7): the only copy of a plugin is the gateway's (or its plugin host's), which serves every session of its profile, so a key-less inject had no chat to target and was refused. The gateway now picks the target, in order: 1. The plugin is handling a local session's own call (its slash command, a hook fired for that session, a tool or hook in its turn): the message goes to that session. The key is read from the calling context, so this never guesses, however many sessions are open. pre_command deliveries now bind the session they fire for. Under plugins.isolation: host the forwarded ctx call already runs in its caller's context. 2. No session in the calling context (a timer, the plugin's own thread): the one local session of the plugin's profile launched by the classic CLI (frozen policy source "cli") with a viewer attached. TUI, Desktop, messaging and other profiles' sessions are never picked this way. 3. Otherwise (none, or several): refused. inject_message returns False, nothing is injected, and one warning per plugin and reason (rate limited) names the plugin and says to pass a session key. A resolved target is committed through the keyed path's producer admission: a queued input in that session's FIFO, run once and in order. A key-less call made for a messaging chat keeps main's refusal; a keyed inject is unchanged; allow_gateway_injection is still required. The classic chat view's client-side inject slot (gateway_chat_inject) is removed: nothing in that process calls it any more. Off the gateway the in-process _cli_ref path is unchanged. The plugin guide describes the three rules and recommends passing a session key. Refs #134889 (PLG-7) Refs #139523 Refs #139847
71 lines
3.6 KiB
Python
71 lines
3.6 KiB
Python
"""Host routing for ``PluginContext.inject_message``: classic CLI REPL, Ink TUI / desktop, messaging
|
|
gateway (existing ``session_key`` or a new session at ``origin``)."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Mapping
|
|
from typing import Any, Optional
|
|
|
|
logger = logging.getLogger("hermes_cli.plugins")
|
|
|
|
|
|
def inject_plugin_message(
|
|
ctx: Any, content: str, role: str, *, session_key: Optional[str],
|
|
origin: Optional[Mapping[str, Any]],
|
|
) -> bool:
|
|
manager = ctx._manager
|
|
msg = content if role == "user" else f"[{role}] {content}"
|
|
cli = manager._cli_ref
|
|
# An origin names a messaging chat; the local REPL is never that chat.
|
|
if cli is not None and origin is None:
|
|
queue_ = cli._interrupt_queue if getattr(cli, "_agent_running", False) else cli._pending_input
|
|
queue_.put(msg)
|
|
return True
|
|
if session_key and origin is not None:
|
|
logger.warning("inject_message: pass session_key or origin, not both")
|
|
return False
|
|
# Neither: the live gateway picks the chat (gateway/plugin_keyless_inject.py) from the session this
|
|
# call works for, read here on the caller's own context; no gateway keeps main's refusal.
|
|
keyless = not session_key and origin is None
|
|
if keyless and not manager.has_gateway_message_injector:
|
|
logger.warning("inject_message: gateway mode requires an existing session_key or an origin")
|
|
return False
|
|
if origin is not None and not isinstance(origin, Mapping):
|
|
logger.warning("inject_message: origin must be a mapping in the SessionSource.to_dict() shape")
|
|
return False
|
|
if not ctx._gateway_injection_allowed():
|
|
logger.warning("inject_message: gateway injection denied for plugin %s; set "
|
|
"plugins.entries.%s.allow_gateway_injection: true to allow it",
|
|
ctx.plugin_id, ctx.plugin_id)
|
|
return False
|
|
# TUI/desktop host is a different slot. It accepts only when it owns this
|
|
# session_key; a miss falls through so a co-resident messaging gateway
|
|
# still receives its own keys. An exception fails closed — do not also
|
|
# hand the same text to the gateway.
|
|
if session_key and manager.has_tui_message_injector:
|
|
try:
|
|
if manager.inject_tui_message(session_key=session_key, content=msg, plugin_id=ctx.plugin_id):
|
|
return True
|
|
except Exception:
|
|
logger.warning("inject_message: TUI scheduling failed for plugin %s", ctx.plugin_id, exc_info=True)
|
|
return False
|
|
if not manager.has_gateway_message_injector:
|
|
logger.warning("inject_message: no live gateway is available")
|
|
return False
|
|
# The manager's immutable home is the plugin's profile: the gateway creates an origin's session
|
|
# there or nowhere, and a keyed local session must belong to it (never the ambient HERMES_HOME
|
|
# of whatever thread called us).
|
|
if keyless:
|
|
from gateway.session_context import bound_session_key
|
|
target: dict[str, Any] = {"keyless": True, "calling_session_key": bound_session_key(),
|
|
"plugin_home": manager.home_path}
|
|
elif origin is None:
|
|
target = {"session_key": session_key, "plugin_home": manager.home_path}
|
|
else:
|
|
target = {"origin": dict(origin), "plugin_home": manager.home_path}
|
|
try:
|
|
return bool(manager.inject_gateway_message(**target, content=msg, plugin_id=ctx.plugin_id))
|
|
except Exception:
|
|
logger.warning("inject_message: gateway scheduling failed for plugin %s", ctx.plugin_id, exc_info=True)
|
|
return False
|