Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions src/nomotic/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -2879,6 +2879,24 @@ def _compute_reversibility_ucs_increase(
}
return increases.get(level, config.irreversible_ucs_increase)

def _run_post_verdict_processing(
self,
action: Action,
context: AgentContext,
verdict: GovernanceVerdict,
) -> None:
"""Run post-verdict listeners and optional lifecycle pipelines."""
for listener in self._listeners:
listener(verdict)

# UAHS post-evaluation pipeline (opt-in)
if self._enable_uahs:
self._post_evaluate_uahs(context.agent_id, verdict)

# Lifecycle event detection
if self.config.enable_lifecycle_hooks:
self._check_lifecycle_transitions(action.agent_id or context.agent_id, context)

def _record_verdict(
self, action: Action, context: AgentContext, verdict: GovernanceVerdict,
*, tracer: Any = None, derived_thresholds: Any = None,
Expand Down Expand Up @@ -3077,16 +3095,7 @@ def _record_verdict(
self._append_history(context.agent_id, record)
context.action_history.append(record)

for listener in self._listeners:
listener(verdict)

# UAHS post-evaluation pipeline (opt-in)
if self._enable_uahs:
self._post_evaluate_uahs(context.agent_id, verdict)

# Lifecycle event detection
if self.config.enable_lifecycle_hooks:
self._check_lifecycle_transitions(action.agent_id or context.agent_id, context)
self._run_post_verdict_processing(action, context, verdict)

# BehaviorLedger: build and store the complete decision record
if self._behavior_ledger is not None and tracer is not None:
Expand Down
48 changes: 47 additions & 1 deletion tests/test_runtime.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Tests for the governance runtime — the full pipeline."""

from nomotic.types import Action, AgentContext, TrustProfile, Verdict
from unittest.mock import MagicMock

from nomotic.types import Action, AgentContext, GovernanceVerdict, TrustProfile, Verdict
from nomotic.runtime import GovernanceRuntime, RuntimeConfig
from nomotic.interrupt import InterruptScope

Expand Down Expand Up @@ -189,3 +191,47 @@ def test_low_trust_triggers_human_override(self):
# Trust should be very low by now
trust = runtime.get_trust_profile("agent-1").overall_trust
assert trust < 0.3

def test_run_post_verdict_processing_calls_listener_uahs_and_lifecycle(self):
runtime = GovernanceRuntime()
events = []
runtime._listeners = [lambda v: events.append(v.action_id)]
runtime._enable_uahs = True
runtime._post_evaluate_uahs = MagicMock()
runtime._check_lifecycle_transitions = MagicMock()

action = _action("read", target="db")
context = _ctx(agent_id="agent-1")
verdict = GovernanceVerdict(
action_id=action.id,
verdict=Verdict.ALLOW,
ucs=0.9,
reasoning="ok",
)

runtime._run_post_verdict_processing(action, context, verdict)

assert events == [action.id]
runtime._post_evaluate_uahs.assert_called_once_with("agent-1", verdict)
runtime._check_lifecycle_transitions.assert_called_once_with("agent-1", context)

def test_run_post_verdict_processing_respects_disabled_flags(self):
runtime = GovernanceRuntime(RuntimeConfig(enable_lifecycle_hooks=False))
runtime._listeners = []
runtime._enable_uahs = False
runtime._post_evaluate_uahs = MagicMock()
runtime._check_lifecycle_transitions = MagicMock()

action = Action(agent_id="", action_type="read", target="db")
context = _ctx(agent_id="agent-fallback")
verdict = GovernanceVerdict(
action_id=action.id,
verdict=Verdict.ALLOW,
ucs=0.9,
reasoning="ok",
)

runtime._run_post_verdict_processing(action, context, verdict)

runtime._post_evaluate_uahs.assert_not_called()
runtime._check_lifecycle_transitions.assert_not_called()
Loading