# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-reconcile-agent-card-capability-declarations-against-observed-task-state-transitions-in-a2a-v1-0.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-reconcile-agent-card-capability-declarations-against-observed-task-state-transitions-in-a2a-v1-0.warrant.json
# Markdown SHA-256:   7d0ac1f88e7162b490e1a907bdf8c51b5918d9ea16da64561123b4b9a4bd7dd8
# Code AST SHA-256:   63b1d91ddc646b6446fc031aa008a3dd145bc4205135ef3a2e860d242b130b22
# Verified Classes:   AgentCardDivergenceError, AgentCard, ObservedTask
# Defensive Guards:   3 fail-closed invariants (zero external deps)

import time
from dataclasses import dataclass, field
from typing import Literal, Optional

TaskState = Literal["submitted", "working", "input-required", "completed", "failed", "canceled"]


class AgentCardDivergenceError(RuntimeError):
    """Raised when an agent's observed task lifecycle contradicts its Agent Card declarations."""

    def __init__(self, task_id: str, declared: str, observed: str, detail: str):
        self.task_id = task_id
        self.declared = declared
        self.observed = observed
        self.detail = detail
        super().__init__(
            f"task {task_id!r}: card declared {declared!r} but observed {observed!r} ({detail})"
        )


@dataclass
class AgentCard:
    name: str
    streaming: bool
    state_transition_history: bool
    skills: list[str] = field(default_factory=list)


@dataclass
class ObservedTask:
    task_id: str
    states: list[TaskState] = field(default_factory=list)
    first_seen: float = field(default_factory=time.monotonic)

    def record(self, state: TaskState) -> None:
        if not self.states or self.states[-1] != state:
            self.states.append(state)


def reconcile_continuation(
    card: AgentCard,
    task: ObservedTask,
    new_task_id: str,
    wait_deadline_s: float = 30.0,
) -> None:
    """
    Enforce the invariant: lifecycle decisions follow observed state, not the Card.

    A server may legally spawn a new task id on a continuation attempt, but if the
    Card advertises stateTransitionHistory and the client has already observed
    `working`, the absence of an `input-required` transition before the deadline
    is a divergence the client must surface, not silently absorb.
    """
    if new_task_id != task.task_id:
        # Legal per A2A v1.0, but only if the Card did not promise history continuity.
        if card.state_transition_history and "working" in task.states:
            raise AgentCardDivergenceError(
                task_id=task.task_id,
                declared="stateTransitionHistory=true (continuous task id)",
                observed=f"new task id {new_task_id!r} on continuation",
                detail="server spawned a fresh task instead of transitioning to input-required",
            )
        return

    elapsed = time.monotonic() - task.first_seen
    if elapsed > wait_deadline_s and task.states and task.states[-1] == "working":
        if card.state_transition_history:
            raise AgentCardDivergenceError(
                task_id=task.task_id,
                declared="stateTransitionHistory=true (input-required reachable)",
                observed=f"stuck in 'working' for {elapsed:.1f}s",
                detail="no input-required transition emitted before deadline",
            )
        # Card did not promise history: fall back to polling, do not raise.
        return

    if "input-required" in task.states and "working" not in task.states:
        raise AgentCardDivergenceError(
            task_id=task.task_id,
            declared="lifecycle ordering submitted->working->input-required",
            observed="input-required without prior working",
            detail="state machine ordering violated",
        )
