Skip to main content

a2a_rs/domain/core/
task.rs

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/// States a task can be in during its lifecycle.
18///
19/// Tasks progress through various states from submission to completion:
20/// - `Submitted`: Task has been received and is queued for processing
21/// - `Working`: Task is currently being processed
22/// - `InputRequired`: Task needs additional input from the user
23/// - `Completed`: Task has finished successfully
24/// - `Canceled`: Task was canceled before completion
25/// - `Failed`: Task encountered an error and could not complete
26/// - `Rejected`: Task was rejected (invalid, unauthorized, etc.)
27/// - `AuthRequired`: Task requires authentication to proceed
28/// - `Unknown`: Task state could not be determined
29#[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/// Status of a task including state, optional message, and timestamp.
44///
45/// Represents a point-in-time status of a task, including its current state,
46/// an optional status message providing additional context, and the timestamp
47/// when this status was recorded.
48///
49/// # Example
50/// ```rust
51/// use a2a_rs::{TaskStatus, TaskState};
52/// use chrono::Utc;
53///
54/// let status = TaskStatus {
55///     state: TaskState::Working,
56///     message: None,
57///     timestamp: Some(Utc::now()),
58/// };
59/// ```
60#[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/// A task in the A2A protocol with status, history, and artifacts.
80///
81/// Tasks represent units of work that agents process. Each task has:
82/// - A unique ID and context ID for tracking
83/// - Current status including state and optional message
84/// - Optional artifacts produced during processing
85/// - Optional message history for the conversation
86/// - Optional metadata for additional context
87///
88/// # Example
89/// ```rust
90/// use a2a_rs::{Task, TaskStatus, TaskState};
91///
92/// let task = Task::builder()
93///     .id("task-123".to_string())
94///     .context_id("ctx-456".to_string())
95///     .status(TaskStatus {
96///         state: TaskState::Working,
97///         message: None,
98///         timestamp: None,
99///     })
100///     .build();
101/// ```
102#[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, // Always "task"
117}
118
119/// Parameters for identifying a task by ID.
120///
121/// Simple structure containing a task ID and optional metadata
122/// for task identification in API requests.
123#[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/// Parameters for querying a task with optional history constraints.
131///
132/// Allows querying a task by ID with optional limits on the amount
133/// of history to return and additional metadata.
134#[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/// Configuration options for sending messages including output modes and notifications.
144///
145/// Specifies how a message should be processed and delivered:
146/// - `accepted_output_modes`: Output formats the client can handle (v0.3.0: now optional)
147/// - `history_length`: Limit on conversation history to include
148/// - `push_notification_config`: Settings for push notifications
149/// - `blocking`: Whether the request should wait for completion
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct MessageSendConfiguration {
152    /// Output formats the client can handle (v0.3.0: changed to optional)
153    #[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/// Parameters for sending a message with optional configuration.
170///
171/// Contains the message to send along with optional configuration
172/// that controls how the message is processed and delivered.
173#[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/// Parameters for sending a task (legacy)
183#[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/// Configuration for task push notifications
198#[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/// Parameters for listing tasks with filtering and pagination (v0.3.0).
207///
208/// Allows querying tasks with various filters and pagination support.
209/// Results can be filtered by context, status, and update time.
210///
211/// # Example
212/// ```rust
213/// use a2a_rs::{ListTasksParams, TaskState};
214///
215/// let params = ListTasksParams {
216///     context_id: Some("ctx-123".to_string()),
217///     status: Some(TaskState::Working),
218///     page_size: Some(20),
219///     page_token: None,
220///     history_length: Some(5),
221///     include_artifacts: Some(true),
222///     last_updated_after: None,
223///     metadata: None,
224/// };
225/// ```
226#[derive(Debug, Clone, Serialize, Deserialize, Default)]
227pub struct ListTasksParams {
228    /// Filter tasks by context ID
229    #[serde(skip_serializing_if = "Option::is_none", rename = "contextId")]
230    pub context_id: Option<String>,
231    /// Filter tasks by their current status state
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub status: Option<TaskState>,
234    /// Maximum number of tasks to return (1-100, default 50)
235    #[serde(skip_serializing_if = "Option::is_none", rename = "pageSize")]
236    pub page_size: Option<i32>,
237    /// Token for pagination from previous response
238    #[serde(skip_serializing_if = "Option::is_none", rename = "pageToken")]
239    pub page_token: Option<String>,
240    /// Number of recent messages to include in each task (default 0)
241    #[serde(skip_serializing_if = "Option::is_none", rename = "historyLength")]
242    pub history_length: Option<i32>,
243    /// Whether to include artifacts in the response (default false)
244    #[serde(skip_serializing_if = "Option::is_none", rename = "includeArtifacts")]
245    pub include_artifacts: Option<bool>,
246    /// Filter tasks updated after this timestamp (milliseconds since epoch)
247    #[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/// Result object for tasks/list method (v0.3.0).
254///
255/// Contains the list of tasks matching the query criteria along with
256/// pagination information for retrieving additional results.
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct ListTasksResult {
259    /// Array of tasks matching the criteria
260    pub tasks: Vec<Task>,
261    /// Total number of tasks available (before pagination)
262    #[serde(rename = "totalSize")]
263    pub total_size: i32,
264    /// Maximum number of tasks in this response
265    #[serde(rename = "pageSize")]
266    pub page_size: i32,
267    /// Token for next page (empty string if no more results)
268    #[serde(rename = "nextPageToken")]
269    pub next_page_token: String,
270}
271
272/// Parameters for getting a specific push notification config (v0.3.0).
273///
274/// Enhanced version that allows retrieving a specific config by ID,
275/// supporting multiple notification callbacks per task.
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct GetTaskPushNotificationConfigParams {
278    /// Task ID
279    pub id: String,
280    /// Specific config ID to retrieve (optional)
281    #[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/// Parameters for listing all push notification configs for a task (v0.3.0).
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ListTaskPushNotificationConfigParams {
293    /// Task ID
294    pub id: String,
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub metadata: Option<Map<String, Value>>,
297}
298
299/// Parameters for deleting a push notification config (v0.3.0).
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct DeleteTaskPushNotificationConfigParams {
302    /// Task ID
303    pub id: String,
304    /// Config ID to delete
305    #[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    /// Create a new task with the given ID in the submitted state
313    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    /// Create a new task with the given ID and context ID in the submitted state
330    pub fn with_context(id: String, context_id: String) -> Self {
331        Self::new(id, context_id)
332    }
333
334    /// Update the task status
335    #[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        // Set the new status
346        self.status = TaskStatus {
347            state: state.clone(),
348            message: message.clone(),
349            timestamp: Some(Utc::now()),
350        };
351
352        // Add message to history if provided and state_transition_history is enabled
353        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    /// Get a copy of this task with history limited to the specified length
380    ///
381    /// This method follows the A2A spec for history truncation:
382    /// - If no history_length is provided, returns the full history
383    /// - If history_length is 0, removes history entirely
384    /// - If history_length is less than the current history size,
385    ///   keeps only the most recent messages (truncates from the beginning)
386    #[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 no history limit specified or no history, return as is
393        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        // Limit history if specified
411        if let Some(history) = &mut task_copy.history {
412            let original_size = history.len();
413
414            if limit == 0 {
415                // If limit is 0, remove history entirely
416                #[cfg(feature = "tracing")]
417                tracing::debug!("Removing all history (limit = 0)");
418                task_copy.history = None;
419            } else if history.len() > limit {
420                // If history is longer than limit, truncate it
421                // Keep the most recent messages by removing from the beginning
422                // For example, if history has 10 items and limit is 3, we skip 7 items (10-3)
423                // and keep items 8, 9, and 10
424                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            // Otherwise, if history.len() <= limit, we keep the full history
439        }
440
441        task_copy
442    }
443
444    /// Add an artifact to the task
445    #[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    /// Validate a task (useful after building with builder)
463    #[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        // Validate that kind is "task"
474        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        // Validate message IDs are unique if history exists
483        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        // Validate all messages in history
501        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        // Validate status message if present
513        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}