|
|
@@ -16,6 +16,11 @@ import logging
|
|
|
import time
|
|
|
from typing import Any, Callable, Dict, List, Optional, Sequence
|
|
|
|
|
|
+from agent.failures import (
|
|
|
+ FailureDetail,
|
|
|
+ FailureDisposition,
|
|
|
+ ToolExecutionError,
|
|
|
+)
|
|
|
from agent.tools.models import ToolCapability
|
|
|
|
|
|
from agent.tools.url_matcher import filter_by_url
|
|
|
@@ -54,6 +59,22 @@ class ToolStats:
|
|
|
}
|
|
|
|
|
|
|
|
|
+def _record_tool_outcome(stats: ToolStats, start_time: float, *, success: bool) -> None:
|
|
|
+ if success:
|
|
|
+ stats.success_count += 1
|
|
|
+ else:
|
|
|
+ stats.failure_count += 1
|
|
|
+ stats.total_duration += time.time() - start_time
|
|
|
+
|
|
|
+
|
|
|
+def _failure_result(failure: FailureDetail) -> Dict[str, Any]:
|
|
|
+ payload = {"failure": failure.to_dict()}
|
|
|
+ return {
|
|
|
+ "text": json.dumps(payload, ensure_ascii=False, indent=2),
|
|
|
+ "_control": {"failure": failure.to_dict()},
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
class ToolRegistry:
|
|
|
"""工具注册表"""
|
|
|
_REGISTRATION_FIELDS = (
|
|
|
@@ -361,9 +382,14 @@ class ToolRegistry:
|
|
|
JSON 字符串格式的结果
|
|
|
"""
|
|
|
if name not in self._tools:
|
|
|
- error_msg = f"Unknown tool: {name}"
|
|
|
- logger.error(f"[ToolRegistry] {error_msg}")
|
|
|
- return json.dumps({"error": error_msg}, ensure_ascii=False)
|
|
|
+ failure = FailureDetail(
|
|
|
+ code="UNKNOWN_TOOL",
|
|
|
+ message=f"Unknown tool: {name}",
|
|
|
+ disposition=FailureDisposition.ABORT_RUN,
|
|
|
+ source_tool=name,
|
|
|
+ )
|
|
|
+ logger.error("[ToolRegistry] %s", failure.message)
|
|
|
+ return _failure_result(failure)
|
|
|
|
|
|
start_time = time.time()
|
|
|
stats = self._stats[name]
|
|
|
@@ -442,26 +468,33 @@ class ToolRegistry:
|
|
|
else:
|
|
|
result = func(**kwargs)
|
|
|
|
|
|
- # 记录成功
|
|
|
- stats.success_count += 1
|
|
|
- duration = time.time() - start_time
|
|
|
- stats.total_duration += duration
|
|
|
-
|
|
|
# 返回结果:ToolResult 转为可序列化格式
|
|
|
if isinstance(result, str):
|
|
|
+ _record_tool_outcome(stats, start_time, success=True)
|
|
|
return result
|
|
|
|
|
|
# 处理 ToolResult 对象
|
|
|
from agent.tools.models import ToolResult
|
|
|
if isinstance(result, ToolResult):
|
|
|
+ failure = result.failure
|
|
|
+ if failure is None and result.error:
|
|
|
+ failure = FailureDetail(
|
|
|
+ code="TOOL_REPORTED_ERROR",
|
|
|
+ message=result.error,
|
|
|
+ disposition=FailureDisposition.RETRY_CALL,
|
|
|
+ source_tool=name,
|
|
|
+ )
|
|
|
+ result.failure = failure
|
|
|
ret = {"text": result.to_llm_message()}
|
|
|
+ if failure is not None:
|
|
|
+ ret["_control"] = {"failure": failure.to_dict()}
|
|
|
|
|
|
# Runner 消费的控制字段与业务文本分离,不能靠解析 output 猜测终止。
|
|
|
if result.terminate_run:
|
|
|
- ret["_control"] = {
|
|
|
+ ret.setdefault("_control", {}).update({
|
|
|
"terminate_run": True,
|
|
|
"result_summary": result.result_summary or result.long_term_memory or result.output,
|
|
|
- }
|
|
|
+ })
|
|
|
|
|
|
# 保留images
|
|
|
if result.images:
|
|
|
@@ -471,24 +504,46 @@ class ToolRegistry:
|
|
|
if result.tool_usage:
|
|
|
ret["tool_usage"] = result.tool_usage
|
|
|
|
|
|
+ _record_tool_outcome(stats, start_time, success=failure is None)
|
|
|
+
|
|
|
# 向后兼容:只有 text 时返回字符串;有控制信息时必须保留 dict。
|
|
|
if len(ret) == 1:
|
|
|
return ret["text"]
|
|
|
return ret
|
|
|
|
|
|
+ _record_tool_outcome(stats, start_time, success=True)
|
|
|
return json.dumps(result, ensure_ascii=False, indent=2)
|
|
|
|
|
|
- except Exception as e:
|
|
|
- # 记录失败
|
|
|
- stats.failure_count += 1
|
|
|
- duration = time.time() - start_time
|
|
|
- stats.total_duration += duration
|
|
|
-
|
|
|
- error_msg = f"Error executing tool '{name}': {str(e)}"
|
|
|
- logger.error(f"[ToolRegistry] {error_msg}")
|
|
|
- import traceback
|
|
|
- logger.error(traceback.format_exc())
|
|
|
- return json.dumps({"error": error_msg}, ensure_ascii=False)
|
|
|
+ except ToolExecutionError as exc:
|
|
|
+ _record_tool_outcome(stats, start_time, success=False)
|
|
|
+ failure = exc.failure
|
|
|
+ if failure.source_tool is None:
|
|
|
+ failure = FailureDetail(
|
|
|
+ code=failure.code,
|
|
|
+ message=failure.message,
|
|
|
+ disposition=failure.disposition,
|
|
|
+ source_tool=name,
|
|
|
+ details=failure.details,
|
|
|
+ )
|
|
|
+ logger.warning(
|
|
|
+ "[ToolRegistry] expected rejection tool=%s code=%s disposition=%s: %s",
|
|
|
+ name,
|
|
|
+ failure.code,
|
|
|
+ failure.disposition.value,
|
|
|
+ failure.message,
|
|
|
+ )
|
|
|
+ return _failure_result(failure)
|
|
|
+ except Exception:
|
|
|
+ _record_tool_outcome(stats, start_time, success=False)
|
|
|
+ logger.exception("[ToolRegistry] unexpected failure executing tool '%s'", name)
|
|
|
+ return _failure_result(
|
|
|
+ FailureDetail(
|
|
|
+ code="UNEXPECTED_TOOL_ERROR",
|
|
|
+ message="The tool failed unexpectedly",
|
|
|
+ disposition=FailureDisposition.ABORT_RUN,
|
|
|
+ source_tool=name,
|
|
|
+ )
|
|
|
+ )
|
|
|
|
|
|
def get_stats(self, tool_name: Optional[str] = None) -> Dict[str, Dict[str, Any]]:
|
|
|
"""
|