Files
yawan-workbench/server/app/hermes_core.py
T

166 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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",
]