"""工具结果失败分类的单一事实源。""" from __future__ import annotations import json import re from typing import Any, Optional, Tuple _RE_UUID = re.compile( r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-" r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" ) _RE_HEX = re.compile(r"0x[0-9a-fA-F]+") _RE_PATH = re.compile(r"(?:[A-Za-z]:)?(?:[/\\][\w.\-一-鿿*]+){2,}") _RE_NUM = re.compile(r"\d+") _RE_WS = re.compile(r"\s+") _EXIT_TAIL = re.compile(r"\[exit (\d+)\]\s*$") _STREAM_MARKS = ("[stdout]", "[stderr]") _BARE_EXIT_RE = re.compile(r"^exit \d+$") def normalize_failure_signature(value: str) -> str: value = _RE_UUID.sub("", value) value = _RE_HEX.sub("", value) value = _RE_PATH.sub("", value) value = _RE_NUM.sub("N", value) return _RE_WS.sub(" ", value).strip()[:120] def classify_failure(content: str) -> Optional[Tuple[str, str]]: """返回 ``(kind, 原始签名行)``;正常工具结果返回 None。""" head = content.lstrip() if "command timed out" in content: return "timeout", "command timed out" if head.startswith("[Error"): return "error", head.splitlines()[0] match = _EXIT_TAIL.search(content) if match and match.group(1) != "0": lines = [ line.strip() for line in content.splitlines()[:-1] if line.strip() and line.strip() not in _STREAM_MARKS ] return "exit", (lines[-1] if lines else f"exit {match.group(1)}") return None def is_bare_exit_signature(value: str) -> bool: return _BARE_EXIT_RE.match(value) is not None def _command_hint(command: str) -> str: command = command.strip().lower() if not command: return "" if re.search(r"\b(?:apt|dpkg|pip)\b.*\b(?:list|show)\b", command): return "dependency probe" if re.search(r"(?:^|[;&|]\s*)(?:which|whereis|command\s+-v)\b", command): return "dependency probe" if re.search(r"(?:^|[;&|]\s*)(?:grep|rg)\b", command): return "search/no match" match = re.search(r"(?:^|&&|;|\|)\s*([a-z0-9_.-]+)", command) return match.group(1) if match else "" def shell_command_hint(prior_payload: Any, tool_call_id: str) -> str: """从历史 assistant tool_call 中提取稳定的 shell 命令类别。""" if not isinstance(prior_payload, dict): return "" calls = prior_payload.get("tool_calls") or [] candidates = [] for call in calls: if not isinstance(call, dict): continue function = call.get("function") or {} if function.get("name") != "shell": continue if tool_call_id and call.get("id") == tool_call_id: candidates = [call] break candidates.append(call) if len(candidates) != 1: return "" return shell_command_hint_from_arguments( (candidates[0].get("function") or {}).get("arguments") ) def shell_command_hint_from_arguments(arguments: Any) -> str: try: args = json.loads(arguments) if isinstance(arguments, str) else arguments except (TypeError, ValueError): return "" if not isinstance(args, dict): return "" return _command_hint(str(args.get("command") or "")) def failure_category(kind: str, signature_line: str, sample: str) -> str: if kind != "exit": return "failure" if signature_line.startswith("[GATE FAIL]"): return "quality_gate" if ( ("[篇幅核算]" in sample or "[字数核算]" in sample) and re.search(r"\[WARN\]\s*\d+\s*项超出\s*/\s*\d+\s*项不足", sample) ): return "quality_gate" if "[质量检查]" in sample and "[WARN] 共发现" in sample: return "quality_gate" return "failure" def structured_failure( tool: str, content: str, *, arguments: Any = None, ) -> Optional[dict[str, str]]: """把工具结果转成可直接持久化的稳定失败事件。""" hit = classify_failure(content) if hit is None: return None kind, signature_line = hit if tool == "shell" and kind == "exit" and _BARE_EXIT_RE.match(signature_line): hint = shell_command_hint_from_arguments(arguments) if hint: signature_line = f"{signature_line} ({hint})" return { "tool": tool or "?", "failure_kind": kind, "signature": normalize_failure_signature(signature_line), "category": failure_category(kind, signature_line, content), "sample": content[:300], }