Source code for tools.reload_tools

"""Hot-reload all tools without restarting the bot."""

from __future__ import annotations

import asyncio
import jsonutil as json
import logging

from tools import ToolRegistry
from tools.alter_privileges import PRIVILEGES, has_privilege

logger = logging.getLogger(__name__)

TOOL_NAME = "reload_tools"
TOOL_DESCRIPTION = (
    "Reload all tools registered with the bot's tool system. "
    "Useful when tools have been added or modified at runtime."
)
TOOL_PARAMETERS = {
    "type": "object",
    "properties": {},
}


[docs] async def run(ctx=None, **_kwargs) -> str: """Execute this tool and return the result. Args: ctx: Tool execution context providing access to bot internals. Returns: str: Result string. """ try: registry = getattr(ctx, "tool_registry", None) if ctx else None config = getattr(ctx, "config", None) if ctx else None if registry is None: return json.dumps( {"success": False, "error": "Tool registry not available via ctx"} ) if config is None: return json.dumps( {"success": False, "error": "Config not available via ctx"} ) user_id = getattr(ctx, "user_id", "") or "" redis = getattr(ctx, "redis", None) if not await has_privilege( redis, user_id, PRIVILEGES["UNSANDBOXED_EXEC"], config ): return json.dumps( { "success": False, "error": ( "reload_tools requires UNSANDBOXED_EXEC. " "Ask an admin to grant it with alter_privileges." ), } ) from tool_loader import load_tools tm = registry.task_manager def _load_into_new_registry() -> ToolRegistry: """Build a fresh ``ToolRegistry`` and load all tools into it. Synchronous closure executed off the event loop (via ``asyncio.to_thread`` in the enclosing :func:`run`) because ``load_tools`` performs blocking module import and exec. It constructs a new registry of the same concrete type as the active one, wiring in the captured ``task_manager`` (``tm``), then calls ``load_tools`` from ``tool_loader`` to discover and register every tool under ``config.tools_dir``. The newly built registry is returned to the caller, which atomically swaps its ``_tools`` into the live registry; this closure itself performs no Redis, KG, LLM, or HTTP work and does not mutate the existing registry. Returns: ToolRegistry: A freshly populated registry instance ready to replace the live one's tool table. Defined inline within :func:`run` and invoked only there; it has no other internal callers. """ fresh = type(registry)(task_manager=tm) load_tools(config.tools_dir, fresh) return fresh # load_tools does sync import/exec — keep it off the event loop. new_registry = await asyncio.to_thread(_load_into_new_registry) with registry._lock: old_count = len(registry._tools) old_permissions = dict(registry._permissions) registry._tools = new_registry._tools registry.invalidate_cache() registry._permissions = old_permissions new_count = len(registry._tools) logger.info("Tool reload completed: %d -> %d tools", old_count, new_count) return json.dumps( { "success": True, "old_count": old_count, "new_count": new_count, "message": f"Tools reloaded successfully: {old_count} -> {new_count} tools", } ) except Exception as e: logger.error("Error reloading tools: %s", e, exc_info=True) return json.dumps({"success": False, "error": f"Error reloading tools: {e}"})