Compare commits

...

1 Commits

Author SHA1 Message Date
Tomu Hirata 44ec6b0411 refactor(inner): remove legacy PolicyEngine from omnigent.inner.policies
The inner PolicyEngine was a simplified, stateless predecessor to the
production engine in omnigent.runtime.policies.engine. It was never
exported from omnigent.__init__ and had no callers outside of
tests/inner/test_policies.py. All production code and tests use the
runtime engine instead.

- Delete PolicyEngine class from omnigent/inner/policies.py
- Remove TestPolicyEngine from tests/inner/test_policies.py
- Update docstring cross-references to point at the runtime engine

Co-authored-by: Tomu Hirata
2026-06-22 11:45:56 +09:00
6 changed files with 5 additions and 225 deletions
-65
View File
@@ -607,71 +607,6 @@ class PromptPolicy(Policy):
)
# ---------------------------------------------------------------------------
# PolicyEngine
# ---------------------------------------------------------------------------
class PolicyEngine:
"""Evaluates all applicable policies for a given phase."""
def __init__(self, policies: dict[str, Policy]) -> None:
self.policies = policies
self._session: Any | None = None
def bind_runtime(self, runtime_context: PolicyRuntimeContext) -> None:
for policy in self.policies.values():
policy.bind_runtime(runtime_context)
def bind_session(self, session: Any) -> None:
"""Bind to a session so policies can read session labels."""
self._session = session
for policy in self.policies.values():
_bind_session_recursive(policy, session)
async def evaluate(
self,
content: PolicyContent,
phase: str,
context: PolicyContext | None = None,
) -> PolicyResult:
"""Run all policies that apply to this phase.
Uses max-action semantics: DENY > ASK > ALLOW. DENY short-circuits
immediately; ASK is noted but evaluation continues so a later policy
can still escalate to DENY.
"""
accumulated_set_labels: dict[str, str] = {}
worst_result: PolicyResult | None = None
for _name, policy in self.policies.items():
if phase in policy.on:
result = await policy.evaluate(content, phase, context)
if result.set_labels:
accumulated_set_labels.update(result.set_labels)
if result.action == PolicyAction.DENY:
result.set_labels = accumulated_set_labels
self._apply_label_changes(accumulated_set_labels)
return result
if result.action == PolicyAction.ASK and worst_result is None:
worst_result = result
self._apply_label_changes(accumulated_set_labels)
if worst_result is not None:
worst_result.set_labels = accumulated_set_labels
return worst_result
return PolicyResult(action=PolicyAction.ALLOW, set_labels=accumulated_set_labels)
def _apply_label_changes(self, set_labels: dict[str, str]) -> None:
"""Apply label changes to the bound session."""
if set_labels and self._session is not None:
for key, value in set_labels.items():
self._session._apply_root_label_update(str(key), str(value))
def reset_turn(self) -> None:
"""Reset per-turn state on all policies."""
for policy in self.policies.values():
policy.reset_turn()
def _merge_executor_specs(
base: ExecutorSpec | None,
override: ExecutorSpec | None,
+2 -2
View File
@@ -80,7 +80,7 @@ class Policy(ABC):
rate-limit factory in
``examples/_shared/rate_limit_policy.py`` — override
this to clear those counters at the start of each
turn. Mirrors :meth:`omnigent.inner.policies.Policy.reset_turn`.
turn. Mirrors :meth:`omnigent.runtime.policies.engine.PolicyEngine.reset_turn`.
The runtime calls this once per "turn" — defined as one
user prompt → terminal assistant response cycle, which
@@ -91,5 +91,5 @@ class Policy(ABC):
Deliberately a concrete no-op (not ``@abstractmethod``)
so subclasses opt INTO per-turn lifecycle handling
rather than being forced to override; same convention
as :meth:`omnigent.inner.policies.Policy.reset_turn`.
as :meth:`omnigent.runtime.policies.engine.PolicyEngine.reset_turn`.
"""
+1 -1
View File
@@ -158,7 +158,7 @@ class FunctionPolicy(Policy):
would accumulate forever and a "15 calls per turn"
limit would silently degrade to "15 calls per session"
under Omnigent mode — see
:meth:`omnigent.inner.policies.FunctionPolicy.reset_turn`
:meth:`omnigent.runtime.policies.engine.PolicyEngine.reset_turn`
for the native equivalent we mirror.
Stateless callables (no ``reset_turn`` attribute) are a
+1 -1
View File
@@ -784,7 +784,7 @@ class PolicyEngine:
Stateless policies — the default — no-op.
Mirrors the omnigent-native semantics in
:meth:`omnigent.inner.policies.PolicyEngine.reset_turn`.
:meth:`omnigent.runtime.policies.engine.PolicyEngine.reset_turn`.
Without this hook, legacy ``max_tool_calls_per_turn``
callables silently degrade to per-session limits under
Omnigent mode.
-155
View File
@@ -14,7 +14,6 @@ from omnigent.inner.executor import MockExecutor
from omnigent.inner.policies import (
FunctionPolicy,
PolicyAction,
PolicyEngine,
PolicyResult,
PolicyRuntimeContext,
PromptPolicy,
@@ -277,160 +276,6 @@ class TestPromptPolicy(unittest.TestCase):
_run(_t())
class TestPolicyEngine(unittest.TestCase):
def test_all_policies_evaluated(self):
calls = []
def track_a(c, p):
calls.append("a")
return PolicyResult(action=PolicyAction.ALLOW)
def track_b(c, p):
calls.append("b")
return PolicyResult(action=PolicyAction.ALLOW)
async def _t():
e = PolicyEngine(
{
"a": FunctionPolicy(name="a", on=["request"], callable=track_a),
"b": FunctionPolicy(name="b", on=["request"], callable=track_b),
}
)
r = await e.evaluate("t", "request")
self.assertEqual(r.action, PolicyAction.ALLOW)
self.assertEqual(calls, ["a", "b"])
_run(_t())
def test_phase_filtering(self):
calls = []
def track(event, config):
calls.append(event["type"])
return PolicyResult(action=PolicyAction.ALLOW)
async def _t():
e = PolicyEngine(
{
"io": FunctionPolicy(name="io", on=["request"], callable=track),
"oo": FunctionPolicy(name="oo", on=["response"], callable=track),
}
)
await e.evaluate("t", "request")
self.assertEqual(calls, ["request"])
calls.clear()
await e.evaluate("t", "response")
self.assertEqual(calls, ["response"])
_run(_t())
def test_deny_short_circuits(self):
"""DENY stops evaluation immediately — later policies don't run."""
calls = []
def block_it(c, p):
calls.append("blocker")
return PolicyResult(action=PolicyAction.DENY, reason="no")
def after(c, p):
calls.append("after")
return PolicyResult(action=PolicyAction.ALLOW)
async def _t():
e = PolicyEngine(
{
"b": FunctionPolicy(name="b", on=["request"], callable=block_it),
"a": FunctionPolicy(name="a", on=["request"], callable=after),
}
)
r = await e.evaluate("t", "request")
self.assertEqual(r.action, PolicyAction.DENY)
self.assertEqual(calls, ["blocker"])
_run(_t())
def test_ask_continues_evaluation(self):
"""ASK does not stop evaluation — later policies still run."""
calls = []
def ask_it(c, p):
calls.append("ask")
return PolicyResult(action=PolicyAction.ASK, reason="approve?")
def after(c, p):
calls.append("after")
return PolicyResult(action=PolicyAction.ALLOW)
async def _t():
e = PolicyEngine(
{
"a": FunctionPolicy(name="a", on=["request"], callable=ask_it),
"b": FunctionPolicy(name="b", on=["request"], callable=after),
}
)
r = await e.evaluate("t", "request")
self.assertEqual(r.action, PolicyAction.ASK)
self.assertEqual(calls, ["ask", "after"])
_run(_t())
def test_deny_after_ask_wins(self):
"""If an earlier policy says ASK but a later one says DENY, DENY wins."""
async def _t():
e = PolicyEngine(
{
"a": FunctionPolicy(
name="a",
on=["request"],
callable=lambda c, p: PolicyResult(
action=PolicyAction.ASK, reason="approve?"
),
),
"b": FunctionPolicy(
name="b",
on=["request"],
callable=lambda c, p: PolicyResult(
action=PolicyAction.DENY, reason="blocked"
),
),
}
)
r = await e.evaluate("t", "request")
self.assertEqual(r.action, PolicyAction.DENY)
self.assertEqual(r.reason, "blocked")
_run(_t())
def test_first_ask_reason_kept(self):
"""When multiple policies ASK, the first ASK's reason is returned."""
async def _t():
e = PolicyEngine(
{
"a": FunctionPolicy(
name="a",
on=["request"],
callable=lambda c, p: PolicyResult(
action=PolicyAction.ASK, reason="first"
),
),
"b": FunctionPolicy(
name="b",
on=["request"],
callable=lambda c, p: PolicyResult(
action=PolicyAction.ASK, reason="second"
),
),
}
)
r = await e.evaluate("t", "request")
self.assertEqual(r.action, PolicyAction.ASK)
self.assertEqual(r.reason, "first")
_run(_t())
async def _evaluate_capturing(
callable_fn: Callable[[list[tuple[Any, ...]]], Callable[..., Any]],
) -> list[tuple[Any, ...]]:
@@ -876,7 +876,7 @@ def test_function_policy_reset_turn_invokes_callable_attribute(
on the wrapped callable and invoke it. This is how legacy
omnigent policies like ``max_tool_calls_per_turn`` clear
per-turn accumulators between turns — see
:meth:`omnigent.inner.policies.FunctionPolicy.reset_turn`
:meth:`omnigent.runtime.policies.engine.PolicyEngine.reset_turn`
for the native implementation we mirror.
What breaks if this fails: the rate-limit factory in