mirror of
https://github.com/MemeMeow-Studio/MemeMeow.git
synced 2026-10-17 04:41:01 +08:00
167 lines
7.1 KiB
Python
167 lines
7.1 KiB
Python
# Executor 进程循环中的分析用量控制,负责就绪检查及终止原因判定。
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from decimal import Decimal
|
|
import json
|
|
import os
|
|
|
|
from pathlib import Path
|
|
import socket
|
|
import struct
|
|
import time
|
|
|
|
from executor.analysis_usage import AnalysisUsageError, read_analysis_usage
|
|
|
|
PLUGIN_VERSION = "1.18.18"
|
|
|
|
|
|
class AnalysisControlError(RuntimeError):
|
|
"""携带可公开错误码和受保护原因,交给现有 supervisor 回收进程。"""
|
|
|
|
def __init__(self, code: str, reason: str):
|
|
"""保存控制失败类别和阶段,不包含模型正文。"""
|
|
self.code, self.reason = code, reason
|
|
super().__init__(reason)
|
|
|
|
|
|
@dataclass
|
|
class AnalysisMonitor:
|
|
"""当前 attempt 的检查状态;恢复输入保留原策略和最近可信金额。"""
|
|
|
|
attempt_id: str
|
|
policy: dict[str, object]
|
|
database: Path
|
|
directory: Path
|
|
status_path: Path
|
|
startup_deadline: float
|
|
status_socket: socket.socket | None = None
|
|
expected_pid: int | None = None
|
|
observed_cost: Decimal = Decimal(0)
|
|
usage_checked_at: str | None = None
|
|
reminder_sent: bool = False
|
|
ready: bool = False
|
|
next_check: float = 0
|
|
reminder_error: str | None = None
|
|
reminder_error_detail: str | None = None
|
|
failure_reason: str | None = None
|
|
|
|
def check(self, session_id: str | None, *, exited: bool = False) -> None:
|
|
"""轮询当前 attempt 的就绪及金额;终止原因通过异常交给进程管理者。"""
|
|
now = time.monotonic()
|
|
if not exited and now < self.next_check:
|
|
return
|
|
self.next_check = now + 0.25
|
|
status = None
|
|
if self.status_socket is not None:
|
|
status = self._read_socket_status()
|
|
elif self.status_path.exists():
|
|
try:
|
|
status = json.loads(self.status_path.read_text(encoding="utf-8"))
|
|
except (OSError, UnicodeError, ValueError) as exc:
|
|
raise AnalysisControlError("agent_analysis_reminder_plugin_unavailable", "plugin_status_unreadable") from exc
|
|
if status is not None:
|
|
if (
|
|
not isinstance(status, dict)
|
|
or status.get("attempt_id") != self.attempt_id
|
|
or status.get("plugin_version") != PLUGIN_VERSION
|
|
or status.get("policy_version") != self.policy["version"]
|
|
):
|
|
raise AnalysisControlError("agent_analysis_reminder_plugin_unavailable", "plugin_attempt_binding_mismatch")
|
|
self.ready = status.get("ready") is True
|
|
self.reminder_sent = self.reminder_sent or status.get("reminder_sent") is True
|
|
self.reminder_error = status.get("error") if isinstance(status.get("error"), str) else None
|
|
self.reminder_error_detail = status.get("error_detail") if isinstance(status.get("error_detail"), str) else None
|
|
if not self.ready and self.reminder_error:
|
|
if self.reminder_error not in {
|
|
"analysis_plugin_runtime_version_incompatible",
|
|
"analysis_plugin_initialization_failed",
|
|
}:
|
|
raise AnalysisControlError("agent_analysis_reminder_plugin_unavailable", "plugin_error_status_invalid")
|
|
raise AnalysisControlError("agent_analysis_reminder_plugin_unavailable", self.reminder_error)
|
|
if session_id and status.get("session_id") not in (None, session_id):
|
|
raise AnalysisControlError("agent_analysis_usage_unavailable", "plugin_session_binding_mismatch")
|
|
if session_id:
|
|
try:
|
|
usage = read_analysis_usage(
|
|
self.database, session_id=session_id, directory=self.directory,
|
|
minimum_cost=self.observed_cost,
|
|
)
|
|
except AnalysisUsageError as exc:
|
|
raise AnalysisControlError(exc.code, exc.reason) from exc
|
|
self.observed_cost = usage.observed_cost
|
|
self.usage_checked_at = usage.checked_at
|
|
elif exited or now >= self.startup_deadline:
|
|
raise AnalysisControlError("agent_analysis_usage_unavailable", "session_binding_deadline_exceeded")
|
|
if self.observed_cost >= Decimal(str(self.policy["termination_cost"])):
|
|
raise AnalysisControlError("agent_maximum_analysis_depth_exceeded", "termination_cost_reached")
|
|
if not self.ready and (exited or now >= self.startup_deadline):
|
|
reason = "process_exited_before_plugin_ready" if exited else "plugin_readiness_deadline_exceeded"
|
|
raise AnalysisControlError("agent_analysis_reminder_plugin_unavailable", reason)
|
|
|
|
def _read_socket_status(self) -> dict[str, object] | None:
|
|
"""读取 Executor 创建的状态 socket。
|
|
|
|
由 AnalysisMonitor.check 轮询调用;无可读连接时返回 None,收到合法状态时返回 JSON 对象。
|
|
"""
|
|
|
|
assert self.status_socket is not None
|
|
self.status_socket.setblocking(False)
|
|
try:
|
|
connection, _address = self.status_socket.accept()
|
|
except BlockingIOError:
|
|
return None
|
|
try:
|
|
connection.settimeout(0.5)
|
|
peer_pid, _peer_uid, _peer_gid = struct.unpack(
|
|
"3i",
|
|
connection.getsockopt(
|
|
socket.SOL_SOCKET,
|
|
socket.SO_PEERCRED,
|
|
struct.calcsize("3i"),
|
|
),
|
|
)
|
|
if self.expected_pid is not None and peer_pid != self.expected_pid:
|
|
try:
|
|
expected_session = os.getsid(self.expected_pid)
|
|
peer_session = os.getsid(peer_pid)
|
|
except OSError:
|
|
return None
|
|
if peer_session != expected_session:
|
|
return None
|
|
if self.expected_pid is None:
|
|
self.expected_pid = peer_pid
|
|
chunks: list[bytes] = []
|
|
try:
|
|
while True:
|
|
chunk = connection.recv(65536)
|
|
if not chunk:
|
|
break
|
|
chunks.append(chunk)
|
|
if sum(map(len, chunks)) > 65536:
|
|
raise AnalysisControlError(
|
|
"agent_analysis_reminder_plugin_unavailable",
|
|
"plugin_status_too_large",
|
|
)
|
|
except socket.timeout as exc:
|
|
raise AnalysisControlError(
|
|
"agent_analysis_reminder_plugin_unavailable",
|
|
"plugin_status_read_timeout",
|
|
) from exc
|
|
try:
|
|
value = json.loads(b"".join(chunks).decode("utf-8"))
|
|
except (UnicodeError, ValueError) as exc:
|
|
raise AnalysisControlError(
|
|
"agent_analysis_reminder_plugin_unavailable",
|
|
"plugin_status_unreadable",
|
|
) from exc
|
|
if not isinstance(value, dict):
|
|
raise AnalysisControlError(
|
|
"agent_analysis_reminder_plugin_unavailable",
|
|
"plugin_status_unreadable",
|
|
)
|
|
return value
|
|
finally:
|
|
connection.close()
|