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}