File size: 2,092 Bytes
b6c100d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
"""Common tool interface: every tool runs under a hard timeout and returns

a structured ToolResult. No bare `except: pass` anywhere — every failure is

categorized and surfaced to the planning loop.

"""
from __future__ import annotations

import concurrent.futures
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional


@dataclass
class ToolResult:
    success: bool
    data: str = ""
    error: str = ""
    error_type: str = ""  # "TIMEOUT" | "INPUT_ERROR" | "EXECUTION_ERROR" | "NETWORK_ERROR"

    def for_llm(self) -> str:
        if self.success:
            return self.data
        return f"TOOL FAILED ({self.error_type}): {self.error}"


class Tool(ABC):
    name: str = "tool"
    description: str = ""
    args_hint: str = ""
    timeout_seconds: int = 15

    def execute(self, **kwargs) -> ToolResult:
        """Run the tool body in a worker thread with a hard wall-clock timeout."""
        with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
            future = pool.submit(self._run_safe, **kwargs)
            try:
                return future.result(timeout=self.timeout_seconds)
            except concurrent.futures.TimeoutError:
                future.cancel()
                return ToolResult(
                    success=False,
                    error=f"{self.name} exceeded {self.timeout_seconds}s timeout",
                    error_type="TIMEOUT",
                )

    def _run_safe(self, **kwargs) -> ToolResult:
        try:
            return self._run(**kwargs)
        except TypeError as exc:
            return ToolResult(success=False, error=f"bad arguments: {exc}",
                              error_type="INPUT_ERROR")
        except Exception as exc:  # categorized, logged, surfaced — never swallowed
            return ToolResult(success=False, error=f"{type(exc).__name__}: {exc}",
                              error_type="EXECUTION_ERROR")

    @abstractmethod
    def _run(self, **kwargs) -> ToolResult:
        ...