1use bon::Builder;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value};
5
6#[cfg(feature = "tracing")]
7use tracing::instrument;
8
9use super::{
10 agent::PushNotificationConfig,
11 message::{Artifact, Message},
12};
13
14#[cfg(feature = "tracing")]
15use crate::measure_duration;
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30#[serde(rename_all = "kebab-case")]
31pub enum TaskState {
32 Submitted,
33 Working,
34 InputRequired,
35 Completed,
36 Canceled,
37 Failed,
38 Rejected,
39 AuthRequired,
40 Unknown,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct TaskStatus {
62 pub state: TaskState,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub message: Option<Message>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub timestamp: Option<DateTime<Utc>>,
67}
68
69impl Default for TaskStatus {
70 fn default() -> Self {
71 Self {
72 state: TaskState::Submitted,
73 message: None,
74 timestamp: Some(Utc::now()),
75 }
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
103pub struct Task {
104 pub id: String,
105 #[serde(rename = "contextId")]
106 pub context_id: String,
107 #[builder(default = TaskStatus::default())]
108 pub status: TaskStatus,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub artifacts: Option<Vec<Artifact>>,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub history: Option<Vec<Message>>,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub metadata: Option<Map<String, Value>>,
115 #[builder(default = "task".to_string())]
116 pub kind: String, }
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct TaskIdParams {
125 pub id: String,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub metadata: Option<Map<String, Value>>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct TaskQueryParams {
136 pub id: String,
137 #[serde(skip_serializing_if = "Option::is_none", rename = "historyLength")]
138 pub history_length: Option<u32>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub metadata: Option<Map<String, Value>>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct MessageSendConfiguration {
152 #[serde(
154 skip_serializing_if = "Option::is_none",
155 rename = "acceptedOutputModes"
156 )]
157 pub accepted_output_modes: Option<Vec<String>>,
158 #[serde(skip_serializing_if = "Option::is_none", rename = "historyLength")]
159 pub history_length: Option<u32>,
160 #[serde(
161 skip_serializing_if = "Option::is_none",
162 rename = "pushNotificationConfig"
163 )]
164 pub push_notification_config: Option<PushNotificationConfig>,
165 #[serde(skip_serializing_if = "Option::is_none")]
166 pub blocking: Option<bool>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct MessageSendParams {
175 pub message: Message,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub configuration: Option<MessageSendConfiguration>,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 pub metadata: Option<Map<String, Value>>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct TaskSendParams {
185 pub id: String,
186 #[serde(skip_serializing_if = "Option::is_none", rename = "sessionId")]
187 pub session_id: Option<String>,
188 pub message: Message,
189 #[serde(skip_serializing_if = "Option::is_none", rename = "pushNotification")]
190 pub push_notification: Option<PushNotificationConfig>,
191 #[serde(skip_serializing_if = "Option::is_none", rename = "historyLength")]
192 pub history_length: Option<u32>,
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub metadata: Option<Map<String, Value>>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct TaskPushNotificationConfig {
200 #[serde(rename = "taskId")]
201 pub task_id: String,
202 #[serde(rename = "pushNotificationConfig")]
203 pub push_notification_config: PushNotificationConfig,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize, Default)]
227pub struct ListTasksParams {
228 #[serde(skip_serializing_if = "Option::is_none", rename = "contextId")]
230 pub context_id: Option<String>,
231 #[serde(skip_serializing_if = "Option::is_none")]
233 pub status: Option<TaskState>,
234 #[serde(skip_serializing_if = "Option::is_none", rename = "pageSize")]
236 pub page_size: Option<i32>,
237 #[serde(skip_serializing_if = "Option::is_none", rename = "pageToken")]
239 pub page_token: Option<String>,
240 #[serde(skip_serializing_if = "Option::is_none", rename = "historyLength")]
242 pub history_length: Option<i32>,
243 #[serde(skip_serializing_if = "Option::is_none", rename = "includeArtifacts")]
245 pub include_artifacts: Option<bool>,
246 #[serde(skip_serializing_if = "Option::is_none", rename = "lastUpdatedAfter")]
248 pub last_updated_after: Option<i64>,
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub metadata: Option<Map<String, Value>>,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct ListTasksResult {
259 pub tasks: Vec<Task>,
261 #[serde(rename = "totalSize")]
263 pub total_size: i32,
264 #[serde(rename = "pageSize")]
266 pub page_size: i32,
267 #[serde(rename = "nextPageToken")]
269 pub next_page_token: String,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct GetTaskPushNotificationConfigParams {
278 pub id: String,
280 #[serde(
282 skip_serializing_if = "Option::is_none",
283 rename = "pushNotificationConfigId"
284 )]
285 pub push_notification_config_id: Option<String>,
286 #[serde(skip_serializing_if = "Option::is_none")]
287 pub metadata: Option<Map<String, Value>>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ListTaskPushNotificationConfigParams {
293 pub id: String,
295 #[serde(skip_serializing_if = "Option::is_none")]
296 pub metadata: Option<Map<String, Value>>,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct DeleteTaskPushNotificationConfigParams {
302 pub id: String,
304 #[serde(rename = "pushNotificationConfigId")]
306 pub push_notification_config_id: String,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub metadata: Option<Map<String, Value>>,
309}
310
311impl Task {
312 pub fn new(id: String, context_id: String) -> Self {
314 Self {
315 id,
316 context_id,
317 status: TaskStatus {
318 state: TaskState::Submitted,
319 message: None,
320 timestamp: Some(Utc::now()),
321 },
322 artifacts: None,
323 history: None,
324 metadata: None,
325 kind: "task".to_string(),
326 }
327 }
328
329 pub fn with_context(id: String, context_id: String) -> Self {
331 Self::new(id, context_id)
332 }
333
334 #[cfg_attr(feature = "tracing", instrument(skip(self, message), fields(
336 task.id = %self.id,
337 task.old_state = ?self.status.state,
338 task.new_state = ?state,
339 task.has_message = message.is_some()
340 )))]
341 pub fn update_status(&mut self, state: TaskState, message: Option<Message>) {
342 #[cfg(feature = "tracing")]
343 tracing::info!("Updating task status");
344
345 self.status = TaskStatus {
347 state: state.clone(),
348 message: message.clone(),
349 timestamp: Some(Utc::now()),
350 };
351
352 if let Some(msg) = message {
354 if let Some(history) = &mut self.history {
355 #[cfg(feature = "tracing")]
356 tracing::info!(
357 "Adding message to history: role={:?}, message_id={}, current_size={}, new_size={}",
358 msg.role,
359 msg.message_id,
360 history.len(),
361 history.len() + 1
362 );
363 history.push(msg);
364 } else {
365 #[cfg(feature = "tracing")]
366 tracing::info!(
367 "Creating new history with message: role={:?}, message_id={}",
368 msg.role,
369 msg.message_id
370 );
371 self.history = Some(vec![msg]);
372 }
373 }
374
375 #[cfg(feature = "tracing")]
376 tracing::info!("Task status updated successfully");
377 }
378
379 #[cfg_attr(feature = "tracing", instrument(skip(self), fields(
387 task.id = %self.id,
388 history.current_size = self.history.as_ref().map(|h| h.len()).unwrap_or(0),
389 history.requested_limit = ?history_length
390 )))]
391 pub fn with_limited_history(&self, history_length: Option<u32>) -> Self {
392 if history_length.is_none() || self.history.is_none() {
394 #[cfg(feature = "tracing")]
395 tracing::debug!("No history truncation needed");
396 return self.clone();
397 }
398
399 #[cfg(feature = "tracing")]
400 let _span = tracing::Span::current();
401
402 let limit: usize = history_length.unwrap().try_into().unwrap_or(usize::MAX);
403
404 #[cfg(feature = "tracing")]
405 let mut task_copy = measure_duration!(_span, "operation.duration_ms", { self.clone() });
406
407 #[cfg(not(feature = "tracing"))]
408 let mut task_copy = self.clone();
409
410 if let Some(history) = &mut task_copy.history {
412 let original_size = history.len();
413
414 if limit == 0 {
415 #[cfg(feature = "tracing")]
417 tracing::debug!("Removing all history (limit = 0)");
418 task_copy.history = None;
419 } else if history.len() > limit {
420 let items_to_skip = history.len() - limit;
425 #[cfg(feature = "tracing")]
426 tracing::debug!(
427 "Truncating history from {} to {} items (removing {} oldest)",
428 original_size,
429 limit,
430 items_to_skip
431 );
432
433 *history = history.iter().skip(items_to_skip).cloned().collect();
434 } else {
435 #[cfg(feature = "tracing")]
436 tracing::debug!("History size ({}) within limit ({})", original_size, limit);
437 }
438 }
440
441 task_copy
442 }
443
444 #[cfg_attr(feature = "tracing", instrument(skip(self, artifact), fields(
446 task.id = %self.id,
447 artifact.id = %artifact.artifact_id,
448 artifacts.count = self.artifacts.as_ref().map(|a| a.len()).unwrap_or(0)
449 )))]
450 pub fn add_artifact(&mut self, artifact: Artifact) {
451 if let Some(artifacts) = &mut self.artifacts {
452 #[cfg(feature = "tracing")]
453 tracing::debug!("Adding artifact to existing list");
454 artifacts.push(artifact);
455 } else {
456 #[cfg(feature = "tracing")]
457 tracing::debug!("Creating new artifacts list with artifact");
458 self.artifacts = Some(vec![artifact]);
459 }
460 }
461
462 #[cfg_attr(feature = "tracing", instrument(skip(self), fields(
464 task.id = %self.id,
465 task.kind = %self.kind,
466 task.state = ?self.status.state,
467 history.size = self.history.as_ref().map(|h| h.len()).unwrap_or(0)
468 )))]
469 pub fn validate(&self) -> Result<(), crate::domain::A2AError> {
470 #[cfg(feature = "tracing")]
471 tracing::debug!("Validating task");
472
473 if self.kind != "task" {
475 #[cfg(feature = "tracing")]
476 tracing::error!("Invalid task kind: {}", self.kind);
477 return Err(crate::domain::A2AError::InvalidParams(
478 "Task kind must be 'task'".to_string(),
479 ));
480 }
481
482 if let Some(hist) = &self.history {
484 #[cfg(feature = "tracing")]
485 tracing::trace!("Checking for duplicate message IDs in history");
486
487 let mut message_ids = std::collections::HashSet::new();
488 for message in hist {
489 if !message_ids.insert(&message.message_id) {
490 #[cfg(feature = "tracing")]
491 tracing::error!("Duplicate message ID found: {}", message.message_id);
492 return Err(crate::domain::A2AError::InvalidParams(format!(
493 "Duplicate message ID in history: {}",
494 message.message_id
495 )));
496 }
497 }
498 }
499
500 if let Some(hist) = &self.history {
502 #[cfg(feature = "tracing")]
503 tracing::trace!("Validating {} messages in history", hist.len());
504
505 for (index, message) in hist.iter().enumerate() {
506 #[cfg(feature = "tracing")]
507 tracing::trace!("Validating message {} in history", index);
508 message.validate()?;
509 }
510 }
511
512 if let Some(msg) = &self.status.message {
514 #[cfg(feature = "tracing")]
515 tracing::trace!("Validating status message");
516 msg.validate()?;
517 }
518
519 #[cfg(feature = "tracing")]
520 tracing::debug!("Task validation successful");
521 Ok(())
522 }
523}