877 lines
38 KiB
Python
877 lines
38 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"
|
|
|
|
|
|
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 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 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)
|
|
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
|
|
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)
|
|
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",
|
|
"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",
|
|
]
|