edangx100's picture
Update app and control-plane modules to latest
fbe9dad
Raw
History Blame Contribute Delete
4.62 kB
"""Structured action contract.
The agent's only output is a typed, structured object describing a single action
it wishes to take. That object is validated *locally* — before it ever reaches
the governance gate — so that malformed proposals (missing fields, bad enum
values, wrong types) are rejected with a clear error rather than being parsed
out of free-text. The agent never executes; it only proposes.
Two models live here:
- :class:`ProposedAction` — the action itself (the rows of the action table).
- :class:`AgentDecision` — the enclosing object the agent emits: a summary, a
risk level, the proposed action, and any prohibited actions it detected while
reasoning (surfaced, never acted upon).
"""
from __future__ import annotations
from enum import Enum
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
class AutonomyTier(str, Enum):
"""The four autonomy tiers."""
L0_READ_ONLY = "L0_READ_ONLY"
L1_RECOMMEND_ONLY = "L1_RECOMMEND_ONLY"
L2_BOUNDED_ACTION = "L2_BOUNDED_ACTION"
L3_APPROVAL_REQUIRED_ACTION = "L3_APPROVAL_REQUIRED_ACTION"
class Backend(str, Enum):
"""The four execution backends."""
DIRECT_API = "direct_api"
FUNCTION_CALL = "function_call"
MCP_CLIENT = "mcp_client"
CLI_EXECUTOR = "cli_executor"
class RiskLevel(str, Enum):
"""Agent-assessed risk level for the enclosing decision."""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
# Reject unknown fields and forbid coercion that would silently accept the wrong
# type (e.g. an int where a string is required). `strict=True` makes Pydantic
# refuse to coerce "1" -> 1 and similar, which the "wrong type" rule needs.
_MODEL_CONFIG = ConfigDict(extra="forbid", strict=True)
class ProposedAction(BaseModel):
"""A single action the agent proposes.
Required string fields must be non-empty; `arguments` is a structured map and
`evidence` is a list of identifiers. Validation failures raise
``pydantic.ValidationError``.
"""
model_config = _MODEL_CONFIG
incident_id: str = Field(min_length=1, description="Incident/case identifier.")
agent_id: str = Field(min_length=1, description="Identity of the proposing agent.")
# `strict=False` lets the enum accept its string value (the agent emits JSON
# strings); membership is still enforced, so unknown values are rejected.
autonomy_tier: AutonomyTier = Field(
strict=False, description="Tier the action requires."
)
backend: Backend = Field(strict=False, description="Target execution path.")
action_name: str = Field(min_length=1, description="Requested backend action.")
arguments: dict[str, Any] = Field(
default_factory=dict, description="Arguments for the action."
)
reason: str = Field(min_length=1, description="Why the action is proposed.")
evidence: list[str] = Field(
default_factory=list, description="Supporting evidence identifiers."
)
@field_validator("incident_id", "agent_id", "action_name", "reason")
@classmethod
def _not_blank(cls, v: str) -> str:
"""Reject whitespace-only values that ``min_length`` alone would pass."""
if not v.strip():
raise ValueError("must not be empty or whitespace")
return v
@field_validator("evidence")
@classmethod
def _evidence_ids_non_empty(cls, v: list[str]) -> list[str]:
"""Evidence entries are identifiers; none may be blank."""
for item in v:
if not item.strip():
raise ValueError("evidence identifiers must not be empty")
return v
class AgentDecision(BaseModel):
"""The enclosing object the agent emits.
Carries a human-readable summary, an assessed risk level, the proposed
action, and any prohibited actions the agent detected while reasoning but did
not act on.
"""
model_config = _MODEL_CONFIG
summary: str = Field(min_length=1, description="Human-readable decision summary.")
risk_level: RiskLevel = Field(strict=False, description="Assessed risk level.")
proposed_action: ProposedAction = Field(description="The single proposed action.")
prohibited_actions_detected: list[str] = Field(
default_factory=list,
description="Prohibited actions the agent detected but did not act on.",
)
@field_validator("summary")
@classmethod
def _summary_not_blank(cls, v: str) -> str:
if not v.strip():
raise ValueError("summary must not be empty or whitespace")
return v