refactor(tools-layer): model_tools/mcp_serve/toolsets — pack signatures & literals, merge baseline helper, drop redundant branches; -175 LOC
This commit is contained in:
+47
-96
@@ -50,10 +50,9 @@ def _get_sessions_dir() -> Path:
|
||||
|
||||
|
||||
def _read_state_db_mtime() -> float:
|
||||
db_file = _hermes_home() / "state.db"
|
||||
try:
|
||||
return db_file.stat().st_mtime if db_file.exists() else 0.0
|
||||
except OSError:
|
||||
return (_hermes_home() / "state.db").stat().st_mtime
|
||||
except OSError: # missing file included
|
||||
return 0.0
|
||||
|
||||
|
||||
@@ -172,9 +171,7 @@ def _load_sessions_index_from_json() -> dict:
|
||||
which needs GatewayConfig). Keys starting with "_" are metadata sentinels
|
||||
(e.g. "_README"), not session entries."""
|
||||
data = _read_json(_get_sessions_dir() / "sessions.json")
|
||||
if isinstance(data, dict):
|
||||
return {k: v for k, v in data.items() if not str(k).startswith("_")}
|
||||
return {}
|
||||
return {k: v for k, v in data.items() if not str(k).startswith("_")} if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def _load_channel_directory() -> dict:
|
||||
@@ -195,10 +192,7 @@ def _extract_message_content(msg: dict) -> str:
|
||||
"""Extract text content from a message, handling multi-part content."""
|
||||
content = msg.get("content", "")
|
||||
if isinstance(content, list):
|
||||
return "\n".join(
|
||||
p.get("text", "") for p in content
|
||||
if isinstance(p, dict) and p.get("type") == "text"
|
||||
)
|
||||
return "\n".join(p.get("text", "") for p in content if isinstance(p, dict) and p.get("type") == "text")
|
||||
return str(content) if content else ""
|
||||
|
||||
|
||||
@@ -206,7 +200,6 @@ def _extract_attachments(msg: dict) -> List[dict]:
|
||||
"""Non-text attachments: image/file content blocks plus MEDIA: tags in the text."""
|
||||
attachments = []
|
||||
content = msg.get("content", "")
|
||||
|
||||
for part in content if isinstance(content, list) else ():
|
||||
if not isinstance(part, dict):
|
||||
continue
|
||||
@@ -221,12 +214,8 @@ def _extract_attachments(msg: dict) -> List[dict]:
|
||||
continue
|
||||
if url:
|
||||
attachments.append({"type": "image", "url": url})
|
||||
|
||||
text = _extract_message_content(msg)
|
||||
if text:
|
||||
for match in re.finditer(r'MEDIA:\s*(\S+)', text):
|
||||
attachments.append({"type": "media", "path": match.group(1)})
|
||||
|
||||
for match in re.finditer(r'MEDIA:\s*(\S+)', _extract_message_content(msg)):
|
||||
attachments.append({"type": "media", "path": match.group(1)})
|
||||
return attachments
|
||||
|
||||
|
||||
@@ -308,10 +297,8 @@ class EventBridge:
|
||||
|
||||
def _matching(self, after_cursor: int, session_key: Optional[str], limit: int) -> List[dict]:
|
||||
with self._lock:
|
||||
return [
|
||||
e.as_dict() for e in self._queue
|
||||
if e.cursor > after_cursor and (not session_key or e.session_key == session_key)
|
||||
][:limit]
|
||||
return [e.as_dict() for e in self._queue
|
||||
if e.cursor > after_cursor and (not session_key or e.session_key == session_key)][:limit]
|
||||
|
||||
def poll_events(self, after_cursor: int = 0, session_key: Optional[str] = None, limit: int = 20) -> dict:
|
||||
"""Return events since after_cursor, optionally filtered by session_key."""
|
||||
@@ -343,10 +330,8 @@ class EventBridge:
|
||||
approval = self._pending_approvals.pop(approval_id, None)
|
||||
if not approval:
|
||||
return {"error": f"Approval not found: {approval_id}"}
|
||||
self._enqueue(QueueEvent( # cursor is assigned by _enqueue
|
||||
0, "approval_resolved", approval.get("session_key", ""),
|
||||
{"approval_id": approval_id, "decision": decision},
|
||||
))
|
||||
self._enqueue(QueueEvent(0, "approval_resolved", approval.get("session_key", ""), # cursor set by _enqueue
|
||||
{"approval_id": approval_id, "decision": decision}))
|
||||
return {"resolved": True, "approval_id": approval_id, "decision": decision}
|
||||
|
||||
def _enqueue(self, event: QueueEvent) -> None:
|
||||
@@ -360,32 +345,30 @@ class EventBridge:
|
||||
self._new_event.set()
|
||||
|
||||
def _establish_baseline(self) -> None:
|
||||
db = _get_session_db()
|
||||
if db:
|
||||
try:
|
||||
self._establish_baseline_with_db(db)
|
||||
finally:
|
||||
_close_quietly(db, "baseline")
|
||||
|
||||
def _establish_baseline_with_db(self, db) -> None:
|
||||
"""Record per-session latest timestamps and the state.db mtime WITHOUT
|
||||
emitting events. Only sessions existing now are baselined; later ones
|
||||
default to last_seen=0.0 in _poll_once, so their first message is delivered."""
|
||||
self._state_db_mtime = _read_state_db_mtime()
|
||||
db = _get_session_db()
|
||||
if not db:
|
||||
return
|
||||
try:
|
||||
self._cached_sessions_index = _load_sessions_index()
|
||||
except Exception:
|
||||
self._cached_sessions_index = {}
|
||||
for session_key, entry in self._cached_sessions_index.items():
|
||||
session_id = entry.get("session_id", "")
|
||||
if not session_id:
|
||||
continue
|
||||
self._state_db_mtime = _read_state_db_mtime()
|
||||
try:
|
||||
latest = _latest_ts(db.get_messages(session_id))
|
||||
self._cached_sessions_index = _load_sessions_index()
|
||||
except Exception:
|
||||
continue
|
||||
if latest > 0.0:
|
||||
self._last_poll_timestamps[session_key] = latest
|
||||
self._cached_sessions_index = {}
|
||||
for session_key, entry in self._cached_sessions_index.items():
|
||||
session_id = entry.get("session_id", "")
|
||||
if not session_id:
|
||||
continue
|
||||
try:
|
||||
latest = _latest_ts(db.get_messages(session_id))
|
||||
except Exception:
|
||||
continue
|
||||
if latest > 0.0:
|
||||
self._last_poll_timestamps[session_key] = latest
|
||||
finally:
|
||||
_close_quietly(db, "baseline")
|
||||
|
||||
def _poll_loop(self):
|
||||
"""Background loop: poll SessionDB for new messages."""
|
||||
@@ -429,11 +412,8 @@ class EventBridge:
|
||||
continue
|
||||
if not messages:
|
||||
continue
|
||||
|
||||
for msg in messages:
|
||||
if msg.get("role", "") not in {"user", "assistant"}:
|
||||
continue
|
||||
if _ts_float(msg.get("timestamp", 0)) <= last_seen:
|
||||
if msg.get("role", "") not in {"user", "assistant"} or _ts_float(msg.get("timestamp", 0)) <= last_seen:
|
||||
continue
|
||||
content = _extract_message_content(msg)
|
||||
if not content:
|
||||
@@ -442,7 +422,6 @@ class EventBridge:
|
||||
"role": msg.get("role", ""), "content": content[:500],
|
||||
"timestamp": str(msg.get("timestamp", "")), "message_id": str(msg.get("id", "")),
|
||||
}))
|
||||
|
||||
latest = _latest_ts(messages)
|
||||
if latest > last_seen:
|
||||
self._last_poll_timestamps[session_key] = latest
|
||||
@@ -507,8 +486,7 @@ class _ToolHandlers:
|
||||
"user_name": origin.get("user_name", ""), "updated_at": entry.get("updated_at", ""),
|
||||
})
|
||||
|
||||
conversations.sort(key=lambda c: c.get("updated_at", ""), reverse=True)
|
||||
conversations = conversations[:limit]
|
||||
conversations = sorted(conversations, key=lambda c: c.get("updated_at", ""), reverse=True)[:limit]
|
||||
return json.dumps({"count": len(conversations), "conversations": conversations}, indent=2)
|
||||
|
||||
def conversation_get(self, session_key: str) -> str:
|
||||
@@ -522,8 +500,7 @@ class _ToolHandlers:
|
||||
return json.dumps({"error": f"Conversation not found: {session_key}"})
|
||||
origin = entry.get("origin", {})
|
||||
return json.dumps({
|
||||
"session_key": session_key,
|
||||
"session_id": entry.get("session_id", ""),
|
||||
"session_key": session_key, "session_id": entry.get("session_id", ""),
|
||||
"platform": entry.get("platform") or origin.get("platform", ""),
|
||||
"chat_type": entry.get("chat_type", origin.get("chat_type", "")),
|
||||
"display_name": entry.get("display_name", ""),
|
||||
@@ -553,15 +530,11 @@ class _ToolHandlers:
|
||||
role = msg.get("role", "")
|
||||
content = _extract_message_content(msg) if role in {"user", "assistant"} else ""
|
||||
if content:
|
||||
filtered.append({
|
||||
"id": str(msg.get("id", "")), "role": role,
|
||||
"content": content[:2000], "timestamp": msg.get("timestamp", ""),
|
||||
})
|
||||
filtered.append({"id": str(msg.get("id", "")), "role": role,
|
||||
"content": content[:2000], "timestamp": msg.get("timestamp", "")})
|
||||
messages = filtered[-limit:]
|
||||
return json.dumps({
|
||||
"session_key": session_key, "count": len(messages),
|
||||
"total_in_session": len(filtered), "messages": messages,
|
||||
}, indent=2)
|
||||
return json.dumps({"session_key": session_key, "count": len(messages),
|
||||
"total_in_session": len(filtered), "messages": messages}, indent=2)
|
||||
|
||||
def attachments_fetch(self, session_key: str, message_id: str) -> str:
|
||||
"""List non-text attachments for a message in a conversation.
|
||||
@@ -614,9 +587,7 @@ class _ToolHandlers:
|
||||
after_cursor = _coerce_int(after_cursor, default=0, minimum=0, maximum=10**18)
|
||||
timeout_ms = _coerce_int(timeout_ms, default=30000, minimum=0, maximum=300000) # cap 5 min
|
||||
event = self.bridge.wait_for_event(after_cursor=after_cursor, session_key=session_key, timeout_ms=timeout_ms)
|
||||
if event:
|
||||
return json.dumps({"event": event}, indent=2)
|
||||
return json.dumps({"event": None, "reason": "timeout"}, indent=2)
|
||||
return json.dumps({"event": event} if event else {"event": None, "reason": "timeout"}, indent=2)
|
||||
|
||||
def messages_send(self, target: str, message: str) -> str:
|
||||
"""Send a message to a platform conversation.
|
||||
@@ -656,25 +627,19 @@ class _ToolHandlers:
|
||||
directory = _load_channel_directory()
|
||||
if not directory:
|
||||
# No cached directory: derive send targets from the routing index.
|
||||
targets = []
|
||||
seen = set()
|
||||
targets, seen = [], set()
|
||||
for key, entry in _load_sessions_index().items():
|
||||
origin = entry.get("origin", {})
|
||||
p = entry.get("platform") or origin.get("platform", "")
|
||||
chat_id = origin.get("chat_id", "")
|
||||
if not p or not chat_id or not _platform_matches(platform, p):
|
||||
continue
|
||||
target_str = f"{p}:{chat_id}"
|
||||
if target_str in seen:
|
||||
if not p or not chat_id or not _platform_matches(platform, p) or target_str in seen:
|
||||
continue
|
||||
seen.add(target_str)
|
||||
targets.append({
|
||||
"target": target_str, "platform": p,
|
||||
"name": entry.get("display_name") or origin.get("chat_name", ""),
|
||||
"chat_type": entry.get("chat_type", origin.get("chat_type", "")),
|
||||
})
|
||||
targets.append({"target": target_str, "platform": p,
|
||||
"name": entry.get("display_name") or origin.get("chat_name", ""),
|
||||
"chat_type": entry.get("chat_type", origin.get("chat_type", ""))})
|
||||
return json.dumps({"count": len(targets), "channels": targets}, indent=2)
|
||||
|
||||
channels = []
|
||||
for plat, entries_list in directory.get("platforms", {}).items():
|
||||
if not _platform_matches(platform, plat) or not isinstance(entries_list, list):
|
||||
@@ -682,10 +647,8 @@ class _ToolHandlers:
|
||||
for ch in entries_list:
|
||||
if isinstance(ch, dict):
|
||||
chat_id = ch.get("id", ch.get("chat_id", ""))
|
||||
channels.append({
|
||||
"target": f"{plat}:{chat_id}" if chat_id else plat, "platform": plat,
|
||||
"name": ch.get("name", ch.get("display_name", "")), "chat_type": ch.get("type", ""),
|
||||
})
|
||||
channels.append({"target": f"{plat}:{chat_id}" if chat_id else plat, "platform": plat,
|
||||
"name": ch.get("name", ch.get("display_name", "")), "chat_type": ch.get("type", "")})
|
||||
return json.dumps({"count": len(channels), "channels": channels}, indent=2)
|
||||
|
||||
def permissions_list_open(self) -> str:
|
||||
@@ -706,10 +669,7 @@ class _ToolHandlers:
|
||||
decision: One of "allow-once", "allow-always", or "deny"
|
||||
"""
|
||||
if decision not in {"allow-once", "allow-always", "deny"}:
|
||||
return json.dumps({
|
||||
"error": f"Invalid decision: {decision}. "
|
||||
f"Must be allow-once, allow-always, or deny"
|
||||
})
|
||||
return json.dumps({"error": f"Invalid decision: {decision}. Must be allow-once, allow-always, or deny"})
|
||||
return json.dumps(self.bridge.respond_to_approval(id, decision), indent=2)
|
||||
|
||||
|
||||
@@ -724,10 +684,7 @@ _TOOL_NAMES = (
|
||||
def create_mcp_server(event_bridge: Optional[EventBridge] = None) -> "MCPServer":
|
||||
"""Create and return the Hermes MCP server with all tools registered."""
|
||||
if not _MCP_SERVER_AVAILABLE:
|
||||
raise ImportError(
|
||||
"MCP server requires the 'mcp' package. "
|
||||
f"Install with: {sys.executable} -m pip install 'mcp'"
|
||||
)
|
||||
raise ImportError(f"MCP server requires the 'mcp' package. Install with: {sys.executable} -m pip install 'mcp'")
|
||||
mcp = MCPServer("hermes", instructions=(
|
||||
"Hermes Agent messaging bridge. Use these tools to interact with "
|
||||
"conversations across Telegram, Discord, Slack, WhatsApp, Signal, "
|
||||
@@ -742,19 +699,13 @@ def create_mcp_server(event_bridge: Optional[EventBridge] = None) -> "MCPServer"
|
||||
def run_mcp_server(verbose: bool = False) -> None:
|
||||
"""Start the Hermes MCP server on stdio."""
|
||||
if not _MCP_SERVER_AVAILABLE:
|
||||
print(
|
||||
"Error: MCP server requires the 'mcp' package.\n"
|
||||
f"Install with: {sys.executable} -m pip install 'mcp'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("Error: MCP server requires the 'mcp' package.\n"
|
||||
f"Install with: {sys.executable} -m pip install 'mcp'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG if verbose else logging.WARNING, stream=sys.stderr)
|
||||
|
||||
bridge = EventBridge()
|
||||
bridge.start()
|
||||
server = create_mcp_server(event_bridge=bridge)
|
||||
|
||||
import asyncio
|
||||
|
||||
async def _run():
|
||||
|
||||
+58
-133
@@ -18,30 +18,17 @@ import threading
|
||||
import time
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
|
||||
from tools.registry import (
|
||||
CHECK_FN_CACHE_BYPASS,
|
||||
check_fn_cache_scope,
|
||||
discover_builtin_tools,
|
||||
registry,
|
||||
tool_error,
|
||||
)
|
||||
from tools.registry import CHECK_FN_CACHE_BYPASS, check_fn_cache_scope, discover_builtin_tools, registry, tool_error
|
||||
from tools.registry import _MAX_TOOL_ERROR_CHARS as _TOOL_ERROR_MAX_LEN
|
||||
from toolsets import resolve_toolset, validate_toolset
|
||||
from tools.arg_coercion import ( # noqa: F401 # re-exported: tests and tools/ import these from model_tools
|
||||
_coerce_boolean,
|
||||
_coerce_json,
|
||||
_coerce_number,
|
||||
_coerce_value,
|
||||
_normalize_json_strings_for_schema,
|
||||
_schema_accepts_kind,
|
||||
_schema_allows_null,
|
||||
coerce_tool_args,
|
||||
_coerce_boolean, _coerce_json, _coerce_number, _coerce_value, _normalize_json_strings_for_schema,
|
||||
_schema_accepts_kind, _schema_allows_null, coerce_tool_args,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_post_tool_call_hook_suppressed: ContextVar[bool] = ContextVar(
|
||||
"post_tool_call_hook_suppressed", default=False
|
||||
)
|
||||
_post_tool_call_hook_suppressed: ContextVar[bool] = ContextVar("post_tool_call_hook_suppressed", default=False)
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -188,10 +175,8 @@ _LEGACY_TOOLSET_MAP = {
|
||||
"vision_tools": ["vision_analyze"],
|
||||
"image_tools": ["image_generate"],
|
||||
"skills_tools": ["skills_list", "skill_view", "skill_manage"],
|
||||
"browser_tools": [
|
||||
"browser_navigate", "browser_snapshot", "browser_click", "browser_type", "browser_scroll",
|
||||
"browser_back", "browser_press", "browser_get_images", "browser_vision", "browser_console",
|
||||
],
|
||||
"browser_tools": ["browser_navigate", "browser_snapshot", "browser_click", "browser_type", "browser_scroll",
|
||||
"browser_back", "browser_press", "browser_get_images", "browser_vision", "browser_console"],
|
||||
"cronjob_tools": ["cronjob_manage"],
|
||||
"file_tools": ["read_file", "write_file", "patch", "search_files"],
|
||||
"tts_tools": ["text_to_speech"],
|
||||
@@ -216,12 +201,8 @@ def _clear_tool_defs_cache() -> None:
|
||||
_tool_defs_cache.clear()
|
||||
|
||||
|
||||
def get_tool_definitions(
|
||||
enabled_toolsets: Optional[List[str]] = None,
|
||||
disabled_toolsets: Optional[List[str]] = None,
|
||||
quiet_mode: bool = False,
|
||||
skip_tool_search_assembly: bool = False,
|
||||
) -> List[Dict[str, Any]]:
|
||||
def get_tool_definitions(enabled_toolsets: Optional[List[str]] = None, disabled_toolsets: Optional[List[str]] = None,
|
||||
quiet_mode: bool = False, skip_tool_search_assembly: bool = False) -> List[Dict[str, Any]]:
|
||||
"""Tool definitions for model API calls, filtered by toolset.
|
||||
|
||||
enabled_toolsets None = all; disabled_toolsets are subtracted after enabling.
|
||||
@@ -232,10 +213,8 @@ def get_tool_definitions(
|
||||
def compute():
|
||||
return _compute_tool_definitions(enabled_toolsets, disabled_toolsets, quiet_mode,
|
||||
skip_tool_search_assembly=skip_tool_search_assembly)
|
||||
|
||||
if not quiet_mode:
|
||||
return compute()
|
||||
|
||||
cache_key = _tool_defs_cache_key(enabled_toolsets, disabled_toolsets, skip_tool_search_assembly)
|
||||
with _tool_defs_cache_lock:
|
||||
cached = _tool_defs_cache.get(cache_key) if cache_key is not None else None
|
||||
@@ -276,10 +255,8 @@ def _tool_defs_cache_key(
|
||||
except (FileNotFoundError, OSError, ImportError):
|
||||
cfg_fp = None
|
||||
return (
|
||||
registry.current_scope_key(),
|
||||
frozenset(enabled_toolsets) if enabled_toolsets is not None else None,
|
||||
frozenset(disabled_toolsets) if disabled_toolsets else None,
|
||||
registry._generation, cfg_fp,
|
||||
registry.current_scope_key(), frozenset(enabled_toolsets) if enabled_toolsets is not None else None,
|
||||
frozenset(disabled_toolsets) if disabled_toolsets else None, registry._generation, cfg_fp,
|
||||
bool(os.environ.get("HERMES_KANBAN_TASK")), bool(skip_tool_search_assembly),
|
||||
_is_delegated_child_context(), _is_dispatcher_owned_worker(), profile_scope,
|
||||
)
|
||||
@@ -318,23 +295,15 @@ def _apply_toolset_selection(tools: set, names: List[str], quiet_mode: bool, *,
|
||||
print(f"{icon} {label} '{name}': {', '.join(resolved) if resolved else 'no tools'}")
|
||||
|
||||
|
||||
def _select_tool_names(
|
||||
enabled_toolsets: Optional[List[str]],
|
||||
disabled_toolsets: Optional[List[str]],
|
||||
quiet_mode: bool,
|
||||
) -> set:
|
||||
def _select_tool_names(enabled_toolsets: Optional[List[str]], disabled_toolsets: Optional[List[str]], quiet_mode: bool) -> set:
|
||||
"""Tool names requested by the toolset selection (before check_fn filtering)."""
|
||||
tools: set = set()
|
||||
if enabled_toolsets is not None:
|
||||
enabled = list(enabled_toolsets)
|
||||
# Dispatcher-spawned kanban workers always get the lifecycle handoff
|
||||
# tools, even when the assignee profile restricts its chat toolsets.
|
||||
if (
|
||||
os.environ.get("HERMES_KANBAN_TASK")
|
||||
and not _is_delegated_child_context()
|
||||
and _is_dispatcher_owned_worker()
|
||||
and "kanban" not in enabled
|
||||
):
|
||||
if (os.environ.get("HERMES_KANBAN_TASK") and not _is_delegated_child_context()
|
||||
and _is_dispatcher_owned_worker() and "kanban" not in enabled):
|
||||
enabled.append("kanban")
|
||||
_apply_toolset_selection(tools, enabled, quiet_mode, disable=False)
|
||||
else:
|
||||
@@ -453,12 +422,8 @@ _TOOL_SEARCH_LISTING_FORMS = {
|
||||
}
|
||||
|
||||
|
||||
def _compute_tool_definitions(
|
||||
enabled_toolsets: Optional[List[str]] = None,
|
||||
disabled_toolsets: Optional[List[str]] = None,
|
||||
quiet_mode: bool = False,
|
||||
skip_tool_search_assembly: bool = False,
|
||||
) -> List[Dict[str, Any]]:
|
||||
def _compute_tool_definitions(enabled_toolsets: Optional[List[str]] = None, disabled_toolsets: Optional[List[str]] = None,
|
||||
quiet_mode: bool = False, skip_tool_search_assembly: bool = False) -> List[Dict[str, Any]]:
|
||||
"""Uncached implementation of :func:`get_tool_definitions`."""
|
||||
tools_to_include = _select_tool_names(enabled_toolsets, disabled_toolsets, quiet_mode)
|
||||
# Registry returns only tools whose check_fn passes.
|
||||
@@ -467,11 +432,8 @@ def _compute_tool_definitions(
|
||||
_last_resolved_tool_names = [t["function"]["name"] for t in filtered_tools]
|
||||
|
||||
if not quiet_mode:
|
||||
if filtered_tools:
|
||||
print(f"🛠️ Final tool selection ({len(filtered_tools)} tools): {', '.join(_last_resolved_tool_names)}")
|
||||
else:
|
||||
print("🛠️ No tools selected (all filtered out or unavailable)")
|
||||
|
||||
print(f"🛠️ Final tool selection ({len(filtered_tools)} tools): {', '.join(_last_resolved_tool_names)}"
|
||||
if filtered_tools else "🛠️ No tools selected (all filtered out or unavailable)")
|
||||
# Normalize schema shapes llama.cpp's grammar converter rejects (bare
|
||||
# "type": "object", string-valued nodes from malformed MCP servers).
|
||||
try:
|
||||
@@ -490,12 +452,10 @@ def _compute_tool_definitions(
|
||||
if not skip_tool_search_assembly and ts_cfg.enabled != "off":
|
||||
assembly = assemble_tool_defs(filtered_tools, context_length=_resolve_active_context_length(), config=ts_cfg)
|
||||
if assembly.activated and not quiet_mode:
|
||||
print(
|
||||
f"🔎 Tool Search (tier {assembly.tier}): {assembly.deferred_count} "
|
||||
f"MCP/plugin tools deferred (~{assembly.deferred_tokens} tokens) behind "
|
||||
f"tool_search/describe/call — "
|
||||
f"{_TOOL_SEARCH_LISTING_FORMS.get(assembly.listing_form, assembly.listing_form)}."
|
||||
)
|
||||
print(f"🔎 Tool Search (tier {assembly.tier}): {assembly.deferred_count} "
|
||||
f"MCP/plugin tools deferred (~{assembly.deferred_tokens} tokens) behind "
|
||||
f"tool_search/describe/call — "
|
||||
f"{_TOOL_SEARCH_LISTING_FORMS.get(assembly.listing_form, assembly.listing_form)}.")
|
||||
filtered_tools = assembly.tool_defs
|
||||
except Exception as e: # pragma: no cover — never break tool loading
|
||||
logger.warning("Tool search assembly skipped: %s", e)
|
||||
@@ -551,10 +511,8 @@ def _resolve_active_context_length() -> int:
|
||||
return cached_ctx
|
||||
except Exception:
|
||||
pass
|
||||
return int(get_model_context_length(
|
||||
model_id, base_url=base_url, api_key=api_key,
|
||||
config_context_length=config_ctx, provider=provider,
|
||||
) or 0)
|
||||
return int(get_model_context_length(model_id, base_url=base_url, api_key=api_key,
|
||||
config_context_length=config_ctx, provider=provider) or 0)
|
||||
except Exception as e:
|
||||
logger.debug("Could not resolve active context length: %s", e)
|
||||
return 0
|
||||
@@ -586,7 +544,6 @@ _TOOL_ERROR_STRIP_RES = (
|
||||
re.compile(r'\s*```\s*$', re.MULTILINE),
|
||||
re.compile(r'<!\[CDATA\[.*?\]\]>', re.DOTALL),
|
||||
)
|
||||
from tools.registry import _MAX_TOOL_ERROR_CHARS as _TOOL_ERROR_MAX_LEN
|
||||
|
||||
|
||||
def _sanitize_tool_error(error_msg: str) -> str:
|
||||
@@ -636,9 +593,8 @@ def _tool_result_observer_fields(tool_name: str, result: Any) -> tuple[str, Opti
|
||||
def _emit_post_tool_call_hook(
|
||||
*, function_name: str, function_args: Dict[str, Any], result: Any,
|
||||
task_id: Optional[str] = None, session_id: Optional[str] = None, tool_call_id: Optional[str] = None,
|
||||
turn_id: Optional[str] = None, api_request_id: Optional[str] = None,
|
||||
duration_ms: int = 0, status: Optional[str] = None,
|
||||
error_type: Optional[str] = None, error_message: Optional[str] = None,
|
||||
turn_id: Optional[str] = None, api_request_id: Optional[str] = None, duration_ms: int = 0,
|
||||
status: Optional[str] = None, error_type: Optional[str] = None, error_message: Optional[str] = None,
|
||||
middleware_trace: Optional[List[Dict[str, Any]]] = None,
|
||||
) -> None:
|
||||
"""Emit the ``post_tool_call`` observer hook; gated on has_hook, and ok/error
|
||||
@@ -661,10 +617,8 @@ def _emit_post_tool_call_hook(
|
||||
logger.debug("post_tool_call hook error: %s", _hook_err)
|
||||
|
||||
|
||||
def _dispatch_bridge_tool(
|
||||
function_name: str, function_args: Dict[str, Any],
|
||||
enabled_toolsets: Optional[List[str]], disabled_toolsets: Optional[List[str]],
|
||||
):
|
||||
def _dispatch_bridge_tool(function_name: str, function_args: Dict[str, Any],
|
||||
enabled_toolsets: Optional[List[str]], disabled_toolsets: Optional[List[str]]):
|
||||
"""Handle a Tool Search bridge call (tool_search / tool_describe / tool_call).
|
||||
|
||||
None when *function_name* is not a bridge tool; ``(result, None)`` for a
|
||||
@@ -680,10 +634,8 @@ def _dispatch_bridge_tool(
|
||||
# Un-collapsed catalog scoped to the session's toolsets, so a restricted
|
||||
# session (subagent, kanban worker) can't reach the whole registry via the bridge.
|
||||
try:
|
||||
current_defs = get_tool_definitions(
|
||||
enabled_toolsets=enabled_toolsets, disabled_toolsets=disabled_toolsets,
|
||||
quiet_mode=True, skip_tool_search_assembly=True,
|
||||
) or []
|
||||
current_defs = get_tool_definitions(enabled_toolsets=enabled_toolsets, disabled_toolsets=disabled_toolsets,
|
||||
quiet_mode=True, skip_tool_search_assembly=True) or []
|
||||
except Exception:
|
||||
current_defs = []
|
||||
args = function_args or {}
|
||||
@@ -697,10 +649,8 @@ def _dispatch_bridge_tool(
|
||||
# Defense in depth: resolve_underlying_call only checks the global
|
||||
# registry; also require membership in the session-scoped catalog.
|
||||
if underlying_name not in ts.scoped_deferrable_names(current_defs):
|
||||
return tool_error(
|
||||
f"'{underlying_name}' is not available in this session. "
|
||||
"Use tool_search to find tools you can call."
|
||||
), None
|
||||
return tool_error(f"'{underlying_name}' is not available in this session. "
|
||||
"Use tool_search to find tools you can call."), None
|
||||
# Validate against the deferred tool's concrete schema — the generic
|
||||
# ``arguments: object`` bridge schema can't enforce it.
|
||||
probe_err = ts.validate_deferred_call_args(underlying_name, underlying_args)
|
||||
@@ -722,10 +672,9 @@ def _apply_request_middleware(
|
||||
return function_args, dict(function_args), trace
|
||||
|
||||
|
||||
def _pre_dispatch_guards(
|
||||
function_name: str, function_args: Dict[str, Any], skip_pre_tool_call_hook: bool,
|
||||
ids: _CallIds, middleware_trace: List[Dict[str, Any]],
|
||||
) -> Tuple[Dict[str, Any], Optional[Tuple[Any, str, Optional[str]]]]:
|
||||
def _pre_dispatch_guards(function_name: str, function_args: Dict[str, Any], skip_pre_tool_call_hook: bool,
|
||||
ids: _CallIds, middleware_trace: List[Dict[str, Any]],
|
||||
) -> Tuple[Dict[str, Any], Optional[Tuple[Any, str, Optional[str]]]]:
|
||||
"""Plugin pre_tool_call hook, then ACP edit approval.
|
||||
|
||||
``(args, None)`` to proceed (args possibly plugin-modified), or
|
||||
@@ -766,9 +715,8 @@ def _approval_observability(ids: _CallIds):
|
||||
"""Bind the approval observability context (turn/tool_call/session ids) for the block."""
|
||||
try:
|
||||
from tools.approval import reset_current_observability_context, set_current_observability_context
|
||||
tokens = set_current_observability_context(
|
||||
turn_id=ids.turn_id or "", tool_call_id=ids.tool_call_id or "", session_id=ids.session_id or "",
|
||||
)
|
||||
tokens = set_current_observability_context(turn_id=ids.turn_id or "", tool_call_id=ids.tool_call_id or "",
|
||||
session_id=ids.session_id or "")
|
||||
except Exception:
|
||||
yield
|
||||
return
|
||||
@@ -781,10 +729,8 @@ def _approval_observability(ids: _CallIds):
|
||||
pass
|
||||
|
||||
|
||||
def _execute_tool(
|
||||
function_name: str, function_args: Dict[str, Any], original_args: Dict[str, Any], ids: _CallIds,
|
||||
*, user_task: Optional[str], enabled_tools: Optional[List[str]], skip_tool_execution_middleware: bool,
|
||||
) -> Any:
|
||||
def _execute_tool(function_name: str, function_args: Dict[str, Any], original_args: Dict[str, Any], ids: _CallIds,
|
||||
*, user_task: Optional[str], enabled_tools: Optional[List[str]], skip_tool_execution_middleware: bool) -> Any:
|
||||
"""Run the registry handler (through tool-execution middleware unless skipped)
|
||||
with the approval observability context bound for the duration."""
|
||||
dispatch_kwargs: Dict[str, Any] = {"task_id": ids.task_id, "session_id": ids.session_id}
|
||||
@@ -802,14 +748,12 @@ def _execute_tool(
|
||||
if skip_tool_execution_middleware:
|
||||
return _dispatch(function_args)
|
||||
from hermes_cli.middleware import run_tool_execution_middleware
|
||||
return run_tool_execution_middleware(
|
||||
function_name, function_args, _dispatch, original_args=original_args, **ids.hook_kwargs(),
|
||||
)
|
||||
return run_tool_execution_middleware(function_name, function_args, _dispatch, original_args=original_args,
|
||||
**ids.hook_kwargs())
|
||||
|
||||
|
||||
def _apply_transform_tool_result_hook(
|
||||
function_name: str, function_args: Dict[str, Any], result: Any, duration_ms: int, ids: _CallIds,
|
||||
) -> Any:
|
||||
def _apply_transform_tool_result_hook(function_name: str, function_args: Dict[str, Any], result: Any, duration_ms: int,
|
||||
ids: _CallIds) -> Any:
|
||||
"""transform_tool_result: plugins may replace the final result string.
|
||||
|
||||
Runs after post_tool_call and before the result enters context. Fail-open;
|
||||
@@ -819,14 +763,10 @@ def _apply_transform_tool_result_hook(
|
||||
from hermes_cli.lifecycle import has_hook, invoke_hook
|
||||
if has_hook("transform_tool_result"):
|
||||
status, error_type, error_message = _tool_result_observer_fields(function_name, result)
|
||||
hook_results = invoke_hook(
|
||||
"transform_tool_result", tool_name=function_name, args=function_args, result=result,
|
||||
**ids.hook_kwargs(), duration_ms=duration_ms,
|
||||
status=status, error_type=error_type, error_message=error_message,
|
||||
)
|
||||
for hook_result in hook_results:
|
||||
if isinstance(hook_result, str):
|
||||
return hook_result
|
||||
hook_results = invoke_hook("transform_tool_result", tool_name=function_name, args=function_args,
|
||||
result=result, **ids.hook_kwargs(), duration_ms=duration_ms,
|
||||
status=status, error_type=error_type, error_message=error_message)
|
||||
return next((r for r in hook_results if isinstance(r, str)), result)
|
||||
except Exception as _hook_err:
|
||||
logger.debug("transform_tool_result hook error: %s", _hook_err)
|
||||
return result
|
||||
@@ -837,21 +777,12 @@ def _elapsed_ms(start: float) -> int:
|
||||
|
||||
|
||||
def handle_function_call(
|
||||
function_name: str,
|
||||
function_args: Dict[str, Any],
|
||||
task_id: Optional[str] = None,
|
||||
tool_call_id: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
turn_id: Optional[str] = None,
|
||||
api_request_id: Optional[str] = None,
|
||||
user_task: Optional[str] = None,
|
||||
enabled_tools: Optional[List[str]] = None,
|
||||
skip_pre_tool_call_hook: bool = False,
|
||||
skip_tool_request_middleware: bool = False,
|
||||
skip_tool_execution_middleware: bool = False,
|
||||
tool_request_middleware_trace: Optional[List[Dict[str, Any]]] = None,
|
||||
enabled_toolsets: Optional[List[str]] = None,
|
||||
disabled_toolsets: Optional[List[str]] = None,
|
||||
function_name: str, function_args: Dict[str, Any], task_id: Optional[str] = None,
|
||||
tool_call_id: Optional[str] = None, session_id: Optional[str] = None, turn_id: Optional[str] = None,
|
||||
api_request_id: Optional[str] = None, user_task: Optional[str] = None, enabled_tools: Optional[List[str]] = None,
|
||||
skip_pre_tool_call_hook: bool = False, skip_tool_request_middleware: bool = False,
|
||||
skip_tool_execution_middleware: bool = False, tool_request_middleware_trace: Optional[List[Dict[str, Any]]] = None,
|
||||
enabled_toolsets: Optional[List[str]] = None, disabled_toolsets: Optional[List[str]] = None,
|
||||
) -> str:
|
||||
"""Route a tool call through hooks/middleware to the registry; returns a JSON string.
|
||||
|
||||
@@ -884,12 +815,9 @@ def handle_function_call(
|
||||
if underlying is None:
|
||||
return _emit(result, duration_ms=_elapsed_ms(start))
|
||||
return handle_function_call(
|
||||
*underlying, task_id=task_id, tool_call_id=tool_call_id, session_id=session_id,
|
||||
turn_id=turn_id, api_request_id=api_request_id, user_task=user_task,
|
||||
enabled_tools=enabled_tools, skip_pre_tool_call_hook=skip_pre_tool_call_hook,
|
||||
skip_tool_request_middleware=skip_tool_request_middleware,
|
||||
skip_tool_execution_middleware=skip_tool_execution_middleware,
|
||||
tool_request_middleware_trace=list(trace),
|
||||
*underlying, **asdict(ids), user_task=user_task, enabled_tools=enabled_tools,
|
||||
skip_pre_tool_call_hook=skip_pre_tool_call_hook, skip_tool_request_middleware=skip_tool_request_middleware,
|
||||
skip_tool_execution_middleware=skip_tool_execution_middleware, tool_request_middleware_trace=list(trace),
|
||||
enabled_toolsets=enabled_toolsets, disabled_toolsets=disabled_toolsets,
|
||||
)
|
||||
|
||||
@@ -916,11 +844,8 @@ def handle_function_call(
|
||||
|
||||
# duration_ms (monotonic) is exposed to post_tool_call / transform_tool_result.
|
||||
start = time.monotonic()
|
||||
result = _execute_tool(
|
||||
function_name, function_args, original_args, ids,
|
||||
user_task=user_task, enabled_tools=enabled_tools,
|
||||
skip_tool_execution_middleware=skip_tool_execution_middleware,
|
||||
)
|
||||
result = _execute_tool(function_name, function_args, original_args, ids, user_task=user_task,
|
||||
enabled_tools=enabled_tools, skip_tool_execution_middleware=skip_tool_execution_middleware)
|
||||
duration_ms = _elapsed_ms(start)
|
||||
_emit(result, duration_ms=duration_ms)
|
||||
return _apply_transform_tool_result_hook(function_name, function_args, result, duration_ms, ids)
|
||||
|
||||
+23
-74
@@ -79,10 +79,7 @@ TOOLSETS = {
|
||||
["x_search"],
|
||||
),
|
||||
"vision": _ts("Image analysis and vision tools", ["vision_analyze"]),
|
||||
"video": _ts(
|
||||
"Video analysis and understanding tools (opt-in, not in default toolset)",
|
||||
["video_analyze"],
|
||||
),
|
||||
"video": _ts("Video analysis and understanding tools (opt-in, not in default toolset)", ["video_analyze"]),
|
||||
"image_gen": _ts("Creative generation tools (images)", ["image_generate"]),
|
||||
"video_gen": _ts(
|
||||
"Video generation tools. Single ``video_generate`` tool covers text-to-video "
|
||||
@@ -97,10 +94,7 @@ TOOLSETS = {
|
||||
"or keyboard focus. Works with any tool-capable model.",
|
||||
["computer_use"],
|
||||
),
|
||||
"terminal": _ts(
|
||||
"Terminal/command execution and process management tools",
|
||||
["terminal", "process_manage"],
|
||||
),
|
||||
"terminal": _ts("Terminal/command execution and process management tools", ["terminal", "process_manage"]),
|
||||
"skills": _ts(
|
||||
"Access, create, edit, and manage skill documents with specialized "
|
||||
"instructions and knowledge",
|
||||
@@ -121,18 +115,12 @@ TOOLSETS = {
|
||||
"search (content + files)",
|
||||
["read_file", "write_file", "patch", "search_files"],
|
||||
),
|
||||
"tts": _ts(
|
||||
"Text-to-speech: convert text to audio with Edge TTS (free), ElevenLabs, OpenAI, or xAI",
|
||||
["text_to_speech"],
|
||||
),
|
||||
"tts": _ts("Text-to-speech: convert text to audio with Edge TTS (free), ElevenLabs, OpenAI, or xAI", ["text_to_speech"]),
|
||||
"todo": _ts("Task planning and tracking for multi-step work", ["todo_list"]),
|
||||
"memory": _ts("Persistent memory across sessions (personal notes + user profile)", ["memory"]),
|
||||
"context_engine": _ts("Runtime tools exposed by the active context engine"),
|
||||
"session_search": _ts("Search and recall past conversations with summarization", ["session_search"]),
|
||||
"project": _ts(
|
||||
"Desktop Projects — create/switch named workspaces (GUI sessions only)",
|
||||
["desktop_project"],
|
||||
),
|
||||
"project": _ts("Desktop Projects — create/switch named workspaces (GUI sessions only)", ["desktop_project"]),
|
||||
"bot_room": _ts("Verified text-only Group Chat turn capabilities"),
|
||||
|
||||
# GUI-renderer affordances, enabled per desktop-sourced SESSION by the GUI
|
||||
@@ -141,17 +129,12 @@ TOOLSETS = {
|
||||
"desktop_ui": _ts(
|
||||
"Desktop GUI affordances — in-app terminal/browser panes, pane focus, "
|
||||
"reactions (GUI sessions only)",
|
||||
[
|
||||
"read_terminal", "close_terminal", "desktop_preview", "drive_preview",
|
||||
"annotate_preview", "read_window_below", "focus_pane", "react_to_message",
|
||||
"setup_mcp", "gui_tour", "show_tip",
|
||||
],
|
||||
["read_terminal", "close_terminal", "desktop_preview", "drive_preview",
|
||||
"annotate_preview", "read_window_below", "focus_pane", "react_to_message",
|
||||
"setup_mcp", "gui_tour", "show_tip"],
|
||||
),
|
||||
"clarify": _ts("Ask the user clarifying questions (multiple-choice or open-ended)", ["clarify"]),
|
||||
"code_execution": _ts(
|
||||
"Run Python scripts that call tools programmatically (reduces LLM round trips)",
|
||||
["execute_code"],
|
||||
),
|
||||
"code_execution": _ts("Run Python scripts that call tools programmatically (reduces LLM round trips)", ["execute_code"]),
|
||||
"delegation": _ts("Spawn subagents with isolated context for complex subtasks", ["delegate_task"]),
|
||||
"homeassistant": _ts("Home Assistant smart home control and monitoring", _HA_TOOLS),
|
||||
"kanban": _ts(
|
||||
@@ -164,31 +147,19 @@ TOOLSETS = {
|
||||
"files, and (for orchestrators) list, unblock, and fan out tasks.",
|
||||
[t for t in _HERMES_CORE_TOOLS if t.startswith("kanban_")],
|
||||
),
|
||||
"discord": _ts(
|
||||
"Discord read and participate tools (fetch messages, search members, create threads)",
|
||||
["discord"],
|
||||
),
|
||||
"discord_admin": _ts(
|
||||
"Discord server management (list channels/roles, pin messages, assign roles)",
|
||||
["discord_admin"],
|
||||
),
|
||||
"discord": _ts("Discord read and participate tools (fetch messages, search members, create threads)", ["discord"]),
|
||||
"discord_admin": _ts("Discord server management (list channels/roles, pin messages, assign roles)", ["discord_admin"]),
|
||||
"yuanbao": _ts("Yuanbao platform tools - group info, member queries, DM, stickers", _YUANBAO_TOOLS),
|
||||
"feishu_doc": _ts("Read Feishu/Lark document content", ["feishu_doc_read"]),
|
||||
"feishu_drive": _ts("Feishu/Lark document comment operations (list, reply, add)", _FEISHU_TOOLS[1:]),
|
||||
"spotify": _ts(
|
||||
"Native Spotify playback, search, playlist, album, and library tools",
|
||||
[
|
||||
"spotify_playback", "spotify_devices", "spotify_queue", "spotify_search",
|
||||
"spotify_playlists", "spotify_albums", "spotify_library",
|
||||
],
|
||||
["spotify_playback", "spotify_devices", "spotify_queue", "spotify_search",
|
||||
"spotify_playlists", "spotify_albums", "spotify_library"],
|
||||
),
|
||||
|
||||
# Scenario-specific toolsets
|
||||
"debugging": _ts(
|
||||
"Debugging and troubleshooting toolkit",
|
||||
["terminal", "process_manage"],
|
||||
includes=["web", "file"],
|
||||
),
|
||||
"debugging": _ts("Debugging and troubleshooting toolkit", ["terminal", "process_manage"], includes=["web", "file"]),
|
||||
"safe": _ts("Safe toolkit without terminal access", [], includes=["web", "vision", "image_gen"]),
|
||||
|
||||
# Coding posture, auto-selected in a code workspace (agent/coding_context.py).
|
||||
@@ -220,22 +191,16 @@ TOOLSETS = {
|
||||
# Mirrors hermes-cli; `hermes tools` platform config filters it down and
|
||||
# _get_platform_tools() drops _DEFAULT_OFF_TOOLSETS unless user-enabled.
|
||||
"hermes-cron": _bundle("Default cron toolset - same core tools as hermes-cli; gated by `hermes tools`"),
|
||||
"hermes-telegram": _bundle(
|
||||
"Telegram bot toolset - full access for personal use (terminal has safety checks)"
|
||||
),
|
||||
"hermes-telegram": _bundle("Telegram bot toolset - full access for personal use (terminal has safety checks)"),
|
||||
"hermes-discord": _bundle(
|
||||
"Discord bot toolset - full access (terminal has safety checks via dangerous "
|
||||
"command approval)",
|
||||
["discord", "discord_admin"],
|
||||
),
|
||||
"hermes-whatsapp": _bundle(
|
||||
"WhatsApp bot toolset - similar to Telegram (personal messaging, more trusted)"
|
||||
),
|
||||
"hermes-whatsapp": _bundle("WhatsApp bot toolset - similar to Telegram (personal messaging, more trusted)"),
|
||||
"hermes-slack": _bundle("Slack bot toolset - full access for workspace use (terminal has safety checks)"),
|
||||
"hermes-signal": _bundle("Signal bot toolset - encrypted messaging platform (full access)"),
|
||||
"hermes-bluebubbles": _bundle(
|
||||
"BlueBubbles iMessage bot toolset - Apple iMessage via local BlueBubbles server"
|
||||
),
|
||||
"hermes-bluebubbles": _bundle("BlueBubbles iMessage bot toolset - Apple iMessage via local BlueBubbles server"),
|
||||
"hermes-homeassistant": _bundle("Home Assistant bot toolset - smart home event monitoring and control"),
|
||||
"hermes-email": _bundle("Email bot toolset - interact with Hermes via email (IMAP/SMTP)"),
|
||||
"hermes-mattermost": _bundle("Mattermost bot toolset - self-hosted team messaging (full access)"),
|
||||
@@ -245,9 +210,7 @@ TOOLSETS = {
|
||||
"hermes-weixin": _bundle("Weixin bot toolset - personal WeChat messaging via iLink (full access)"),
|
||||
"hermes-qqbot": _bundle("QQBot toolset - QQ messaging via Official Bot API v2 (full access)"),
|
||||
"hermes-wecom": _bundle("WeCom bot toolset - enterprise WeChat messaging (full access)"),
|
||||
"hermes-wecom-callback": _bundle(
|
||||
"WeCom callback toolset - enterprise self-built app messaging (full access)"
|
||||
),
|
||||
"hermes-wecom-callback": _bundle("WeCom callback toolset - enterprise self-built app messaging (full access)"),
|
||||
"hermes-yuanbao": {
|
||||
"description": "Yuanbao Bot 元宝消息平台工具集 - 群信息、成员查询、私聊、贴纸表情",
|
||||
"tools": _HERMES_CORE_TOOLS + _YUANBAO_TOOLS,
|
||||
@@ -255,10 +218,7 @@ TOOLSETS = {
|
||||
"includes": [],
|
||||
},
|
||||
"hermes-sms": _bundle("SMS bot toolset - interact with Hermes via SMS (Twilio)"),
|
||||
"hermes-webhook": _ts(
|
||||
"Webhook toolset - receive and process external webhook events",
|
||||
_HERMES_WEBHOOK_SAFE_TOOLS,
|
||||
),
|
||||
"hermes-webhook": _ts("Webhook toolset - receive and process external webhook events", _HERMES_WEBHOOK_SAFE_TOOLS),
|
||||
"hermes-gateway": _ts(
|
||||
"Gateway toolset - union of all messaging platform tools",
|
||||
[],
|
||||
@@ -284,12 +244,9 @@ def _registry():
|
||||
|
||||
def _registry_call(method: str, default):
|
||||
"""registry.<method>() or *default* when the registry is unavailable or the call fails."""
|
||||
registry = _registry()
|
||||
if registry is None:
|
||||
return default
|
||||
try:
|
||||
return getattr(registry, method)()
|
||||
except Exception:
|
||||
return getattr(_registry(), method)()
|
||||
except Exception: # registry None (AttributeError) or the call failed
|
||||
return default
|
||||
|
||||
|
||||
@@ -328,12 +285,7 @@ def get_toolset(name: str, *, include_registry: bool = True) -> Optional[Dict[st
|
||||
if not registry_toolset:
|
||||
return None
|
||||
description = f"MCP server '{name}' tools"
|
||||
|
||||
return {
|
||||
"description": description,
|
||||
"tools": registry.get_tool_names_for_toolset(registry_toolset),
|
||||
"includes": [],
|
||||
}
|
||||
return {"description": description, "tools": registry.get_tool_names_for_toolset(registry_toolset), "includes": []}
|
||||
|
||||
|
||||
def bundle_non_core_tools(toolset_name: str) -> Set[str]:
|
||||
@@ -348,8 +300,7 @@ def bundle_non_core_tools(toolset_name: str) -> Set[str]:
|
||||
if not (ts_def and "tools" in ts_def):
|
||||
return set(resolve_toolset(toolset_name)) - core
|
||||
to_remove = set(ts_def["tools"])
|
||||
for inc in ts_def.get("includes", []):
|
||||
inc_def = get_toolset(inc)
|
||||
for inc_def in map(get_toolset, ts_def.get("includes", [])):
|
||||
if inc_def and "tools" in inc_def:
|
||||
to_remove.update(inc_def["tools"])
|
||||
return to_remove - core
|
||||
@@ -443,9 +394,7 @@ def get_all_toolsets() -> Dict[str, Dict[str, Any]]:
|
||||
"""All toolset definitions: static plus plugin-registered."""
|
||||
result = dict(TOOLSETS)
|
||||
for display_name in _plugin_display_names():
|
||||
if display_name in result:
|
||||
continue
|
||||
toolset = get_toolset(display_name)
|
||||
toolset = None if display_name in result else get_toolset(display_name)
|
||||
if toolset:
|
||||
result[display_name] = toolset
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user