|
|
|
@@ -0,0 +1,567 @@
|
|
|
|
|
"""Contracts for project-scoped operational tasks and human-approved actions."""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from typing import Annotated, Literal
|
|
|
|
|
from uuid import UUID
|
|
|
|
|
|
|
|
|
|
from pydantic import Field, JsonValue, model_validator
|
|
|
|
|
|
|
|
|
|
from app.domain.schemas.shared import NonEmptyStr, PositiveInt, StrictModel
|
|
|
|
|
from app.domain.schemas.operational_data import TaskDataSnapshot
|
|
|
|
|
from app.domain.schemas.workbench_runtime import (
|
|
|
|
|
WorkbenchDateTime,
|
|
|
|
|
WorkbenchRuntimeWorkOrderSubtask,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TaskType = Literal["scheduled_flow", "external_work_order", "event_investigation"]
|
|
|
|
|
TaskStatus = Literal[
|
|
|
|
|
"queued", "running", "waiting_human", "completed", "failed", "cancelled"
|
|
|
|
|
]
|
|
|
|
|
TaskLifecycleStatus = Literal[
|
|
|
|
|
"open", "waiting_human", "completed", "failed", "cancelled"
|
|
|
|
|
]
|
|
|
|
|
OperationalTaskRunStatus = Literal[
|
|
|
|
|
"queued", "claimed", "running", "succeeded", "failed", "timed_out", "cancelled"
|
|
|
|
|
]
|
|
|
|
|
TaskPriority = Literal["normal", "urgent"]
|
|
|
|
|
TaskSourceKind = Literal["schedule", "external_system", "operator", "agent"]
|
|
|
|
|
TriggerKind = Literal["schedule", "external_sync", "event", "manual"]
|
|
|
|
|
EvidenceKind = Literal["system_data", "external_feedback", "human_input", "synthetic"]
|
|
|
|
|
PlanNodeStatus = Literal[
|
|
|
|
|
"pending", "waiting", "running", "completed", "failed", "skipped", "cancelled"
|
|
|
|
|
]
|
|
|
|
|
ActionKind = Literal["create_work_order", "issue_command"]
|
|
|
|
|
ApprovalStatus = Literal["pending", "approved", "rejected"]
|
|
|
|
|
ExecutionStatus = Literal[
|
|
|
|
|
"not_submitted", "submitted", "succeeded", "failed", "unsupported", "unknown"
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TaskSource(StrictModel):
|
|
|
|
|
kind: TaskSourceKind
|
|
|
|
|
external_id: NonEmptyStr | None = None
|
|
|
|
|
external_revision: PositiveInt | None = None
|
|
|
|
|
event_kind: NonEmptyStr | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TaskTrigger(StrictModel):
|
|
|
|
|
kind: TriggerKind
|
|
|
|
|
occurred_at: WorkbenchDateTime
|
|
|
|
|
detail: NonEmptyStr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OperationalTaskCreate(StrictModel):
|
|
|
|
|
request_id: UUID
|
|
|
|
|
task_type: TaskType
|
|
|
|
|
workflow: NonEmptyStr
|
|
|
|
|
title: NonEmptyStr
|
|
|
|
|
summary: NonEmptyStr
|
|
|
|
|
priority: TaskPriority = "normal"
|
|
|
|
|
source: TaskSource
|
|
|
|
|
trigger: TaskTrigger
|
|
|
|
|
scheduled_for: WorkbenchDateTime | None = None
|
|
|
|
|
agent_session_id: NonEmptyStr | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExecutionPlanNode(StrictModel):
|
|
|
|
|
id: NonEmptyStr
|
|
|
|
|
title: NonEmptyStr
|
|
|
|
|
owner: Literal["agent", "human"]
|
|
|
|
|
status: PlanNodeStatus
|
|
|
|
|
detail: NonEmptyStr
|
|
|
|
|
evidence_refs: list[NonEmptyStr] = Field(default_factory=list)
|
|
|
|
|
action_refs: list[UUID] = Field(default_factory=list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExecutionPlanEdge(StrictModel):
|
|
|
|
|
from_node: NonEmptyStr = Field(alias="from")
|
|
|
|
|
to_node: NonEmptyStr = Field(alias="to")
|
|
|
|
|
condition: NonEmptyStr | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExecutionPlanInput(StrictModel):
|
|
|
|
|
schema_name: Literal["execution-plan@1"] = Field(alias="schema")
|
|
|
|
|
title: NonEmptyStr
|
|
|
|
|
reason: NonEmptyStr
|
|
|
|
|
nodes: Annotated[list[ExecutionPlanNode], Field(min_length=1, max_length=100)]
|
|
|
|
|
edges: Annotated[list[ExecutionPlanEdge], Field(max_length=300)] = Field(
|
|
|
|
|
default_factory=list
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@model_validator(mode="after")
|
|
|
|
|
def validate_graph(self):
|
|
|
|
|
node_ids = [node.id for node in self.nodes]
|
|
|
|
|
if len(set(node_ids)) != len(node_ids):
|
|
|
|
|
raise ValueError("execution plan node ids must be unique")
|
|
|
|
|
known = set(node_ids)
|
|
|
|
|
graph = {node_id: [] for node_id in node_ids}
|
|
|
|
|
indegree = {node_id: 0 for node_id in node_ids}
|
|
|
|
|
seen_edges: set[tuple[str, str]] = set()
|
|
|
|
|
for edge in self.edges:
|
|
|
|
|
pair = (edge.from_node, edge.to_node)
|
|
|
|
|
if edge.from_node not in known or edge.to_node not in known:
|
|
|
|
|
raise ValueError("execution plan edges must reference known nodes")
|
|
|
|
|
if edge.from_node == edge.to_node or pair in seen_edges:
|
|
|
|
|
raise ValueError("execution plan edges must be unique and non-reflexive")
|
|
|
|
|
seen_edges.add(pair)
|
|
|
|
|
graph[edge.from_node].append(edge.to_node)
|
|
|
|
|
indegree[edge.to_node] += 1
|
|
|
|
|
pending = [node_id for node_id, value in indegree.items() if value == 0]
|
|
|
|
|
visited = 0
|
|
|
|
|
while pending:
|
|
|
|
|
current = pending.pop()
|
|
|
|
|
visited += 1
|
|
|
|
|
for target in graph[current]:
|
|
|
|
|
indegree[target] -= 1
|
|
|
|
|
if indegree[target] == 0:
|
|
|
|
|
pending.append(target)
|
|
|
|
|
if visited != len(node_ids):
|
|
|
|
|
raise ValueError("execution plan must be acyclic")
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EvidenceInput(StrictModel):
|
|
|
|
|
evidence_id: NonEmptyStr
|
|
|
|
|
kind: EvidenceKind
|
|
|
|
|
summary: NonEmptyStr
|
|
|
|
|
source_label: NonEmptyStr
|
|
|
|
|
source_ref: NonEmptyStr | None = None
|
|
|
|
|
observed_at: WorkbenchDateTime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EvidenceRecord(EvidenceInput):
|
|
|
|
|
recorded_at: WorkbenchDateTime
|
|
|
|
|
recorded_by: NonEmptyStr
|
|
|
|
|
has_snapshot: bool = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class StoredEvidenceRecord(EvidenceRecord):
|
|
|
|
|
data_snapshot: TaskDataSnapshot | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvestigationReportInput(StrictModel):
|
|
|
|
|
title: NonEmptyStr
|
|
|
|
|
current_conclusion: NonEmptyStr
|
|
|
|
|
conclusion_boundary: NonEmptyStr
|
|
|
|
|
evidence_refs: list[NonEmptyStr] = Field(default_factory=list)
|
|
|
|
|
recommendations: list[NonEmptyStr] = Field(default_factory=list)
|
|
|
|
|
pending_items: list[NonEmptyStr] = Field(default_factory=list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExecutionPlanSnapshot(StrictModel):
|
|
|
|
|
revision: PositiveInt
|
|
|
|
|
execution_generation: PositiveInt | None = None
|
|
|
|
|
source_session_id: NonEmptyStr
|
|
|
|
|
published_at: WorkbenchDateTime
|
|
|
|
|
published_by: NonEmptyStr
|
|
|
|
|
plan: ExecutionPlanInput
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvestigationReportRevision(StrictModel):
|
|
|
|
|
revision: PositiveInt
|
|
|
|
|
execution_generation: PositiveInt | None = None
|
|
|
|
|
source_session_id: NonEmptyStr
|
|
|
|
|
published_at: WorkbenchDateTime
|
|
|
|
|
published_by: NonEmptyStr
|
|
|
|
|
report: InvestigationReportInput
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AgentArtifactsCommand(StrictModel):
|
|
|
|
|
request_id: UUID
|
|
|
|
|
source_session_id: NonEmptyStr
|
|
|
|
|
run_id: UUID | None = None
|
|
|
|
|
evidence: list[EvidenceInput] = Field(default_factory=list, max_length=100)
|
|
|
|
|
plan: ExecutionPlanInput | None = None
|
|
|
|
|
report: InvestigationReportInput | None = None
|
|
|
|
|
|
|
|
|
|
@model_validator(mode="after")
|
|
|
|
|
def require_artifact(self):
|
|
|
|
|
if not self.evidence and self.plan is None and self.report is None:
|
|
|
|
|
raise ValueError("at least one artifact must be supplied")
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CreateWorkOrderPayload(StrictModel):
|
|
|
|
|
title: NonEmptyStr
|
|
|
|
|
description: NonEmptyStr
|
|
|
|
|
work_order_type: NonEmptyStr
|
|
|
|
|
priority: TaskPriority = "normal"
|
|
|
|
|
scheduled_at: WorkbenchDateTime | None = None
|
|
|
|
|
location: NonEmptyStr | None = None
|
|
|
|
|
assignee: NonEmptyStr | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CommandTarget(StrictModel):
|
|
|
|
|
kind: NonEmptyStr
|
|
|
|
|
id: NonEmptyStr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class IssueCommandPayload(StrictModel):
|
|
|
|
|
command_type: NonEmptyStr
|
|
|
|
|
target: CommandTarget
|
|
|
|
|
parameters: dict[str, JsonValue]
|
|
|
|
|
impact_summary: NonEmptyStr
|
|
|
|
|
preconditions: list[NonEmptyStr] = Field(min_length=1)
|
|
|
|
|
expires_at: WorkbenchDateTime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ActionPayload = CreateWorkOrderPayload | IssueCommandPayload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ActionProposalCommand(StrictModel):
|
|
|
|
|
request_id: UUID
|
|
|
|
|
kind: ActionKind
|
|
|
|
|
reason: NonEmptyStr
|
|
|
|
|
payload: ActionPayload
|
|
|
|
|
evidence_refs: list[NonEmptyStr] = Field(default_factory=list)
|
|
|
|
|
node_id: NonEmptyStr | None = None
|
|
|
|
|
|
|
|
|
|
@model_validator(mode="after")
|
|
|
|
|
def validate_payload_kind(self):
|
|
|
|
|
expected = CreateWorkOrderPayload if self.kind == "create_work_order" else IssueCommandPayload
|
|
|
|
|
if not isinstance(self.payload, expected):
|
|
|
|
|
raise ValueError("action payload does not match its kind")
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ActionDecisionCommand(StrictModel):
|
|
|
|
|
request_id: UUID
|
|
|
|
|
expected_version: PositiveInt
|
|
|
|
|
decision: Literal["approved", "rejected"]
|
|
|
|
|
comment: NonEmptyStr | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ActionExecuteCommand(StrictModel):
|
|
|
|
|
request_id: UUID
|
|
|
|
|
expected_version: PositiveInt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ActionReconcileCommand(StrictModel):
|
|
|
|
|
request_id: UUID
|
|
|
|
|
expected_version: PositiveInt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ActionReceipt(StrictModel):
|
|
|
|
|
adapter: NonEmptyStr
|
|
|
|
|
receipt_id: NonEmptyStr
|
|
|
|
|
submitted_at: WorkbenchDateTime
|
|
|
|
|
completed_at: WorkbenchDateTime | None = None
|
|
|
|
|
external_status: NonEmptyStr
|
|
|
|
|
detail: NonEmptyStr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HumanApprovalAction(StrictModel):
|
|
|
|
|
action_id: UUID
|
|
|
|
|
task_id: UUID
|
|
|
|
|
version: PositiveInt
|
|
|
|
|
kind: ActionKind
|
|
|
|
|
reason: NonEmptyStr
|
|
|
|
|
payload: ActionPayload
|
|
|
|
|
evidence_refs: list[NonEmptyStr]
|
|
|
|
|
node_id: NonEmptyStr | None = None
|
|
|
|
|
required_approval_permission: NonEmptyStr
|
|
|
|
|
required_execution_permission: NonEmptyStr
|
|
|
|
|
approval_status: ApprovalStatus
|
|
|
|
|
execution_status: ExecutionStatus
|
|
|
|
|
execution_request_id: UUID | None = None
|
|
|
|
|
proposed_by: NonEmptyStr
|
|
|
|
|
proposed_at: WorkbenchDateTime
|
|
|
|
|
decided_by: NonEmptyStr | None = None
|
|
|
|
|
decided_at: WorkbenchDateTime | None = None
|
|
|
|
|
decision_comment: NonEmptyStr | None = None
|
|
|
|
|
executed_by: NonEmptyStr | None = None
|
|
|
|
|
executed_at: WorkbenchDateTime | None = None
|
|
|
|
|
receipt: ActionReceipt | None = None
|
|
|
|
|
execution_detail: NonEmptyStr | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExternalWorkOrderSnapshot(StrictModel):
|
|
|
|
|
work_order_id: UUID
|
|
|
|
|
revision: PositiveInt
|
|
|
|
|
title: NonEmptyStr
|
|
|
|
|
summary: NonEmptyStr
|
|
|
|
|
status: Literal[
|
|
|
|
|
"draft", "open", "in_progress", "awaiting_reply", "completed", "cancelled"
|
|
|
|
|
]
|
|
|
|
|
priority: Literal["low", "normal", "high", "urgent"]
|
|
|
|
|
assignee: NonEmptyStr | None = None
|
|
|
|
|
scheduled_at: WorkbenchDateTime | None = None
|
|
|
|
|
work_order_type: Literal["general", "water_quality_complaint", "burst_report"] = "general"
|
|
|
|
|
location: NonEmptyStr | None = None
|
|
|
|
|
scenario_code: NonEmptyStr | None = None
|
|
|
|
|
subtasks: Annotated[
|
|
|
|
|
list[WorkbenchRuntimeWorkOrderSubtask], Field(min_length=1, max_length=50)
|
|
|
|
|
] | None = None
|
|
|
|
|
origin: Literal["synthetic"]
|
|
|
|
|
updated_at: WorkbenchDateTime
|
|
|
|
|
|
|
|
|
|
@model_validator(mode="after")
|
|
|
|
|
def unique_subtasks(self):
|
|
|
|
|
if self.subtasks is not None and len(
|
|
|
|
|
{item.id for item in self.subtasks}
|
|
|
|
|
) != len(self.subtasks):
|
|
|
|
|
raise ValueError("work order subtask ids must be unique")
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OperationalTaskRun(StrictModel):
|
|
|
|
|
run_id: UUID
|
|
|
|
|
task_id: UUID
|
|
|
|
|
generation: PositiveInt
|
|
|
|
|
execution_request_id: UUID
|
|
|
|
|
status: OperationalTaskRunStatus
|
|
|
|
|
revision: PositiveInt
|
|
|
|
|
agent_session_id: NonEmptyStr | None = None
|
|
|
|
|
lease_expires_at: WorkbenchDateTime | None = None
|
|
|
|
|
heartbeat_at: WorkbenchDateTime | None = None
|
|
|
|
|
started_at: WorkbenchDateTime | None = None
|
|
|
|
|
finished_at: WorkbenchDateTime | None = None
|
|
|
|
|
failure_code: NonEmptyStr | None = None
|
|
|
|
|
failure_detail: NonEmptyStr | None = None
|
|
|
|
|
dispatch_attempts: int = Field(default=0, ge=0)
|
|
|
|
|
next_dispatch_at: WorkbenchDateTime
|
|
|
|
|
created_at: WorkbenchDateTime
|
|
|
|
|
updated_at: WorkbenchDateTime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OperationalTaskSummary(StrictModel):
|
|
|
|
|
project_id: UUID
|
|
|
|
|
task_id: UUID
|
|
|
|
|
task_type: TaskType
|
|
|
|
|
workflow: NonEmptyStr
|
|
|
|
|
lifecycle_status: TaskLifecycleStatus = "open"
|
|
|
|
|
active_run: OperationalTaskRun | None = None
|
|
|
|
|
# Compatibility fields for the staged 014 -> 015 rollout. New consumers
|
|
|
|
|
# must use lifecycle_status and active_run.
|
|
|
|
|
status: TaskStatus
|
|
|
|
|
priority: TaskPriority
|
|
|
|
|
revision: PositiveInt
|
|
|
|
|
execution_generation: PositiveInt
|
|
|
|
|
execution_request_id: UUID
|
|
|
|
|
title: NonEmptyStr
|
|
|
|
|
summary: NonEmptyStr
|
|
|
|
|
source: TaskSource
|
|
|
|
|
trigger: TaskTrigger
|
|
|
|
|
scheduled_for: WorkbenchDateTime | None
|
|
|
|
|
agent_session_id: NonEmptyStr | None
|
|
|
|
|
agent_lease_expires_at: WorkbenchDateTime | None
|
|
|
|
|
current_plan_revision: PositiveInt | None
|
|
|
|
|
current_report_revision: PositiveInt | None
|
|
|
|
|
created_at: WorkbenchDateTime
|
|
|
|
|
updated_at: WorkbenchDateTime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OperationalTaskPermissions(StrictModel):
|
|
|
|
|
can_publish_artifacts: bool
|
|
|
|
|
can_propose_actions: bool
|
|
|
|
|
can_sync_external_feedback: bool
|
|
|
|
|
can_decide_work_orders: bool
|
|
|
|
|
can_execute_work_orders: bool
|
|
|
|
|
can_decide_commands: bool
|
|
|
|
|
can_execute_commands: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OperationalTaskDetail(OperationalTaskSummary):
|
|
|
|
|
latest_plan: ExecutionPlanSnapshot | None
|
|
|
|
|
latest_report: InvestigationReportRevision | None
|
|
|
|
|
evidence: list[EvidenceRecord]
|
|
|
|
|
actions: list[HumanApprovalAction]
|
|
|
|
|
permissions: OperationalTaskPermissions
|
|
|
|
|
work_order_snapshot: ExternalWorkOrderSnapshot | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OperationalTaskPage(StrictModel):
|
|
|
|
|
items: list[OperationalTaskSummary]
|
|
|
|
|
limit: PositiveInt
|
|
|
|
|
offset: int = Field(ge=0)
|
|
|
|
|
next_offset: int | None = Field(default=None, ge=0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OperationalTaskRunPage(StrictModel):
|
|
|
|
|
items: list[OperationalTaskRun]
|
|
|
|
|
next_cursor: NonEmptyStr | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AgentTaskContext(StrictModel):
|
|
|
|
|
task: OperationalTaskDetail
|
|
|
|
|
approved_data_scopes: list[NonEmptyStr]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AgentRunCommand(StrictModel):
|
|
|
|
|
request_id: UUID
|
|
|
|
|
expected_revision: PositiveInt
|
|
|
|
|
source_session_id: NonEmptyStr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AgentRunFailureCommand(AgentRunCommand):
|
|
|
|
|
failure_code: NonEmptyStr
|
|
|
|
|
failure_detail: NonEmptyStr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AgentRunCancelCommand(StrictModel):
|
|
|
|
|
request_id: UUID
|
|
|
|
|
expected_revision: PositiveInt
|
|
|
|
|
reason: NonEmptyStr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AgentRunDispatchFailureCommand(StrictModel):
|
|
|
|
|
request_id: UUID
|
|
|
|
|
expected_revision: PositiveInt
|
|
|
|
|
failure_code: NonEmptyStr
|
|
|
|
|
failure_detail: NonEmptyStr
|
|
|
|
|
retryable: bool = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OperationalTaskRetryCommand(StrictModel):
|
|
|
|
|
request_id: UUID
|
|
|
|
|
reason: NonEmptyStr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RecoverExpiredLeasesCommand(StrictModel):
|
|
|
|
|
limit: int = Field(default=20, ge=1, le=100)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RecoverExpiredLeasesResult(StrictModel):
|
|
|
|
|
task_ids: list[UUID]
|
|
|
|
|
recovered_at: WorkbenchDateTime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AgentArtifactsResult(StrictModel):
|
|
|
|
|
task: OperationalTaskDetail
|
|
|
|
|
plan_revision: PositiveInt | None
|
|
|
|
|
report_revision: PositiveInt | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TaskDataQueryResult(StrictModel):
|
|
|
|
|
evidence: EvidenceRecord
|
|
|
|
|
snapshot: TaskDataSnapshot
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExternalFeedbackEvidence(StrictModel):
|
|
|
|
|
evidence_id: NonEmptyStr
|
|
|
|
|
kind: Literal["external_feedback", "synthetic"] = "external_feedback"
|
|
|
|
|
summary: NonEmptyStr
|
|
|
|
|
source_label: NonEmptyStr
|
|
|
|
|
source_ref: NonEmptyStr | None = None
|
|
|
|
|
observed_at: WorkbenchDateTime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExternalFeedbackCommand(StrictModel):
|
|
|
|
|
request_id: UUID
|
|
|
|
|
source_event_id: NonEmptyStr
|
|
|
|
|
source_revision: PositiveInt
|
|
|
|
|
status: Literal[
|
|
|
|
|
"scheduled", "assigned", "in_progress", "awaiting_feedback",
|
|
|
|
|
"completed", "cancelled", "failed"
|
|
|
|
|
]
|
|
|
|
|
occurred_at: WorkbenchDateTime
|
|
|
|
|
detail: NonEmptyStr
|
|
|
|
|
evidence: ExternalFeedbackEvidence | None = None
|
|
|
|
|
work_order_snapshot: ExternalWorkOrderSnapshot | None = None
|
|
|
|
|
|
|
|
|
|
@model_validator(mode="after")
|
|
|
|
|
def snapshot_matches_feedback_revision(self):
|
|
|
|
|
if self.work_order_snapshot is not None:
|
|
|
|
|
work_order_id = str(self.work_order_snapshot.work_order_id)
|
|
|
|
|
if self.source_event_id not in {
|
|
|
|
|
work_order_id, f"peripheral_sample:{work_order_id}"
|
|
|
|
|
}:
|
|
|
|
|
raise ValueError("work_order_snapshot id must match source_event_id")
|
|
|
|
|
if self.work_order_snapshot.revision != self.source_revision:
|
|
|
|
|
raise ValueError("work_order_snapshot revision must match source_revision")
|
|
|
|
|
expected_status = {
|
|
|
|
|
"draft": "scheduled",
|
|
|
|
|
"open": "assigned",
|
|
|
|
|
"in_progress": "in_progress",
|
|
|
|
|
"awaiting_reply": "awaiting_feedback",
|
|
|
|
|
"completed": "completed",
|
|
|
|
|
"cancelled": "cancelled",
|
|
|
|
|
}[self.work_order_snapshot.status]
|
|
|
|
|
if self.status != expected_status:
|
|
|
|
|
raise ValueError("work_order_snapshot status must match feedback status")
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExternalFeedbackResult(StrictModel):
|
|
|
|
|
task: OperationalTaskDetail
|
|
|
|
|
source_event_id: NonEmptyStr
|
|
|
|
|
source_revision: PositiveInt
|
|
|
|
|
accepted_at: WorkbenchDateTime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HumanEvidenceCommand(StrictModel):
|
|
|
|
|
request_id: UUID
|
|
|
|
|
evidence: EvidenceInput
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OperationalTaskNotification(StrictModel):
|
|
|
|
|
notification_id: UUID
|
|
|
|
|
task_id: UUID
|
|
|
|
|
kind: Literal[
|
|
|
|
|
"task_created", "action_pending", "report_updated", "task_completed",
|
|
|
|
|
"task_failed", "run_delivery_degraded"
|
|
|
|
|
]
|
|
|
|
|
title: NonEmptyStr
|
|
|
|
|
description: NonEmptyStr
|
|
|
|
|
required_permission: NonEmptyStr | None = None
|
|
|
|
|
status: Literal["unread", "read"]
|
|
|
|
|
version: PositiveInt
|
|
|
|
|
created_at: WorkbenchDateTime
|
|
|
|
|
read_at: WorkbenchDateTime | None = None
|
|
|
|
|
read_by: NonEmptyStr | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OperationalTaskNotificationPage(StrictModel):
|
|
|
|
|
items: list[OperationalTaskNotification]
|
|
|
|
|
limit: PositiveInt
|
|
|
|
|
offset: int = Field(ge=0)
|
|
|
|
|
next_offset: int | None = Field(default=None, ge=0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class NotificationAcknowledgeCommand(StrictModel):
|
|
|
|
|
request_id: UUID
|
|
|
|
|
expected_version: PositiveInt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OperationalTaskScheduleCreate(StrictModel):
|
|
|
|
|
request_id: UUID
|
|
|
|
|
schedule_key: NonEmptyStr
|
|
|
|
|
title: NonEmptyStr
|
|
|
|
|
summary: NonEmptyStr
|
|
|
|
|
interval_seconds: int = Field(ge=60, le=31_536_000)
|
|
|
|
|
next_run_at: WorkbenchDateTime
|
|
|
|
|
priority: TaskPriority = "normal"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OperationalTaskSchedule(StrictModel):
|
|
|
|
|
schedule_id: UUID
|
|
|
|
|
schedule_key: NonEmptyStr
|
|
|
|
|
title: NonEmptyStr
|
|
|
|
|
summary: NonEmptyStr
|
|
|
|
|
interval_seconds: int = Field(ge=60, le=31_536_000)
|
|
|
|
|
next_run_at: WorkbenchDateTime
|
|
|
|
|
priority: TaskPriority
|
|
|
|
|
enabled: bool
|
|
|
|
|
version: PositiveInt
|
|
|
|
|
created_at: WorkbenchDateTime
|
|
|
|
|
updated_at: WorkbenchDateTime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OperationalTaskSchedulePage(StrictModel):
|
|
|
|
|
items: list[OperationalTaskSchedule]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OperationalTaskScheduleUpdate(StrictModel):
|
|
|
|
|
request_id: UUID
|
|
|
|
|
expected_version: PositiveInt
|
|
|
|
|
enabled: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DispatchDueSchedulesCommand(StrictModel):
|
|
|
|
|
limit: int = Field(default=20, ge=1, le=100)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DispatchDueSchedulesResult(StrictModel):
|
|
|
|
|
task_ids: list[UUID]
|
|
|
|
|
dispatched_at: WorkbenchDateTime
|