Model/domain/tasks/subtasks.py
Khalim Conn-Kowlessar c64bd8fd85 A recorded batch failure carries structured details onto the sub_task outputs 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:50:10 +00:00

73 lines
2.5 KiB
Python

from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Optional
from uuid import UUID, uuid4
class SubTaskFailure(Exception):
"""A *recorded* failure: the work ran, its outcome belongs on the SubTask
record, and the message must NOT be retried (ADR-0055 — status is a
record, not retry fuel). Infra crashes raise anything else and keep their
retry semantics. ``details`` lands on the failed SubTask's outputs."""
def __init__(
self, message: str, details: Optional[dict[str, Any]] = None
) -> None:
super().__init__(message)
self.details = details
class SubTaskStatus(str, Enum):
WAITING = "waiting"
IN_PROGRESS = "in progress"
COMPLETE = "complete"
FAILED = "failed"
@dataclass
class SubTask:
id: UUID
task_id: UUID
status: SubTaskStatus = SubTaskStatus.WAITING
inputs: Optional[dict[str, Any]] = None
outputs: Optional[dict[str, Any]] = None
cloud_logs_url: Optional[str] = None
job_started: Optional[datetime] = None
job_completed: Optional[datetime] = None
@classmethod
def create(
cls, *, task_id: UUID, inputs: Optional[dict[str, Any]] = None
) -> "SubTask":
return cls(
id=uuid4(),
task_id=task_id,
status=SubTaskStatus.WAITING,
inputs=inputs,
)
def start(self, cloud_logs_url: Optional[str] = None) -> None:
# FAILED may restart: a failed batch is re-run by re-sending its own
# inputs, and its completion un-fails the parent Task (ADR-0055).
if self.status is SubTaskStatus.COMPLETE:
raise ValueError(f"cannot start subtask in status {self.status}")
if self.job_started is None:
self.job_started = datetime.now(timezone.utc)
self.status = SubTaskStatus.IN_PROGRESS
self.job_completed = None
if cloud_logs_url is not None:
self.cloud_logs_url = cloud_logs_url
def complete(self, result: Any = None) -> None:
self.status = SubTaskStatus.COMPLETE
self.job_completed = datetime.now(timezone.utc)
if result is not None:
self.outputs = {"result": result}
def fail(self, error: BaseException) -> None:
self.status = SubTaskStatus.FAILED
self.job_completed = datetime.now(timezone.utc)
self.outputs = {"error": str(error)}
if isinstance(error, SubTaskFailure) and error.details is not None:
self.outputs.update(error.details)