import uuid from datetime import date, datetime from typing import Self from pydantic import BaseModel, ConfigDict, field_validator, model_validator from api.models.task import TaskPriority, TaskStatus from api.task_status import normalize_task_status class TaskStatusMixin(BaseModel): @field_validator("status", mode="before", check_fields=False) @classmethod def _normalize_status(cls, value): if value is None: return value return normalize_task_status(value) class TaskCreate(TaskStatusMixin): workstream_id: uuid.UUID title: str description: str | None = None status: TaskStatus = TaskStatus.todo priority: TaskPriority = TaskPriority.medium assignee: str | None = None due_date: date | None = None blocking_reason: str | None = None needs_human: bool = False intervention_note: str | None = None parent_task_id: uuid.UUID | None = None @model_validator(mode="after") def intervention_note_required_when_flagged(self) -> Self: if self.needs_human and not self.intervention_note: raise ValueError("intervention_note is required when needs_human is True") return self class TaskUpdate(TaskStatusMixin): title: str | None = None description: str | None = None status: TaskStatus | None = None priority: TaskPriority | None = None assignee: str | None = None due_date: date | None = None blocking_reason: str | None = None needs_human: bool | None = None intervention_note: str | None = None parent_task_id: uuid.UUID | None = None # Token passthrough — three tiers (highest precision wins): # 1. tokens_in + tokens_out → exact counts; note defaults to "measured" # 2. workplan_tokens_in + workplan_tokens_out → prorated across task count (note="workplan") # 3. neither provided, status=done → heuristic 1000/500 (note="heuristic") # token_note overrides the auto-assigned note for Tier 1 only (e.g. "userbased") # suppress_token_event lets file/cache sync update status without recording usage. tokens_in: int | None = None tokens_out: int | None = None workplan_tokens_in: int | None = None workplan_tokens_out: int | None = None token_note: str | None = None model: str | None = None agent: str | None = None session_id: str | None = None suppress_token_event: bool | None = None @model_validator(mode="after") def blocking_reason_required_when_human_waiting(self) -> Self: if self.status == TaskStatus.wait and self.needs_human and not self.blocking_reason: raise ValueError("blocking_reason is required when a human-blocked task is waiting") return self @model_validator(mode="after") def intervention_note_required_when_flagged(self) -> Self: if self.needs_human and not self.intervention_note: raise ValueError("intervention_note is required when needs_human is True") return self class TaskRead(TaskStatusMixin): model_config = ConfigDict(from_attributes=True) id: uuid.UUID workstream_id: uuid.UUID title: str description: str | None = None status: TaskStatus priority: TaskPriority assignee: str | None = None due_date: date | None = None blocking_reason: str | None = None needs_human: bool intervention_note: str | None = None parent_task_id: uuid.UUID | None = None created_at: datetime updated_at: datetime