revert: remove trailing empty assistant message stripping

Reverts the sanitizer addition from PR #2466 (originally #2129).
We already have _empty_content_retries handling for reasoning-only
responses. The trailing strip risks silently eating valid messages
and is redundant with existing empty-content handling.
This commit is contained in:
Teknium 2026-03-22 04:55:34 -07:00
parent 5407d12bc6
commit 34be3f8be6
No known key found for this signature in database
8 changed files with 220 additions and 152 deletions

View file

@ -23,12 +23,6 @@ Tool registration
-----------------
``PluginContext.register_tool()`` delegates to ``tools.registry.register()``
so plugin-defined tools appear alongside the built-in tools.
Slash command registration
--------------------------
``PluginContext.register_command()`` adds a slash command to the central
``COMMAND_REGISTRY`` so it appears in /help, autocomplete, and gateway
dispatch. Handlers receive the argument string and return a response.
"""
from __future__ import annotations
@ -101,7 +95,6 @@ class LoadedPlugin:
module: Optional[types.ModuleType] = None
tools_registered: List[str] = field(default_factory=list)
hooks_registered: List[str] = field(default_factory=list)
commands_registered: List[str] = field(default_factory=list)
enabled: bool = False
error: Optional[str] = None
@ -148,45 +141,6 @@ class PluginContext:
self._manager._plugin_tool_names.add(name)
logger.debug("Plugin %s registered tool: %s", self.manifest.name, name)
# -- command registration ------------------------------------------------
def register_command(
self,
name: str,
handler: Callable,
description: str = "",
aliases: tuple[str, ...] = (),
args_hint: str = "",
cli_only: bool = False,
gateway_only: bool = False,
) -> None:
"""Register a slash command in the central command registry.
The *handler* is called with a single ``args`` string (everything
after the command name) and should return a string to display to the
user, or ``None`` for no output. Async handlers are also supported
(they will be awaited in the gateway).
The command automatically appears in ``/help``, tab-autocomplete,
Telegram bot menu, Slack subcommand mapping, and gateway dispatch.
"""
from hermes_cli.commands import CommandDef, register_plugin_command
cmd_def = CommandDef(
name=name,
description=description or f"Plugin command: {name}",
category="Plugins",
aliases=aliases,
args_hint=args_hint,
cli_only=cli_only,
gateway_only=gateway_only,
)
register_plugin_command(cmd_def)
self._manager._plugin_commands[name] = handler
for alias in aliases:
self._manager._plugin_commands[alias] = handler
logger.debug("Plugin %s registered command: /%s", self.manifest.name, name)
# -- hook registration --------------------------------------------------
def register_hook(self, hook_name: str, callback: Callable) -> None:
@ -218,7 +172,6 @@ class PluginManager:
self._plugins: Dict[str, LoadedPlugin] = {}
self._hooks: Dict[str, List[Callable]] = {}
self._plugin_tool_names: Set[str] = set()
self._plugin_commands: Dict[str, Callable] = {}
self._discovered: bool = False
# -----------------------------------------------------------------------
@ -372,14 +325,6 @@ class PluginManager:
for h in p.hooks_registered
}
)
loaded.commands_registered = [
c for c in self._plugin_commands
if c not in {
n
for name, p in self._plugins.items()
for n in p.commands_registered
}
]
loaded.enabled = True
except Exception as exc:
@ -475,7 +420,6 @@ class PluginManager:
"enabled": loaded.enabled,
"tools": len(loaded.tools_registered),
"hooks": len(loaded.hooks_registered),
"commands": len(loaded.commands_registered),
"error": loaded.error,
}
)
@ -512,6 +456,46 @@ def get_plugin_tool_names() -> Set[str]:
return get_plugin_manager()._plugin_tool_names
def get_plugin_command_handler(name: str) -> Optional[Callable]:
"""Return the handler for a plugin-registered slash command, or None."""
return get_plugin_manager()._plugin_commands.get(name)
def get_plugin_toolsets() -> List[tuple]:
"""Return plugin toolsets as ``(key, label, description)`` tuples.
Used by the ``hermes tools`` TUI so plugin-provided toolsets appear
alongside the built-in ones and can be toggled on/off per platform.
"""
manager = get_plugin_manager()
if not manager._plugin_tool_names:
return []
try:
from tools.registry import registry
except Exception:
return []
# Group plugin tool names by their toolset
toolset_tools: Dict[str, List[str]] = {}
toolset_plugin: Dict[str, LoadedPlugin] = {}
for tool_name in manager._plugin_tool_names:
entry = registry._tools.get(tool_name)
if not entry:
continue
ts = entry.toolset
toolset_tools.setdefault(ts, []).append(entry.name)
# Map toolsets back to the plugin that registered them
for _name, loaded in manager._plugins.items():
for tool_name in loaded.tools_registered:
entry = registry._tools.get(tool_name)
if entry and entry.toolset in toolset_tools:
toolset_plugin.setdefault(entry.toolset, loaded)
result = []
for ts_key in sorted(toolset_tools):
plugin = toolset_plugin.get(ts_key)
label = f"🔌 {ts_key.replace('_', ' ').title()}"
if plugin and plugin.manifest.description:
desc = plugin.manifest.description
else:
desc = ", ".join(sorted(toolset_tools[ts_key]))
result.append((ts_key, label, desc))
return result