Skip to main content

lc_a2a/protocol/
task.rs

1use serde::{Deserialize, Serialize};
2
3use super::message::A2AMessage;
4
5/// Task lifecycle status.
6///
7/// Serialized using the wire names from the A2A v0.3 specification
8/// (`input-required`, `auth-required`, ...).
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum TaskStatus {
11    /// Task has been submitted but not yet started.
12    #[serde(rename = "submitted")]
13    Submitted,
14    /// Task is currently being processed.
15    #[serde(rename = "working")]
16    Working,
17    /// Task requires additional input from the user.
18    #[serde(rename = "input-required")]
19    InputRequired,
20    /// Task completed successfully.
21    #[serde(rename = "completed")]
22    Completed,
23    /// Task failed.
24    #[serde(rename = "failed")]
25    Failed,
26    /// Task was cancelled.
27    #[serde(rename = "cancelled")]
28    Cancelled,
29    /// Task was rejected by the agent (e.g. refused work).
30    #[serde(rename = "rejected")]
31    Rejected,
32    /// Task requires authentication before it can proceed.
33    #[serde(rename = "auth-required")]
34    AuthRequired,
35    /// Task expired before reaching a terminal state.
36    #[serde(rename = "expired")]
37    Expired,
38}
39
40impl TaskStatus {
41    /// Whether a task in this status may legally transition to `target`.
42    ///
43    /// This is the A2A task lifecycle state machine:
44    ///
45    /// ```text
46    ///               ┌──────────────┐
47    ///               ▼              │
48    /// auth-required ─┐            working ──→ completed
49    ///                │             │  ├──→ failed
50    /// submitted ─────┼─→ working ──┘  ├──→ input-required ──→ working
51    ///    │           │                └──→ cancelled
52    ///    ├──→ rejected
53    ///    ├──→ cancelled
54    ///    └──→ expired
55    /// ```
56    ///
57    /// Terminal states (`completed`, `failed`, `cancelled`, `rejected`,
58    /// `expired`) have no outgoing transitions.
59    pub fn can_transition_to(&self, target: &TaskStatus) -> bool {
60        use TaskStatus::*;
61        matches!(
62            (self, target),
63            (Submitted, Working)
64                | (Submitted, Rejected)
65                | (Submitted, Cancelled)
66                | (Submitted, Expired)
67                | (Working, Completed)
68                | (Working, Failed)
69                | (Working, InputRequired)
70                | (Working, Cancelled)
71                | (Working, Expired)
72                | (InputRequired, Working)
73                | (InputRequired, Cancelled)
74                | (InputRequired, Expired)
75                | (AuthRequired, Submitted)
76                | (AuthRequired, Expired)
77        )
78    }
79
80    /// Whether this status is terminal (no further transitions allowed).
81    pub fn is_terminal(&self) -> bool {
82        matches!(
83            self,
84            TaskStatus::Completed
85                | TaskStatus::Failed
86                | TaskStatus::Cancelled
87                | TaskStatus::Rejected
88                | TaskStatus::Expired
89        )
90    }
91}
92
93impl std::fmt::Display for TaskStatus {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        let s = match self {
96            TaskStatus::Submitted => "submitted",
97            TaskStatus::Working => "working",
98            TaskStatus::InputRequired => "input-required",
99            TaskStatus::Completed => "completed",
100            TaskStatus::Failed => "failed",
101            TaskStatus::Cancelled => "cancelled",
102            TaskStatus::Rejected => "rejected",
103            TaskStatus::AuthRequired => "auth-required",
104            TaskStatus::Expired => "expired",
105        };
106        f.write_str(s)
107    }
108}
109
110/// A unit of work in the A2A protocol.
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct A2ATask {
113    /// Unique task identifier.
114    pub id: String,
115    /// The message that initiated this task.
116    ///
117    /// Kept for single-message compatibility; the full history lives in
118    /// [`messages`](A2ATask::messages) and always starts with this message.
119    pub message: A2AMessage,
120    /// Current status of the task.
121    pub status: TaskStatus,
122    /// Identifier of the caller/organization that created this task (P1-4).
123    ///
124    /// Used for ownership authorization: `tasks/get`/`tasks/cancel` from a
125    /// different caller are rejected with a `403`-style error.
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub owner: Option<String>,
128    /// Full message history for multi-turn dialogue (P2-2).
129    ///
130    /// The first element is the initiating message (equal to [`message`](Self::message));
131    /// subsequent turns are appended by `tasks/send` with a `taskId`. The
132    /// chain is invoked over this whole history for continued tasks.
133    #[serde(default, skip_serializing_if = "Vec::is_empty")]
134    pub messages: Vec<A2AMessage>,
135}
136
137impl A2ATask {
138    /// Create a new task with `Submitted` status.
139    pub fn new(id: impl Into<String>, message: A2AMessage) -> Self {
140        let id = id.into();
141        Self {
142            messages: vec![message.clone()],
143            id,
144            message,
145            status: TaskStatus::Submitted,
146            owner: None,
147        }
148    }
149
150    /// Set the task status.
151    pub fn with_status(mut self, status: TaskStatus) -> Self {
152        self.status = status;
153        self
154    }
155
156    /// Set the task owner (P1-4).
157    pub fn with_owner(mut self, owner: impl Into<String>) -> Self {
158        self.owner = Some(owner.into());
159        self
160    }
161
162    /// Append a message to the multi-turn history (P2-2).
163    pub fn push_message(&mut self, msg: A2AMessage) {
164        self.messages.push(msg);
165    }
166
167    /// The full message history for this task (P2-2).
168    ///
169    /// Guaranteed to be non-empty and to start with the initiating message.
170    /// Borrows the in-memory history when populated; falls back to a
171    /// single-element owned history for tasks deserialized from an
172    /// old single-message wire payload.
173    pub fn message_history(&self) -> std::borrow::Cow<'_, [A2AMessage]> {
174        if self.messages.is_empty() {
175            std::borrow::Cow::Owned(vec![self.message.clone()])
176        } else {
177            std::borrow::Cow::Borrowed(&self.messages)
178        }
179    }
180}