|
@@ -5,11 +5,15 @@ from __future__ import annotations
|
|
|
import base64
|
|
import base64
|
|
|
import hashlib
|
|
import hashlib
|
|
|
import json
|
|
import json
|
|
|
|
|
+from functools import wraps
|
|
|
from typing import Any
|
|
from typing import Any
|
|
|
|
|
|
|
|
-from agent import ToolRegistry
|
|
|
|
|
|
|
+from agent import FailureDetail, FailureDisposition, ToolExecutionError, ToolRegistry
|
|
|
from agent.tools.models import ToolResult
|
|
from agent.tools.models import ToolResult
|
|
|
|
|
|
|
|
|
|
+from script_build_host.domain.errors import ScriptBuildError
|
|
|
|
|
+
|
|
|
|
|
+from .failures import classify_script_tool_failure
|
|
|
from .gateway import LegacyScriptToolGateway
|
|
from .gateway import LegacyScriptToolGateway
|
|
|
|
|
|
|
|
TASK_PRESET_BY_KIND = {
|
|
TASK_PRESET_BY_KIND = {
|
|
@@ -169,14 +173,22 @@ def register_script_tools(registry: ToolRegistry, gateway: LegacyScriptToolGatew
|
|
|
|
|
|
|
|
async def dispatch_script_tasks(
|
|
async def dispatch_script_tasks(
|
|
|
task_ids: list[str], context: dict[str, Any] | None = None
|
|
task_ids: list[str], context: dict[str, Any] | None = None
|
|
|
- ) -> str:
|
|
|
|
|
|
|
+ ) -> str | ToolResult:
|
|
|
"""Dispatch allowlisted script Tasks through durable operations."""
|
|
"""Dispatch allowlisted script Tasks through durable operations."""
|
|
|
|
|
|
|
|
results = await gateway.dispatch_script_tasks(
|
|
results = await gateway.dispatch_script_tasks(
|
|
|
task_ids=task_ids,
|
|
task_ids=task_ids,
|
|
|
context=context or {},
|
|
context=context or {},
|
|
|
)
|
|
)
|
|
|
- return _json(results)
|
|
|
|
|
|
|
+ result_json = _json(results)
|
|
|
|
|
+ failure = _strongest_cycle_failure(results)
|
|
|
|
|
+ if failure is None:
|
|
|
|
|
+ return result_json
|
|
|
|
|
+ return ToolResult(
|
|
|
|
|
+ title="One or more dispatched script tasks failed",
|
|
|
|
|
+ output=result_json,
|
|
|
|
|
+ failure=failure,
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
_register(registry, dispatch_script_tasks, capabilities=["task_control"])
|
|
_register(registry, dispatch_script_tasks, capabilities=["task_control"])
|
|
|
|
|
|
|
@@ -754,8 +766,29 @@ def _register(
|
|
|
capabilities: list[str],
|
|
capabilities: list[str],
|
|
|
schema: dict[str, Any] | None = None,
|
|
schema: dict[str, Any] | None = None,
|
|
|
) -> None:
|
|
) -> None:
|
|
|
|
|
+ @wraps(func)
|
|
|
|
|
+ async def guarded(*args: Any, **kwargs: Any) -> Any:
|
|
|
|
|
+ try:
|
|
|
|
|
+ return await func(*args, **kwargs)
|
|
|
|
|
+ except ScriptBuildError as exc:
|
|
|
|
|
+ raise ToolExecutionError(
|
|
|
|
|
+ classify_script_tool_failure(
|
|
|
|
|
+ exc,
|
|
|
|
|
+ source_tool=func.__name__,
|
|
|
|
|
+ context=kwargs.get("context"),
|
|
|
|
|
+ )
|
|
|
|
|
+ ) from exc
|
|
|
|
|
+ except ValueError as exc:
|
|
|
|
|
+ raise ToolExecutionError(
|
|
|
|
|
+ classify_script_tool_failure(
|
|
|
|
|
+ exc,
|
|
|
|
|
+ source_tool=func.__name__,
|
|
|
|
|
+ context=kwargs.get("context"),
|
|
|
|
|
+ )
|
|
|
|
|
+ ) from exc
|
|
|
|
|
+
|
|
|
registry.register(
|
|
registry.register(
|
|
|
- func,
|
|
|
|
|
|
|
+ guarded,
|
|
|
schema=schema,
|
|
schema=schema,
|
|
|
hidden_params=["context"],
|
|
hidden_params=["context"],
|
|
|
groups=["script_build"],
|
|
groups=["script_build"],
|
|
@@ -763,6 +796,42 @@ def _register(
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _strongest_cycle_failure(results: Any) -> FailureDetail | None:
|
|
|
|
|
+ if not isinstance(results, (list, tuple)):
|
|
|
|
|
+ return None
|
|
|
|
|
+ priority = {
|
|
|
|
|
+ FailureDisposition.RETRY_CALL: 0,
|
|
|
|
|
+ FailureDisposition.REPAIR_ATTEMPT: 1,
|
|
|
|
|
+ FailureDisposition.REPLAN_TASK: 2,
|
|
|
|
|
+ FailureDisposition.ABORT_RUN: 3,
|
|
|
|
|
+ }
|
|
|
|
|
+ strongest: FailureDetail | None = None
|
|
|
|
|
+ for result in results:
|
|
|
|
|
+ value = result.get("failure") if isinstance(result, dict) else getattr(result, "failure", None)
|
|
|
|
|
+ if value is None:
|
|
|
|
|
+ continue
|
|
|
|
|
+ failure = FailureDetail.from_dict(value)
|
|
|
|
|
+ result_value = result if isinstance(result, dict) else result.to_dict()
|
|
|
|
|
+ details = {
|
|
|
|
|
+ **dict(failure.details),
|
|
|
|
|
+ **{
|
|
|
|
|
+ key: result_value[key]
|
|
|
|
|
+ for key in ("task_id", "attempt_id", "validation_id")
|
|
|
|
|
+ if result_value.get(key) not in (None, "")
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+ failure = FailureDetail(
|
|
|
|
|
+ code=failure.code,
|
|
|
|
|
+ message=failure.message,
|
|
|
|
|
+ disposition=failure.disposition,
|
|
|
|
|
+ source_tool=failure.source_tool,
|
|
|
|
|
+ details=details,
|
|
|
|
|
+ )
|
|
|
|
|
+ if strongest is None or priority[failure.disposition] > priority[strongest.disposition]:
|
|
|
|
|
+ strongest = failure
|
|
|
|
|
+ return strongest
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _decode_schema() -> dict[str, Any]:
|
|
def _decode_schema() -> dict[str, Any]:
|
|
|
return {
|
|
return {
|
|
|
"type": "function",
|
|
"type": "function",
|