Merge pull request #2268 from NousResearch/hermes/hermes-5d6932ba
fix(tools): disabled toolsets re-enable themselves after hermes tools
This commit is contained in:
commit
52adc8873b
4 changed files with 128 additions and 13 deletions
|
|
@ -2498,8 +2498,22 @@ class GatewayRunner:
|
||||||
|
|
||||||
# Parse provider:model syntax
|
# Parse provider:model syntax
|
||||||
target_provider, new_model = parse_model_input(args, current_provider)
|
target_provider, new_model = parse_model_input(args, current_provider)
|
||||||
|
|
||||||
|
# Detect custom/local provider — skip auto-detection to prevent
|
||||||
|
# silently accepting an OpenRouter model name on a localhost endpoint.
|
||||||
|
# Users must use explicit provider:model syntax to switch away.
|
||||||
|
_resolved_base = ""
|
||||||
|
try:
|
||||||
|
from hermes_cli.runtime_provider import resolve_runtime_provider as _rtp
|
||||||
|
_resolved_base = _rtp(requested=current_provider).get("base_url", "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
is_custom = current_provider == "custom" or (
|
||||||
|
"localhost" in _resolved_base or "127.0.0.1" in _resolved_base
|
||||||
|
)
|
||||||
|
|
||||||
# Auto-detect provider when no explicit provider:model syntax was used
|
# Auto-detect provider when no explicit provider:model syntax was used
|
||||||
if target_provider == current_provider:
|
if target_provider == current_provider and not is_custom:
|
||||||
from hermes_cli.models import detect_provider_for_model
|
from hermes_cli.models import detect_provider_for_model
|
||||||
detected = detect_provider_for_model(new_model, current_provider)
|
detected = detect_provider_for_model(new_model, current_provider)
|
||||||
if detected:
|
if detected:
|
||||||
|
|
@ -2580,7 +2594,18 @@ class GatewayRunner:
|
||||||
# Clear fallback state since user explicitly chose a model
|
# Clear fallback state since user explicitly chose a model
|
||||||
self._effective_model = None
|
self._effective_model = None
|
||||||
self._effective_provider = None
|
self._effective_provider = None
|
||||||
return f"🤖 Model changed to `{new_model}` ({persist_note}){provider_note}{warning}\n_(takes effect on next message)_"
|
|
||||||
|
# Helpful hint when staying on a custom/local endpoint
|
||||||
|
custom_hint = ""
|
||||||
|
if is_custom and not provider_changed:
|
||||||
|
endpoint = _resolved_base or "custom endpoint"
|
||||||
|
custom_hint = (
|
||||||
|
f"\n**Endpoint:** `{endpoint}`"
|
||||||
|
"\n_To switch providers, use_ `/model provider:model`"
|
||||||
|
"\n_e.g._ `/model openrouter:anthropic/claude-sonnet-4`"
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"🤖 Model changed to `{new_model}` ({persist_note}){provider_note}{warning}{custom_hint}\n_(takes effect on next message)_"
|
||||||
|
|
||||||
async def _handle_provider_command(self, event: MessageEvent) -> str:
|
async def _handle_provider_command(self, event: MessageEvent) -> str:
|
||||||
"""Handle /provider command - show available providers."""
|
"""Handle /provider command - show available providers."""
|
||||||
|
|
|
||||||
|
|
@ -367,13 +367,24 @@ 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]
|
||||||
|
|
||||||
# Resolve to individual tool names, then map back to which
|
configurable_keys = {ts_key for ts_key, _, _ in CONFIGURABLE_TOOLSETS}
|
||||||
# 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))
|
||||||
|
|
@ -386,23 +397,37 @@ def _get_platform_tools(config: dict, platform: str) -> Set[str]:
|
||||||
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 toolset entries (like MCP server names)
|
Preserves any non-configurable, non-composite entries (like MCP server
|
||||||
that were already in the config for this platform.
|
names) 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", {})
|
||||||
|
|
||||||
# Get the set of all configurable toolset keys
|
# Keys the user can toggle in the checklist UI
|
||||||
configurable_keys = {ts_key for ts_key, _, _ in CONFIGURABLE_TOOLSETS}
|
configurable_keys = {ts_key for ts_key, _, _ in CONFIGURABLE_TOOLSETS}
|
||||||
|
|
||||||
|
# Keys that are known composite/individual toolsets in toolsets.py
|
||||||
|
# (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 any entries that are NOT configurable toolsets (i.e. MCP server names)
|
# Preserve entries that are neither configurable toolsets nor known
|
||||||
|
# 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
|
if entry not in configurable_keys and entry not in known_toolset_keys
|
||||||
}
|
}
|
||||||
|
|
||||||
# Merge preserved entries with new enabled toolsets
|
# Merge preserved entries with new enabled toolsets
|
||||||
|
|
|
||||||
|
|
@ -214,3 +214,61 @@ class TestSessionSearch:
|
||||||
# Current session should be skipped, only other_sid should appear
|
# Current session should be skipped, only other_sid should appear
|
||||||
assert result["sessions_searched"] == 1
|
assert result["sessions_searched"] == 1
|
||||||
assert current_sid not in [r.get("session_id") for r in result.get("results", [])]
|
assert current_sid not in [r.get("session_id") for r in result.get("results", [])]
|
||||||
|
|
||||||
|
def test_current_child_session_excludes_parent_lineage(self):
|
||||||
|
"""Compression/delegation parents should be excluded for the active child session."""
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
from tools.session_search_tool import session_search
|
||||||
|
|
||||||
|
mock_db = MagicMock()
|
||||||
|
mock_db.search_messages.return_value = [
|
||||||
|
{"session_id": "parent_sid", "content": "match", "source": "cli",
|
||||||
|
"session_started": 1709500000, "model": "test"},
|
||||||
|
]
|
||||||
|
|
||||||
|
def _get_session(session_id):
|
||||||
|
if session_id == "child_sid":
|
||||||
|
return {"parent_session_id": "parent_sid"}
|
||||||
|
if session_id == "parent_sid":
|
||||||
|
return {"parent_session_id": None}
|
||||||
|
return None
|
||||||
|
|
||||||
|
mock_db.get_session.side_effect = _get_session
|
||||||
|
|
||||||
|
result = json.loads(session_search(
|
||||||
|
query="test", db=mock_db, current_session_id="child_sid",
|
||||||
|
))
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert result["count"] == 0
|
||||||
|
assert result["results"] == []
|
||||||
|
assert result["sessions_searched"] == 0
|
||||||
|
|
||||||
|
def test_current_root_session_excludes_child_lineage(self):
|
||||||
|
"""Delegation child hits should be excluded when they resolve to the current root session."""
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
from tools.session_search_tool import session_search
|
||||||
|
|
||||||
|
mock_db = MagicMock()
|
||||||
|
mock_db.search_messages.return_value = [
|
||||||
|
{"session_id": "child_sid", "content": "match", "source": "cli",
|
||||||
|
"session_started": 1709500000, "model": "test"},
|
||||||
|
]
|
||||||
|
|
||||||
|
def _get_session(session_id):
|
||||||
|
if session_id == "root_sid":
|
||||||
|
return {"parent_session_id": None}
|
||||||
|
if session_id == "child_sid":
|
||||||
|
return {"parent_session_id": "root_sid"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
mock_db.get_session.side_effect = _get_session
|
||||||
|
|
||||||
|
result = json.loads(session_search(
|
||||||
|
query="test", db=mock_db, current_session_id="root_sid",
|
||||||
|
))
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert result["count"] == 0
|
||||||
|
assert result["results"] == []
|
||||||
|
assert result["sessions_searched"] == 0
|
||||||
|
|
|
||||||
|
|
@ -251,13 +251,20 @@ def session_search(
|
||||||
break
|
break
|
||||||
return sid
|
return sid
|
||||||
|
|
||||||
# Group by resolved (parent) session_id, dedup, skip current session
|
current_lineage_root = (
|
||||||
|
_resolve_to_parent(current_session_id) if current_session_id else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Group by resolved (parent) session_id, dedup, skip the current
|
||||||
|
# session lineage. Compression and delegation create child sessions
|
||||||
|
# that still belong to the same active conversation.
|
||||||
seen_sessions = {}
|
seen_sessions = {}
|
||||||
for result in raw_results:
|
for result in raw_results:
|
||||||
raw_sid = result["session_id"]
|
raw_sid = result["session_id"]
|
||||||
resolved_sid = _resolve_to_parent(raw_sid)
|
resolved_sid = _resolve_to_parent(raw_sid)
|
||||||
# Skip the current session — the agent already has that context
|
# Skip the current session lineage — the agent already has that
|
||||||
if current_session_id and resolved_sid == current_session_id:
|
# context, even if older turns live in parent fragments.
|
||||||
|
if current_lineage_root and resolved_sid == current_lineage_root:
|
||||||
continue
|
continue
|
||||||
if current_session_id and raw_sid == current_session_id:
|
if current_session_id and raw_sid == current_session_id:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue