orchestrata_ultimate.orchestrator.engine
Reference › orchestrata_ultimate › orchestrator › engine
UOxEngine is the governance decision point of the Orchestrator volk (U:Ox): it answers "may this volk run this tool with these parameters?" with allow, deny, escalate or require-approval. The Claude tool interceptor (core.adapters.claude.tool_interceptor) is its importer, so every governed tool call reaches it through evaluate_policy. See the orchestrator package narrative for how this governance substrate sits under the intent and pipeline flows.
A PolicyContext carries the session id, volk id, tool name, parameters, an optional pre-parsed intent and a timestamp; its constructor rejects empty identifiers, and to_cache_key derives the cache key. The engine is built from a rules directory, a RedisStateManager, a GraphAuditTrail (graph_audit), a PrologBridge and a cache TTL (300 seconds by default), creating any that are missing and raising TypeError or ValueError for bad arguments. initialize starts the graph audit and loads the Prolog rules when governance.pl exists; shutdown and health_check complete the lifecycle.
evaluate_policy works in order: look up a cached decision in Redis (a failed lookup is logged and ignored); use the supplied intent or have _parse_intent_from_context build one through intent_parser; apply _evaluate_intent; record the decision with _record_audit; cache the GovernanceResult with the TTL. _evaluate_intent denies protected paths, then queries the Prolog predicate governance:evaluate_policy/3 with a term cleaned by _sanitize_prolog_term (restricted character set, length limit). The engine is fail-closed: an unparsable intent, a Prolog error, an unknown decision value or an empty result all yield DENY with a reason. start_session and end_session record volk sessions in Redis, and facts are handled by fact_manager.
At a glance
| Source | src/orchestrata_ultimate/orchestrator/engine.py |
| Lines of code | 751 |
| Classes | 4 |
| Functions | 2 |
| Imports (internal) | orchestrata_ultimate.core.base.traces, orchestrata_ultimate.core.enums, orchestrata_ultimate.core.paths, orchestrata_ultimate.infra.prolog_bridge, orchestrata_ultimate.infra.redis_state, orchestrata_ultimate.orchestrator.fact_manager, orchestrata_ultimate.orchestrator.graph_audit, orchestrata_ultimate.orchestrator.intent_parser |
| Imported by | orchestrata_ultimate.core.adapters.claude.tool_interceptor |
| Tests that import it | tests/generated_orchestrator/test_engine_generated.py, tests/orchestrator/test_engine_behavioral.py, tests/orchestrator/test_engine_extended.py, tests/orchestrator/test_tool_interceptor_governance.py |
Class diagram
Dependency diagram
API
Constants
| Name | Type | Value |
|---|---|---|
DEFAULT_CACHE_TTL_SECONDS |
int |
300 |
MAX_PARAMETER_SIZE_BYTES |
int |
1024 * 1024 |
MAX_PROLOG_TERM_LENGTH |
int |
4096 |
Classes
GovernanceDecision (enum)
Bases: Enum · Line 58
Policy evaluation decision.
| Member | Value |
|---|---|
ALLOW |
'allow' |
DENY |
'deny' |
ESCALATE |
'escalate' |
REQUIRE_APPROVAL |
'require_approval' |
PolicyContext (dataclass)
Decorators: dataclass · Line 79
Context for policy evaluation.
| Attribute | Type | Default |
|---|---|---|
session_id |
str |
|
volk_id |
str |
|
tool |
str |
|
parameters |
dict[str, Any] |
|
intent |
Intent | None |
None |
timestamp |
datetime |
field(default_factory=_utc_now) |
| Method | Signature | Summary |
|---|---|---|
__post_init__ |
__post_init__() -> None |
Validate context after initialization. |
to_cache_key |
to_cache_key() -> str |
Generate secure cache key for this context. |
GovernanceResult (dataclass)
Decorators: dataclass, traces(spec='000', story='US-007', reqs=['REQ-000-061']) · Line 144
Result of policy evaluation.
| Attribute | Type | Default |
|---|---|---|
decision |
GovernanceDecision |
|
reason |
str |
|
intent |
Intent | None |
None |
audit_id |
str | None |
None |
cached |
bool |
False |
metadata |
dict[str, Any] |
field(default_factory=dict) |
UOxEngine
Decorators: traces(spec='001', story='US-003', reqs=['REQ-001-US-003-002']) · Line 164
U:Ox governance engine.
| Method | Signature | Summary |
|---|---|---|
_validate_cache_ttl staticmethod |
_validate_cache_ttl(cache_ttl_seconds: int) -> None |
Raise ValueError if cache_ttl_seconds is not a non-negative integer. |
_validate_dependencies staticmethod |
_validate_dependencies(redis: RedisStateManager | None, graph: GraphAuditTrail | None, prolog: PrologBridge | None) -> None |
Type-check optional dependency objects via duck typing. |
_init_fact_manager |
_init_fact_manager() -> FactManager |
Build a FactManager wired to this engine's Prolog bridge. |
__init__ |
__init__(rules_dir: Path | None = None, redis: RedisStateManager | None = None, graph: GraphAuditTrail | None = None, prolog: PrologBridge | None = None, cache_ttl_seconds: int = DEFAULT_CACHE_TTL_SECONDS) -> None |
Initialize governance engine. |
initialize |
async initialize() -> None |
Initialize the engine and load rules. |
shutdown |
async shutdown() -> None |
Shutdown the engine and cleanup resources. |
health_check |
async health_check() -> dict[str, Any] |
Check health of all engine components. |
evaluate_policy |
async evaluate_policy(context: PolicyContext) -> GovernanceResult |
Evaluate policy for given context. |
_parse_intent_from_context |
_parse_intent_from_context(context: PolicyContext) -> Intent |
Parse intent from context parameters. |
_sanitize_prolog_term |
_sanitize_prolog_term(term: str) -> str |
Sanitize a Prolog term to prevent injection attacks. |
_evaluate_intent |
_evaluate_intent(intent: Intent, context: PolicyContext) -> tuple[GovernanceDecision, str] |
Evaluate intent against governance rules. |
_is_protected_path |
_is_protected_path(path: str) -> bool |
Check if path is protected. |
_record_audit |
async _record_audit(context: PolicyContext, decision: GovernanceDecision, reason: str, intent: Intent | None) -> str | None |
Record governance decision in audit trail. |
start_session |
start_session(session_id: str, volk_id: str, goals: list[str]) -> bool |
Start a new session. |
end_session |
end_session(session_id: str, volk_id: str, outcome: str) -> bool |
End a session. |
_serialize_result |
_serialize_result(result: GovernanceResult) -> str |
Serialize result for caching. |
_deserialize_result |
_deserialize_result(data: str) -> GovernanceResult |
Deserialize cached result. |
Functions
| Function | Signature | Summary |
|---|---|---|
_utc_now |
_utc_now() -> datetime |
Get current UTC time with timezone info. |
_sanitize_for_cache_key traces |
_sanitize_for_cache_key(value: str) -> str |
Sanitize string for use in cache key - only alphanumeric and limited chars. |
Source
# mypy: ignore-errors
"""U:Ox governance engine with Prolog-based policy evaluation.
Provides deterministic governance decisions using:
- Prolog rules for policy evaluation
- Redis for hot state and caching
- Graph for audit trail persistence
- Intent parsing for tool call analysis
Based on DESIGN_DECISIONS.md and INFRASTRUCTURE.md specifications.
"""
import hashlib
import json
import logging
import re
import uuid
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import Enum
from pathlib import Path
from typing import Any, cast
from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.core.enums import FactScope
from orchestrata_ultimate.core.paths import get_prolog_dir
from orchestrata_ultimate.infra.prolog_bridge import PrologBridge
from orchestrata_ultimate.infra.redis_state import (
RedisStateManager,
SessionState,
)
from orchestrata_ultimate.orchestrator.fact_manager import FactManager
from orchestrata_ultimate.orchestrator.graph_audit import (
DecisionOutcome,
GraphAuditTrail,
)
from orchestrata_ultimate.orchestrator.graph_audit import (
PolicyDecision as GraphPolicyDecision,
)
from orchestrata_ultimate.orchestrator.intent_parser import (
Action,
Intent,
IntentParser,
ResourceType,
)
# Spec: 001 - Orchestration Kernel
# Configuration constants
DEFAULT_CACHE_TTL_SECONDS: int = 300
MAX_PARAMETER_SIZE_BYTES: int = 1024 * 1024 # 1MB limit for parameters
MAX_PROLOG_TERM_LENGTH: int = (
4096 # Truncation limit for sanitised Prolog terms; guards against DoS via oversized inputs
)
class GovernanceDecision(Enum):
"""Policy evaluation decision."""
ALLOW = "allow"
DENY = "deny"
ESCALATE = "escalate"
REQUIRE_APPROVAL = "require_approval"
def _utc_now() -> datetime:
"""Get current UTC time with timezone info."""
return datetime.now(UTC)
@traces(spec="001", story="US-003", reqs=["REQ-001-US-003-005"])
def _sanitize_for_cache_key(value: str) -> str:
"""Sanitize string for use in cache key - only alphanumeric and limited chars."""
return re.sub(r"[^a-zA-Z0-9_\-.]", "_", value)[:256]
@dataclass
class PolicyContext:
"""Context for policy evaluation.
Contains all information needed to evaluate a governance decision:
- Session and volk identification
- Tool and parameters
- Optional pre-parsed intent
- Timestamp for audit trail
"""
session_id: str
volk_id: str
tool: str
parameters: dict[str, Any]
intent: Intent | None = None
timestamp: datetime = field(default_factory=_utc_now)
@traces(spec="001", story="US-004", reqs=["REQ-001-US-004-017"])
def __post_init__(self) -> None:
"""Validate context after initialization."""
if not self.session_id or not isinstance(self.session_id, str):
raise ValueError("session_id must be a non-empty string")
if not self.volk_id or not isinstance(self.volk_id, str):
raise ValueError("volk_id must be a non-empty string")
if not self.tool or not isinstance(self.tool, str):
raise ValueError("tool must be a non-empty string")
if not isinstance(self.parameters, dict):
raise ValueError("parameters must be a dictionary")
# Validate parameter size to prevent DoS
try:
params_json = json.dumps(self.parameters, default=str)
if len(params_json.encode("utf-8")) > MAX_PARAMETER_SIZE_BYTES:
raise ValueError(
f"parameters exceed maximum size of {MAX_PARAMETER_SIZE_BYTES} bytes"
)
except (TypeError, ValueError) as e:
if "exceed maximum size" in str(e):
raise
# Allow non-JSON-serializable parameters but log warning
@traces(spec="001", story="US-003", reqs=["REQ-001-US-003-005"])
def to_cache_key(self) -> str:
"""Generate secure cache key for this context.
Uses SHA256 hash to prevent cache key injection attacks.
All inputs are sanitized before hashing.
"""
# Sanitize inputs before hashing
safe_volk = _sanitize_for_cache_key(self.volk_id)
safe_tool = _sanitize_for_cache_key(self.tool)
# Create deterministic key from context using secure hash
try:
params_str = json.dumps(self.parameters, sort_keys=True, default=str)
except TypeError, ValueError:
# Fallback to empty dict representation for non-serializable params
params_str = "{}"
# Use full SHA256 hash for collision resistance
key_data = f"{safe_volk}:{safe_tool}:{params_str}"
return hashlib.sha256(key_data.encode("utf-8")).hexdigest()
@dataclass
@traces(spec="000", story="US-007", reqs=["REQ-000-061"])
class GovernanceResult:
"""Result of policy evaluation.
Contains:
- Decision (ALLOW/DENY/ESCALATE)
- Reason for decision
- Parsed intent
- Audit ID for traceability
- Whether result was cached
"""
decision: GovernanceDecision
reason: str
intent: Intent | None = None
audit_id: str | None = None
cached: bool = False
metadata: dict[str, Any] = field(default_factory=dict)
@traces(spec="001", story="US-003", reqs=["REQ-001-US-003-002"])
class UOxEngine:
"""U:Ox governance engine.
Coordinates policy evaluation using:
- Prolog rules for deterministic decisions
- Intent parsing for semantic analysis
- Redis for hot state and caching
- Graph for audit trail
- Fact management for dynamic rules
"""
# Protected paths that require special approval
PROTECTED_PATHS = {
"/etc/",
"/sys/",
"/proc/",
"/dev/",
"/boot/",
"/root/",
}
@staticmethod
def _validate_cache_ttl(cache_ttl_seconds: int) -> None:
"""Raise ValueError if cache_ttl_seconds is not a non-negative integer."""
if not isinstance(cache_ttl_seconds, int) or cache_ttl_seconds < 0:
raise ValueError("cache_ttl_seconds must be a non-negative integer")
@staticmethod
def _validate_dependencies(
redis: RedisStateManager | None,
graph: GraphAuditTrail | None,
prolog: PrologBridge | None,
) -> None:
"""Type-check optional dependency objects via duck typing."""
if redis is not None:
if not hasattr(redis, "client") or not hasattr(redis, "create_session"):
raise TypeError(
f"redis must be RedisStateManager-compatible, got {type(redis).__name__}"
)
if graph is not None:
if not hasattr(graph, "start") or not hasattr(graph, "log_event"):
raise TypeError(
f"graph must be GraphAuditTrail-compatible, got {type(graph).__name__}"
)
if prolog is not None:
if not hasattr(prolog, "query") or not hasattr(prolog, "assert_fact"):
raise TypeError(
f"prolog must be PrologBridge-compatible, got {type(prolog).__name__}"
)
@traces(spec="001", story="US-004", reqs=["REQ-001-US-004-011"])
def _init_fact_manager(self) -> FactManager:
"""Build a FactManager wired to this engine's Prolog bridge."""
return FactManager(
assert_fn=lambda pred, args: self.prolog.assert_fact(
pred, list(args), FactScope.SESSION
),
retract_fn=self.prolog.retract_fact,
)
def __init__(
self,
rules_dir: Path | None = None,
redis: RedisStateManager | None = None,
graph: GraphAuditTrail | None = None,
prolog: PrologBridge | None = None,
cache_ttl_seconds: int = DEFAULT_CACHE_TTL_SECONDS,
) -> None:
"""Initialize governance engine.
Args:
rules_dir: Directory containing Prolog rules
redis: Redis state manager (created if not provided)
graph: Graph audit trail (created if not provided)
prolog: Prolog bridge (created if not provided)
cache_ttl_seconds: TTL for cached governance decisions
Raises:
TypeError: If provided dependencies are of wrong type
ValueError: If cache_ttl_seconds is invalid
"""
self._logger = logging.getLogger(__name__)
self._validate_cache_ttl(cache_ttl_seconds)
self._cache_ttl_seconds = cache_ttl_seconds
self.rules_dir = rules_dir or get_prolog_dir()
self._validate_dependencies(redis, graph, prolog)
self.redis = redis or RedisStateManager()
self.graph = graph or GraphAuditTrail()
self.prolog = prolog or PrologBridge(rules_dir=self.rules_dir)
self.intent_parser = IntentParser()
self.fact_manager = self._init_fact_manager()
self._initialized = False
async def initialize(self) -> None:
"""Initialize the engine and load rules."""
if self._initialized:
return
# Start graph audit trail - use interface method with async support
try:
await self.graph.start()
except AttributeError:
self._logger.warning("Graph audit trail does not have start method")
except Exception as e:
self._logger.error(f"Failed to start graph audit trail: {e}")
# Load Prolog rules
governance_rules = self.rules_dir / "governance.pl"
if governance_rules.exists():
self.prolog.load_all_rules()
self._initialized = True
async def shutdown(self) -> None:
"""Shutdown the engine and cleanup resources."""
if not self._initialized:
return
# Close connections - use interface methods with proper error handling
try:
self.redis.close()
except AttributeError:
self._logger.debug("Redis state manager does not have close method")
except Exception as e:
self._logger.error(f"Error closing Redis connection: {e}")
try:
await self.graph.close()
except AttributeError:
self._logger.debug("Graph audit trail does not have close method")
except Exception as e:
self._logger.error(f"Error closing graph connection: {e}")
self._initialized = False
async def health_check(self) -> dict[str, Any]:
"""Check health of all engine components.
Returns:
Dictionary with health status of each component and error details.
"""
health: dict[str, Any] = {
"initialized": self._initialized,
"redis": False,
"redis_error": None,
"graph": False,
"graph_error": None,
"prolog": False,
"prolog_error": None,
}
# Check Redis
try:
self.redis.client.ping()
health["redis"] = True
except Exception as e:
self._logger.error(f"Redis health check failed: {e}")
health["redis_error"] = str(e)
# Check graph - use async interface method
try:
repo = getattr(self.graph, "_repo", None)
if repo is not None:
await repo.verify_connectivity()
health["graph"] = True
else:
health["graph_error"] = "No driver available"
except Exception as e:
self._logger.error(f"Graph health check failed: {e}")
health["graph_error"] = str(e)
# Check Prolog - verify it can execute a simple query
try:
if self.prolog is not None:
# Actually verify Prolog is working with a simple query
result = self.prolog.query_bool("true")
health["prolog"] = result
if not result:
health["prolog_error"] = (
"Prolog query returned false" # pragma: no cover
)
except Exception as e:
self._logger.error(f"Prolog health check failed: {e}")
health["prolog_error"] = str(e)
return health
@traces(
spec="001", story="US-003", reqs=["REQ-001-US-003-005", "REQ-001-US-003-009"]
)
async def evaluate_policy(self, context: PolicyContext) -> GovernanceResult:
"""Evaluate policy for given context.
Process:
1. Check cache for previous decision
2. Parse intent if not provided
3. Evaluate against Prolog rules
4. Apply built-in safety checks
5. Cache result
6. Record audit trail
Args:
context: Policy evaluation context
Returns:
GovernanceResult with decision and reasoning
"""
# Check cache first
cache_key = f"governance:{context.to_cache_key()}"
try:
# get() is typed as redis-py's shared ResponseT (Awaitable[Any] |
# Any) because the same command mixin backs both sync and async
# clients; this sync client always returns str or None.
cached = cast("str | None", self.redis.client.get(cache_key))
if cached:
result = self._deserialize_result(cached)
result.cached = True
return result
except Exception as e:
# Cache miss is not fatal - continue with evaluation
self._logger.warning(f"Cache lookup failed: {e}")
# Parse intent if not provided
intent = context.intent
if intent is None:
intent = self._parse_intent_from_context(context)
# Handle case where intent parsing fails
if intent is None:
self._logger.error("Failed to parse intent from context")
return GovernanceResult(
decision=GovernanceDecision.DENY,
reason="Failed to parse intent - denied for safety",
intent=None,
audit_id=None,
cached=False,
)
# Evaluate policy
decision, reason = self._evaluate_intent(intent, context)
# Record audit trail with async support
audit_id = await self._record_audit(context, decision, reason, intent)
# Create result
result = GovernanceResult(
decision=decision,
reason=reason,
intent=intent,
audit_id=audit_id,
cached=False,
)
# Cache result with configurable TTL
try:
self.redis.client.setex(
cache_key, self._cache_ttl_seconds, self._serialize_result(result)
)
except Exception as e:
# Cache write failure is not fatal
self._logger.warning(f"Cache write failed: {e}")
return result
@traces(spec="001", story="US-003", reqs=["REQ-001-US-003-017"])
def _parse_intent_from_context(self, context: PolicyContext) -> Intent:
"""Parse intent from context parameters.
Args:
context: Policy context
Returns:
Parsed Intent object
"""
# Extract action from tool name
tool_to_action = {
"Read": Action.READ,
"Write": Action.WRITE,
"Edit": Action.MODIFY,
"Bash": Action.EXECUTE,
"Delete": Action.DELETE,
}
action = tool_to_action.get(context.tool, Action.READ)
# Extract resource path and type
resource_path = "/"
resource_type = ResourceType.SYSTEM
if "file_path" in context.parameters:
resource_path = context.parameters["file_path"]
resource_type = ResourceType.FILE
elif "path" in context.parameters:
resource_path = context.parameters["path"]
resource_type = ResourceType.DIRECTORY
elif "command" in context.parameters:
resource_path = context.parameters["command"]
resource_type = ResourceType.COMMAND
return Intent(
action=action,
resource_type=resource_type,
resource_path=resource_path,
)
def _sanitize_prolog_term(self, term: str) -> str:
"""Sanitize a Prolog term to prevent injection attacks.
Args:
term: Raw Prolog term string
Returns:
Sanitized term safe for use in queries
"""
# Remove potentially dangerous characters
# Allow only alphanumeric, underscores, parentheses, brackets, commas, quotes, and spaces
# Square brackets [] are needed for Prolog lists
sanitized = re.sub(r'[^\w\s(),\[\]\'"\-./:]', "", term)
# Limit length to prevent DoS
return sanitized[:MAX_PROLOG_TERM_LENGTH]
@traces(
spec="001", story="US-003", reqs=["REQ-001-US-003-001", "REQ-001-US-003-008"]
)
def _evaluate_intent(
self, intent: Intent, context: PolicyContext
) -> tuple[GovernanceDecision, str]:
"""Evaluate intent against governance rules.
Args:
intent: Parsed intent
context: Policy context
Returns:
Tuple of (decision, reason)
"""
# Check protected paths
if self._is_protected_path(intent.resource_path):
return (GovernanceDecision.DENY, f"Protected path: {intent.resource_path}")
# Query Prolog rules - FAIL CLOSED (DENY) on any error
try:
# Sanitize the Prolog term to prevent injection
prolog_term = self._sanitize_prolog_term(intent.to_prolog())
# Use governance module prefix for proper module resolution
query = f"governance:evaluate_policy({prolog_term}, Decision, Reason)"
query_result = self.prolog.query(query)
results = query_result.solutions # QueryResult.solutions contains the list
if results:
result = results[0]
decision_str = result.get("Decision", "deny")
reason = result.get("Reason", "Policy evaluation succeeded")
try:
decision = GovernanceDecision(decision_str)
return (decision, reason)
except ValueError:
self._logger.error(f"Invalid decision from Prolog: {decision_str}")
return (
GovernanceDecision.DENY,
f"Invalid policy decision: {decision_str}",
)
# No results from Prolog - DENY by default
self._logger.warning("No policy evaluation results from Prolog")
return (GovernanceDecision.DENY, "No policy matched - denied by default")
except Exception as e:
# CRITICAL: Policy evaluation failure = DENY (fail closed)
self._logger.error(f"Policy evaluation failed: {e}")
return (GovernanceDecision.DENY, f"Policy system error: {e}")
@traces(spec="001", story="US-003", reqs=["REQ-001-US-003-013"])
def _is_protected_path(self, path: str) -> bool:
"""Check if path is protected.
Args:
path: Path to check
Returns:
True if path is protected
"""
return any(path.startswith(protected) for protected in self.PROTECTED_PATHS)
@traces(spec="015", story="US-001", reqs=["REQ-015-115"])
async def _record_audit(
self,
context: PolicyContext,
decision: GovernanceDecision,
reason: str,
intent: Intent | None,
) -> str | None:
"""Record governance decision in audit trail.
Args:
context: Policy context
decision: Governance decision
reason: Decision reason
intent: Parsed intent
Returns:
Audit ID if recorded, None otherwise
"""
# Map GovernanceDecision to DecisionOutcome
outcome_map = {
GovernanceDecision.ALLOW: DecisionOutcome.ALLOWED,
GovernanceDecision.DENY: DecisionOutcome.DENIED,
GovernanceDecision.ESCALATE: DecisionOutcome.ESCALATED,
GovernanceDecision.REQUIRE_APPROVAL: DecisionOutcome.PENDING,
}
decision_id = str(uuid.uuid4())
policy_decision = GraphPolicyDecision(
decision_id=decision_id,
session_id=context.session_id,
volk_id=context.volk_id,
intent=intent.to_string() if intent else "",
tool=context.tool,
outcome=outcome_map[decision],
reason=reason,
timestamp=context.timestamp,
cached=False,
)
try:
# Record in graph with async support
await self.graph.log_policy_decision(policy_decision)
return decision_id
except Exception as e:
# Log the failure - audit is critical for compliance
self._logger.critical(
f"AUDIT FAILURE: Could not record decision {decision_id}: {e}"
)
# Queue for retry in Redis
try:
retry_key = f"audit:retry:{decision_id}"
retry_data = {
"decision_id": decision_id,
"session_id": context.session_id,
"volk_id": context.volk_id,
"intent": intent.to_string() if intent else "",
"tool": context.tool,
"outcome": outcome_map[decision].value,
"reason": reason,
"timestamp": context.timestamp.isoformat(),
"retry_count": 0,
"error": str(e),
}
self.redis.client.setex(
retry_key, 86400, json.dumps(retry_data)
) # 24h TTL
self._logger.warning(f"Queued audit {decision_id} for retry")
except Exception as retry_err:
self._logger.critical(
f"CRITICAL: Cannot queue audit for retry: {retry_err}"
)
return decision_id # Return ID even if audit failed - decision was made
@traces(
spec="001",
story="US-004",
reqs=["REQ-001-US-004-001", "REQ-001-US-004-005"],
)
def start_session(self, session_id: str, volk_id: str, goals: list[str]) -> bool:
"""Start a new session.
Args:
session_id: Session identifier
volk_id: Volk identifier
goals: Session goals
Returns:
True if session started successfully
Raises:
RuntimeError: If session creation fails
"""
try:
session = SessionState(
session_id=session_id,
volk_id=volk_id,
status="active",
goals=goals,
context={},
started_at=_utc_now(),
last_activity=_utc_now(),
)
result = self.redis.create_session(session)
if not result:
self._logger.error(f"Failed to create session {session_id}")
return result
except Exception as e:
self._logger.error(f"Session start failed for {session_id}: {e}")
raise RuntimeError(f"Failed to start session: {e}") from e
@traces(
spec="001",
story="US-004",
reqs=["REQ-001-US-004-023"],
)
def end_session(self, session_id: str, volk_id: str, outcome: str) -> bool:
"""End a session.
Args:
session_id: Session identifier
volk_id: Volk identifier
outcome: Session outcome
Returns:
True if session ended successfully
Raises:
RuntimeError: If session deletion fails
"""
try:
result = self.redis.delete_session(session_id)
if not result:
self._logger.warning(
f"Session {session_id} not found or already deleted"
)
return result
except Exception as e:
self._logger.error(f"Session end failed for {session_id}: {e}")
raise RuntimeError(f"Failed to end session: {e}") from e
@traces(
spec="001",
story="US-010",
reqs=["REQ-001-US-003-005", "REQ-001-US-010-016"],
)
def _serialize_result(self, result: GovernanceResult) -> str:
"""Serialize result for caching.
Args:
result: Governance result
Returns:
JSON string
"""
return json.dumps(
{
"decision": result.decision.value,
"reason": result.reason,
"intent": result.intent.to_string() if result.intent else None,
"audit_id": result.audit_id,
"metadata": result.metadata,
}
)
@traces(
spec="001",
story="US-010",
reqs=["REQ-001-US-003-005", "REQ-001-US-010-016"],
)
def _deserialize_result(self, data: str) -> GovernanceResult:
"""Deserialize cached result.
Args:
data: JSON string
Returns:
Governance result
"""
obj = json.loads(data)
# Parse intent if present
intent = None
if obj.get("intent"):
intent = self.intent_parser.parse(obj["intent"])
return GovernanceResult(
decision=GovernanceDecision(obj["decision"]),
reason=obj["reason"],
intent=intent,
audit_id=obj.get("audit_id"),
metadata=obj.get("metadata", {}),
)