Ctrl K
ComunidadGuías…

butler/tools/registry_invoke_ops.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Registered tool handler invocation with error envelope (P0-A / P2-F)."""

from __future__ import annotations

import logging
from typing import Any, Callable

from tenacity import RetryError

from butler.core.effects import with_retry

logger = logging.getLogger(__name__)

# Tools with side effects that should NOT be retried (idempotency risk)
NO_RETRY_TOOLS = frozenset({
    "write_file",
    "patch",
    "delete_file",
    "terminal",
    "opencode_task",
    "delegate_task",
    "run_workflow",
    "mcp_install",
    "mcp_remove",
})


def call_tool_with_retry(
    name: str,
    fn: Callable[[], Any],
    *,
    max_attempts: int = 2,
    wait_seconds: float = 0.1,
    retry_on: tuple[type[Exception], ...] = (OSError, ConnectionError),
) -> Any:
    """
    Call a tool function with retry logic, respecting NO_RETRY_TOOLS.

    Args:
        name: Tool name (used for NO_RETRY_TOOLS check)
        fn: Callable to invoke
        max_attempts: Maximum number of attempts
        wait_seconds: Wait time between retries
        retry_on: Exception types to retry on

    Returns:
        The result of calling fn()

    Raises:
        Original exception if retries are exhausted
        Any exception from fn() if tool is in NO_RETRY_TOOLS
    """
    if name in NO_RETRY_TOOLS:
        return fn()

    @with_retry(max_attempts=max_attempts, wait_seconds=wait_seconds, retry_on=retry_on)
    def _call_with_retry() -> Any:
        return fn()

    try:
        return _call_with_retry()
    except RetryError as exc:
        original_exc = getattr(exc, 'last_attempt', None)
        if original_exc is not None:
            original_exc = getattr(original_exc, 'exception', None)
        raise original_exc if original_exc is not None else exc


def invoke_registered_tool_handler(
    *,
    name: str,
    args: dict[str, Any],
    call_args: dict[str, Any],
    handler: Any,
    started_at: float,
    finalize_result: Callable[..., str],
    apply_hooks: Callable[..., str],
) -> str:
    from butler.tools.tool_implicit_context import merge_implicit_tool_args

    merged = merge_implicit_tool_args(call_args)

    try:
        result = call_tool_with_retry(name, lambda: handler(**merged))
        if name == "web_search":
            from butler.tools.registry_gates import note_web_search_outcome

            note_web_search_outcome(result)
        return apply_hooks(
            name,
            args,
            finalize_result(name, args, result, started_at=started_at),
        )
    except Exception as exc:
        logger.error("Tool %s failed: %s", name, exc)
        from butler.tools.registry_gates import tool_error_payload

        payload = tool_error_payload(name, exc)
        err_result = finalize_result(
            name,
            args,
            payload,
            started_at=started_at,
        )
        return apply_hooks(name, args, err_result, failed=True)