entropy-sdk 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- entropy_sdk/__init__.py +25 -0
- entropy_sdk/adapters/__init__.py +5 -0
- entropy_sdk/adapters/base.py +31 -0
- entropy_sdk/adapters/langchain.py +52 -0
- entropy_sdk/adapters/pydanticai.py +43 -0
- entropy_sdk/adapters/raw.py +38 -0
- entropy_sdk/fallback.py +45 -0
- entropy_sdk/gate.py +82 -0
- entropy_sdk/gear.py +35 -0
- entropy_sdk/policy.py +55 -0
- entropy_sdk/py.typed +0 -0
- entropy_sdk/runtime.py +278 -0
- entropy_sdk/state.py +156 -0
- entropy_sdk-0.1.5.dist-info/METADATA +198 -0
- entropy_sdk-0.1.5.dist-info/RECORD +18 -0
- entropy_sdk-0.1.5.dist-info/WHEEL +5 -0
- entropy_sdk-0.1.5.dist-info/licenses/LICENSE +21 -0
- entropy_sdk-0.1.5.dist-info/top_level.txt +1 -0
entropy_sdk/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
entropy-sdk — EntropyRuntime 控制层的可嵌入 SDK
|
|
3
|
+
|
|
4
|
+
论文同款核心抽象(arXiv:2607.00334):
|
|
5
|
+
Gear 五级档位 · Utility Gate 效用门 · 事件驱动 Fallback · append-only 审计链。
|
|
6
|
+
|
|
7
|
+
设计铁律:
|
|
8
|
+
1. 核心纯 stdlib,零依赖;
|
|
9
|
+
2. fail-closed —— 效用函数必须显式注入,门是唯一调度通道;
|
|
10
|
+
3. 每次判定自动落审计链,为经验验证生产数据。
|
|
11
|
+
"""
|
|
12
|
+
from .adapters.raw import RawAction, action, observe
|
|
13
|
+
from .fallback import FallbackConfig
|
|
14
|
+
from .gate import GateDecision, UtilityGate
|
|
15
|
+
from .gear import Gear
|
|
16
|
+
from .policy import GearPolicy
|
|
17
|
+
from .runtime import CycleResult, EntropyRuntime
|
|
18
|
+
from .state import AuditLog, RuntimeState
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.5"
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Gear", "UtilityGate", "GateDecision", "GearPolicy", "FallbackConfig",
|
|
23
|
+
"RuntimeState", "AuditLog", "EntropyRuntime", "CycleResult",
|
|
24
|
+
"RawAction", "action", "observe",
|
|
25
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""适配器基座:一个动作协议 + 一个提案协议,接任何 agent 框架。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
from ..gear import Gear
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@runtime_checkable
|
|
10
|
+
class GatedAction(Protocol):
|
|
11
|
+
"""被门控的动作必须声明自己需要的最低档位。
|
|
12
|
+
|
|
13
|
+
这是嵌套动作空间 A0⊂…⊂A4 的实现侧契约:
|
|
14
|
+
required_gear=G0 的动作(只读)在任何档位都允许;
|
|
15
|
+
required_gear=G3 的动作(有副作用)只在 Execute 以上放行。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
required_gear: int | Gear
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentAdapter(Protocol):
|
|
22
|
+
"""Agent 框架适配协议(对齐生产仓 interfaces/agent_adapter.py 的轻量版)。"""
|
|
23
|
+
|
|
24
|
+
def propose(self, state: Any, gear: Gear, history: list[dict]) -> GatedAction:
|
|
25
|
+
"""按当前档位生成候选动作。实现侧应遵守档位语义:
|
|
26
|
+
档位越低,生成的动作越保守。"""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
def execute(self, action: GatedAction) -> Any:
|
|
30
|
+
"""真正执行。只会在门放行后被调用。"""
|
|
31
|
+
...
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""LangChain 适配:把 Tool 包成"门不过就不执行"的安全工具。
|
|
2
|
+
|
|
3
|
+
需要 ``pip install entropy-sdk[langchain]``。
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..gear import Gear
|
|
10
|
+
from ..runtime import EntropyRuntime, _safe_gear
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def gated_tool(runtime: EntropyRuntime, tool: Any, required_gear: int | Gear = Gear.EXECUTE,
|
|
14
|
+
state_fn=lambda *a, **k: None):
|
|
15
|
+
"""把一个 LangChain Tool 包装成受 EntropyRuntime 门控的工具。
|
|
16
|
+
|
|
17
|
+
门拒绝时返回拒绝说明字符串(不执行、不抛异常),
|
|
18
|
+
让 LLM 在下一轮自行调整——拒绝本身是反馈信号。
|
|
19
|
+
"""
|
|
20
|
+
# FIX5-2:与 runtime 同一严格度——构造期即拒非法 gear(3.0/"3"/True 等),
|
|
21
|
+
# 消灭「adapter 静默截断、runtime 拒绝」的两处判定不一致
|
|
22
|
+
gear = _safe_gear(required_gear)
|
|
23
|
+
if gear is None:
|
|
24
|
+
raise ValueError(f"invalid required_gear {required_gear!r} "
|
|
25
|
+
"(want Gear instance or plain int 0-4)")
|
|
26
|
+
|
|
27
|
+
def _run(*args: Any, **kwargs: Any) -> str:
|
|
28
|
+
from .raw import action # 延迟导入,避免硬依赖
|
|
29
|
+
|
|
30
|
+
# FIX-7:description 透传,效用函数可按工具身份定价
|
|
31
|
+
act = action(tool.func if hasattr(tool, "func") else tool,
|
|
32
|
+
*args, required_gear=gear,
|
|
33
|
+
description=getattr(tool, "description", "") or getattr(tool, "name", ""),
|
|
34
|
+
**kwargs)
|
|
35
|
+
result = runtime.step(state=state_fn(), action=act, execute=lambda a: a())
|
|
36
|
+
if result.executed:
|
|
37
|
+
return str(result.result)
|
|
38
|
+
if result.suspended:
|
|
39
|
+
return "[GATE] runtime suspended after repeated rejections; human review required."
|
|
40
|
+
reason = result.gate.reason if result.gate else "gear does not permit this action"
|
|
41
|
+
return f"[GATE] action rejected: {reason}. Propose a safer alternative."
|
|
42
|
+
|
|
43
|
+
from langchain_core.tools import StructuredTool # noqa: 仅在调用时需要
|
|
44
|
+
|
|
45
|
+
return StructuredTool.from_function(
|
|
46
|
+
func=_run,
|
|
47
|
+
name=f"gated_{getattr(tool, 'name', 'tool')}",
|
|
48
|
+
description=(getattr(tool, "description", "") or "")
|
|
49
|
+
+ f" [safety-gated, requires gear {gear.label}]",
|
|
50
|
+
# FIX-6:透传原工具的 args_schema——不带它,带参工具没有正常调用路径
|
|
51
|
+
args_schema=getattr(tool, "args_schema", None),
|
|
52
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""PydanticAI 适配:工具函数装饰器,门控逻辑与框架解耦。
|
|
2
|
+
|
|
3
|
+
需要 ``pip install entropy-sdk[pydanticai]``。
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
from ..gear import Gear
|
|
11
|
+
from ..runtime import EntropyRuntime
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def gated(runtime: EntropyRuntime, required_gear: int | Gear = Gear.EXECUTE,
|
|
15
|
+
state_fn=lambda *a, **k: None):
|
|
16
|
+
"""装饰任意工具函数,使其经过 EntropyRuntime 效用门。
|
|
17
|
+
|
|
18
|
+
拒绝时返回说明字符串而非抛异常,让 agent 把拒绝当作反馈。
|
|
19
|
+
用法::
|
|
20
|
+
|
|
21
|
+
@gated(runtime, required_gear=Gear.EXECUTE)
|
|
22
|
+
def delete_records(table: str) -> str: ...
|
|
23
|
+
"""
|
|
24
|
+
def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
25
|
+
@functools.wraps(fn)
|
|
26
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
27
|
+
from .raw import action
|
|
28
|
+
|
|
29
|
+
# FIX-7:description 透传(docstring 优先,函数名兜底),效用可按工具身份定价
|
|
30
|
+
act = action(fn, *args, required_gear=required_gear,
|
|
31
|
+
description=(fn.__doc__ or "").strip() or fn.__name__,
|
|
32
|
+
**kwargs)
|
|
33
|
+
result = runtime.step(state=state_fn(), action=act, execute=lambda a: a())
|
|
34
|
+
if result.executed:
|
|
35
|
+
return result.result
|
|
36
|
+
if result.suspended:
|
|
37
|
+
return "[GATE] runtime suspended; human review required."
|
|
38
|
+
reason = result.gate.reason if result.gate else "gear does not permit this action"
|
|
39
|
+
return f"[GATE] rejected: {reason}"
|
|
40
|
+
|
|
41
|
+
return wrapper
|
|
42
|
+
|
|
43
|
+
return decorator
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""零框架适配:把任意 Python callable 包装成被门控的动作。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from ..gear import Gear
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class RawAction:
|
|
12
|
+
"""最小动作包装。"""
|
|
13
|
+
|
|
14
|
+
fn: Callable[..., Any]
|
|
15
|
+
args: tuple = ()
|
|
16
|
+
kwargs: dict = field(default_factory=dict)
|
|
17
|
+
required_gear: int | Gear = Gear.EXECUTE
|
|
18
|
+
description: str = ""
|
|
19
|
+
|
|
20
|
+
def __call__(self) -> Any:
|
|
21
|
+
return self.fn(*self.args, **self.kwargs)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def action(
|
|
25
|
+
fn: Callable[..., Any],
|
|
26
|
+
*args: Any,
|
|
27
|
+
required_gear: int | Gear = Gear.EXECUTE,
|
|
28
|
+
description: str = "",
|
|
29
|
+
**kwargs: Any,
|
|
30
|
+
) -> RawAction:
|
|
31
|
+
"""一行包装:``action(send_email, to, body, required_gear=Gear.EXECUTE)``。"""
|
|
32
|
+
return RawAction(fn=fn, args=args, kwargs=kwargs,
|
|
33
|
+
required_gear=required_gear, description=description)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def observe(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> RawAction:
|
|
37
|
+
"""只读动作快捷包装:required_gear=G0,任何档位可执行。"""
|
|
38
|
+
return action(fn, *args, required_gear=Gear.OBSERVE, **kwargs)
|
entropy_sdk/fallback.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
事件驱动 Fallback(论文 §4,Theorem 4)
|
|
3
|
+
|
|
4
|
+
拒绝后的恢复路径:
|
|
5
|
+
1. 记录拒绝,σ 上升;
|
|
6
|
+
2. 由 proposer 生成备选动作 a′,重新过门;
|
|
7
|
+
3. k 次备选全败 → 降一档,下周期待定;
|
|
8
|
+
4. m 次连续拒绝 → 降到 G0 并挂起,等待人工复核。
|
|
9
|
+
|
|
10
|
+
Theorem 4 保证:任何错误态至多 |G|−1=4 步降到 G0,
|
|
11
|
+
G0 的只读动作平凡地满足 U ≥ 0 —— 系统永远有恢复路径,不需要重启。
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import math
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any, Callable
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class FallbackConfig:
|
|
23
|
+
max_alternatives: int = 3 # k:每档最多尝试的备选数
|
|
24
|
+
max_consecutive_rejections: int = 5 # m:连续拒绝上限,触发 G0 挂起
|
|
25
|
+
|
|
26
|
+
def __post_init__(self):
|
|
27
|
+
# FIX-3b + FIX2-2 + FIX2-3:构造期校验(fail-closed)
|
|
28
|
+
# max_alternatives:0 合法(关闭 fallback 的显式语义,v0.1.2 恢复;
|
|
29
|
+
# v0.1 的 >=1 校验误杀了该意图——CHANGELOG 标注破坏性恢复);上界 100
|
|
30
|
+
# 防 10**9 级每周期巨大循环。两参数均须有限整数。
|
|
31
|
+
for name in ("max_alternatives", "max_consecutive_rejections"):
|
|
32
|
+
v = getattr(self, name)
|
|
33
|
+
if isinstance(v, bool) or not (
|
|
34
|
+
isinstance(v, int)
|
|
35
|
+
or (isinstance(v, float) and math.isfinite(v) and v.is_integer())
|
|
36
|
+
):
|
|
37
|
+
raise ValueError(f"{name} must be a finite integer, got {v!r}")
|
|
38
|
+
if not 0 <= self.max_alternatives <= 100:
|
|
39
|
+
raise ValueError("max_alternatives must be in [0, 100] (0 = fallback off)")
|
|
40
|
+
if self.max_consecutive_rejections < 1:
|
|
41
|
+
raise ValueError("max_consecutive_rejections must be >= 1")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# 备选生成器签名:(state, rejected_action, attempt_index) -> alternative action | None
|
|
45
|
+
AlternativeProposer = Callable[[Any, Any, int], Any | None]
|
entropy_sdk/gate.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility Gate(论文 Definitions 2–3,Theorem 2)
|
|
3
|
+
|
|
4
|
+
门是唯一调度通道:动作被执行当且仅当 Gate=1,即 U(s,a) >= theta。
|
|
5
|
+
本 SDK 不内置任何效用函数——U 必须由使用者按领域注入。
|
|
6
|
+
未注入效用函数时构造即报错(fail-closed,无 fail-open 开关)。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any, Callable, Protocol
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UtilityFunction(Protocol):
|
|
17
|
+
"""效用函数协议:U: S × A → R。
|
|
18
|
+
|
|
19
|
+
推荐形态(论文 §4):U(s,a) = α·Δtask + β·safety − γ·cost,
|
|
20
|
+
多智能体场景可把传感器熵项 H_i 折进 cost(论文 §6.1)。
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __call__(self, state: Any, action: Any) -> float: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class GateDecision:
|
|
28
|
+
"""一次门判定的完整记录(审计用)。"""
|
|
29
|
+
|
|
30
|
+
admitted: bool
|
|
31
|
+
utility: float
|
|
32
|
+
theta: float
|
|
33
|
+
reason: str = ""
|
|
34
|
+
meta: dict = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UtilityGate:
|
|
38
|
+
"""二元效用门:Gate(s,a) = 1 iff U(s,a) >= θ。"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, utility: UtilityFunction | Callable[[Any, Any], float], theta: float = 0.0):
|
|
41
|
+
if utility is None:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"UtilityGate requires an explicit utility function. "
|
|
44
|
+
"There is no fail-open mode: the gate is the sole dispatch channel."
|
|
45
|
+
)
|
|
46
|
+
# FIX2-2:theta 必须有限(拒 NaN/±inf)且非负
|
|
47
|
+
if not math.isfinite(theta) or theta < 0:
|
|
48
|
+
raise ValueError("theta must be finite and >= 0 (paper Definition 3)")
|
|
49
|
+
self._utility = utility
|
|
50
|
+
self.theta = float(theta)
|
|
51
|
+
|
|
52
|
+
def evaluate(self, state: Any, action: Any) -> GateDecision:
|
|
53
|
+
# FIX-2:效用函数异常视同 U=−∞ 拒绝(fail-closed),不穿透 step();
|
|
54
|
+
# meta 载明错误类型,由 runtime 落 gate_error 审计(与 execute 异常分属)。
|
|
55
|
+
try:
|
|
56
|
+
u = float(self._utility(state, action))
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
err = f"{type(exc).__name__}: {exc}"
|
|
59
|
+
return GateDecision(
|
|
60
|
+
admitted=False,
|
|
61
|
+
utility=float("-inf"),
|
|
62
|
+
theta=self.theta,
|
|
63
|
+
reason=f"gate_error: utility raised {err} (treated as U=-inf)",
|
|
64
|
+
meta={"gate_error": err},
|
|
65
|
+
)
|
|
66
|
+
# FIX4-1:非有限效用门内校验——同一把尺两个方向都闭上:
|
|
67
|
+
# NaN 靠 IEEE 碰巧 fail-closed,但 +inf >= θ 恒真会放行(fail-open)。
|
|
68
|
+
# 非有限一律拒绝,reason 载明值(审计落盘时经 FIX2-6 带符号消毒标记)。
|
|
69
|
+
if not math.isfinite(u):
|
|
70
|
+
return GateDecision(
|
|
71
|
+
admitted=False,
|
|
72
|
+
utility=u,
|
|
73
|
+
theta=self.theta,
|
|
74
|
+
reason=f"nonfinite utility: {u!r} (fail-closed)",
|
|
75
|
+
)
|
|
76
|
+
admitted = u >= self.theta
|
|
77
|
+
return GateDecision(
|
|
78
|
+
admitted=admitted,
|
|
79
|
+
utility=u,
|
|
80
|
+
theta=self.theta,
|
|
81
|
+
reason="" if admitted else f"U={u:.4f} < theta={self.theta:.4f}",
|
|
82
|
+
)
|
entropy_sdk/gear.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gear 状态系统(论文 Definition 1)
|
|
3
|
+
|
|
4
|
+
五级执行档位,动作空间单调嵌套:A0 ⊂ A1 ⊂ A2 ⊂ A3 ⊂ A4 = A。
|
|
5
|
+
档位的语义是"动作空间的上限",不是速度——降级收缩的是 agent 能做什么,
|
|
6
|
+
不是做得多慢。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from enum import IntEnum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Gear(IntEnum):
|
|
14
|
+
OBSERVE = 0 # G0: 只读观察 / 安全保持,无副作用
|
|
15
|
+
SUGGEST = 1 # G1: 生成候选计划,无外部副作用
|
|
16
|
+
PLAN = 2 # G2: 有界、可逆或保全性恢复动作
|
|
17
|
+
EXECUTE = 3 # G3: 可独立选择有副作用的动作
|
|
18
|
+
INTEGRATE = 4 # G4: 系统级协调(多智能体下为涌现属性,见论文 §6.3)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def label(self) -> str:
|
|
22
|
+
return _LABELS[self]
|
|
23
|
+
|
|
24
|
+
def permits(self, required: "Gear") -> bool:
|
|
25
|
+
"""嵌套动作空间:当前档位 g 允许所有 required_gear <= g 的动作。"""
|
|
26
|
+
return Gear(required) <= self
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_LABELS = {
|
|
30
|
+
Gear.OBSERVE: "Observe",
|
|
31
|
+
Gear.SUGGEST: "Suggest",
|
|
32
|
+
Gear.PLAN: "Plan",
|
|
33
|
+
Gear.EXECUTE: "Execute",
|
|
34
|
+
Gear.INTEGRATE: "Integrate",
|
|
35
|
+
}
|
entropy_sdk/policy.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gear 转移策略(论文 §4 Gear state machine)
|
|
3
|
+
|
|
4
|
+
慢升快降(earned autonomy):
|
|
5
|
+
- 升档:σ < σ_low 且连续 h 个干净周期 → 升一档
|
|
6
|
+
- 降档:σ > σ_high 或 ϵ=1 → 立即降一档
|
|
7
|
+
- 否则:保持
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from .gear import Gear
|
|
16
|
+
from .state import RuntimeState
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class GearPolicy:
|
|
21
|
+
sigma_low: float = 0.3 # 升档门槛
|
|
22
|
+
sigma_high: float = 1.0 # 降档门槛
|
|
23
|
+
patience: int = 3 # h:升档所需连续干净周期
|
|
24
|
+
sigma_decay: float = 0.1 # δ:动作被接受时 σ 的衰减
|
|
25
|
+
sigma_step: float = 0.1 # Δσ:fallback 失败时 σ 的增量
|
|
26
|
+
|
|
27
|
+
def __post_init__(self):
|
|
28
|
+
# FIX-3a + FIX2-2:非法参数构造即抛(fail-closed)
|
|
29
|
+
# ——finite 校验:patience=NaN 会静默锁死 G0(clean_streak>=NaN 恒 False),
|
|
30
|
+
# patience=1.5 会静默变 2;σ 族 NaN/±inf 同理拒收
|
|
31
|
+
for name in ("sigma_low", "sigma_high", "sigma_decay", "sigma_step"):
|
|
32
|
+
v = getattr(self, name)
|
|
33
|
+
if not (isinstance(v, (int, float)) and math.isfinite(v)):
|
|
34
|
+
raise ValueError(f"{name} must be finite, got {v!r}")
|
|
35
|
+
if isinstance(self.patience, bool) or not (
|
|
36
|
+
isinstance(self.patience, int)
|
|
37
|
+
or (isinstance(self.patience, float) and self.patience.is_integer())
|
|
38
|
+
):
|
|
39
|
+
raise ValueError(f"patience must be an integer, got {self.patience!r}")
|
|
40
|
+
if self.patience < 1:
|
|
41
|
+
raise ValueError("patience must be >= 1")
|
|
42
|
+
if self.sigma_decay < 0:
|
|
43
|
+
raise ValueError("sigma_decay must be >= 0")
|
|
44
|
+
if self.sigma_step < 0:
|
|
45
|
+
raise ValueError("sigma_step must be >= 0")
|
|
46
|
+
if not self.sigma_low < self.sigma_high:
|
|
47
|
+
raise ValueError("sigma_low must be < sigma_high")
|
|
48
|
+
|
|
49
|
+
def next_gear(self, state: RuntimeState) -> Gear:
|
|
50
|
+
g = state.gear
|
|
51
|
+
if state.sigma > self.sigma_high or state.error:
|
|
52
|
+
return Gear(max(int(g) - 1, int(Gear.OBSERVE)))
|
|
53
|
+
if state.sigma < self.sigma_low and state.clean_streak >= self.patience:
|
|
54
|
+
return Gear(min(int(g) + 1, int(Gear.INTEGRATE)))
|
|
55
|
+
return g
|
entropy_sdk/py.typed
ADDED
|
File without changes
|
entropy_sdk/runtime.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EntropyRuntime 控制循环(论文 §4 Algorithm 1)
|
|
3
|
+
|
|
4
|
+
每周期四阶段:观察与档位评估 → 动作生成 → 效用门 → 执行与反馈。
|
|
5
|
+
门是唯一调度通道:被拒绝的动作绝不执行(Theorem 2)。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
from .fallback import AlternativeProposer, FallbackConfig
|
|
13
|
+
from .gate import GateDecision, UtilityFunction, UtilityGate
|
|
14
|
+
from .gear import Gear
|
|
15
|
+
from .policy import GearPolicy
|
|
16
|
+
from .state import AuditLog, RuntimeState
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _safe_gear(value: Any) -> Gear | None:
|
|
20
|
+
"""把调用方声明的 required_gear 收敛成合法 Gear;非法值返回 None(FIX-4)。
|
|
21
|
+
|
|
22
|
+
required_gear 是调用方声明契约(attestation)——标签不可信时
|
|
23
|
+
fail-closed:视同「不允许」,走档位前置闸拒绝路径,不抛栈穿透。
|
|
24
|
+
"""
|
|
25
|
+
if isinstance(value, Gear):
|
|
26
|
+
return value
|
|
27
|
+
if isinstance(value, bool):
|
|
28
|
+
# IntEnum 值查找里 True==1 会静默成 SUGGEST——显式拒绝(已实测验证的洞)
|
|
29
|
+
return None
|
|
30
|
+
if not isinstance(value, int):
|
|
31
|
+
# 拒绝 float/str/None/object(含 3.9、3.7、"3"、"EXECUTE")
|
|
32
|
+
return None
|
|
33
|
+
try:
|
|
34
|
+
return Gear(value)
|
|
35
|
+
except (TypeError, ValueError, OverflowError):
|
|
36
|
+
# OverflowError:±inf 的 int() 转换(DEF-1 并案)
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class CycleResult:
|
|
42
|
+
"""单周期结果。"""
|
|
43
|
+
|
|
44
|
+
executed: bool
|
|
45
|
+
action: Any
|
|
46
|
+
gate: GateDecision | None
|
|
47
|
+
gear_before: Gear
|
|
48
|
+
gear_after: Gear
|
|
49
|
+
used_fallback: bool = False
|
|
50
|
+
suspended: bool = False
|
|
51
|
+
result: Any = None
|
|
52
|
+
error: str | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class EntropyRuntime:
|
|
56
|
+
"""可嵌入的档位安全控制层。
|
|
57
|
+
|
|
58
|
+
用法(最小闭环)::
|
|
59
|
+
|
|
60
|
+
runtime = EntropyRuntime(utility=my_utility, theta=0.15)
|
|
61
|
+
result = runtime.step(
|
|
62
|
+
state=my_state,
|
|
63
|
+
action=agent_proposed_action, # 必须带 required_gear
|
|
64
|
+
execute=lambda a: actually_do(a),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
动作对象只需暴露 ``required_gear``(int 或 Gear);
|
|
68
|
+
可用 ``adapters.raw.action()`` 一键包装任意 callable。
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
utility: UtilityFunction,
|
|
74
|
+
theta: float = 0.0,
|
|
75
|
+
policy: GearPolicy | None = None,
|
|
76
|
+
fallback: FallbackConfig | None = None,
|
|
77
|
+
audit_log: AuditLog | str | None = None,
|
|
78
|
+
initial_gear: Gear = Gear.OBSERVE,
|
|
79
|
+
):
|
|
80
|
+
self.gate = UtilityGate(utility, theta=theta)
|
|
81
|
+
self.policy = policy or GearPolicy()
|
|
82
|
+
self.fallback_cfg = fallback or FallbackConfig()
|
|
83
|
+
self.audit = audit_log if isinstance(audit_log, AuditLog) else AuditLog(audit_log)
|
|
84
|
+
# FIX3-4:initial_gear 与 required_gear 同一严格度(strict attestation)
|
|
85
|
+
gear0 = _safe_gear(initial_gear)
|
|
86
|
+
if gear0 is None:
|
|
87
|
+
raise ValueError(f"invalid initial_gear {initial_gear!r} "
|
|
88
|
+
"(want Gear instance or plain int 0-4)")
|
|
89
|
+
self.state = RuntimeState(gear=gear0)
|
|
90
|
+
self.audit.record("init", gear=int(self.state.gear), theta=theta)
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------------ #
|
|
93
|
+
|
|
94
|
+
def step(
|
|
95
|
+
self,
|
|
96
|
+
state: Any,
|
|
97
|
+
action: Any,
|
|
98
|
+
execute: Callable[[Any], Any],
|
|
99
|
+
propose_alternative: AlternativeProposer | None = None,
|
|
100
|
+
) -> CycleResult:
|
|
101
|
+
"""执行一个控制周期(Algorithm 1 的一次迭代)。"""
|
|
102
|
+
if self.state.suspended:
|
|
103
|
+
self.audit.record("suspended_skip", cycle=self.state.cycle)
|
|
104
|
+
return CycleResult(
|
|
105
|
+
executed=False, action=None, gate=None,
|
|
106
|
+
gear_before=self.state.gear, gear_after=self.state.gear,
|
|
107
|
+
suspended=True, error="suspended: awaiting human review",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
gear_before = self.state.gear
|
|
111
|
+
result = self._dispatch(state, action, execute, propose_alternative)
|
|
112
|
+
|
|
113
|
+
# 更新 σ / ϵ / clean_streak(Algorithm 1, lines 7/11/13)
|
|
114
|
+
if result.executed and not result.used_fallback:
|
|
115
|
+
self.state.sigma = max(0.0, self.state.sigma - self.policy.sigma_decay)
|
|
116
|
+
self.state.error = False
|
|
117
|
+
self.state.clean_streak += 1
|
|
118
|
+
self.state.consecutive_rejections = 0
|
|
119
|
+
elif result.executed and result.used_fallback:
|
|
120
|
+
self.state.error = False # σ 保持不变
|
|
121
|
+
self.state.clean_streak += 1
|
|
122
|
+
self.state.consecutive_rejections = 0
|
|
123
|
+
else:
|
|
124
|
+
self.state.sigma += self.policy.sigma_step
|
|
125
|
+
self.state.error = True
|
|
126
|
+
self.state.clean_streak = 0
|
|
127
|
+
self.state.consecutive_rejections += 1
|
|
128
|
+
|
|
129
|
+
# 换挡(π_G)
|
|
130
|
+
gear_after = self.policy.next_gear(self.state)
|
|
131
|
+
if gear_after != self.state.gear:
|
|
132
|
+
self.audit.record(
|
|
133
|
+
"gear_transition", cycle=self.state.cycle,
|
|
134
|
+
**{"from": int(self.state.gear), "to": int(gear_after)},
|
|
135
|
+
sigma=round(self.state.sigma, 4),
|
|
136
|
+
)
|
|
137
|
+
# FIX2-4 + FIX3-2:换挡即清零 clean_streak——每一档都要重新挣满 h 个
|
|
138
|
+
# 连续干净周期。升降档同律:FIX2-4 原判词「拒绝分支已先清零」不覆盖
|
|
139
|
+
# σ 降档路径(成功执行但 σ 超阈的降档会残留 streak,窄 σ 带下提前 1 周期升档)。
|
|
140
|
+
if gear_after != self.state.gear:
|
|
141
|
+
self.state.clean_streak = 0
|
|
142
|
+
self.state.gear = gear_after
|
|
143
|
+
|
|
144
|
+
# m 次连续拒绝 → G0 挂起(Theorem 4 的终点)
|
|
145
|
+
if self.state.consecutive_rejections >= self.fallback_cfg.max_consecutive_rejections:
|
|
146
|
+
# FIX-5a:挂起导致的硬降档也要在链上留 gear_transition(换挡不许静默)
|
|
147
|
+
if self.state.gear != Gear.OBSERVE:
|
|
148
|
+
self.audit.record(
|
|
149
|
+
"gear_transition", cycle=self.state.cycle,
|
|
150
|
+
**{"from": int(self.state.gear), "to": int(Gear.OBSERVE)},
|
|
151
|
+
sigma=round(self.state.sigma, 4), reason="suspend",
|
|
152
|
+
)
|
|
153
|
+
self.state.gear = Gear.OBSERVE
|
|
154
|
+
self.state.suspended = True
|
|
155
|
+
result.suspended = True
|
|
156
|
+
self.audit.record("suspend", cycle=self.state.cycle,
|
|
157
|
+
consecutive_rejections=self.state.consecutive_rejections)
|
|
158
|
+
|
|
159
|
+
result.gear_before = gear_before
|
|
160
|
+
result.gear_after = self.state.gear
|
|
161
|
+
self.state.cycle += 1
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
def resume(self) -> None:
|
|
165
|
+
"""人工复核后解除挂起(从 G0 重新开始挣档位)。"""
|
|
166
|
+
self.state.suspended = False
|
|
167
|
+
self.state.consecutive_rejections = 0
|
|
168
|
+
self.state.error = False
|
|
169
|
+
self.audit.record("resume", cycle=self.state.cycle)
|
|
170
|
+
|
|
171
|
+
# ------------------------------------------------------------------ #
|
|
172
|
+
|
|
173
|
+
def _dispatch(
|
|
174
|
+
self,
|
|
175
|
+
state: Any,
|
|
176
|
+
action: Any,
|
|
177
|
+
execute: Callable[[Any], Any],
|
|
178
|
+
propose_alternative: AlternativeProposer | None,
|
|
179
|
+
) -> CycleResult:
|
|
180
|
+
required = _safe_gear(getattr(action, "required_gear", Gear.EXECUTE))
|
|
181
|
+
|
|
182
|
+
# FIX-4:非法 required_gear 视同档位不允许(fail-closed),不抛栈穿透
|
|
183
|
+
if required is None:
|
|
184
|
+
self.audit.record(
|
|
185
|
+
"gate_decision", cycle=self.state.cycle, admitted=False,
|
|
186
|
+
reason=f"invalid required_gear "
|
|
187
|
+
f"{getattr(action, 'required_gear', None)!r} (attestation rejected)",
|
|
188
|
+
)
|
|
189
|
+
alt = self._try_alternatives(state, action, execute, propose_alternative)
|
|
190
|
+
if alt is not None:
|
|
191
|
+
return alt
|
|
192
|
+
return CycleResult(False, action, None, self.state.gear, self.state.gear)
|
|
193
|
+
|
|
194
|
+
# 档位前置闸:超出当前动作空间的动作连门都不进
|
|
195
|
+
if not self.state.gear.permits(required):
|
|
196
|
+
self.audit.record(
|
|
197
|
+
"gate_decision", cycle=self.state.cycle, admitted=False,
|
|
198
|
+
reason=f"gear {self.state.gear.label} does not permit {required.label}",
|
|
199
|
+
)
|
|
200
|
+
alt = self._try_alternatives(state, action, execute, propose_alternative)
|
|
201
|
+
if alt is not None:
|
|
202
|
+
return alt
|
|
203
|
+
return CycleResult(False, action, None, self.state.gear, self.state.gear)
|
|
204
|
+
|
|
205
|
+
decision = self.gate.evaluate(state, action)
|
|
206
|
+
self.audit.record(
|
|
207
|
+
"gate_decision", cycle=self.state.cycle, admitted=decision.admitted,
|
|
208
|
+
utility=round(decision.utility, 4), theta=decision.theta, reason=decision.reason,
|
|
209
|
+
)
|
|
210
|
+
# FIX-2:效用异常视同 U=−∞ 拒绝(fail-closed),审计事件与 execute 异常分属
|
|
211
|
+
if decision.meta.get("gate_error"):
|
|
212
|
+
self.audit.record("gate_error", cycle=self.state.cycle,
|
|
213
|
+
error=decision.meta["gate_error"])
|
|
214
|
+
if decision.admitted:
|
|
215
|
+
return self._run(action, execute, decision, used_fallback=False)
|
|
216
|
+
|
|
217
|
+
alt = self._try_alternatives(state, action, execute, propose_alternative)
|
|
218
|
+
if alt is not None:
|
|
219
|
+
return alt
|
|
220
|
+
return CycleResult(False, action, decision, self.state.gear, self.state.gear)
|
|
221
|
+
|
|
222
|
+
def _try_alternatives(
|
|
223
|
+
self,
|
|
224
|
+
state: Any,
|
|
225
|
+
rejected: Any,
|
|
226
|
+
execute: Callable[[Any], Any],
|
|
227
|
+
propose_alternative: AlternativeProposer | None,
|
|
228
|
+
) -> CycleResult | None:
|
|
229
|
+
if propose_alternative is None or self.fallback_cfg.max_alternatives == 0:
|
|
230
|
+
# FIX2-3:max_alternatives=0 是合法配置——关闭 fallback,
|
|
231
|
+
# proposer 零调用,直接进 σ/降档/挂起流程
|
|
232
|
+
return None
|
|
233
|
+
for i in range(self.fallback_cfg.max_alternatives):
|
|
234
|
+
# FIX-1:proposer 异常视同无备选——记审计后走正常拒绝分支,
|
|
235
|
+
# 不穿透 step()(对齐 Theorem 4:拒绝路径的 σ/挂起语义照常生效)
|
|
236
|
+
try:
|
|
237
|
+
alt = propose_alternative(state, rejected, i)
|
|
238
|
+
except Exception as exc:
|
|
239
|
+
self.audit.record(
|
|
240
|
+
"proposer_error", cycle=self.state.cycle, attempt_index=i,
|
|
241
|
+
error=type(exc).__name__,
|
|
242
|
+
)
|
|
243
|
+
break
|
|
244
|
+
if alt is None:
|
|
245
|
+
break
|
|
246
|
+
required = _safe_gear(getattr(alt, "required_gear", Gear.EXECUTE))
|
|
247
|
+
if required is None: # FIX-4:非法 gear 视同不允许
|
|
248
|
+
self.audit.record(
|
|
249
|
+
"gate_decision", cycle=self.state.cycle, admitted=False,
|
|
250
|
+
reason=f"fallback#{i}: invalid required_gear "
|
|
251
|
+
f"{getattr(alt, 'required_gear', None)!r} (attestation rejected)",
|
|
252
|
+
)
|
|
253
|
+
continue
|
|
254
|
+
if not self.state.gear.permits(required):
|
|
255
|
+
continue
|
|
256
|
+
decision = self.gate.evaluate(state, alt)
|
|
257
|
+
self.audit.record(
|
|
258
|
+
"gate_decision", cycle=self.state.cycle, admitted=decision.admitted,
|
|
259
|
+
utility=round(decision.utility, 4), theta=decision.theta,
|
|
260
|
+
reason=f"fallback#{i}: {decision.reason}",
|
|
261
|
+
)
|
|
262
|
+
if decision.meta.get("gate_error"):
|
|
263
|
+
self.audit.record("gate_error", cycle=self.state.cycle,
|
|
264
|
+
error=decision.meta["gate_error"])
|
|
265
|
+
if decision.admitted:
|
|
266
|
+
return self._run(alt, execute, decision, used_fallback=True)
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
def _run(self, action, execute, decision, used_fallback) -> CycleResult:
|
|
270
|
+
try:
|
|
271
|
+
out = execute(action)
|
|
272
|
+
self.audit.record("execute", cycle=self.state.cycle, fallback=used_fallback)
|
|
273
|
+
return CycleResult(True, action, decision, self.state.gear, self.state.gear,
|
|
274
|
+
used_fallback=used_fallback, result=out)
|
|
275
|
+
except Exception as exc: # 执行异常按拒绝处理:σ 上升、ϵ=1
|
|
276
|
+
self.audit.record("execute_error", cycle=self.state.cycle, error=str(exc))
|
|
277
|
+
return CycleResult(False, action, decision, self.state.gear, self.state.gear,
|
|
278
|
+
used_fallback=used_fallback, error=str(exc))
|
entropy_sdk/state.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
运行时状态 ρ=(g, σ, ϵ) 与 append-only 审计链(论文 Definition 4)
|
|
3
|
+
|
|
4
|
+
审计链是 SDK 的一等公民:每次门判定、换挡、fallback、挂起都落一条 JSONL。
|
|
5
|
+
这条链就是论文 Section 5/8 所需的实证数据来源(gate 接受率、gear 转移直方图)。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import math
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import asdict, dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .gear import Gear
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RuntimeState:
|
|
21
|
+
"""单 agent 运行时状态(环境状态 s 由使用者持有,此处只保留控制量)。"""
|
|
22
|
+
|
|
23
|
+
gear: Gear = Gear.OBSERVE
|
|
24
|
+
sigma: float = 0.0 # σ:累积不稳定度
|
|
25
|
+
error: bool = False # ϵ:错误标志
|
|
26
|
+
cycle: int = 0 # t:离散周期计数
|
|
27
|
+
clean_streak: int = 0 # 连续干净周期数(升档耐心计数)
|
|
28
|
+
consecutive_rejections: int = 0
|
|
29
|
+
suspended: bool = False # m 次连续拒绝后挂起,等待人工复核
|
|
30
|
+
|
|
31
|
+
def snapshot(self) -> dict:
|
|
32
|
+
d = asdict(self)
|
|
33
|
+
d["gear"] = int(self.gear)
|
|
34
|
+
d["gear_label"] = self.gear.label
|
|
35
|
+
return d
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# FIX5-1:嵌套容器内非有限 float 的递归消毒深度上限(防深递归炸栈)
|
|
39
|
+
_SANITIZE_MAX_DEPTH = 32
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _sanitize_nonfinite(value: Any, _depth: int = 0, _seen: set | None = None) -> Any:
|
|
43
|
+
"""递归消毒嵌套 dict/list/tuple 里的非有限 float → 字符串标记
|
|
44
|
+
("nan"/"+inf"/"-inf",与顶层记号同族)。
|
|
45
|
+
|
|
46
|
+
防循环引用:沿当前递归路径的 id set(离开分支即摘除,故共享但不成环的
|
|
47
|
+
兄弟引用不误伤);防深递归:深度超 _SANITIZE_MAX_DEPTH 截断为标记。
|
|
48
|
+
非容器、非浮点类型原样透传。
|
|
49
|
+
"""
|
|
50
|
+
if isinstance(value, float):
|
|
51
|
+
if math.isfinite(value):
|
|
52
|
+
return value
|
|
53
|
+
return "nan" if math.isnan(value) else ("+inf" if value > 0 else "-inf")
|
|
54
|
+
if isinstance(value, (dict, list, tuple)):
|
|
55
|
+
if _seen is None:
|
|
56
|
+
_seen = set()
|
|
57
|
+
if _depth >= _SANITIZE_MAX_DEPTH or id(value) in _seen:
|
|
58
|
+
return "<truncated:depth-or-cycle>"
|
|
59
|
+
_seen.add(id(value))
|
|
60
|
+
try:
|
|
61
|
+
if isinstance(value, dict):
|
|
62
|
+
return {k: _sanitize_nonfinite(v, _depth + 1, _seen)
|
|
63
|
+
for k, v in value.items()}
|
|
64
|
+
if isinstance(value, list):
|
|
65
|
+
return [_sanitize_nonfinite(v, _depth + 1, _seen) for v in value]
|
|
66
|
+
return tuple(_sanitize_nonfinite(v, _depth + 1, _seen) for v in value)
|
|
67
|
+
finally:
|
|
68
|
+
_seen.discard(id(value))
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AuditLog:
|
|
73
|
+
"""append-only JSONL 审计链。只追加,不修改,不落盘失败则抛错(fail-closed)。"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, path: str | Path | None = None):
|
|
76
|
+
self.path = Path(path) if path else None
|
|
77
|
+
self._buffer: list[dict] = []
|
|
78
|
+
self.corrupt_lines: int = 0 # FIX-5c:最近一次读盘跳过的坏行数(内存模式恒 0)
|
|
79
|
+
# FIX2-5:文件模式读缓存——按 (mtime_ns, size) 失效。
|
|
80
|
+
# 选型理由:iter_entries() 生成器救不了 metrics 的重复读
|
|
81
|
+
#(acceptance_rate + histogram 各过一遍),缓存一次失效模型两者通吃。
|
|
82
|
+
self._cache_key: tuple | None = None
|
|
83
|
+
self._cache_entries: list[dict] = []
|
|
84
|
+
|
|
85
|
+
def record(self, kind: str, **fields: Any) -> dict:
|
|
86
|
+
# FIX-5d:非有限浮点(NaN/Inf)消毒为 null + 同名字段加 _nonfinite 标记,
|
|
87
|
+
# 保证 JSONL 对严格解析器合法(门对 NaN 效用的 fail-closed 行为不变)
|
|
88
|
+
clean: dict[str, Any] = {}
|
|
89
|
+
for k, v in fields.items():
|
|
90
|
+
if isinstance(v, float) and not math.isfinite(v):
|
|
91
|
+
clean[k] = None
|
|
92
|
+
# FIX2-6:消毒标记带符号(+inf / -inf / nan),保住信息区分度
|
|
93
|
+
clean[f"{k}_nonfinite"] = (
|
|
94
|
+
"nan" if math.isnan(v) else ("+inf" if v > 0 else "-inf")
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
# FIX5-1:嵌套容器递归消毒(顶层既有行为与标记格式不动)
|
|
98
|
+
clean[k] = _sanitize_nonfinite(v)
|
|
99
|
+
entry = {"ts": time.time(), "kind": kind, **clean}
|
|
100
|
+
if self.path:
|
|
101
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
with self.path.open("a", encoding="utf-8") as f:
|
|
103
|
+
f.write(json.dumps(entry, ensure_ascii=False, default=str) + "\n")
|
|
104
|
+
else:
|
|
105
|
+
self._buffer.append(entry)
|
|
106
|
+
return entry
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def entries(self) -> list[dict]:
|
|
110
|
+
# FIX-5b:文件模式与 metrics 同语义——读盘返回(README 主推路径不再恒空)
|
|
111
|
+
# FIX2-5:经缓存(mtime+size 失效),metrics 连读不重复扫盘
|
|
112
|
+
# FIX3-1:对外逐条浅拷贝——调用方涂改返回值不污染缓存与磁盘真值
|
|
113
|
+
#(条目值无嵌套结构,浅拷贝足够;内部 metrics 继续用缓存引用)
|
|
114
|
+
return [dict(e) for e in self._read_all()]
|
|
115
|
+
|
|
116
|
+
def _read_all(self) -> list[dict]:
|
|
117
|
+
if self.path and self.path.exists():
|
|
118
|
+
st = self.path.stat()
|
|
119
|
+
key = (st.st_mtime_ns, st.st_size)
|
|
120
|
+
if key != self._cache_key:
|
|
121
|
+
entries = []
|
|
122
|
+
corrupt = 0
|
|
123
|
+
with self.path.open(encoding="utf-8") as f:
|
|
124
|
+
for line in f:
|
|
125
|
+
line = line.strip()
|
|
126
|
+
if not line:
|
|
127
|
+
continue
|
|
128
|
+
try:
|
|
129
|
+
entries.append(json.loads(line))
|
|
130
|
+
except json.JSONDecodeError:
|
|
131
|
+
# FIX-5c:坏行跳过并计数,不崩
|
|
132
|
+
corrupt += 1
|
|
133
|
+
self._cache_key = key
|
|
134
|
+
self._cache_entries = entries
|
|
135
|
+
self.corrupt_lines = corrupt
|
|
136
|
+
return list(self._cache_entries)
|
|
137
|
+
self.corrupt_lines = 0
|
|
138
|
+
return list(self._buffer)
|
|
139
|
+
|
|
140
|
+
def gear_histogram(self) -> dict[int, int]:
|
|
141
|
+
"""gear 转移直方图(Theorem 3 最终镇定的经验证据)。"""
|
|
142
|
+
hist: dict[int, int] = {}
|
|
143
|
+
for e in self._iter_all():
|
|
144
|
+
if e.get("kind") == "gear_transition":
|
|
145
|
+
hist[e["to"]] = hist.get(e["to"], 0) + 1
|
|
146
|
+
return hist
|
|
147
|
+
|
|
148
|
+
def gate_acceptance_rate(self) -> float | None:
|
|
149
|
+
"""门接受率(Theorem 1 关键假设 p1 >= p3 的经验观测口径)。"""
|
|
150
|
+
decisions = [e for e in self._iter_all() if e.get("kind") == "gate_decision"]
|
|
151
|
+
if not decisions:
|
|
152
|
+
return None
|
|
153
|
+
return sum(1 for e in decisions if e["admitted"]) / len(decisions)
|
|
154
|
+
|
|
155
|
+
def _iter_all(self):
|
|
156
|
+
yield from self._read_all()
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: entropy-sdk
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: Gear-based safety control layer for autonomous agents (EntropyRuntime core, embeddable SDK)
|
|
5
|
+
Author-email: Wang Miaosheng <wmsmiaosheng@outlook.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/CYD-PRC/entropy-sdk
|
|
8
|
+
Project-URL: Paper, https://arxiv.org/abs/2607.00334
|
|
9
|
+
Keywords: ai-safety,agent,governance,runtime-verification,gear
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Provides-Extra: langchain
|
|
19
|
+
Requires-Dist: langchain-core<1.0,>=0.2.43; extra == "langchain"
|
|
20
|
+
Provides-Extra: pydanticai
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# entropy-sdk
|
|
26
|
+
|
|
27
|
+
**An embeddable, gear-based safety control layer for autonomous agents** — the
|
|
28
|
+
SDK distillation of the EntropyRuntime paper's core abstractions
|
|
29
|
+
([arXiv:2607.00334](https://arxiv.org/abs/2607.00334)).
|
|
30
|
+
|
|
31
|
+
> Make an AI's degree of autonomy observable, governable, and accountable.
|
|
32
|
+
|
|
33
|
+
[](https://github.com/CYD-PRC/entropy-sdk/actions/workflows/test.yml)
|
|
34
|
+

|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
> Test readings are reported from **raw CI logs, not badge conclusions**:
|
|
38
|
+
> latest verified reading — **96 passed / 0 skipped** across Python 3.10–3.13
|
|
39
|
+
> (Actions run inspected line-by-line). A green badge alone is not evidence.
|
|
40
|
+
|
|
41
|
+
Unlike [CYD-PRC/entropyruntime](https://github.com/CYD-PRC/entropyruntime)
|
|
42
|
+
(the full production system — FastAPI + PostgreSQL + Redis + OPA), this
|
|
43
|
+
repository is the **zero-dependency, embeddable** minimal control layer:
|
|
44
|
+
`pip install` and five lines of code wire it into any agent loop.
|
|
45
|
+
|
|
46
|
+
## Core abstractions
|
|
47
|
+
|
|
48
|
+
| Abstraction | Paper | SDK |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| Five-level gear ladder G0–G4 | Definition 1 (𝒜₀⊂…⊂𝒜₄) | `Gear` |
|
|
51
|
+
| Utility gate U(s,a) ≥ θ | Definitions 2–3, Theorem 2 | `UtilityGate` |
|
|
52
|
+
| Slow-up-fast-down state machine | §4 Gear state machine | `GearPolicy` |
|
|
53
|
+
| Event-driven fallback | Theorem 4 | `FallbackConfig` |
|
|
54
|
+
| Runtime state ρ=(g,σ,ϵ) | Definition 4 | `RuntimeState` |
|
|
55
|
+
| Audit chain | §5/§8 empirical requirements | `AuditLog` (append-only JSONL) |
|
|
56
|
+
|
|
57
|
+
## Design rules
|
|
58
|
+
|
|
59
|
+
1. **Pure stdlib core, zero dependencies.** Framework adapters (LangChain /
|
|
60
|
+
PydanticAI) ship as optional extras.
|
|
61
|
+
2. **fail-closed.** The utility function must be injected explicitly; there is
|
|
62
|
+
no fail-open switch — the utility gate is the sole dispatch channel
|
|
63
|
+
(Theorem 2, enforced in code).
|
|
64
|
+
3. **Audit chain built in.** Every gate decision, gear transition, and
|
|
65
|
+
suspension is appended to JSONL. `gate_acceptance_rate()` and
|
|
66
|
+
`gear_histogram()` produce the empirical data that the paper's Theorem 1
|
|
67
|
+
assumption and Theorem 3 prediction call for.
|
|
68
|
+
|
|
69
|
+
## Quickstart
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install entropy-sdk
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from entropy_sdk import EntropyRuntime, Gear, action, observe
|
|
77
|
+
|
|
78
|
+
# 1. Inject your domain utility function (the SDK ships no built-in U)
|
|
79
|
+
def my_utility(state, act):
|
|
80
|
+
return state.task_gain(act) + 2.0 * state.safety(act) - 0.5 * act.cost
|
|
81
|
+
|
|
82
|
+
# Note: action()'s **kwargs are call parameters of the wrapped function,
|
|
83
|
+
# not utility metadata. Feed metadata to the utility via post-creation
|
|
84
|
+
# assignment (act.utility_value = ... style).
|
|
85
|
+
# 2. Create the runtime (theta is the only safety-vs-output knob)
|
|
86
|
+
runtime = EntropyRuntime(utility=my_utility, theta=0.15,
|
|
87
|
+
audit_log="audit.jsonl")
|
|
88
|
+
|
|
89
|
+
# 3. Every agent action goes through the gate
|
|
90
|
+
result = runtime.step(
|
|
91
|
+
state=my_state,
|
|
92
|
+
action=action(delete_records, "users", required_gear=Gear.EXECUTE),
|
|
93
|
+
execute=lambda a: a(),
|
|
94
|
+
propose_alternative=my_fallback_planner, # optional: rejected-action fallback
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if result.suspended:
|
|
98
|
+
alert_human() # m consecutive rejections -> suspend at G0, await review
|
|
99
|
+
runtime.resume() # after human review, gears must be re-earned
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Gear semantics
|
|
103
|
+
|
|
104
|
+
| Gear | Name | Permitted actions |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| G0 | Observe | Read-only observation, safe holding |
|
|
107
|
+
| G1 | Suggest | Side-effect-free candidate plans |
|
|
108
|
+
| G2 | Plan | Bounded, reversible recovery actions |
|
|
109
|
+
| G3 | Execute | Independently chosen side-effecting actions |
|
|
110
|
+
| G4 | Integrate | System-level coordination (an emergent property of all-G3 fleets) |
|
|
111
|
+
|
|
112
|
+
**Slow up, fast down** (earned autonomy): escalation requires σ < σ_low **and**
|
|
113
|
+
h consecutive clean cycles — every gear must be re-earned (v0.1.2+);
|
|
114
|
+
de-escalation on σ overflow or error is immediate. Autonomy is earned, and it
|
|
115
|
+
can always be revoked.
|
|
116
|
+
|
|
117
|
+
A runnable version of this narrative lives in
|
|
118
|
+
[`examples/lifecycle_demo.py`](examples/lifecycle_demo.py)
|
|
119
|
+
(`python examples/lifecycle_demo.py`).
|
|
120
|
+
|
|
121
|
+
## Framework adapters
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
# LangChain: pip install entropy-sdk[langchain]
|
|
125
|
+
from entropy_sdk.adapters.langchain import gated_tool
|
|
126
|
+
safe_tool = gated_tool(runtime, my_tool, required_gear=Gear.EXECUTE)
|
|
127
|
+
|
|
128
|
+
# PydanticAI-compatible callable decorator: pip install entropy-sdk[pydanticai]
|
|
129
|
+
from entropy_sdk.adapters.pydanticai import gated
|
|
130
|
+
|
|
131
|
+
@gated(runtime, required_gear=Gear.EXECUTE)
|
|
132
|
+
def delete_records(table: str) -> str: ...
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
On rejection the adapters **return an explanatory string instead of raising** —
|
|
136
|
+
rejection itself is feedback the agent can act on next turn.
|
|
137
|
+
|
|
138
|
+
(Note on naming: this is a **PydanticAI-compatible callable decorator**, not a
|
|
139
|
+
true PydanticAI adapter — it is a pure stdlib decorator with zero framework
|
|
140
|
+
dependencies (the `pydanticai` extra list is intentionally empty, FIX-8) and has
|
|
141
|
+
**not been validated against a real PydanticAI runtime**. Just
|
|
142
|
+
`from entropy_sdk.adapters.pydanticai import gated`.)
|
|
143
|
+
|
|
144
|
+
(LangChain boundary: `args_schema` passthrough only works for real
|
|
145
|
+
StructuredTool instances carrying a schema; bare tool objects (func only, no
|
|
146
|
+
args_schema) remain limited by the wrapper's `*args/**kwargs` signature.)
|
|
147
|
+
|
|
148
|
+
## Threat model boundaries (paper armor — read before deploying)
|
|
149
|
+
|
|
150
|
+
1. **The gate is the sole dispatch channel — only inside execution paths the
|
|
151
|
+
SDK controls.** A caller holding the raw callable can bypass the gate
|
|
152
|
+
(`tool.func(...)` skips it); the SDK governs calls that go through
|
|
153
|
+
`runtime.step`, not references in the caller's hands.
|
|
154
|
+
2. **`required_gear` is a caller-side attestation contract.** The SDK trusts
|
|
155
|
+
the label's truthfulness; whether labels may be trusted (who is allowed to
|
|
156
|
+
tag an action's gear) is the deployer's responsibility — the SDK
|
|
157
|
+
fail-closes on *invalid* values, but is not responsible for
|
|
158
|
+
under-tagged powerful actions.
|
|
159
|
+
3. **The gate governs invocation, not transactions.** Side effects that
|
|
160
|
+
already happened inside `execute` are not rolled back — the semantics are
|
|
161
|
+
invocation control, not atomicity. Implement compensation in your own
|
|
162
|
+
`execute` if you need transactional behavior.
|
|
163
|
+
4. **`resume()` does not clear σ** (design semantics, not a bug): human
|
|
164
|
+
review ≠ instant restoration of trust — σ persists after resuming, and
|
|
165
|
+
gears must be re-earned through clean cycles.
|
|
166
|
+
5. **`except Exception` does not cover `BaseException`**: if a
|
|
167
|
+
proposer/execute callback raises SystemExit/KeyboardInterrupt, the state
|
|
168
|
+
machine can still desynchronize — this is standard Python practice;
|
|
169
|
+
callers must not raise BaseException inside callbacks.
|
|
170
|
+
6. **Concurrency**: the runtime is lock-free. Under current CPython (GIL),
|
|
171
|
+
8000/8000 cycles measured with no lost updates; on free-threaded Python
|
|
172
|
+
(3.13t+), the read-modify-write of cycle/σ can lose updates — serialize
|
|
173
|
+
externally when using multiple threads.
|
|
174
|
+
7. **A deleted audit file reads as empty** (fail-open read path): in file
|
|
175
|
+
mode, `entries`/metrics return empty results for a missing chain file
|
|
176
|
+
rather than raising — asymmetric with the fail-closed write path. Deployers
|
|
177
|
+
should monitor the audit file's existence (a missing append-only chain file
|
|
178
|
+
is itself an event).
|
|
179
|
+
|
|
180
|
+
## Empirical data API
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
runtime.audit.gate_acceptance_rate() # gate acceptance rate (Theorem 1, p1≥p3)
|
|
184
|
+
runtime.audit.gear_histogram() # gear transition histogram (Theorem 3)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Every run produces data for the framework's empirical validation — a unique
|
|
188
|
+
value of the SDK relative to the full system.
|
|
189
|
+
|
|
190
|
+
## Changelog & security record
|
|
191
|
+
|
|
192
|
+
- [CHANGELOG.md](CHANGELOG.md) — versioned changes, including behavior-change
|
|
193
|
+
and compatibility notes.
|
|
194
|
+
- 中文文档:[README.zh-CN.md](README.zh-CN.md)
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT © Wang Miaosheng (ORCID: 0009-0003-2767-2421) — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
entropy_sdk/__init__.py,sha256=GVGNLD7hYPV8HiemshtT9Rf7Pr1nvNiJw6DqI6rITpI,909
|
|
2
|
+
entropy_sdk/fallback.py,sha256=b0qq7OHj6o5EZHOJ2EJRZzFUyYQFC21RZneKG8H0S2A,1937
|
|
3
|
+
entropy_sdk/gate.py,sha256=j6G979OiYmcpn940IfvN_5ymxVETjAfDLvk_0lZaypg,3121
|
|
4
|
+
entropy_sdk/gear.py,sha256=XBBoYP4g5cev9jvxdYtSfEUMG0kuuVcCiR_AHCJ1Q3Q,1116
|
|
5
|
+
entropy_sdk/policy.py,sha256=Bn3BknnhXmv5aoCVb8s1VvN9wrx2ZCKQ8sSW7UUKJ0U,2242
|
|
6
|
+
entropy_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
entropy_sdk/runtime.py,sha256=_lnXUkIM1ME46diElxjyeNmd2oiXV8SVesQvODNIHzY,12182
|
|
8
|
+
entropy_sdk/state.py,sha256=tX_tmg4kCcSyIH2Da2wiRiLJBesSgZelQkkPBXPVnvs,6760
|
|
9
|
+
entropy_sdk/adapters/__init__.py,sha256=0GJwytV8g_S73_H-BJUyxdu1bl9mi5k5SrwDnQP6NNQ,256
|
|
10
|
+
entropy_sdk/adapters/base.py,sha256=bRLemADErSHEf-rN9M7L1rXJDfZsTUvvr-erMzsRNDA,1067
|
|
11
|
+
entropy_sdk/adapters/langchain.py,sha256=DLQdp8oBlYOe_J_HBb5V9JjOYvsh8x1T3UMaXFQ35as,2383
|
|
12
|
+
entropy_sdk/adapters/pydanticai.py,sha256=o3Li0oeZhj8nud5wqEIYhP91i4pXzGssEa3bWM0T7YY,1622
|
|
13
|
+
entropy_sdk/adapters/raw.py,sha256=oFDi7zoUqCJx4oA9Ym95kROO77JhUlyJr9dlWYcg81o,1138
|
|
14
|
+
entropy_sdk-0.1.5.dist-info/licenses/LICENSE,sha256=vWN2USlCU8ER_YfGPw5SdLnANPTMrifsp6BepZI9p_A,1081
|
|
15
|
+
entropy_sdk-0.1.5.dist-info/METADATA,sha256=J8qFtvBEqXI7uBNpzlbd2RMSiiX6BdH767r0i8LYXqw,9002
|
|
16
|
+
entropy_sdk-0.1.5.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
17
|
+
entropy_sdk-0.1.5.dist-info/top_level.txt,sha256=T23JbLtxHkqL56xH8SatGgs2ppiZBMz8_dahdJHyFko,12
|
|
18
|
+
entropy_sdk-0.1.5.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wang Miaosheng / CYD-PRC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
entropy_sdk
|