1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7use super::authority::{AgentArtifactRefV1, TaskDescriptorV1};
8use super::{MAX_HISTORY, TASK_DESCRIPTOR_VERSION, TASK_STATUS_VERSION};
9
10use crate::core::context_admission::carrier::{Withheld, admit_for_store};
11use crate::core::context_admission::stored::serve_stored;
12
13const REASON_CREATED: &str = "task created";
16const REASON_MATERIALIZED: &str = "remote task materialized";
17pub(crate) const REASON_INPUT_RECEIVED: &str = "input received via A2A";
18pub(crate) const REASON_CANCELED_VIA_A2A: &str = "canceled via A2A";
19
20fn engine_reason(reason: &str) -> bool {
21 matches!(
22 reason,
23 REASON_CREATED | REASON_MATERIALIZED | REASON_INPUT_RECEIVED | REASON_CANCELED_VIA_A2A
24 )
25}
26
27fn serve_text(text: &str) -> Result<String, Withheld> {
32 serve_stored(text, None, None)
33}
34
35fn served_text(text: &str) -> String {
38 serve_text(text).unwrap_or_else(|withheld| withheld.notice())
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42pub enum TaskState {
43 Created,
44 Working,
45 InputRequired,
46 Completed,
47 Failed,
48 Canceled,
49}
50
51impl std::fmt::Display for TaskState {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 match self {
54 TaskState::Created => write!(f, "created"),
55 TaskState::Working => write!(f, "working"),
56 TaskState::InputRequired => write!(f, "input-required"),
57 TaskState::Completed => write!(f, "completed"),
58 TaskState::Failed => write!(f, "failed"),
59 TaskState::Canceled => write!(f, "canceled"),
60 }
61 }
62}
63
64impl TaskState {
65 pub fn parse_str(s: &str) -> Option<Self> {
66 match s {
67 "created" => Some(Self::Created),
68 "working" => Some(Self::Working),
69 "input-required" | "input_required" => Some(Self::InputRequired),
70 "completed" => Some(Self::Completed),
71 "failed" => Some(Self::Failed),
72 "canceled" | "cancelled" => Some(Self::Canceled),
73 _ => None,
74 }
75 }
76
77 pub fn is_terminal(&self) -> bool {
78 matches!(self, Self::Completed | Self::Failed | Self::Canceled)
79 }
80
81 pub fn can_transition_to(&self, next: &TaskState) -> bool {
82 match self {
83 TaskState::Created => matches!(
84 next,
85 TaskState::Working | TaskState::Canceled | TaskState::Failed
86 ),
87 TaskState::Working => matches!(
88 next,
89 TaskState::InputRequired
90 | TaskState::Completed
91 | TaskState::Failed
92 | TaskState::Canceled
93 ),
94 TaskState::InputRequired => matches!(
95 next,
96 TaskState::Working | TaskState::Canceled | TaskState::Failed
97 ),
98 TaskState::Completed | TaskState::Failed | TaskState::Canceled => false,
99 }
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110#[serde(deny_unknown_fields)]
111pub struct TaskStatusV1 {
112 pub schema_version: u32,
113 #[serde(with = "task_state_wire")]
114 pub state: TaskState,
115 pub timestamp: DateTime<Utc>,
116}
117
118mod task_state_wire {
119 use super::TaskState;
120 use serde::{Deserialize, Deserializer, Serializer};
121
122 pub(super) fn serialize<S: Serializer>(
123 state: &TaskState,
124 serializer: S,
125 ) -> Result<S::Ok, S::Error> {
126 serializer.serialize_str(&state.to_string())
127 }
128
129 pub(super) fn deserialize<'de, D: Deserializer<'de>>(
130 deserializer: D,
131 ) -> Result<TaskState, D::Error> {
132 let raw = String::deserialize(deserializer)?;
133 TaskState::parse_str(&raw)
134 .ok_or_else(|| serde::de::Error::custom(format!("unknown task state: {raw}")))
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct TaskMessage {
140 pub role: String,
141 pub parts: Vec<TaskPart>,
142 pub timestamp: DateTime<Utc>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(tag = "type")]
147pub enum TaskPart {
148 #[serde(rename = "text")]
149 Text { text: String },
150 #[serde(rename = "data")]
151 Data { mime_type: String, data: String },
152 #[serde(rename = "file")]
153 File {
154 name: String,
155 mime_type: Option<String>,
156 data: Option<String>,
157 uri: Option<String>,
158 },
159}
160
161impl TaskPart {
162 fn try_map(&self, text: fn(&str) -> Result<String, Withheld>) -> Result<Self, Withheld> {
165 Ok(match self {
166 Self::Text { text: value } => Self::Text { text: text(value)? },
167 Self::Data { mime_type, data } => Self::Data {
168 mime_type: text(mime_type)?,
169 data: text(data)?,
170 },
171 Self::File {
172 name,
173 mime_type,
174 data,
175 uri,
176 } => Self::File {
177 name: text(name)?,
178 mime_type: mime_type.as_deref().map(text).transpose()?,
179 data: data.as_deref().map(text).transpose()?,
180 uri: uri.as_deref().map(text).transpose()?,
181 },
182 })
183 }
184
185 pub(crate) fn admitted(&self) -> Result<Self, Withheld> {
189 self.try_map(admit_for_store)
190 }
191
192 fn served(&self) -> Self {
195 self.try_map(serve_text)
196 .unwrap_or_else(|withheld| Self::Text {
197 text: withheld.notice(),
198 })
199 }
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct TaskTransition {
204 pub from: TaskState,
205 pub to: TaskState,
206 pub timestamp: DateTime<Utc>,
207 pub reason: Option<String>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct Task {
212 pub id: String,
213 pub from_agent: String,
214 pub to_agent: String,
215 pub state: TaskState,
216 pub description: String,
217 pub messages: Vec<TaskMessage>,
218 pub artifacts: Vec<TaskPart>,
219 pub history: Vec<TaskTransition>,
220 pub metadata: HashMap<String, String>,
221 pub created_at: DateTime<Utc>,
222 pub updated_at: DateTime<Utc>,
223 #[serde(default)]
224 pub tenant_id: Option<String>,
225 #[serde(default)]
226 pub project_id: Option<String>,
227 #[serde(default)]
228 pub action: Option<String>,
229 #[serde(default)]
230 pub idempotency_key: Option<String>,
231 #[serde(default)]
232 pub authority_key_id: Option<String>,
233 #[serde(default)]
234 pub descriptor_digest: Option<String>,
235 #[serde(default)]
236 pub artifact_refs: Vec<AgentArtifactRefV1>,
237}
238
239impl Task {
240 pub fn new(from_agent: &str, to_agent: &str, description: &str) -> Self {
241 let now = Utc::now();
242 let id = format!("task-{}", uuid::Uuid::new_v4());
243
244 Self {
245 id,
246 from_agent: from_agent.to_string(),
247 to_agent: to_agent.to_string(),
248 state: TaskState::Created,
249 description: description.to_string(),
250 messages: vec![TaskMessage {
251 role: from_agent.to_string(),
252 parts: vec![TaskPart::Text {
253 text: description.to_string(),
254 }],
255 timestamp: now,
256 }],
257 artifacts: Vec::new(),
258 history: vec![TaskTransition {
259 from: TaskState::Created,
260 to: TaskState::Created,
261 timestamp: now,
262 reason: Some(REASON_CREATED.to_string()),
263 }],
264 metadata: HashMap::new(),
265 created_at: now,
266 updated_at: now,
267 tenant_id: None,
268 project_id: None,
269 action: None,
270 idempotency_key: None,
271 authority_key_id: None,
272 descriptor_digest: None,
273 artifact_refs: Vec::new(),
274 }
275 }
276
277 pub(super) fn from_remote_descriptor(
281 descriptor: &TaskDescriptorV1,
282 task_id: String,
283 ) -> Result<Self, Withheld> {
284 let now = Utc::now();
285 let description = admit_for_store(&descriptor.description)?;
286 let parts = vec![TaskPart::Text {
287 text: description.clone(),
288 }];
289 let mut metadata = HashMap::new();
290 metadata.insert(
291 "schema_version".to_string(),
292 TASK_DESCRIPTOR_VERSION.to_string(),
293 );
294 metadata.insert("action".to_string(), descriptor.action.clone());
295 Ok(Self {
296 id: task_id,
297 from_agent: descriptor.sender.clone(),
298 to_agent: descriptor.recipient.clone(),
299 state: TaskState::Created,
300 description,
301 messages: vec![TaskMessage {
302 role: descriptor.sender.clone(),
303 parts,
304 timestamp: now,
305 }],
306 artifacts: Vec::new(),
307 history: vec![TaskTransition {
308 from: TaskState::Created,
309 to: TaskState::Created,
310 timestamp: now,
311 reason: Some(REASON_MATERIALIZED.to_string()),
312 }],
313 metadata,
314 created_at: now,
315 updated_at: now,
316 tenant_id: Some(descriptor.tenant_id.clone()),
317 project_id: Some(descriptor.project_id.clone()),
318 action: Some(descriptor.action.clone()),
319 idempotency_key: Some(descriptor.idempotency_key.clone()),
320 authority_key_id: Some(descriptor.key_id.clone()),
321 descriptor_digest: Some(descriptor.content_digest()),
322 artifact_refs: descriptor.artifact_refs.clone(),
323 })
324 }
325
326 #[must_use]
334 pub fn served(&self) -> Self {
335 let mut task = self.clone();
336 task.description = self.served_description();
337 for message in &mut task.messages {
338 for part in &mut message.parts {
339 *part = part.served();
340 }
341 }
342 for artifact in &mut task.artifacts {
343 *artifact = artifact.served();
344 }
345 for value in task.metadata.values_mut() {
346 *value = served_text(value);
347 }
348 for transition in &mut task.history {
349 if let Some(reason) = transition
350 .reason
351 .as_mut()
352 .filter(|reason| !engine_reason(reason.as_str()))
353 {
354 *reason = served_text(reason);
355 }
356 }
357 task
358 }
359
360 #[must_use]
363 pub fn served_description(&self) -> String {
364 served_text(&self.description)
365 }
366
367 pub fn status_v1(&self) -> TaskStatusV1 {
368 TaskStatusV1 {
369 schema_version: TASK_STATUS_VERSION,
370 state: self.state.clone(),
371 timestamp: self.updated_at,
372 }
373 }
374
375 pub fn transition(&mut self, new_state: TaskState, reason: Option<&str>) -> Result<(), String> {
376 if self.history.len() >= MAX_HISTORY {
377 return Err("task transition history limit exceeded".to_string());
378 }
379 if self.history.len() == MAX_HISTORY - 1 && !new_state.is_terminal() {
381 return Err("task transition history reserved for terminal state".to_string());
382 }
383 if !self.state.can_transition_to(&new_state) {
384 return Err(format!(
385 "invalid transition: {} → {}",
386 self.state, new_state
387 ));
388 }
389
390 let now = Utc::now();
391 if now < self.updated_at {
393 return Err("task clock precedes stored state".to_string());
394 }
395 let reason = match reason {
398 Some(reason) if !engine_reason(reason) => {
399 Some(admit_for_store(reason).map_err(|withheld| withheld.message("task reason"))?)
400 }
401 other => other.map(str::to_owned),
402 };
403 self.history.push(TaskTransition {
404 from: self.state.clone(),
405 to: new_state.clone(),
406 timestamp: now,
407 reason,
408 });
409
410 self.state = new_state;
411 self.updated_at = now;
412 Ok(())
413 }
414
415 pub fn add_message(&mut self, role: &str, parts: Vec<TaskPart>) {
416 self.messages.push(TaskMessage {
417 role: role.to_string(),
418 parts,
419 timestamp: Utc::now(),
420 });
421 self.updated_at = Utc::now();
422 }
423
424 pub(crate) fn append_message(
428 &mut self,
429 role: &str,
430 parts: &[TaskPart],
431 ) -> Result<(), Withheld> {
432 let parts = parts
433 .iter()
434 .map(TaskPart::admitted)
435 .collect::<Result<Vec<_>, _>>()?;
436 self.add_message(role, parts);
437 Ok(())
438 }
439
440 pub fn add_artifact(&mut self, artifact: TaskPart) {
441 self.artifacts.push(artifact);
442 self.updated_at = Utc::now();
443 }
444}