diff --git a/backend/app/api/approvals.py b/backend/app/api/approvals.py index 723444c0..511d92e1 100644 --- a/backend/app/api/approvals.py +++ b/backend/app/api/approvals.py @@ -394,10 +394,13 @@ async def create_approval( _actor: ActorContext = ACTOR_DEP, ) -> ApprovalRead: """Create an approval for a board.""" + payload_dict = payload.payload + if payload_dict is None and isinstance(payload.lead_reasoning, str) and payload.lead_reasoning.strip(): + payload_dict = {"reason": payload.lead_reasoning.strip()} task_ids = normalize_task_ids( task_id=payload.task_id, task_ids=payload.task_ids, - payload=payload.payload, + payload=payload_dict, ) task_id = task_ids[0] if task_ids else None if payload.status == "pending": @@ -411,7 +414,7 @@ async def create_approval( task_id=task_id, agent_id=payload.agent_id, action_type=payload.action_type, - payload=payload.payload, + payload=payload_dict, confidence=payload.confidence, rubric_scores=payload.rubric_scores, status=payload.status, diff --git a/backend/app/schemas/approvals.py b/backend/app/schemas/approvals.py index 80ebadb9..6f36be41 100644 --- a/backend/app/schemas/approvals.py +++ b/backend/app/schemas/approvals.py @@ -6,7 +6,7 @@ from datetime import datetime from typing import Literal, Self from uuid import UUID -from pydantic import model_validator +from pydantic import AliasChoices, Field as PydanticField, model_validator from sqlmodel import Field, SQLModel ApprovalStatus = Literal["pending", "approved", "rejected"] @@ -49,9 +49,18 @@ class ApprovalCreate(ApprovalBase): agent_id: UUID | None = None + # Back-compat + ergonomics: some clients send lead reasoning as a top-level + # field (`reasoning` / `lead_reasoning`) rather than nesting under payload.reason. + lead_reasoning: str | None = PydanticField( + default=None, + validation_alias=AliasChoices("lead_reasoning", "reasoning", "leadReasoning"), + ) + @model_validator(mode="after") def validate_lead_reasoning(self) -> Self: """Ensure each approval request includes explicit lead reasoning.""" + if isinstance(self.lead_reasoning, str) and self.lead_reasoning.strip(): + return self payload = self.payload if isinstance(payload, dict): reason = payload.get("reason") diff --git a/backend/tests/test_approvals_schema.py b/backend/tests/test_approvals_schema.py index da7a58ea..11b1ffdf 100644 --- a/backend/tests/test_approvals_schema.py +++ b/backend/tests/test_approvals_schema.py @@ -38,6 +38,17 @@ def test_approval_create_requires_lead_reasoning() -> None: ) +def test_approval_create_accepts_top_level_reasoning_alias() -> None: + model = ApprovalCreate.model_validate( + { + "action_type": "task.update", + "confidence": 80, + "reasoning": "Lead says OK.", + }, + ) + assert model.lead_reasoning == "Lead says OK." + + def test_approval_create_accepts_nested_decision_reason() -> None: model = ApprovalCreate.model_validate( {