From 656dafb07a766207d4d0e0ee6170331269f65755 Mon Sep 17 00:00:00 2001 From: huanghuoguoguo <1051233107@qq.com> Date: Thu, 14 May 2026 09:01:20 +0800 Subject: [PATCH] feat(toolmgr): enhance tool initialization with backend availability checks --- .../pkg/provider/tools/loaders/native.py | 36 +++++++++++++++- .../provider/tools/loaders/skill_authoring.py | 43 ++++++++++++++++--- src/langbot/pkg/provider/tools/toolmgr.py | 10 ----- 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/src/langbot/pkg/provider/tools/loaders/native.py b/src/langbot/pkg/provider/tools/loaders/native.py index b344d7ef..0b9cee53 100644 --- a/src/langbot/pkg/provider/tools/loaders/native.py +++ b/src/langbot/pkg/provider/tools/loaders/native.py @@ -26,6 +26,34 @@ class NativeToolLoader(loader.ToolLoader): def __init__(self, ap): super().__init__(ap) self._tools: list[resource_tool.LLMTool] | None = None + self._backend_available: bool | None = None + + async def initialize(self): + """Check if backend is truly available at startup.""" + self._backend_available = await self._check_backend_available() + if self._backend_available: + self.ap.logger.info('Native sandbox tools (exec/read/write/edit/glob/grep) are available.') + else: + self.ap.logger.warning( + 'Native sandbox tools (exec/read/write/edit/glob/grep) are NOT available. ' + 'No sandbox backend (Docker/nsjail/E2B) is ready. ' + 'The LLM will not have access to code execution or file operation tools.' + ) + + async def _check_backend_available(self) -> bool: + """Check if the box backend is truly available (not just the runtime).""" + box_service = getattr(self.ap, 'box_service', None) + if box_service is None: + return False + if not getattr(box_service, 'available', False): + return False + # Check if backend is truly available via get_status + try: + status = await box_service.get_status() + backend_info = status.get('backend', {}) + return backend_info.get('available', False) + except Exception: + return False async def get_tools(self, bound_plugins: list[str] | None = None) -> list[resource_tool.LLMTool]: if not self._is_sandbox_available(): @@ -225,8 +253,12 @@ class NativeToolLoader(loader.ToolLoader): refresh_skill(selected_skill.get('name', '')) def _is_sandbox_available(self) -> bool: - box_service = getattr(self.ap, 'box_service', None) - return bool(getattr(box_service, 'available', False)) + """Check if sandbox backend is available. + + This checks the cached backend availability from initialization, + not just whether the box_service process is running. + """ + return bool(self._backend_available) def _build_exec_tool(self) -> resource_tool.LLMTool: return resource_tool.LLMTool( diff --git a/src/langbot/pkg/provider/tools/loaders/skill_authoring.py b/src/langbot/pkg/provider/tools/loaders/skill_authoring.py index ed3bf518..6951142b 100644 --- a/src/langbot/pkg/provider/tools/loaders/skill_authoring.py +++ b/src/langbot/pkg/provider/tools/loaders/skill_authoring.py @@ -27,20 +27,51 @@ class SkillToolLoader(loader.ToolLoader): def __init__(self, ap): super().__init__(ap) self._tools: list[resource_tool.LLMTool] = [] + self._sandbox_available: bool = False async def initialize(self): - self._tools = [ - self._build_activate_skill_tool(), - self._build_register_skill_tool(), - ] + # Check if sandbox backend is available (same check as native tools) + self._sandbox_available = await self._check_sandbox_available() + if self._sandbox_available: + self._tools = [ + self._build_activate_skill_tool(), + self._build_register_skill_tool(), + ] + else: + self.ap.logger.info( + 'Skill tools (activate/register_skill) are NOT available. ' + 'No sandbox backend (Docker/nsjail/E2B) is ready.' + ) + + async def _check_sandbox_available(self) -> bool: + """Check if the box backend is truly available (not just the runtime).""" + box_service = getattr(self.ap, 'box_service', None) + if box_service is None: + return False + if not getattr(box_service, 'available', False): + return False + # Check if backend is truly available via get_status + try: + status = await box_service.get_status() + backend_info = status.get('backend', {}) + return backend_info.get('available', False) + except Exception: + return False async def get_tools(self, bound_plugins: list[str] | None = None) -> list[resource_tool.LLMTool]: - if not self._has_skill_manager(): + if not self._is_available(): return [] return list(self._tools) async def has_tool(self, name: str) -> bool: - return self._has_skill_manager() and name in SKILL_TOOL_NAMES + return self._is_available() and name in SKILL_TOOL_NAMES + + def _is_available(self) -> bool: + """Check if skill tools should be available. + + Skill tools require both a skill manager and a sandbox backend. + """ + return self._has_skill_manager() and self._sandbox_available async def invoke_tool(self, name: str, parameters: dict, query) -> typing.Any: if name == ACTIVATE_SKILL_TOOL_NAME: diff --git a/src/langbot/pkg/provider/tools/toolmgr.py b/src/langbot/pkg/provider/tools/toolmgr.py index 0490d4d5..5c510fcd 100644 --- a/src/langbot/pkg/provider/tools/toolmgr.py +++ b/src/langbot/pkg/provider/tools/toolmgr.py @@ -44,16 +44,6 @@ class ToolManager: self.native_tool_loader = native_loader.NativeToolLoader(self.ap) await self.native_tool_loader.initialize() - # Log native (sandbox) tool availability once at startup - box_service = getattr(self.ap, 'box_service', None) - if box_service and getattr(box_service, 'available', False): - self.ap.logger.info('Native sandbox tools (exec/read/write/edit/glob/grep) are available.') - else: - self.ap.logger.warning( - 'Native sandbox tools (exec/read/write/edit/glob/grep) are NOT available. ' - 'Box runtime is not connected — the LLM will not have access to code execution tools.' - ) - self.plugin_tool_loader = plugin_loader.PluginToolLoader(self.ap) await self.plugin_tool_loader.initialize() self.mcp_tool_loader = mcp_loader.MCPLoader(self.ap)