Files

166 lines
6.2 KiB
Python
Raw Permalink Normal View History

"""Hermes Agent 核心机制移植(以雅文工作台为主体的融合模块)。
来源与适配说明(参考 Hermes Agent 2026.8.27 源码):
- IterationBudget ← agent/iteration_budget.py:线程安全的轮次预算计数器,
扩展为「运行级预算」,多个 Agent 会话共享同一个 run 的预算池。
- jittered_backoff ← agent/retry_utils.py:抖动退避替代固定指数退避,
防止多个会话同时撞限流时形成重试尖峰;支持解析 Retry-After。
- 工具停滞护栏 ← agent/tool_guardrails.py:对「同名工具 + 相同参数」的
重复调用做指纹跟踪,重复时注入提示,超限时熔断。
该模块只包含纯逻辑,不依赖数据库,便于单元测试。
"""
from __future__ import annotations
import hashlib
import json
import random
import threading
import time
from dataclasses import dataclass, field
from email.utils import parsedate_to_datetime
from typing import Any
# ---------------- 运行级轮次预算(源自 IterationBudget) ----------------
class IterationBudget:
"""线程安全的运行级轮次预算。
一次任务运行(CampaignRun)创建一个预算池;其中的每个 Agent 会话
(拆解/生产/质检/组合)消耗同一个池子,防止某个失控会话无限消耗
模型调用和点数。execute_code 类可退回的调用可用 refund() 归还。
"""
def __init__(self, max_total: int):
self.max_total = max(1, int(max_total))
self._used = 0
self._lock = threading.Lock()
def consume(self) -> bool:
"""尝试消耗一轮。返回 False 表示预算耗尽,应停止调用模型。"""
with self._lock:
if self._used >= self.max_total:
return False
self._used += 1
return True
def refund(self) -> None:
with self._lock:
if self._used > 0:
self._used -= 1
@property
def used(self) -> int:
with self._lock:
return self._used
@property
def remaining(self) -> int:
with self._lock:
return max(0, self.max_total - self._used)
# ---------------- 抖动退避(源自 retry_utils) ----------------
def parse_retry_after_seconds(value_or_headers: Any) -> float | None:
"""把 Retry-After(数字/HTTP 日期/headers 映射)解析成非负秒数。"""
raw = value_or_headers
if raw is not None and not isinstance(raw, (str, int, float)):
getter = getattr(raw, "get", None)
if callable(getter):
try:
raw = getter("Retry-After")
if raw is None:
raw = getter("retry-after")
except Exception:
raw = None
if raw is None:
return None
try:
seconds = float(raw)
except (TypeError, ValueError):
try:
from datetime import datetime, timezone
parsed = parsedate_to_datetime(str(raw))
if parsed is None:
return None
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
seconds = (parsed - datetime.now(timezone.utc)).total_seconds()
except Exception:
return None
return max(0.0, seconds)
def jittered_backoff(attempt: int, base: float = 1.6, cap: float = 24.0) -> float:
"""全抖动(full jitter)退避:在 [0, min(cap, base*2^n)] 区间随机取值。
与固定 `2 ** n` 相比,多个并发会话不会在同一时刻集体重试,
避免限流恢复瞬间被再次打挂(thundering herd)。
"""
ceiling = min(cap, base * (2 ** max(0, attempt)))
return random.uniform(0, ceiling)
def retry_sleep(attempt: int, retry_after: Any = None,
base: float = 1.6, cap: float = 24.0) -> float:
"""决定一次重试应等待的秒数:优先尊重服务端 Retry-After,否则抖动退避。"""
if retry_after is not None:
seconds = parse_retry_after_seconds(retry_after)
if seconds is not None:
return min(max(seconds, 0.5), cap)
return jittered_backoff(attempt, base=base, cap=cap)
# ---------------- 工具停滞护栏(源自 tool_guardrails) ----------------
def tool_call_fingerprint(tool_name: str, arguments: dict[str, Any] | None) -> str:
"""工具名 + 归一化参数生成指纹,用于识别「原样重跑」的停滞调用。"""
try:
canonical = json.dumps(arguments or {}, ensure_ascii=False,
sort_keys=True, default=str)
except (TypeError, ValueError):
canonical = str(arguments)
raw = f"{tool_name}\0{canonical}".encode("utf-8", errors="ignore")
return hashlib.sha256(raw).hexdigest()
@dataclass
class StallGuard:
"""跟踪一个 Agent 会话内的工具调用指纹,识别重复停滞。
- 同一指纹连续出现达到 ``notice_at`` 次:返回 "notice",运行时把提示注入上下文;
- 达到 ``halt_at`` 次:返回 "halt",熔断本轮循环并标记失败原因。
只做纯决策,不产生副作用(与 Hermes 的无副作用护栏一致)。
"""
notice_at: int = 3
halt_at: int = 5
_counts: dict[str, int] = field(default_factory=dict)
def observe(self, fingerprint: str) -> str:
"""记录一次调用,返回决策:ok | notice | halt。"""
count = self._counts.get(fingerprint, 0) + 1
self._counts[fingerprint] = count
if count >= max(2, self.halt_at):
return "halt"
if count >= max(2, self.notice_at):
return "notice"
return "ok"
def halt_reason(self, tool_name: str) -> str:
return (f"工具「{tool_name}」以相同参数连续调用 {self.halt_at} 次无进展,"
f"已熔断本次会话以保护点数余额")
def stall_notice_text(tool_name: str, count: int) -> str:
return (f"系统提示:工具「{tool_name}」已用完全相同的参数调用 {count} 次,"
f"结果不会变化。请改用不同参数,或直接基于已有结果完成任务。")
__all__ = [
"IterationBudget", "parse_retry_after_seconds", "jittered_backoff", "retry_sleep",
"tool_call_fingerprint", "StallGuard", "stall_notice_text",
]