PMFlow/backend/app/schemas/pmflow_ai_operating_runtime.py

980 lines
42 KiB
Python

"""PMFlow AI multi-agent operating runtime contracts.
These contracts are the Step 1 kernel for moving Space PMFlow AI from a
single answer-oriented agent run to a mission-oriented operating run.
"""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any, Literal
from uuid import uuid4
from pydantic import BaseModel, ConfigDict, Field, field_validator
OPERATING_MISSION_VERSION = "pmflow_ai_operating_mission.v1"
OPERATING_RUN_VERSION = "pmflow_ai_operating_run.v1"
AGENT_ASSIGNMENT_VERSION = "pmflow_ai_agent_assignment.v1"
AGENT_WORK_PRODUCT_VERSION = "pmflow_ai_agent_work_product.v1"
OPERATING_COMMAND_PAYLOAD_VERSION = "pmflow_ai_operating_command_payload.v1"
AGENT_CAPABILITY_CONTRACT_VERSION = "pmflow_ai_agent_capability_contract.v1"
PRODUCT_OBJECT_PROJECTION_VERSION = "pmflow_ai_product_object_projection.v1"
PROJECT_WORLD_STATE_VERSION = "pmflow_ai_project_world_state.v1"
PROJECT_WORLD_FACET_VERSION = "pmflow_ai_project_world_facet.v1"
EVIDENCE_LEDGER_ENTRY_VERSION = "pmflow_ai_evidence_ledger_entry.v1"
CONFLICT_LEDGER_ENTRY_VERSION = "pmflow_ai_conflict_ledger_entry.v1"
AGENT_COUNCIL_REVIEW_VERSION = "pmflow_ai_agent_council_review.v1"
AUTONOMY_GATE_VERSION = "pmflow_ai_autonomy_gate.v1"
PREPARED_ACTION_PACKET_VERSION = "pmflow_ai_prepared_action_packet.v1"
CONFIRMATION_REQUEST_VERSION = "pmflow_ai_confirmation_request.v1"
EXECUTION_PREPARATION_VERSION = "pmflow_ai_execution_preparation.v1"
OPERATING_CYCLE_TRIGGER_VERSION = "pmflow_ai_operating_cycle_trigger.v1"
OUTCOME_OBSERVATION_VERSION = "pmflow_ai_outcome_observation.v1"
LEARNING_LEDGER_VERSION = "pmflow_ai_learning_ledger.v1"
OPERATING_CYCLE_STATE_VERSION = "pmflow_ai_operating_cycle_state.v1"
OPERATING_MISSION_CRITIC_VERSION = "pmflow_ai_operating_mission_critic.v1"
CUSTOMER_SAFE_CLAIM_LEDGER_VERSION = "pmflow_ai_customer_safe_claim_ledger.v1"
CUSTOMER_SAFE_CLAIM_VERSION = "pmflow_ai_customer_safe_claim.v1"
def new_operating_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 PMFlowAIOperatingScope(BaseModel):
model_config = ConfigDict(extra="ignore")
tenant_id: int | None = None
user_id: int | None = None
session_id: int | None = None
object_refs: list[str] = Field(default_factory=list)
current_project_id: int | None = None
time_horizon: str = Field(default="current_operating_cycle", max_length=120)
surface: str = Field(default="space", max_length=120)
@field_validator("object_refs", mode="before")
@classmethod
def _normalize_object_refs(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
out: list[str] = []
for item in rows:
text = clean_text(item, limit=160)
if text and text not in out:
out.append(text)
return out[:12]
@field_validator("time_horizon", "surface", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=120)
class PMFlowAIOperatingMission(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = OPERATING_MISSION_VERSION
mission_id: str = Field(default_factory=lambda: new_operating_id("omis"), max_length=80)
mission_goal: str = Field(default="", max_length=1200)
stage: Literal[
"idea",
"research",
"positioning",
"mvp",
"requirement",
"changeset",
"project_memory",
"work_graph",
"execution_bridge",
"qa",
"release",
"growth",
"revenue_learning",
] = "research"
desired_outcome: str = Field(default="", max_length=1200)
desired_business_outcome: str = Field(default="", max_length=1200)
original_user_input: str = Field(default="", max_length=1600)
project_scope: PMFlowAIOperatingScope = Field(default_factory=PMFlowAIOperatingScope)
target_objects: list[dict[str, Any]] = Field(default_factory=list)
deliverable_type: Literal[
"idea_seed",
"research_note",
"opportunity_brief",
"product_hypothesis",
"mvp_option",
"requirement_draft",
"changeset_draft",
"task_draft",
"execution_package",
"qa_plan",
"release_signal",
"growth_experiment",
"revenue_signal",
"operating_decision",
] = "opportunity_brief"
action_ladder_level: Literal[
"answer",
"diagnose",
"recommend",
"prepare_object",
"prepare_execution",
"execute_after_confirmation",
] = "prepare_object"
success_criteria: list[str] = Field(default_factory=list)
autonomy_level: Literal[
"answer_only",
"read_and_diagnose",
"propose_plan",
"prepare_changes",
"execute_after_confirmation",
"autonomous_cycle_within_policy",
] = "prepare_changes"
decision_boundary: list[str] = Field(default_factory=list)
blocked_boundaries: list[str] = Field(default_factory=list)
required_agents: list[str] = Field(default_factory=list)
next_cycle_policy: dict[str, Any] = Field(default_factory=dict)
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("mission_id", "mission_goal", "desired_outcome", "desired_business_outcome", "original_user_input", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=1600)
@field_validator("success_criteria", "decision_boundary", "blocked_boundaries", "required_agents", mode="before")
@classmethod
def _normalize_list(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
out: list[str] = []
for item in rows:
text = clean_text(item, limit=260)
if text and text not in out:
out.append(text)
return out[:16]
@field_validator("target_objects", mode="before")
@classmethod
def _normalize_target_objects(cls, value: Any) -> list[dict[str, Any]]:
rows = value if isinstance(value, list) else []
out: list[dict[str, Any]] = []
for item in rows[:12]:
if not isinstance(item, dict):
continue
object_ref = clean_text(item.get("object_ref") or item.get("ref") or "", limit=160)
title = clean_text(item.get("title") or item.get("display_name") or object_ref, limit=240)
object_type = clean_text(item.get("object_type") or item.get("type") or (object_ref.split(":", 1)[0] if ":" in object_ref else ""), limit=80)
if object_ref or title:
out.append(
{
"object_ref": object_ref,
"object_type": object_type or "business_object",
"title": title,
"binding_strength": clean_text(item.get("binding_strength") or "mission_scope", limit=80),
}
)
return out
class PMFlowAIAgentCapabilityContract(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = AGENT_CAPABILITY_CONTRACT_VERSION
agent_id: str = Field(..., min_length=1, max_length=80)
display_name: str = Field(default="", max_length=120)
responsibility: str = Field(default="", max_length=500)
input_contract: list[str] = Field(default_factory=list)
output_contract: list[str] = Field(default_factory=list)
allowed_tools: list[str] = Field(default_factory=list)
write_permissions: list[str] = Field(default_factory=list)
autonomy_limit: str = Field(default="propose_plan", max_length=120)
critic_checks: list[str] = Field(default_factory=list)
customer_safe_projection: dict[str, Any] = Field(default_factory=dict)
@field_validator("agent_id", "display_name", "responsibility", "autonomy_limit", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=500)
@field_validator("input_contract", "output_contract", "allowed_tools", "write_permissions", "critic_checks", mode="before")
@classmethod
def _normalize_list(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
out: list[str] = []
for item in rows:
text = clean_text(item, limit=260)
if text and text not in out:
out.append(text)
return out[:24]
class PMFlowAIAgentAssignment(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = AGENT_ASSIGNMENT_VERSION
assignment_id: str = Field(default_factory=lambda: new_operating_id("aasn"), max_length=80)
agent_id: str = Field(default="", max_length=80)
role_goal: str = Field(default="", max_length=1000)
input_refs: list[str] = Field(default_factory=list)
expected_work_product: str = Field(default="", max_length=120)
status: Literal["assigned", "completed", "blocked", "skipped"] = "assigned"
autonomy_limit: str = Field(default="propose_plan", max_length=120)
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("assignment_id", "agent_id", "role_goal", "expected_work_product", "autonomy_limit", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=1000)
@field_validator("input_refs", mode="before")
@classmethod
def _normalize_refs(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
return [clean_text(item, limit=160) for item in rows[:16] if clean_text(item, limit=160)]
class PMFlowAIOperatingMissionCriticResult(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = OPERATING_MISSION_CRITIC_VERSION
critic_id: str = Field(default_factory=lambda: new_operating_id("omcr"), max_length=80)
mission_ok: bool = True
missing_capability_roles: list[str] = Field(default_factory=list)
over_selected_roles: list[str] = Field(default_factory=list)
needs_replan: bool = False
reason: str = Field(default="", max_length=1200)
evidence_alignment: dict[str, Any] = Field(default_factory=dict)
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("critic_id", "reason", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=1200)
@field_validator("missing_capability_roles", "over_selected_roles", mode="before")
@classmethod
def _normalize_roles(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
out: list[str] = []
for item in rows[:16]:
text = clean_text(item, limit=100)
if text and text not in out:
out.append(text)
return out
class PMFlowAIAgentWorkProduct(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = AGENT_WORK_PRODUCT_VERSION
work_product_id: str = Field(default_factory=lambda: new_operating_id("awp"), max_length=80)
assignment_id: str = Field(default="", max_length=80)
agent_id: str = Field(default="", max_length=80)
product_type: str = Field(default="", max_length=120)
status: Literal["draft", "ready", "blocked", "needs_followup"] = "draft"
summary: str = Field(default="", max_length=1200)
structured_output: dict[str, Any] = Field(default_factory=dict)
claims: list[dict[str, Any]] = Field(default_factory=list)
recommendations: list[dict[str, Any]] = Field(default_factory=list)
prepared_actions: list[dict[str, Any]] = Field(default_factory=list)
blocked_actions: list[dict[str, Any]] = Field(default_factory=list)
evidence_refs: list[str] = Field(default_factory=list)
verification: dict[str, Any] = Field(default_factory=dict)
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("work_product_id", "assignment_id", "agent_id", "product_type", "summary", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=1200)
@field_validator("evidence_refs", mode="before")
@classmethod
def _normalize_refs(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
return [clean_text(item, limit=160) for item in rows[:24] if clean_text(item, limit=160)]
class PMFlowAIProductObjectProjection(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = PRODUCT_OBJECT_PROJECTION_VERSION
projection_id: str = Field(default_factory=lambda: new_operating_id("pobj"), max_length=80)
object_type: Literal[
"IdeaSeed",
"ResearchNote",
"OpportunityBrief",
"ProductHypothesis",
"MVPOption",
"RequirementDraft",
"ChangeSetDraft",
"TaskDraft",
"ExecutionPackage",
"QAPlan",
"ReleaseSignal",
"GrowthExperiment",
"RevenueSignal",
] = "OpportunityBrief"
title: str = Field(default="", max_length=240)
summary: str = Field(default="", max_length=1200)
source_agents: list[str] = Field(default_factory=list)
target_refs: list[str] = Field(default_factory=list)
evidence_refs: list[str] = Field(default_factory=list)
fields: dict[str, Any] = Field(default_factory=dict)
action_ladder_level: str = Field(default="prepare_object", max_length=80)
confirmation_required: bool = True
status: Literal["draft", "ready_for_review", "blocked"] = "draft"
blocked_boundaries: list[str] = Field(default_factory=list)
next_cycle_policy: dict[str, Any] = Field(default_factory=dict)
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("projection_id", "title", "summary", "action_ladder_level", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=1200)
@field_validator("source_agents", "target_refs", "evidence_refs", "blocked_boundaries", mode="before")
@classmethod
def _normalize_text_list(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
out: list[str] = []
for item in rows[:24]:
text = clean_text(item, limit=220)
if text and text not in out:
out.append(text)
return out
class PMFlowAIProjectWorldFacet(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = PROJECT_WORLD_FACET_VERSION
facet_key: str = Field(default="", max_length=100)
display_label: str = Field(default="", max_length=160)
status: Literal["ready", "partial", "missing", "unavailable"] = "missing"
summary: str = Field(default="", max_length=1000)
facts: list[dict[str, Any]] = Field(default_factory=list)
evidence_refs: list[str] = Field(default_factory=list)
source_refs: list[dict[str, Any]] = Field(default_factory=list)
gaps: list[str] = Field(default_factory=list)
@field_validator("facet_key", "display_label", "summary", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=1000)
@field_validator("evidence_refs", "gaps", mode="before")
@classmethod
def _normalize_refs(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
return [clean_text(item, limit=180) for item in rows[:24] if clean_text(item, limit=180)]
class PMFlowAIEvidenceLedgerEntry(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = EVIDENCE_LEDGER_ENTRY_VERSION
evidence_id: str = Field(default_factory=lambda: new_operating_id("evd"), max_length=80)
project_ref: str = Field(default="", max_length=160)
facet_key: str = Field(default="", max_length=100)
source_ref: str = Field(default="", max_length=200)
summary: str = Field(default="", max_length=800)
confidence: Literal["high", "medium", "low", "unknown"] = "unknown"
captured_at: str = Field(default_factory=utc_now_iso)
@field_validator("evidence_id", "project_ref", "facet_key", "source_ref", "summary", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=800)
class PMFlowAICustomerSafeClaim(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = CUSTOMER_SAFE_CLAIM_VERSION
claim_id: str = Field(default_factory=lambda: new_operating_id("csc"), max_length=80)
claim_text: str = Field(default="", max_length=900)
source_kind: str = Field(default="", max_length=100)
source_label: str = Field(default="", max_length=160)
source_ref: str = Field(default="", max_length=180)
confidence: Literal["high", "medium", "low", "unknown"] = "unknown"
evidence_refs: list[str] = Field(default_factory=list)
object_refs: list[str] = Field(default_factory=list)
allowed_to_state: bool = True
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("claim_id", "claim_text", "source_kind", "source_label", "source_ref", "confidence", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=900)
@field_validator("evidence_refs", "object_refs", mode="before")
@classmethod
def _normalize_refs(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
out: list[str] = []
for item in rows[:16]:
text = clean_text(item, limit=180)
if text and text not in out:
out.append(text)
return out
class PMFlowAICustomerSafeClaimLedger(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = CUSTOMER_SAFE_CLAIM_LEDGER_VERSION
visibility: Literal["customer"] = "customer"
ledger_id: str = Field(default_factory=lambda: new_operating_id("cscl"), max_length=80)
mission_id: str = Field(default="", max_length=80)
status: Literal["ready", "partial", "empty", "blocked"] = "empty"
claims: list[PMFlowAICustomerSafeClaim] = Field(default_factory=list)
claim_count: int = 0
source_counts: dict[str, int] = Field(default_factory=dict)
source_refs: list[dict[str, Any]] = Field(default_factory=list)
evidence_refs: list[str] = Field(default_factory=list)
blocked_reason: str = Field(default="", max_length=500)
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("ledger_id", "mission_id", "status", "blocked_reason", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=500)
@field_validator("evidence_refs", mode="before")
@classmethod
def _normalize_refs(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
out: list[str] = []
for item in rows[:30]:
text = clean_text(item, limit=180)
if text and text not in out:
out.append(text)
return out
class PMFlowAIConflictLedgerEntry(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = CONFLICT_LEDGER_ENTRY_VERSION
conflict_id: str = Field(default_factory=lambda: new_operating_id("cflt"), max_length=80)
conflict_type: str = Field(default="", max_length=120)
involved_agents: list[str] = Field(default_factory=list)
summary: str = Field(default="", max_length=800)
resolution: Literal["accepted", "resolved", "blocked"] = "resolved"
resolution_reason: str = Field(default="", max_length=800)
severity: Literal["low", "medium", "high"] = "medium"
source_refs: list[str] = Field(default_factory=list)
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("conflict_id", "conflict_type", "summary", "resolution_reason", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=800)
@field_validator("involved_agents", "source_refs", mode="before")
@classmethod
def _normalize_refs(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
out: list[str] = []
for item in rows[:16]:
text = clean_text(item, limit=160)
if text and text not in out:
out.append(text)
return out
class PMFlowAIAgentCouncilReview(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = AGENT_COUNCIL_REVIEW_VERSION
visibility: Literal["customer"] = "customer"
review_id: str = Field(default_factory=lambda: new_operating_id("acrv"), max_length=80)
status: Literal["passed", "passed_with_conflicts", "blocked"] = "passed"
role_reviews: list[dict[str, Any]] = Field(default_factory=list)
conflict_ledger: list[PMFlowAIConflictLedgerEntry] = Field(default_factory=list)
resolved_conflicts: list[dict[str, Any]] = Field(default_factory=list)
risk_vetoes: list[dict[str, Any]] = Field(default_factory=list)
priority_order: list[dict[str, Any]] = Field(default_factory=list)
resolution_summary: str = Field(default="", max_length=1000)
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("review_id", "status", "resolution_summary", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=1000)
class PMFlowAIAutonomyDecision(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = AUTONOMY_GATE_VERSION
decision_id: str = Field(default_factory=lambda: new_operating_id("adec"), max_length=80)
status: Literal["suggest_only", "prepared_waiting_confirmation", "blocked"] = "suggest_only"
autonomy_level: str = Field(default="", max_length=120)
safe_suggestions: 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)
blocked_actions: list[dict[str, Any]] = Field(default_factory=list)
policy_findings: list[str] = Field(default_factory=list)
prohibited_side_effects: list[str] = Field(default_factory=list)
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("decision_id", "status", "autonomy_level", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=160)
@field_validator("policy_findings", "prohibited_side_effects", mode="before")
@classmethod
def _normalize_text_list(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
out: list[str] = []
for item in rows[:16]:
text = clean_text(item, limit=260)
if text and text not in out:
out.append(text)
return out
class PMFlowAIPreparedActionPacket(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = PREPARED_ACTION_PACKET_VERSION
prepared_action_id: str = Field(default_factory=lambda: new_operating_id("pact"), max_length=80)
action_family: Literal[
"task_change",
"experiment_plan",
"project_memory_candidate",
"product_object_projection",
"research_recovery",
"execution_handoff_package",
] = "task_change"
title: str = Field(default="", max_length=240)
target_ref: str = Field(default="", max_length=160)
source_agent: str = Field(default="", max_length=80)
source_action_type: str = Field(default="", max_length=120)
payload: dict[str, Any] = Field(default_factory=dict)
evidence_refs: list[str] = Field(default_factory=list)
confirmation_required: bool = True
confirmation_request_id: str | None = Field(default=None, max_length=80)
status: Literal["prepared_waiting_confirmation", "blocked_by_policy"] = "prepared_waiting_confirmation"
prohibited_side_effects: list[str] = Field(default_factory=list)
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("prepared_action_id", "title", "target_ref", "source_agent", "source_action_type", "confirmation_request_id", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str | None:
if value is None:
return None
return clean_text(value, limit=240)
@field_validator("evidence_refs", "prohibited_side_effects", mode="before")
@classmethod
def _normalize_text_list(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
out: list[str] = []
for item in rows[:16]:
text = clean_text(item, limit=220)
if text and text not in out:
out.append(text)
return out
class PMFlowAIConfirmationRequest(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = CONFIRMATION_REQUEST_VERSION
confirmation_request_id: str = Field(default_factory=lambda: new_operating_id("creq"), max_length=80)
title: str = Field(default="", max_length=240)
target_ref: str = Field(default="", max_length=160)
requested_for: list[str] = Field(default_factory=list)
confirmation_reason: str = Field(default="", max_length=600)
required_before: list[str] = Field(default_factory=list)
action_label: str = Field(default="", max_length=120)
action_type: str = Field(default="", max_length=120)
action_payload: dict[str, Any] = Field(default_factory=dict)
can_execute_after_confirmation: bool = False
status: Literal["waiting_for_user_confirmation", "blocked"] = "waiting_for_user_confirmation"
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("confirmation_request_id", "title", "target_ref", "confirmation_reason", "action_label", "action_type", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=600)
@field_validator("requested_for", "required_before", mode="before")
@classmethod
def _normalize_text_list(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
out: list[str] = []
for item in rows[:16]:
text = clean_text(item, limit=220)
if text and text not in out:
out.append(text)
return out
class PMFlowAIConfirmationExecutionRequest(BaseModel):
"""Customer confirmation execution request.
The server always resolves the real action contract from the persisted
assistant message. Client payload is only used as an idempotency/audit hint.
"""
action_payload: dict[str, Any] = Field(default_factory=dict)
supplement_text: str = Field(default="", max_length=2000)
client_message_id: str = Field(default="", max_length=80)
@field_validator("supplement_text", "client_message_id", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=2000)
class PMFlowAIExecutionPreparation(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = EXECUTION_PREPARATION_VERSION
execution_preparation_id: str = Field(default_factory=lambda: new_operating_id("eprep"), max_length=80)
target_ref: str = Field(default="", max_length=160)
readiness: Literal["prepared_waiting_confirmation", "blocked_by_missing_context"] = "prepared_waiting_confirmation"
handoff_package: dict[str, Any] = Field(default_factory=dict)
required_confirmations: list[str] = Field(default_factory=list)
blocked_actions: list[dict[str, Any]] = Field(default_factory=list)
prohibited_side_effects: list[str] = Field(default_factory=list)
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("execution_preparation_id", "target_ref", "readiness", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=160)
@field_validator("required_confirmations", "prohibited_side_effects", mode="before")
@classmethod
def _normalize_text_list(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
out: list[str] = []
for item in rows[:16]:
text = clean_text(item, limit=220)
if text and text not in out:
out.append(text)
return out
class PMFlowAIOperatingCycleTrigger(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = OPERATING_CYCLE_TRIGGER_VERSION
trigger_id: str = Field(default_factory=lambda: new_operating_id("otrg"), max_length=80)
trigger_type: Literal[
"user_turn",
"signal",
"project_creation",
"task_update",
"execution_result",
"daily_review",
"weekly_strategy_review",
] = "user_turn"
source_ref: str = Field(default="", max_length=180)
project_ref: str = Field(default="", max_length=160)
payload: dict[str, Any] = Field(default_factory=dict)
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("trigger_id", "trigger_type", "source_ref", "project_ref", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=180)
class PMFlowAIOutcomeObservation(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = OUTCOME_OBSERVATION_VERSION
observation_id: str = Field(default_factory=lambda: new_operating_id("oobs"), max_length=80)
outcome_type: Literal[
"recommendation_decision",
"execution_result",
"experiment_result",
"user_preference",
] = "recommendation_decision"
outcome_status: Literal[
"pending",
"accepted",
"rejected",
"succeeded",
"failed",
"partial",
"observed",
] = "pending"
source_ref: str = Field(default="", max_length=180)
target_ref: str = Field(default="", max_length=160)
summary: str = Field(default="", max_length=800)
evidence_refs: list[str] = Field(default_factory=list)
learning_signals: dict[str, Any] = Field(default_factory=dict)
observed_at: str = Field(default_factory=utc_now_iso)
@field_validator("observation_id", "outcome_type", "outcome_status", "source_ref", "target_ref", "summary", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=800)
@field_validator("evidence_refs", mode="before")
@classmethod
def _normalize_text_list(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
out: list[str] = []
for item in rows[:16]:
text = clean_text(item, limit=220)
if text and text not in out:
out.append(text)
return out
class PMFlowAILearningLedger(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = LEARNING_LEDGER_VERSION
ledger_id: str = Field(default_factory=lambda: new_operating_id("lgr"), max_length=80)
scope_ref: str = Field(default="", max_length=160)
learning_items: list[dict[str, Any]] = Field(default_factory=list)
decision_outcomes: list[dict[str, Any]] = Field(default_factory=list)
experiment_results: list[dict[str, Any]] = Field(default_factory=list)
execution_outcomes: list[dict[str, Any]] = Field(default_factory=list)
user_preferences: list[dict[str, Any]] = Field(default_factory=list)
consumed_memory_refs: list[str] = Field(default_factory=list)
updated_at: str = Field(default_factory=utc_now_iso)
@field_validator("ledger_id", "scope_ref", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=160)
@field_validator("consumed_memory_refs", mode="before")
@classmethod
def _normalize_text_list(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
out: list[str] = []
for item in rows[:24]:
text = clean_text(item, limit=220)
if text and text not in out:
out.append(text)
return out
class PMFlowAIOperatingCycleState(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = OPERATING_CYCLE_STATE_VERSION
cycle_id: str = Field(default_factory=lambda: new_operating_id("ocyc"), max_length=80)
trigger: PMFlowAIOperatingCycleTrigger
supported_triggers: list[dict[str, Any]] = Field(default_factory=list)
consumed_learning_ledger: PMFlowAILearningLedger = Field(default_factory=PMFlowAILearningLedger)
outcome_observations: list[PMFlowAIOutcomeObservation] = Field(default_factory=list)
learning_ledger: PMFlowAILearningLedger = Field(default_factory=PMFlowAILearningLedger)
next_cycle_triggers: list[dict[str, Any]] = Field(default_factory=list)
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("cycle_id", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=80)
class PMFlowAIProjectWorldStatePacket(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = PROJECT_WORLD_STATE_VERSION
visibility: Literal["customer"] = "customer"
world_state_id: str = Field(default_factory=lambda: new_operating_id("pws"), max_length=80)
primary_project_ref: str = Field(default="", max_length=160)
project_refs: list[str] = Field(default_factory=list)
status: Literal["ready", "partial", "missing_scope", "unavailable"] = "missing_scope"
facets: list[PMFlowAIProjectWorldFacet] = Field(default_factory=list)
project_operating_evidence_packets: list[dict[str, Any]] = Field(default_factory=list)
evidence_ledger: list[PMFlowAIEvidenceLedgerEntry] = Field(default_factory=list)
customer_safe_claim_ledger: PMFlowAICustomerSafeClaimLedger | None = None
coverage: dict[str, Any] = Field(default_factory=dict)
remaining_gaps: list[str] = Field(default_factory=list)
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("world_state_id", "primary_project_ref", "status", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=160)
@field_validator("project_refs", "remaining_gaps", mode="before")
@classmethod
def _normalize_refs(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
out: list[str] = []
for item in rows[:30]:
text = clean_text(item, limit=180)
if text and text not in out:
out.append(text)
return out
class PMFlowAIOperatingRunEvent(BaseModel):
model_config = ConfigDict(extra="ignore")
event_id: str = Field(default_factory=lambda: new_operating_id("oevt"), max_length=80)
event_type: str = Field(default="", max_length=120)
summary: str = Field(default="", max_length=1000)
agent_id: str | None = Field(default=None, max_length=80)
assignment_id: str | None = Field(default=None, max_length=80)
created_at: str = Field(default_factory=utc_now_iso)
@field_validator("event_id", "event_type", "summary", "agent_id", "assignment_id", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str | None:
if value is None:
return None
return clean_text(value, limit=1000)
class PMFlowAIOperatingPlan(BaseModel):
model_config = ConfigDict(extra="ignore")
strategic_read: str = Field(default="", max_length=1200)
highest_leverage_move: str = Field(default="", max_length=1200)
next_cycle_plan: list[dict[str, Any]] = Field(default_factory=list)
safe_suggestions: list[dict[str, Any]] = Field(default_factory=list)
prepared_changes: list[dict[str, Any]] = Field(default_factory=list)
requires_confirmation: list[dict[str, Any]] = Field(default_factory=list)
blocked_actions: list[dict[str, Any]] = Field(default_factory=list)
evidence_boundary: list[str] = Field(default_factory=list)
council_review: dict[str, Any] = Field(default_factory=dict)
conflict_ledger: list[dict[str, Any]] = Field(default_factory=list)
resolved_conflicts: list[dict[str, Any]] = Field(default_factory=list)
risk_vetoes: list[dict[str, Any]] = Field(default_factory=list)
priority_order: list[dict[str, Any]] = Field(default_factory=list)
autonomy_gate: dict[str, Any] = Field(default_factory=dict)
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)
product_object_projections: list[dict[str, Any]] = Field(default_factory=list)
operating_cycle: dict[str, Any] = Field(default_factory=dict)
consumed_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)
@field_validator("strategic_read", "highest_leverage_move", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=1200)
@field_validator("evidence_boundary", mode="before")
@classmethod
def _normalize_boundary(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
return [clean_text(item, limit=360) for item in rows[:12] if clean_text(item, limit=360)]
class PMFlowAIOperatingRunPacket(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = OPERATING_RUN_VERSION
visibility: Literal["customer"] = "customer"
run_id: str = Field(default_factory=lambda: new_operating_id("orun"), max_length=80)
mission: PMFlowAIOperatingMission
status: Literal["active", "completed", "blocked", "waiting_for_confirmation"] = "active"
assignments: list[PMFlowAIAgentAssignment] = Field(default_factory=list)
project_world_state: PMFlowAIProjectWorldStatePacket | None = None
customer_safe_claim_ledger: PMFlowAICustomerSafeClaimLedger | None = None
work_products: list[PMFlowAIAgentWorkProduct] = Field(default_factory=list)
agent_council_review: PMFlowAIAgentCouncilReview | None = None
autonomy_gate: PMFlowAIAutonomyDecision | None = None
operating_cycle: PMFlowAIOperatingCycleState | None = None
product_object_projections: list[PMFlowAIProductObjectProjection] = Field(default_factory=list)
operating_plan: PMFlowAIOperatingPlan = Field(default_factory=PMFlowAIOperatingPlan)
events: list[PMFlowAIOperatingRunEvent] = Field(default_factory=list)
delivery_ref: 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", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=120)
class PMFlowAIOperatingCommandPayload(BaseModel):
model_config = ConfigDict(extra="ignore")
packet_version: str = OPERATING_COMMAND_PAYLOAD_VERSION
visibility: Literal["customer"] = "customer"
mission: dict[str, Any] = Field(default_factory=dict)
project_focus: dict[str, Any] = Field(default_factory=dict)
project_world_state: dict[str, Any] = Field(default_factory=dict)
agent_council: list[dict[str, Any]] = Field(default_factory=list)
agent_council_review: dict[str, Any] = Field(default_factory=dict)
conflict_ledger: list[dict[str, Any]] = Field(default_factory=list)
priority_order: list[dict[str, Any]] = Field(default_factory=list)
risk_vetoes: list[dict[str, Any]] = Field(default_factory=list)
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)
confirmation_requests: list[dict[str, Any]] = Field(default_factory=list)
execution_preparations: list[dict[str, Any]] = Field(default_factory=list)
product_object_projections: list[dict[str, Any]] = Field(default_factory=list)
strategic_read: str = Field(default="", max_length=1200)
operating_plan: dict[str, Any] = Field(default_factory=dict)
prepared_actions: list[dict[str, Any]] = Field(default_factory=list)
blocked_actions: list[dict[str, Any]] = Field(default_factory=list)
evidence_boundary: list[str] = Field(default_factory=list)
evidence_ledger: list[dict[str, Any]] = Field(default_factory=list)
customer_safe_claim_ledger: dict[str, Any] = Field(default_factory=dict)
learning_updates: list[dict[str, Any]] = Field(default_factory=list)
next_cycle: dict[str, Any] = Field(default_factory=dict)
@field_validator("strategic_read", mode="before")
@classmethod
def _normalize_text(cls, value: Any) -> str:
return clean_text(value, limit=1200)
@field_validator("evidence_boundary", mode="before")
@classmethod
def _normalize_boundary(cls, value: Any) -> list[str]:
rows = value if isinstance(value, list) else []
return [clean_text(item, limit=360) for item in rows[:12] if clean_text(item, limit=360)]
__all__ = [
"PMFlowAIAgentAssignment",
"PMFlowAIAgentCapabilityContract",
"PMFlowAIAgentCouncilReview",
"PMFlowAIAgentWorkProduct",
"PMFlowAIAutonomyDecision",
"PMFlowAICustomerSafeClaim",
"PMFlowAICustomerSafeClaimLedger",
"PMFlowAIConfirmationExecutionRequest",
"PMFlowAIConfirmationRequest",
"PMFlowAIConflictLedgerEntry",
"PMFlowAIEvidenceLedgerEntry",
"PMFlowAIExecutionPreparation",
"PMFlowAILearningLedger",
"PMFlowAIOperatingCycleState",
"PMFlowAIOperatingCycleTrigger",
"PMFlowAIOperatingCommandPayload",
"PMFlowAIOperatingMission",
"PMFlowAIOperatingPlan",
"PMFlowAIOperatingRunEvent",
"PMFlowAIOperatingRunPacket",
"PMFlowAIOperatingScope",
"PMFlowAIOutcomeObservation",
"PMFlowAIPreparedActionPacket",
"PMFlowAIProjectWorldFacet",
"PMFlowAIProjectWorldStatePacket",
"PMFlowAIProductObjectProjection",
"clean_text",
"new_operating_id",
"utc_now_iso",
]