mirror of
https://github.com/MemeMeow-Studio/MemeMeow.git
synced 2026-10-18 13:42:36 +08:00
242 lines
10 KiB
Python
242 lines
10 KiB
Python
"""任务与外部运行时执行的无状态公共合同。
|
|
|
|
本模块位于任务控制面与 OpenCode/executor 适配器之间,只保存可验证的任务、
|
|
scope、workspace、图片版本和 attempt 绑定事实。它不访问数据库、文件系统或
|
|
网络,便于 API、Worker、host runner 和容器 executor 共用同一套 fencing 语义。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import re
|
|
from dataclasses import dataclass, replace
|
|
from datetime import datetime, timezone
|
|
from threading import Lock
|
|
from typing import Any, Mapping
|
|
|
|
|
|
OPAQUE_IDENTIFIER_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,254}\Z")
|
|
ACTIVE_TASK_STATUSES = frozenset({"queued", "running"})
|
|
TERMINAL_TASK_STATUSES = frozenset({"succeeded", "failed", "cancelled", "unknown_execution"})
|
|
EXTERNAL_UNKNOWN_ERRORS = frozenset(
|
|
{
|
|
"unknown_execution",
|
|
"agent_connection_interrupted",
|
|
"agent_executor_unavailable",
|
|
"agent_runtime_unavailable",
|
|
}
|
|
)
|
|
|
|
|
|
class ExecutionBindingError(ValueError):
|
|
"""执行绑定不可信时抛出的稳定错误。
|
|
|
|
``code`` 只包含可对外记录的错误标识;物理路径、token 和原始 payload 不会
|
|
通过异常消息传播到公共任务状态。
|
|
"""
|
|
|
|
def __init__(self, code: str, message: str | None = None) -> None:
|
|
"""创建绑定错误,供入口将其映射到现有错误协议。"""
|
|
self.code = code
|
|
super().__init__(message or code)
|
|
|
|
|
|
def normalize_identifier(value: object, *, kind: str) -> str:
|
|
"""规范化 task/attempt/session/scope 等不透明标识。
|
|
|
|
输入成功时返回原字符串;类型、长度或控制字符不符合协议时抛出
|
|
``ExecutionBindingError``,调用方可在请求边界将其转换为稳定的 mismatch 错误。
|
|
"""
|
|
if not isinstance(value, str) or not value or len(value) > 255 or OPAQUE_IDENTIFIER_RE.fullmatch(value) is None:
|
|
raise ExecutionBindingError(f"{kind}_invalid")
|
|
return value
|
|
|
|
|
|
def utc_timestamp() -> str:
|
|
"""返回 attempt 诊断使用的 UTC ISO 时间。"""
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def stable_input_digest(*values: object) -> str:
|
|
"""为外部 attempt 输入生成稳定摘要,不把 secret 或完整 prompt 写入状态。"""
|
|
encoded = json.dumps(values, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str).encode("utf-8")
|
|
return hashlib.sha256(encoded).hexdigest()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ExecutionBinding:
|
|
"""一次外部执行可接受的身份边界。
|
|
|
|
``claim_generation`` 是数据库 lease fencing 代数;``workspace_selector`` 和
|
|
``image_sha256`` 是可选业务事实,存在时必须在结果采纳前再次匹配。
|
|
"""
|
|
|
|
task_id: str
|
|
attempt_id: str
|
|
scope_id: str
|
|
claim_generation: int | None = None
|
|
image_sha256: str | None = None
|
|
workspace_selector: str | None = None
|
|
input_digest: str | None = None
|
|
session_id: str | None = None
|
|
|
|
def __post_init__(self) -> None:
|
|
"""校验构造时的身份字段,避免无效值进入运行时 adapter。"""
|
|
for value, kind in ((self.task_id, "task"), (self.attempt_id, "attempt"), (self.scope_id, "scope")):
|
|
normalize_identifier(value, kind=kind)
|
|
if self.claim_generation is not None and (not isinstance(self.claim_generation, int) or isinstance(self.claim_generation, bool) or self.claim_generation < 0):
|
|
raise ExecutionBindingError("claim_generation_invalid")
|
|
if self.image_sha256 is not None and (not isinstance(self.image_sha256, str) or not re.fullmatch(r"[0-9a-fA-F]{64}", self.image_sha256)):
|
|
raise ExecutionBindingError("image_version_invalid")
|
|
if self.workspace_selector is not None:
|
|
normalize_identifier(self.workspace_selector, kind="workspace")
|
|
if self.input_digest is not None and re.fullmatch(r"[0-9a-fA-F]{64}", self.input_digest) is None:
|
|
raise ExecutionBindingError("input_digest_invalid")
|
|
if self.session_id is not None:
|
|
normalize_identifier(self.session_id, kind="session")
|
|
|
|
def matches(self, values: Mapping[str, object], *, require_attempt: bool = True) -> bool:
|
|
"""判断可信映射是否与当前绑定一致。
|
|
|
|
``values`` 通常来自数据库 task/attempt 或 executor 响应;缺少必需字段
|
|
会返回 False,避免把不完整响应当作成功。
|
|
"""
|
|
expected = {
|
|
"task_id": self.task_id,
|
|
"scope_id": self.scope_id,
|
|
"claim_generation": self.claim_generation,
|
|
"image_sha256": self.image_sha256,
|
|
"workspace_selector": self.workspace_selector,
|
|
"input_digest": self.input_digest,
|
|
"session_id": self.session_id,
|
|
}
|
|
if require_attempt:
|
|
expected["attempt_id"] = self.attempt_id
|
|
for key, wanted in expected.items():
|
|
if wanted is None:
|
|
continue
|
|
if values.get(key) != wanted:
|
|
return False
|
|
return True
|
|
|
|
def require_matches(self, values: Mapping[str, object], *, require_attempt: bool = True) -> None:
|
|
"""要求可信映射与当前绑定完全一致,否则抛出稳定 fencing 错误。"""
|
|
if not self.matches(values, require_attempt=require_attempt):
|
|
raise ExecutionBindingError("execution_binding_mismatch")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ExecutionAttempt:
|
|
"""逻辑任务的一次实际执行尝试及其有限终态诊断。
|
|
|
|
该值对象不负责启动进程;runtime supervisor 只需在外部副作用前后调用
|
|
``started``/``succeed``/``failed``/``unknown``,即可统一表达恢复和取消边界。
|
|
"""
|
|
|
|
binding: ExecutionBinding
|
|
status: str = "queued"
|
|
external_effect_started: bool = False
|
|
error_code: str | None = None
|
|
started_at: str | None = None
|
|
completed_at: str | None = None
|
|
|
|
def __post_init__(self) -> None:
|
|
"""限制 attempt 状态和错误字段,防止构造出不可收束记录。"""
|
|
if self.status not in {"queued", "running", "succeeded", "failed", "cancelled", "unknown_execution"}:
|
|
raise ExecutionBindingError("attempt_status_invalid")
|
|
if self.status in TERMINAL_TASK_STATUSES and self.completed_at is None:
|
|
raise ExecutionBindingError("attempt_completion_timestamp_required")
|
|
if self.status == "running" and self.started_at is None:
|
|
raise ExecutionBindingError("attempt_start_timestamp_required")
|
|
|
|
@property
|
|
def terminal(self) -> bool:
|
|
"""返回 attempt 是否已进入不可继续执行的终态。"""
|
|
return self.status in TERMINAL_TASK_STATUSES
|
|
|
|
def started(self, *, at: str | None = None) -> "ExecutionAttempt":
|
|
"""标记外部副作用已开始,供 supervisor 启动进程或远端请求后调用。"""
|
|
if self.terminal:
|
|
raise ExecutionBindingError("attempt_already_terminal")
|
|
return replace(self, status="running", external_effect_started=True, started_at=at or utc_timestamp())
|
|
|
|
def succeed(self, *, at: str | None = None) -> "ExecutionAttempt":
|
|
"""在结果已绑定并完整校验后收束成功。"""
|
|
if self.status != "running":
|
|
raise ExecutionBindingError("attempt_not_running")
|
|
return replace(self, status="succeeded", completed_at=at or utc_timestamp(), error_code=None)
|
|
|
|
def failed(self, code: str, *, at: str | None = None) -> "ExecutionAttempt":
|
|
"""收束可证明的失败;外部状态不确定时应调用 ``unknown``。"""
|
|
if self.terminal:
|
|
raise ExecutionBindingError("attempt_already_terminal")
|
|
if not isinstance(code, str) or not code:
|
|
raise ExecutionBindingError("attempt_error_invalid")
|
|
return replace(self, status="failed", completed_at=at or utc_timestamp(), error_code=code)
|
|
|
|
def cancelled(self, *, at: str | None = None) -> "ExecutionAttempt":
|
|
"""收束用户取消或受控终止的 attempt。"""
|
|
if self.terminal:
|
|
raise ExecutionBindingError("attempt_already_terminal")
|
|
return replace(self, status="cancelled", completed_at=at or utc_timestamp(), error_code="task_interrupted")
|
|
|
|
def unknown(self, code: str = "unknown_execution", *, at: str | None = None) -> "ExecutionAttempt":
|
|
"""收束无法证明外部副作用状态的 attempt,禁止隐式重放。"""
|
|
if self.terminal:
|
|
raise ExecutionBindingError("attempt_already_terminal")
|
|
_ = code if code in EXTERNAL_UNKNOWN_ERRORS else "unknown_execution"
|
|
return replace(self, status="unknown_execution", completed_at=at or utc_timestamp(), error_code="unknown_execution")
|
|
|
|
|
|
class AttemptFence:
|
|
"""进程内的 attempt/claim fencing 辅助器。
|
|
|
|
数据库 claim 仍是跨进程权威;本类用于在一个 adapter 内快速拒绝取消后或
|
|
新 attempt 覆盖后的旧回调,避免旧线程继续提交结果。
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
"""创建空 fencing 表。"""
|
|
self._lock = Lock()
|
|
self._bindings: dict[str, ExecutionBinding] = {}
|
|
|
|
def bind(self, binding: ExecutionBinding) -> None:
|
|
"""登记 task 当前绑定;替换旧 attempt 时使旧回调失效。"""
|
|
with self._lock:
|
|
self._bindings[binding.task_id] = binding
|
|
|
|
def accepts(self, binding: ExecutionBinding) -> bool:
|
|
"""返回 task 当前是否仍接受该 attempt 的写回。"""
|
|
with self._lock:
|
|
return self._bindings.get(binding.task_id) == binding
|
|
|
|
def release(self, task_id: str, attempt_id: str | None = None) -> None:
|
|
"""按 task/attempt 清理本地绑定,避免旧任务占用内存。"""
|
|
with self._lock:
|
|
current = self._bindings.get(task_id)
|
|
if current is not None and (attempt_id is None or current.attempt_id == attempt_id):
|
|
self._bindings.pop(task_id, None)
|
|
|
|
|
|
def classify_external_failure(code: str, *, effect_started: bool, status_known: bool) -> str:
|
|
"""把 runtime 失败分类为可重试失败或 fail-closed unknown execution。"""
|
|
if not status_known or (effect_started and code in EXTERNAL_UNKNOWN_ERRORS):
|
|
return "unknown_execution"
|
|
return code if isinstance(code, str) and code else "task_failed"
|
|
|
|
|
|
__all__ = [
|
|
"ACTIVE_TASK_STATUSES",
|
|
"AttemptFence",
|
|
"ExecutionAttempt",
|
|
"ExecutionBinding",
|
|
"ExecutionBindingError",
|
|
"EXTERNAL_UNKNOWN_ERRORS",
|
|
"TERMINAL_TASK_STATUSES",
|
|
"classify_external_failure",
|
|
"normalize_identifier",
|
|
"stable_input_digest",
|
|
"utc_timestamp",
|
|
]
|