PMFlow/backend/app/schemas/pmflow_agent_runtime.py

179 lines
7.6 KiB
Python

"""Customer-safe contracts for the durable PMFlow Agent OS runtime."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4
from pydantic import BaseModel, ConfigDict, Field, field_validator
PMFLOW_AGENT_RUN_PACKET_VERSION = "pmflow_agent_run.v1"
PMFLOW_AGENT_STEP_PACKET_VERSION = "pmflow_agent_step.v1"
PMFLOW_AGENT_OBSERVATION_PACKET_VERSION = "pmflow_agent_observation.v1"
PMFLOW_AGENT_ARTIFACT_PACKET_VERSION = "pmflow_agent_artifact.v1"
PMFLOW_AGENT_VERIFICATION_PACKET_VERSION = "pmflow_agent_verification.v1"
PMFLOW_AGENT_MEMORY_PACKET_VERSION = "pmflow_agent_memory.v1"
def new_pmflow_agent_id(prefix: str) -> str:
return f"{prefix}_{uuid4().hex[:12]}"
def utc_now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def clean_text(value: Any, *, limit: int = 1000) -> str:
return " ".join(str(value or "").strip().split())[:limit]
class PMFlowAgentStepPacket(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = PMFLOW_AGENT_STEP_PACKET_VERSION
step_id: str = Field(default_factory=lambda: new_pmflow_agent_id("step"), max_length=80)
index: int = Field(default=0, ge=0)
kind: str = Field(default="runtime_step", max_length=80)
status: str = Field(default="completed", max_length=32)
label: str = Field(default="", max_length=180)
summary: str = Field(default="", max_length=600)
tool_name: str | None = Field(default=None, max_length=120)
duration_ms: int | None = Field(default=None, ge=0)
@field_validator("step_id", "kind", "status", "label", "summary", "tool_name", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str | None:
if value is None:
return None
return clean_text(value, limit=600)
class PMFlowAgentObservationPacket(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = PMFLOW_AGENT_OBSERVATION_PACKET_VERSION
observation_id: str = Field(default_factory=lambda: new_pmflow_agent_id("obs"), max_length=80)
step_id: str | None = Field(default=None, max_length=80)
source_kind: str = Field(default="agent_runtime", max_length=80)
summary: str = Field(default="", max_length=800)
confidence: str = Field(default="unknown", max_length=32)
facts: list[dict[str, Any]] = Field(default_factory=list)
gaps: list[dict[str, Any]] = Field(default_factory=list)
@field_validator("observation_id", "step_id", "source_kind", "summary", "confidence", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str | None:
if value is None:
return None
return clean_text(value, limit=800)
class PMFlowAgentArtifactPacket(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = PMFLOW_AGENT_ARTIFACT_PACKET_VERSION
artifact_id: str = Field(default_factory=lambda: new_pmflow_agent_id("art"), max_length=80)
kind: str = Field(default="answer", max_length=80)
status: str = Field(default="created", max_length=32)
title: str = Field(default="", max_length=240)
summary: str = Field(default="", max_length=1000)
payload: dict[str, Any] = Field(default_factory=dict)
@field_validator("artifact_id", "kind", "status", "title", "summary", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=1000)
class PMFlowAgentVerificationPacket(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = PMFLOW_AGENT_VERIFICATION_PACKET_VERSION
verification_id: str = Field(default_factory=lambda: new_pmflow_agent_id("ver"), max_length=80)
target_ref: str = Field(default="final_answer", max_length=160)
result: str = Field(default="unknown", max_length=32)
retry_would_change_state: bool = False
findings: list[dict[str, Any]] = Field(default_factory=list)
@field_validator("verification_id", "target_ref", "result", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=160)
class PMFlowAgentMemoryPacket(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = PMFLOW_AGENT_MEMORY_PACKET_VERSION
memory_id: str = Field(default_factory=lambda: new_pmflow_agent_id("mem"), max_length=80)
memory_kind: str = Field(default="workspace_snapshot", max_length=80)
scope_ref: str | None = Field(default=None, max_length=160)
outcome_signal: str | None = Field(default=None, max_length=80)
summary: str = Field(default="", max_length=800)
@field_validator("memory_id", "memory_kind", "scope_ref", "outcome_signal", "summary", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str | None:
if value is None:
return None
return clean_text(value, limit=800)
class PMFlowAgentRunPacket(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = PMFLOW_AGENT_RUN_PACKET_VERSION
visibility: str = "customer"
run_id: str = Field(default_factory=lambda: new_pmflow_agent_id("run"), max_length=80)
status: str = Field(default="active", max_length=32)
goal: str = Field(default="", max_length=1200)
surface: str = Field(default="space", max_length=80)
presentation_mode: str = Field(default="pmflow_ai", max_length=40)
steps: list[PMFlowAgentStepPacket] = Field(default_factory=list)
observations: list[PMFlowAgentObservationPacket] = Field(default_factory=list)
artifacts: list[PMFlowAgentArtifactPacket] = Field(default_factory=list)
verifications: list[PMFlowAgentVerificationPacket] = Field(default_factory=list)
memories: list[PMFlowAgentMemoryPacket] = Field(default_factory=list)
specialist_assignments: list[dict[str, Any]] = Field(default_factory=list)
work_products: list[dict[str, Any]] = Field(default_factory=list)
agent_council_review: dict[str, Any] = Field(default_factory=dict)
operating_plan: dict[str, Any] = Field(default_factory=dict)
autonomy_gate: dict[str, Any] = Field(default_factory=dict)
operating_cycle: dict[str, Any] = Field(default_factory=dict)
consumed_learning_ledger: dict[str, Any] = Field(default_factory=dict)
outcome_observations: list[dict[str, Any]] = Field(default_factory=list)
learning_ledger: dict[str, Any] = Field(default_factory=dict)
learning_updates: list[dict[str, Any]] = Field(default_factory=list)
next_cycle_triggers: list[dict[str, Any]] = Field(default_factory=list)
prepared_action_packets: list[dict[str, Any]] = Field(default_factory=list)
confirmation_requests: list[dict[str, Any]] = Field(default_factory=list)
execution_preparations: list[dict[str, Any]] = Field(default_factory=list)
command_payload: dict[str, Any] = Field(default_factory=dict)
created_at: str = Field(default_factory=utc_now_iso)
completed_at: str | None = None
@field_validator("run_id", "status", "goal", "surface", "presentation_mode", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=1200)
def customer_safe_pmflow_agent_run_packet(value: PMFlowAgentRunPacket | dict[str, Any]) -> dict[str, Any]:
packet = value if isinstance(value, PMFlowAgentRunPacket) else PMFlowAgentRunPacket.model_validate(value)
return packet.model_dump(mode="json", exclude_none=True)
__all__ = [
"PMFlowAgentArtifactPacket",
"PMFlowAgentMemoryPacket",
"PMFlowAgentObservationPacket",
"PMFlowAgentRunPacket",
"PMFlowAgentStepPacket",
"PMFlowAgentVerificationPacket",
"customer_safe_pmflow_agent_run_packet",
"new_pmflow_agent_id",
"utc_now_iso",
]