revert: remove trailing empty assistant message stripping (#2471)
revert: remove trailing empty assistant message stripping
This commit is contained in:
commit
fd32e3d6e8
8 changed files with 220 additions and 152 deletions
|
|
@ -1625,11 +1625,11 @@ def show_config():
|
||||||
print(f" Timeout: {terminal.get('timeout', 60)}s")
|
print(f" Timeout: {terminal.get('timeout', 60)}s")
|
||||||
|
|
||||||
if terminal.get('backend') == 'docker':
|
if terminal.get('backend') == 'docker':
|
||||||
print(f" Docker image: {terminal.get('docker_image', 'python:3.11-slim')}")
|
print(f" Docker image: {terminal.get('docker_image', 'nikolaik/python-nodejs:python3.11-nodejs20')}")
|
||||||
elif terminal.get('backend') == 'singularity':
|
elif terminal.get('backend') == 'singularity':
|
||||||
print(f" Image: {terminal.get('singularity_image', 'docker://python:3.11')}")
|
print(f" Image: {terminal.get('singularity_image', 'docker://nikolaik/python-nodejs:python3.11-nodejs20')}")
|
||||||
elif terminal.get('backend') == 'modal':
|
elif terminal.get('backend') == 'modal':
|
||||||
print(f" Modal image: {terminal.get('modal_image', 'python:3.11')}")
|
print(f" Modal image: {terminal.get('modal_image', 'nikolaik/python-nodejs:python3.11-nodejs20')}")
|
||||||
modal_token = get_env_value('MODAL_TOKEN_ID')
|
modal_token = get_env_value('MODAL_TOKEN_ID')
|
||||||
print(f" Modal token: {'configured' if modal_token else '(not set)'}")
|
print(f" Modal token: {'configured' if modal_token else '(not set)'}")
|
||||||
elif terminal.get('backend') == 'daytona':
|
elif terminal.get('backend') == 'daytona':
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,6 @@ Tool registration
|
||||||
-----------------
|
-----------------
|
||||||
``PluginContext.register_tool()`` delegates to ``tools.registry.register()``
|
``PluginContext.register_tool()`` delegates to ``tools.registry.register()``
|
||||||
so plugin-defined tools appear alongside the built-in tools.
|
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
|
from __future__ import annotations
|
||||||
|
|
@ -101,7 +95,6 @@ class LoadedPlugin:
|
||||||
module: Optional[types.ModuleType] = None
|
module: Optional[types.ModuleType] = None
|
||||||
tools_registered: List[str] = field(default_factory=list)
|
tools_registered: List[str] = field(default_factory=list)
|
||||||
hooks_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
|
enabled: bool = False
|
||||||
error: Optional[str] = None
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
|
@ -148,45 +141,6 @@ class PluginContext:
|
||||||
self._manager._plugin_tool_names.add(name)
|
self._manager._plugin_tool_names.add(name)
|
||||||
logger.debug("Plugin %s registered tool: %s", self.manifest.name, 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 --------------------------------------------------
|
# -- hook registration --------------------------------------------------
|
||||||
|
|
||||||
def register_hook(self, hook_name: str, callback: Callable) -> None:
|
def register_hook(self, hook_name: str, callback: Callable) -> None:
|
||||||
|
|
@ -218,7 +172,6 @@ class PluginManager:
|
||||||
self._plugins: Dict[str, LoadedPlugin] = {}
|
self._plugins: Dict[str, LoadedPlugin] = {}
|
||||||
self._hooks: Dict[str, List[Callable]] = {}
|
self._hooks: Dict[str, List[Callable]] = {}
|
||||||
self._plugin_tool_names: Set[str] = set()
|
self._plugin_tool_names: Set[str] = set()
|
||||||
self._plugin_commands: Dict[str, Callable] = {}
|
|
||||||
self._discovered: bool = False
|
self._discovered: bool = False
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|
@ -372,14 +325,6 @@ class PluginManager:
|
||||||
for h in p.hooks_registered
|
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
|
loaded.enabled = True
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|
@ -475,7 +420,6 @@ class PluginManager:
|
||||||
"enabled": loaded.enabled,
|
"enabled": loaded.enabled,
|
||||||
"tools": len(loaded.tools_registered),
|
"tools": len(loaded.tools_registered),
|
||||||
"hooks": len(loaded.hooks_registered),
|
"hooks": len(loaded.hooks_registered),
|
||||||
"commands": len(loaded.commands_registered),
|
|
||||||
"error": loaded.error,
|
"error": loaded.error,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -512,6 +456,46 @@ def get_plugin_tool_names() -> Set[str]:
|
||||||
return get_plugin_manager()._plugin_tool_names
|
return get_plugin_manager()._plugin_tool_names
|
||||||
|
|
||||||
|
|
||||||
def get_plugin_command_handler(name: str) -> Optional[Callable]:
|
def get_plugin_toolsets() -> List[tuple]:
|
||||||
"""Return the handler for a plugin-registered slash command, or None."""
|
"""Return plugin toolsets as ``(key, label, description)`` tuples.
|
||||||
return get_plugin_manager()._plugin_commands.get(name)
|
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ Interactive setup wizard for Hermes Agent.
|
||||||
Modular wizard with independently-runnable sections:
|
Modular wizard with independently-runnable sections:
|
||||||
1. Model & Provider — choose your AI provider and model
|
1. Model & Provider — choose your AI provider and model
|
||||||
2. Terminal Backend — where your agent runs commands
|
2. Terminal Backend — where your agent runs commands
|
||||||
3. Messaging Platforms — connect Telegram, Discord, etc.
|
3. Agent Settings — iterations, compression, session reset
|
||||||
4. Tools — configure TTS, web search, image generation, etc.
|
4. Messaging Platforms — connect Telegram, Discord, etc.
|
||||||
5. Agent Settings — iterations, compression, session reset
|
5. Tools — configure TTS, web search, image generation, etc.
|
||||||
|
|
||||||
Config files are stored in ~/.hermes/ for easy access.
|
Config files are stored in ~/.hermes/ for easy access.
|
||||||
"""
|
"""
|
||||||
|
|
@ -2037,7 +2037,7 @@ def setup_terminal_backend(config: dict):
|
||||||
|
|
||||||
# Docker image
|
# Docker image
|
||||||
current_image = config.get("terminal", {}).get(
|
current_image = config.get("terminal", {}).get(
|
||||||
"docker_image", "python:3.11-slim"
|
"docker_image", "nikolaik/python-nodejs:python3.11-nodejs20"
|
||||||
)
|
)
|
||||||
image = prompt(" Docker image", current_image)
|
image = prompt(" Docker image", current_image)
|
||||||
config["terminal"]["docker_image"] = image
|
config["terminal"]["docker_image"] = image
|
||||||
|
|
@ -2059,7 +2059,7 @@ def setup_terminal_backend(config: dict):
|
||||||
print_info(f"Found: {sing_bin}")
|
print_info(f"Found: {sing_bin}")
|
||||||
|
|
||||||
current_image = config.get("terminal", {}).get(
|
current_image = config.get("terminal", {}).get(
|
||||||
"singularity_image", "docker://python:3.11-slim"
|
"singularity_image", "docker://nikolaik/python-nodejs:python3.11-nodejs20"
|
||||||
)
|
)
|
||||||
image = prompt(" Container image", current_image)
|
image = prompt(" Container image", current_image)
|
||||||
config["terminal"]["singularity_image"] = image
|
config["terminal"]["singularity_image"] = image
|
||||||
|
|
@ -2261,7 +2261,7 @@ def setup_agent_settings(config: dict):
|
||||||
)
|
)
|
||||||
print_info("Maximum tool-calling iterations per conversation.")
|
print_info("Maximum tool-calling iterations per conversation.")
|
||||||
print_info("Higher = more complex tasks, but costs more tokens.")
|
print_info("Higher = more complex tasks, but costs more tokens.")
|
||||||
print_info("Recommended: 30-60 for most tasks, 100+ for open exploration.")
|
print_info("Default is 90, which works for most tasks. Use 150+ for open exploration.")
|
||||||
|
|
||||||
max_iter_str = prompt("Max iterations", current_max)
|
max_iter_str = prompt("Max iterations", current_max)
|
||||||
try:
|
try:
|
||||||
|
|
@ -2303,7 +2303,7 @@ def setup_agent_settings(config: dict):
|
||||||
|
|
||||||
config.setdefault("compression", {})["enabled"] = True
|
config.setdefault("compression", {})["enabled"] = True
|
||||||
|
|
||||||
current_threshold = config.get("compression", {}).get("threshold", 0.85)
|
current_threshold = config.get("compression", {}).get("threshold", 0.50)
|
||||||
threshold_str = prompt("Compression threshold (0.5-0.95)", str(current_threshold))
|
threshold_str = prompt("Compression threshold (0.5-0.95)", str(current_threshold))
|
||||||
try:
|
try:
|
||||||
threshold = float(threshold_str)
|
threshold = float(threshold_str)
|
||||||
|
|
@ -2313,7 +2313,7 @@ def setup_agent_settings(config: dict):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
print_success(
|
print_success(
|
||||||
f"Context compression threshold set to {config['compression'].get('threshold', 0.85)}"
|
f"Context compression threshold set to {config['compression'].get('threshold', 0.50)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Session Reset Policy ──
|
# ── Session Reset Policy ──
|
||||||
|
|
@ -3248,9 +3248,9 @@ def run_setup_wizard(args):
|
||||||
print_info("We'll walk you through:")
|
print_info("We'll walk you through:")
|
||||||
print_info(" 1. Model & Provider — choose your AI provider and model")
|
print_info(" 1. Model & Provider — choose your AI provider and model")
|
||||||
print_info(" 2. Terminal Backend — where your agent runs commands")
|
print_info(" 2. Terminal Backend — where your agent runs commands")
|
||||||
print_info(" 3. Messaging Platforms — connect Telegram, Discord, etc.")
|
print_info(" 3. Agent Settings — iterations, compression, session reset")
|
||||||
print_info(" 4. Tools — configure TTS, web search, image generation, etc.")
|
print_info(" 4. Messaging Platforms — connect Telegram, Discord, etc.")
|
||||||
print_info(" 5. Agent Settings — iterations, compression, session reset")
|
print_info(" 5. Tools — configure TTS, web search, image generation, etc.")
|
||||||
print()
|
print()
|
||||||
print_info("Press Enter to begin, or Ctrl+C to exit.")
|
print_info("Press Enter to begin, or Ctrl+C to exit.")
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,30 @@ CONFIGURABLE_TOOLSETS = [
|
||||||
# but the setup checklist won't pre-select them for first-time users.
|
# but the setup checklist won't pre-select them for first-time users.
|
||||||
_DEFAULT_OFF_TOOLSETS = {"moa", "homeassistant", "rl"}
|
_DEFAULT_OFF_TOOLSETS = {"moa", "homeassistant", "rl"}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_effective_configurable_toolsets():
|
||||||
|
"""Return CONFIGURABLE_TOOLSETS + any plugin-provided toolsets.
|
||||||
|
|
||||||
|
Plugin toolsets are appended at the end so they appear after the
|
||||||
|
built-in toolsets in the TUI checklist.
|
||||||
|
"""
|
||||||
|
result = list(CONFIGURABLE_TOOLSETS)
|
||||||
|
try:
|
||||||
|
from hermes_cli.plugins import get_plugin_toolsets
|
||||||
|
result.extend(get_plugin_toolsets())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _get_plugin_toolset_keys() -> set:
|
||||||
|
"""Return the set of toolset keys provided by plugins."""
|
||||||
|
try:
|
||||||
|
from hermes_cli.plugins import get_plugin_toolsets
|
||||||
|
return {ts_key for ts_key, _, _ in get_plugin_toolsets()}
|
||||||
|
except Exception:
|
||||||
|
return set()
|
||||||
|
|
||||||
# Platform display config
|
# Platform display config
|
||||||
PLATFORMS = {
|
PLATFORMS = {
|
||||||
"cli": {"label": "🖥️ CLI", "default_toolset": "hermes-cli"},
|
"cli": {"label": "🖥️ CLI", "default_toolset": "hermes-cli"},
|
||||||
|
|
@ -367,71 +391,72 @@ def _get_platform_tools(config: dict, platform: str) -> Set[str]:
|
||||||
default_ts = PLATFORMS[platform]["default_toolset"]
|
default_ts = PLATFORMS[platform]["default_toolset"]
|
||||||
toolset_names = [default_ts]
|
toolset_names = [default_ts]
|
||||||
|
|
||||||
configurable_keys = {ts_key for ts_key, _, _ in CONFIGURABLE_TOOLSETS}
|
# Resolve to individual tool names, then map back to which
|
||||||
|
# configurable toolsets are covered
|
||||||
# If the saved list contains any configurable keys directly, the user
|
|
||||||
# has explicitly configured this platform — use direct membership.
|
|
||||||
# This avoids the subset-inference bug where composite toolsets like
|
|
||||||
# "hermes-cli" (which include all _HERMES_CORE_TOOLS) cause disabled
|
|
||||||
# toolsets to re-appear as enabled.
|
|
||||||
has_explicit_config = any(ts in configurable_keys for ts in toolset_names)
|
|
||||||
|
|
||||||
if has_explicit_config:
|
|
||||||
return {ts for ts in toolset_names if ts in configurable_keys}
|
|
||||||
|
|
||||||
# No explicit config — fall back to resolving composite toolset names
|
|
||||||
# (e.g. "hermes-cli") to individual tool names and reverse-mapping.
|
|
||||||
all_tool_names = set()
|
all_tool_names = set()
|
||||||
for ts_name in toolset_names:
|
for ts_name in toolset_names:
|
||||||
all_tool_names.update(resolve_toolset(ts_name))
|
all_tool_names.update(resolve_toolset(ts_name))
|
||||||
|
|
||||||
|
# Map individual tool names back to configurable toolset keys
|
||||||
enabled_toolsets = set()
|
enabled_toolsets = set()
|
||||||
for ts_key, _, _ in CONFIGURABLE_TOOLSETS:
|
for ts_key, _, _ in CONFIGURABLE_TOOLSETS:
|
||||||
ts_tools = set(resolve_toolset(ts_key))
|
ts_tools = set(resolve_toolset(ts_key))
|
||||||
if ts_tools and ts_tools.issubset(all_tool_names):
|
if ts_tools and ts_tools.issubset(all_tool_names):
|
||||||
enabled_toolsets.add(ts_key)
|
enabled_toolsets.add(ts_key)
|
||||||
|
|
||||||
|
# Plugin toolsets: enabled by default unless explicitly disabled.
|
||||||
|
# A plugin toolset is "known" for a platform once `hermes tools`
|
||||||
|
# has been saved for that platform (tracked via known_plugin_toolsets).
|
||||||
|
# Unknown plugins default to enabled; known-but-absent = disabled.
|
||||||
|
plugin_ts_keys = _get_plugin_toolset_keys()
|
||||||
|
if plugin_ts_keys:
|
||||||
|
known_map = config.get("known_plugin_toolsets", {})
|
||||||
|
known_for_platform = set(known_map.get(platform, []))
|
||||||
|
for pts in plugin_ts_keys:
|
||||||
|
if pts in toolset_names:
|
||||||
|
# Explicitly listed in config — enabled
|
||||||
|
enabled_toolsets.add(pts)
|
||||||
|
elif pts not in known_for_platform:
|
||||||
|
# New plugin not yet seen by hermes tools — default enabled
|
||||||
|
enabled_toolsets.add(pts)
|
||||||
|
# else: known but not in config = user disabled it
|
||||||
|
|
||||||
return enabled_toolsets
|
return enabled_toolsets
|
||||||
|
|
||||||
|
|
||||||
def _save_platform_tools(config: dict, platform: str, enabled_toolset_keys: Set[str]):
|
def _save_platform_tools(config: dict, platform: str, enabled_toolset_keys: Set[str]):
|
||||||
"""Save the selected toolset keys for a platform to config.
|
"""Save the selected toolset keys for a platform to config.
|
||||||
|
|
||||||
Preserves any non-configurable, non-composite entries (like MCP server
|
Preserves any non-configurable toolset entries (like MCP server names)
|
||||||
names) that were already in the config for this platform.
|
that were already in the config for this platform.
|
||||||
|
|
||||||
Composite platform toolsets (hermes-cli, hermes-telegram, etc.) are
|
|
||||||
dropped once the user has explicitly configured individual toolsets —
|
|
||||||
keeping them would override the user's selections because they include
|
|
||||||
all tools via _HERMES_CORE_TOOLS.
|
|
||||||
"""
|
"""
|
||||||
from toolsets import TOOLSETS
|
|
||||||
|
|
||||||
config.setdefault("platform_toolsets", {})
|
config.setdefault("platform_toolsets", {})
|
||||||
|
|
||||||
# Keys the user can toggle in the checklist UI
|
# Get the set of all configurable toolset keys (built-in + plugin)
|
||||||
configurable_keys = {ts_key for ts_key, _, _ in CONFIGURABLE_TOOLSETS}
|
configurable_keys = {ts_key for ts_key, _, _ in CONFIGURABLE_TOOLSETS}
|
||||||
|
plugin_keys = _get_plugin_toolset_keys()
|
||||||
# Keys that are known composite/individual toolsets in toolsets.py
|
configurable_keys |= plugin_keys
|
||||||
# (hermes-cli, hermes-telegram, homeassistant, web, terminal, etc.)
|
|
||||||
known_toolset_keys = set(TOOLSETS.keys())
|
|
||||||
|
|
||||||
# Get existing toolsets for this platform
|
# Get existing toolsets for this platform
|
||||||
existing_toolsets = config.get("platform_toolsets", {}).get(platform, [])
|
existing_toolsets = config.get("platform_toolsets", {}).get(platform, [])
|
||||||
if not isinstance(existing_toolsets, list):
|
if not isinstance(existing_toolsets, list):
|
||||||
existing_toolsets = []
|
existing_toolsets = []
|
||||||
|
|
||||||
# Preserve entries that are neither configurable toolsets nor known
|
# Preserve any entries that are NOT configurable toolsets (i.e. MCP server names)
|
||||||
# composite toolsets — this keeps MCP server names and other custom
|
|
||||||
# entries while dropping composites like "hermes-cli" that would
|
|
||||||
# silently re-enable everything the user just disabled.
|
|
||||||
preserved_entries = {
|
preserved_entries = {
|
||||||
entry for entry in existing_toolsets
|
entry for entry in existing_toolsets
|
||||||
if entry not in configurable_keys and entry not in known_toolset_keys
|
if entry not in configurable_keys
|
||||||
}
|
}
|
||||||
|
|
||||||
# Merge preserved entries with new enabled toolsets
|
# Merge preserved entries with new enabled toolsets
|
||||||
config["platform_toolsets"][platform] = sorted(enabled_toolset_keys | preserved_entries)
|
config["platform_toolsets"][platform] = sorted(enabled_toolset_keys | preserved_entries)
|
||||||
|
|
||||||
|
# Track which plugin toolsets are "known" for this platform so we can
|
||||||
|
# distinguish "new plugin, default enabled" from "user disabled it".
|
||||||
|
if plugin_keys:
|
||||||
|
config.setdefault("known_plugin_toolsets", {})
|
||||||
|
config["known_plugin_toolsets"][platform] = sorted(plugin_keys)
|
||||||
|
|
||||||
save_config(config)
|
save_config(config)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -549,15 +574,17 @@ def _prompt_toolset_checklist(platform_label: str, enabled: Set[str]) -> Set[str
|
||||||
"""Multi-select checklist of toolsets. Returns set of selected toolset keys."""
|
"""Multi-select checklist of toolsets. Returns set of selected toolset keys."""
|
||||||
from hermes_cli.curses_ui import curses_checklist
|
from hermes_cli.curses_ui import curses_checklist
|
||||||
|
|
||||||
|
effective = _get_effective_configurable_toolsets()
|
||||||
|
|
||||||
labels = []
|
labels = []
|
||||||
for ts_key, ts_label, ts_desc in CONFIGURABLE_TOOLSETS:
|
for ts_key, ts_label, ts_desc in effective:
|
||||||
suffix = ""
|
suffix = ""
|
||||||
if not _toolset_has_keys(ts_key) and (TOOL_CATEGORIES.get(ts_key) or TOOLSET_ENV_REQUIREMENTS.get(ts_key)):
|
if not _toolset_has_keys(ts_key) and (TOOL_CATEGORIES.get(ts_key) or TOOLSET_ENV_REQUIREMENTS.get(ts_key)):
|
||||||
suffix = " [no API key]"
|
suffix = " [no API key]"
|
||||||
labels.append(f"{ts_label} ({ts_desc}){suffix}")
|
labels.append(f"{ts_label} ({ts_desc}){suffix}")
|
||||||
|
|
||||||
pre_selected = {
|
pre_selected = {
|
||||||
i for i, (ts_key, _, _) in enumerate(CONFIGURABLE_TOOLSETS)
|
i for i, (ts_key, _, _) in enumerate(effective)
|
||||||
if ts_key in enabled
|
if ts_key in enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -567,7 +594,7 @@ def _prompt_toolset_checklist(platform_label: str, enabled: Set[str]) -> Set[str
|
||||||
pre_selected,
|
pre_selected,
|
||||||
cancel_returns=pre_selected,
|
cancel_returns=pre_selected,
|
||||||
)
|
)
|
||||||
return {CONFIGURABLE_TOOLSETS[i][0] for i in chosen}
|
return {effective[i][0] for i in chosen}
|
||||||
|
|
||||||
|
|
||||||
# ─── Provider-Aware Configuration ────────────────────────────────────────────
|
# ─── Provider-Aware Configuration ────────────────────────────────────────────
|
||||||
|
|
@ -782,7 +809,7 @@ def _configure_simple_requirements(ts_key: str):
|
||||||
if not missing:
|
if not missing:
|
||||||
return
|
return
|
||||||
|
|
||||||
ts_label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts_key), ts_key)
|
ts_label = next((l for k, l, _ in _get_effective_configurable_toolsets() if k == ts_key), ts_key)
|
||||||
print()
|
print()
|
||||||
print(color(f" {ts_label} requires configuration:", Colors.YELLOW))
|
print(color(f" {ts_label} requires configuration:", Colors.YELLOW))
|
||||||
|
|
||||||
|
|
@ -801,7 +828,7 @@ def _reconfigure_tool(config: dict):
|
||||||
"""Let user reconfigure an existing tool's provider or API key."""
|
"""Let user reconfigure an existing tool's provider or API key."""
|
||||||
# Build list of configurable tools that are currently set up
|
# Build list of configurable tools that are currently set up
|
||||||
configurable = []
|
configurable = []
|
||||||
for ts_key, ts_label, _ in CONFIGURABLE_TOOLSETS:
|
for ts_key, ts_label, _ in _get_effective_configurable_toolsets():
|
||||||
cat = TOOL_CATEGORIES.get(ts_key)
|
cat = TOOL_CATEGORIES.get(ts_key)
|
||||||
reqs = TOOLSET_ENV_REQUIREMENTS.get(ts_key)
|
reqs = TOOLSET_ENV_REQUIREMENTS.get(ts_key)
|
||||||
if cat or reqs:
|
if cat or reqs:
|
||||||
|
|
@ -915,7 +942,7 @@ def _reconfigure_simple_requirements(ts_key: str):
|
||||||
if not requirements:
|
if not requirements:
|
||||||
return
|
return
|
||||||
|
|
||||||
ts_label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts_key), ts_key)
|
ts_label = next((l for k, l, _ in _get_effective_configurable_toolsets() if k == ts_key), ts_key)
|
||||||
print()
|
print()
|
||||||
print(color(f" {ts_label}:", Colors.CYAN))
|
print(color(f" {ts_label}:", Colors.CYAN))
|
||||||
|
|
||||||
|
|
@ -954,7 +981,7 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
||||||
|
|
||||||
# Non-interactive summary mode for CLI usage
|
# Non-interactive summary mode for CLI usage
|
||||||
if getattr(args, "summary", False):
|
if getattr(args, "summary", False):
|
||||||
total = len(CONFIGURABLE_TOOLSETS)
|
total = len(_get_effective_configurable_toolsets())
|
||||||
print(color("⚕ Tool Summary", Colors.CYAN, Colors.BOLD))
|
print(color("⚕ Tool Summary", Colors.CYAN, Colors.BOLD))
|
||||||
print()
|
print()
|
||||||
summary = _platform_toolset_summary(config, enabled_platforms)
|
summary = _platform_toolset_summary(config, enabled_platforms)
|
||||||
|
|
@ -965,7 +992,7 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
||||||
print(color(f" {pinfo['label']}", Colors.BOLD) + color(f" ({count}/{total})", Colors.DIM))
|
print(color(f" {pinfo['label']}", Colors.BOLD) + color(f" ({count}/{total})", Colors.DIM))
|
||||||
if enabled:
|
if enabled:
|
||||||
for ts_key in sorted(enabled):
|
for ts_key in sorted(enabled):
|
||||||
label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts_key), ts_key)
|
label = next((l for k, l, _ in _get_effective_configurable_toolsets() if k == ts_key), ts_key)
|
||||||
print(color(f" ✓ {label}", Colors.GREEN))
|
print(color(f" ✓ {label}", Colors.GREEN))
|
||||||
else:
|
else:
|
||||||
print(color(" (none enabled)", Colors.DIM))
|
print(color(" (none enabled)", Colors.DIM))
|
||||||
|
|
@ -992,11 +1019,11 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
||||||
removed = current_enabled - new_enabled
|
removed = current_enabled - new_enabled
|
||||||
if added:
|
if added:
|
||||||
for ts in sorted(added):
|
for ts in sorted(added):
|
||||||
label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts), ts)
|
label = next((l for k, l, _ in _get_effective_configurable_toolsets() if k == ts), ts)
|
||||||
print(color(f" + {label}", Colors.GREEN))
|
print(color(f" + {label}", Colors.GREEN))
|
||||||
if removed:
|
if removed:
|
||||||
for ts in sorted(removed):
|
for ts in sorted(removed):
|
||||||
label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts), ts)
|
label = next((l for k, l, _ in _get_effective_configurable_toolsets() if k == ts), ts)
|
||||||
print(color(f" - {label}", Colors.RED))
|
print(color(f" - {label}", Colors.RED))
|
||||||
|
|
||||||
# Walk through ALL selected tools that have provider options or
|
# Walk through ALL selected tools that have provider options or
|
||||||
|
|
@ -1012,7 +1039,7 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
||||||
print()
|
print()
|
||||||
print(color(f" Configuring {len(to_configure)} tool(s):", Colors.YELLOW))
|
print(color(f" Configuring {len(to_configure)} tool(s):", Colors.YELLOW))
|
||||||
for ts_key in to_configure:
|
for ts_key in to_configure:
|
||||||
label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts_key), ts_key)
|
label = next((l for k, l, _ in _get_effective_configurable_toolsets() if k == ts_key), ts_key)
|
||||||
print(color(f" • {label}", Colors.DIM))
|
print(color(f" • {label}", Colors.DIM))
|
||||||
print(color(" You can skip any tool you don't need right now.", Colors.DIM))
|
print(color(" You can skip any tool you don't need right now.", Colors.DIM))
|
||||||
print()
|
print()
|
||||||
|
|
@ -1034,7 +1061,7 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
||||||
pinfo = PLATFORMS[pkey]
|
pinfo = PLATFORMS[pkey]
|
||||||
current = _get_platform_tools(config, pkey)
|
current = _get_platform_tools(config, pkey)
|
||||||
count = len(current)
|
count = len(current)
|
||||||
total = len(CONFIGURABLE_TOOLSETS)
|
total = len(_get_effective_configurable_toolsets())
|
||||||
platform_choices.append(f"Configure {pinfo['label']} ({count}/{total} enabled)")
|
platform_choices.append(f"Configure {pinfo['label']} ({count}/{total} enabled)")
|
||||||
platform_keys.append(pkey)
|
platform_keys.append(pkey)
|
||||||
|
|
||||||
|
|
@ -1090,10 +1117,10 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
||||||
if added or removed:
|
if added or removed:
|
||||||
print(color(f" {pinfo_inner['label']}:", Colors.DIM))
|
print(color(f" {pinfo_inner['label']}:", Colors.DIM))
|
||||||
for ts in sorted(added):
|
for ts in sorted(added):
|
||||||
label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts), ts)
|
label = next((l for k, l, _ in _get_effective_configurable_toolsets() if k == ts), ts)
|
||||||
print(color(f" + {label}", Colors.GREEN))
|
print(color(f" + {label}", Colors.GREEN))
|
||||||
for ts in sorted(removed):
|
for ts in sorted(removed):
|
||||||
label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts), ts)
|
label = next((l for k, l, _ in _get_effective_configurable_toolsets() if k == ts), ts)
|
||||||
print(color(f" - {label}", Colors.RED))
|
print(color(f" - {label}", Colors.RED))
|
||||||
# Configure API keys for newly enabled tools
|
# Configure API keys for newly enabled tools
|
||||||
for ts_key in sorted(added):
|
for ts_key in sorted(added):
|
||||||
|
|
@ -1106,7 +1133,7 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
||||||
# Update choice labels
|
# Update choice labels
|
||||||
for ci, pk in enumerate(platform_keys):
|
for ci, pk in enumerate(platform_keys):
|
||||||
new_count = len(_get_platform_tools(config, pk))
|
new_count = len(_get_platform_tools(config, pk))
|
||||||
total = len(CONFIGURABLE_TOOLSETS)
|
total = len(_get_effective_configurable_toolsets())
|
||||||
platform_choices[ci] = f"Configure {PLATFORMS[pk]['label']} ({new_count}/{total} enabled)"
|
platform_choices[ci] = f"Configure {PLATFORMS[pk]['label']} ({new_count}/{total} enabled)"
|
||||||
else:
|
else:
|
||||||
print(color(" No changes", Colors.DIM))
|
print(color(" No changes", Colors.DIM))
|
||||||
|
|
@ -1128,11 +1155,11 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
||||||
|
|
||||||
if added:
|
if added:
|
||||||
for ts in sorted(added):
|
for ts in sorted(added):
|
||||||
label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts), ts)
|
label = next((l for k, l, _ in _get_effective_configurable_toolsets() if k == ts), ts)
|
||||||
print(color(f" + {label}", Colors.GREEN))
|
print(color(f" + {label}", Colors.GREEN))
|
||||||
if removed:
|
if removed:
|
||||||
for ts in sorted(removed):
|
for ts in sorted(removed):
|
||||||
label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts), ts)
|
label = next((l for k, l, _ in _get_effective_configurable_toolsets() if k == ts), ts)
|
||||||
print(color(f" - {label}", Colors.RED))
|
print(color(f" - {label}", Colors.RED))
|
||||||
|
|
||||||
# Configure newly enabled toolsets that need API keys
|
# Configure newly enabled toolsets that need API keys
|
||||||
|
|
@ -1151,7 +1178,7 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
||||||
|
|
||||||
# Update the choice label with new count
|
# Update the choice label with new count
|
||||||
new_count = len(_get_platform_tools(config, pkey))
|
new_count = len(_get_platform_tools(config, pkey))
|
||||||
total = len(CONFIGURABLE_TOOLSETS)
|
total = len(_get_effective_configurable_toolsets())
|
||||||
platform_choices[idx] = f"Configure {pinfo['label']} ({new_count}/{total} enabled)"
|
platform_choices[idx] = f"Configure {pinfo['label']} ({new_count}/{total} enabled)"
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
@ -1331,12 +1358,27 @@ def _apply_mcp_change(config: dict, targets: List[str], action: str) -> Set[str]
|
||||||
|
|
||||||
def _print_tools_list(enabled_toolsets: set, mcp_servers: dict, platform: str = "cli"):
|
def _print_tools_list(enabled_toolsets: set, mcp_servers: dict, platform: str = "cli"):
|
||||||
"""Print a summary of enabled/disabled toolsets and MCP tool filters."""
|
"""Print a summary of enabled/disabled toolsets and MCP tool filters."""
|
||||||
|
effective = _get_effective_configurable_toolsets()
|
||||||
|
builtin_keys = {ts_key for ts_key, _, _ in CONFIGURABLE_TOOLSETS}
|
||||||
|
|
||||||
print(f"Built-in toolsets ({platform}):")
|
print(f"Built-in toolsets ({platform}):")
|
||||||
for ts_key, label, _ in CONFIGURABLE_TOOLSETS:
|
for ts_key, label, _ in effective:
|
||||||
|
if ts_key not in builtin_keys:
|
||||||
|
continue
|
||||||
status = (color("✓ enabled", Colors.GREEN) if ts_key in enabled_toolsets
|
status = (color("✓ enabled", Colors.GREEN) if ts_key in enabled_toolsets
|
||||||
else color("✗ disabled", Colors.RED))
|
else color("✗ disabled", Colors.RED))
|
||||||
print(f" {status} {ts_key} {color(label, Colors.DIM)}")
|
print(f" {status} {ts_key} {color(label, Colors.DIM)}")
|
||||||
|
|
||||||
|
# Plugin toolsets
|
||||||
|
plugin_entries = [(k, l) for k, l, _ in effective if k not in builtin_keys]
|
||||||
|
if plugin_entries:
|
||||||
|
print()
|
||||||
|
print(f"Plugin toolsets ({platform}):")
|
||||||
|
for ts_key, label in plugin_entries:
|
||||||
|
status = (color("✓ enabled", Colors.GREEN) if ts_key in enabled_toolsets
|
||||||
|
else color("✗ disabled", Colors.RED))
|
||||||
|
print(f" {status} {ts_key} {color(label, Colors.DIM)}")
|
||||||
|
|
||||||
if mcp_servers:
|
if mcp_servers:
|
||||||
print()
|
print()
|
||||||
print("MCP servers:")
|
print("MCP servers:")
|
||||||
|
|
@ -1375,7 +1417,7 @@ def tools_disable_enable_command(args):
|
||||||
toolset_targets = [t for t in targets if ":" not in t]
|
toolset_targets = [t for t in targets if ":" not in t]
|
||||||
mcp_targets = [t for t in targets if ":" in t]
|
mcp_targets = [t for t in targets if ":" in t]
|
||||||
|
|
||||||
valid_toolsets = {ts_key for ts_key, _, _ in CONFIGURABLE_TOOLSETS}
|
valid_toolsets = {ts_key for ts_key, _, _ in CONFIGURABLE_TOOLSETS} | _get_plugin_toolset_keys()
|
||||||
unknown_toolsets = [t for t in toolset_targets if t not in valid_toolsets]
|
unknown_toolsets = [t for t in toolset_targets if t not in valid_toolsets]
|
||||||
if unknown_toolsets:
|
if unknown_toolsets:
|
||||||
for name in unknown_toolsets:
|
for name in unknown_toolsets:
|
||||||
|
|
|
||||||
|
|
@ -293,15 +293,11 @@ def get_tool_definitions(
|
||||||
for ts_name in get_all_toolsets():
|
for ts_name in get_all_toolsets():
|
||||||
tools_to_include.update(resolve_toolset(ts_name))
|
tools_to_include.update(resolve_toolset(ts_name))
|
||||||
|
|
||||||
# Always include plugin-registered tools — they bypass the toolset filter
|
# Plugin-registered tools are now resolved through the normal toolset
|
||||||
# because their toolsets are dynamic (created at plugin load time).
|
# path — validate_toolset() / resolve_toolset() / get_all_toolsets()
|
||||||
try:
|
# all check the tool registry for plugin-provided toolsets. No bypass
|
||||||
from hermes_cli.plugins import get_plugin_tool_names
|
# needed; plugins respect enabled_toolsets / disabled_toolsets like any
|
||||||
plugin_tools = get_plugin_tool_names()
|
# other toolset.
|
||||||
if plugin_tools:
|
|
||||||
tools_to_include.update(plugin_tools)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Ask the registry for schemas (only returns tools whose check_fn passes)
|
# Ask the registry for schemas (only returns tools whose check_fn passes)
|
||||||
filtered_tools = registry.get_definitions(tools_to_include, quiet=quiet_mode)
|
filtered_tools = registry.get_definitions(tools_to_include, quiet=quiet_mode)
|
||||||
|
|
|
||||||
12
run_agent.py
12
run_agent.py
|
|
@ -2443,18 +2443,6 @@ class AIAgent:
|
||||||
"Pre-call sanitizer: added %d stub tool result(s)",
|
"Pre-call sanitizer: added %d stub tool result(s)",
|
||||||
len(missing_results),
|
len(missing_results),
|
||||||
)
|
)
|
||||||
# 3. Strip trailing empty assistant messages to prevent prefill rejection.
|
|
||||||
# These can leak from Responses API reasoning-only turns (Codex/MiniMax)
|
|
||||||
# where an empty assistant message is required by the Responses API but
|
|
||||||
# must NOT be sent to Chat Completions or Anthropic Messages API providers.
|
|
||||||
while (
|
|
||||||
messages
|
|
||||||
and messages[-1].get("role") == "assistant"
|
|
||||||
and not (messages[-1].get("content") or "").strip()
|
|
||||||
and not messages[-1].get("tool_calls")
|
|
||||||
):
|
|
||||||
logger.debug("Pre-call sanitizer: removed trailing empty assistant message")
|
|
||||||
messages = messages[:-1]
|
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
||||||
|
|
@ -282,7 +282,7 @@ class TestPluginToolVisibility:
|
||||||
"""Plugin-registered tools appear in get_tool_definitions()."""
|
"""Plugin-registered tools appear in get_tool_definitions()."""
|
||||||
|
|
||||||
def test_plugin_tools_in_definitions(self, tmp_path, monkeypatch):
|
def test_plugin_tools_in_definitions(self, tmp_path, monkeypatch):
|
||||||
"""Tools from plugins bypass the toolset filter."""
|
"""Plugin tools are included when their toolset is in enabled_toolsets."""
|
||||||
import hermes_cli.plugins as plugins_mod
|
import hermes_cli.plugins as plugins_mod
|
||||||
|
|
||||||
plugins_dir = tmp_path / "hermes_test" / "plugins"
|
plugins_dir = tmp_path / "hermes_test" / "plugins"
|
||||||
|
|
@ -305,10 +305,22 @@ class TestPluginToolVisibility:
|
||||||
monkeypatch.setattr(plugins_mod, "_plugin_manager", mgr)
|
monkeypatch.setattr(plugins_mod, "_plugin_manager", mgr)
|
||||||
|
|
||||||
from model_tools import get_tool_definitions
|
from model_tools import get_tool_definitions
|
||||||
tools = get_tool_definitions(enabled_toolsets=["terminal"], quiet_mode=True)
|
|
||||||
|
# Plugin tools are included when their toolset is explicitly enabled
|
||||||
|
tools = get_tool_definitions(enabled_toolsets=["terminal", "plugin_vis_plugin"], quiet_mode=True)
|
||||||
tool_names = [t["function"]["name"] for t in tools]
|
tool_names = [t["function"]["name"] for t in tools]
|
||||||
assert "vis_tool" in tool_names
|
assert "vis_tool" in tool_names
|
||||||
|
|
||||||
|
# Plugin tools are excluded when only other toolsets are enabled
|
||||||
|
tools2 = get_tool_definitions(enabled_toolsets=["terminal"], quiet_mode=True)
|
||||||
|
tool_names2 = [t["function"]["name"] for t in tools2]
|
||||||
|
assert "vis_tool" not in tool_names2
|
||||||
|
|
||||||
|
# Plugin tools are included when no toolset filter is active (all enabled)
|
||||||
|
tools3 = get_tool_definitions(quiet_mode=True)
|
||||||
|
tool_names3 = [t["function"]["name"] for t in tools3]
|
||||||
|
assert "vis_tool" in tool_names3
|
||||||
|
|
||||||
|
|
||||||
# ── TestPluginManagerList ──────────────────────────────────────────────────
|
# ── TestPluginManagerList ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
52
toolsets.py
52
toolsets.py
|
|
@ -366,6 +366,13 @@ def resolve_toolset(name: str, visited: Set[str] = None) -> List[str]:
|
||||||
# Get toolset definition
|
# Get toolset definition
|
||||||
toolset = TOOLSETS.get(name)
|
toolset = TOOLSETS.get(name)
|
||||||
if not toolset:
|
if not toolset:
|
||||||
|
# Fall back to tool registry for plugin-provided toolsets
|
||||||
|
if name in _get_plugin_toolset_names():
|
||||||
|
try:
|
||||||
|
from tools.registry import registry
|
||||||
|
return [e.name for e in registry._tools.values() if e.toolset == name]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Collect direct tools
|
# Collect direct tools
|
||||||
|
|
@ -400,24 +407,60 @@ def resolve_multiple_toolsets(toolset_names: List[str]) -> List[str]:
|
||||||
return list(all_tools)
|
return list(all_tools)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_plugin_toolset_names() -> Set[str]:
|
||||||
|
"""Return toolset names registered by plugins (from the tool registry).
|
||||||
|
|
||||||
|
These are toolsets that exist in the registry but not in the static
|
||||||
|
``TOOLSETS`` dict — i.e. they were added by plugins at load time.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from tools.registry import registry
|
||||||
|
return {
|
||||||
|
entry.toolset
|
||||||
|
for entry in registry._tools.values()
|
||||||
|
if entry.toolset not in TOOLSETS
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
def get_all_toolsets() -> Dict[str, Dict[str, Any]]:
|
def get_all_toolsets() -> Dict[str, Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Get all available toolsets with their definitions.
|
Get all available toolsets with their definitions.
|
||||||
|
|
||||||
|
Includes both statically-defined toolsets and plugin-registered ones.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: All toolset definitions
|
Dict: All toolset definitions
|
||||||
"""
|
"""
|
||||||
return TOOLSETS.copy()
|
result = TOOLSETS.copy()
|
||||||
|
# Add plugin-provided toolsets (synthetic entries)
|
||||||
|
for ts_name in _get_plugin_toolset_names():
|
||||||
|
if ts_name not in result:
|
||||||
|
try:
|
||||||
|
from tools.registry import registry
|
||||||
|
tools = [e.name for e in registry._tools.values() if e.toolset == ts_name]
|
||||||
|
result[ts_name] = {
|
||||||
|
"description": f"Plugin toolset: {ts_name}",
|
||||||
|
"tools": tools,
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_toolset_names() -> List[str]:
|
def get_toolset_names() -> List[str]:
|
||||||
"""
|
"""
|
||||||
Get names of all available toolsets (excluding aliases).
|
Get names of all available toolsets (excluding aliases).
|
||||||
|
|
||||||
|
Includes plugin-registered toolset names.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[str]: List of toolset names
|
List[str]: List of toolset names
|
||||||
"""
|
"""
|
||||||
return list(TOOLSETS.keys())
|
names = set(TOOLSETS.keys())
|
||||||
|
names |= _get_plugin_toolset_names()
|
||||||
|
return sorted(names)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -435,7 +478,10 @@ def validate_toolset(name: str) -> bool:
|
||||||
# Accept special alias names for convenience
|
# Accept special alias names for convenience
|
||||||
if name in {"all", "*"}:
|
if name in {"all", "*"}:
|
||||||
return True
|
return True
|
||||||
return name in TOOLSETS
|
if name in TOOLSETS:
|
||||||
|
return True
|
||||||
|
# Check tool registry for plugin-provided toolsets
|
||||||
|
return name in _get_plugin_toolset_names()
|
||||||
|
|
||||||
|
|
||||||
def create_custom_toolset(
|
def create_custom_toolset(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue