Skip to main content

a2a_rs_server/
handler.rs

1//! Message handler trait for pluggable backends
2//!
3//! This module defines the core abstraction for implementing A2A agent backends.
4//! Implement the `MessageHandler` trait to create your own agent backend.
5
6use a2a_rs_core::{AgentCard, Message, SendMessageResponse, Task};
7use async_trait::async_trait;
8use std::sync::Arc;
9
10/// Result type for handler operations
11pub type HandlerResult<T> = Result<T, HandlerError>;
12
13/// Error type for handler operations
14#[derive(Debug, thiserror::Error)]
15pub enum HandlerError {
16    #[error("Processing failed: {message}")]
17    ProcessingFailed {
18        message: String,
19        #[source]
20        source: Option<Box<dyn std::error::Error + Send + Sync>>,
21    },
22    #[error("Backend unavailable: {message}")]
23    BackendUnavailable {
24        message: String,
25        #[source]
26        source: Option<Box<dyn std::error::Error + Send + Sync>>,
27    },
28    #[error("Authentication required: {0}")]
29    AuthRequired(String),
30    #[error("Invalid input: {0}")]
31    InvalidInput(String),
32    #[error("Internal error: {0}")]
33    Internal(#[from] anyhow::Error),
34}
35
36impl HandlerError {
37    pub fn processing_failed(msg: impl Into<String>) -> Self {
38        Self::ProcessingFailed {
39            message: msg.into(),
40            source: None,
41        }
42    }
43
44    pub fn processing_failed_with<E>(msg: impl Into<String>, source: E) -> Self
45    where
46        E: std::error::Error + Send + Sync + 'static,
47    {
48        Self::ProcessingFailed {
49            message: msg.into(),
50            source: Some(Box::new(source)),
51        }
52    }
53
54    pub fn backend_unavailable(msg: impl Into<String>) -> Self {
55        Self::BackendUnavailable {
56            message: msg.into(),
57            source: None,
58        }
59    }
60
61    pub fn backend_unavailable_with<E>(msg: impl Into<String>, source: E) -> Self
62    where
63        E: std::error::Error + Send + Sync + 'static,
64    {
65        Self::BackendUnavailable {
66            message: msg.into(),
67            source: Some(Box::new(source)),
68        }
69    }
70}
71
72/// Authentication context passed to handlers
73#[derive(Clone, Debug)]
74pub struct AuthContext {
75    pub user_id: String,
76    pub access_token: String,
77    pub metadata: Option<serde_json::Value>,
78}
79
80/// Trait for implementing A2A message handlers
81///
82/// Returns `SendMessageResponse` which can be either a Task or a direct Message.
83#[async_trait]
84pub trait MessageHandler: Send + Sync {
85    /// Process an incoming A2A message and return a Task or Message
86    async fn handle_message(
87        &self,
88        message: Message,
89        auth: Option<AuthContext>,
90    ) -> HandlerResult<SendMessageResponse>;
91
92    /// Return the agent card for this handler
93    fn agent_card(&self, base_url: &str) -> AgentCard;
94
95    /// Optional: Handle task cancellation
96    async fn cancel_task(&self, _task_id: &str) -> HandlerResult<()> {
97        Ok(())
98    }
99
100    /// Optional: Check if handler supports streaming
101    fn supports_streaming(&self) -> bool {
102        false
103    }
104
105    /// Optional: Return an extended agent card for authenticated requests
106    async fn extended_agent_card(
107        &self,
108        _base_url: &str,
109        _auth: &AuthContext,
110    ) -> Option<AgentCard> {
111        None
112    }
113}
114
115/// A simple echo handler for testing and demos
116pub struct EchoHandler {
117    pub prefix: String,
118    pub agent_name: String,
119}
120
121impl Default for EchoHandler {
122    fn default() -> Self {
123        Self {
124            prefix: "echo:".to_string(),
125            agent_name: "Echo Agent".to_string(),
126        }
127    }
128}
129
130#[async_trait]
131impl MessageHandler for EchoHandler {
132    async fn handle_message(
133        &self,
134        message: Message,
135        _auth: Option<AuthContext>,
136    ) -> HandlerResult<SendMessageResponse> {
137        use a2a_rs_core::{now_iso8601, Part, Role, TaskState, TaskStatus};
138        use uuid::Uuid;
139
140        let text = message
141            .parts
142            .iter()
143            .filter_map(|p| p.text.as_deref())
144            .collect::<Vec<_>>()
145            .join("\n");
146
147        let task_id = Uuid::new_v4().to_string();
148        let context_id = message.context_id.clone().unwrap_or_default();
149
150        let response = Message {
151            message_id: Uuid::new_v4().to_string(),
152            context_id: message.context_id.clone(),
153            task_id: None,
154            role: Role::Agent,
155            parts: vec![Part::text(format!("{} {}", self.prefix, text))],
156            metadata: None,
157            extensions: vec![],
158            reference_task_ids: None,
159        };
160
161        Ok(SendMessageResponse::Task(Task {
162            id: task_id,
163            context_id,
164            status: TaskStatus {
165                state: TaskState::Completed,
166                message: None,
167                timestamp: Some(now_iso8601()),
168            },
169            history: Some(vec![message, response]),
170            artifacts: None,
171            metadata: None,
172        }))
173    }
174
175    fn agent_card(&self, base_url: &str) -> AgentCard {
176        use a2a_rs_core::{AgentCapabilities, AgentInterface, AgentProvider, AgentSkill, PROTOCOL_VERSION};
177
178        AgentCard {
179            name: self.agent_name.clone(),
180            description: "Simple echo agent for testing A2A protocol".to_string(),
181            supported_interfaces: vec![AgentInterface {
182                url: format!("{}/v1/rpc", base_url),
183                protocol_binding: "JSONRPC".to_string(),
184                protocol_version: PROTOCOL_VERSION.to_string(),
185                tenant: None,
186            }],
187            provider: Some(AgentProvider {
188                organization: "A2A Demo".to_string(),
189                url: "https://github.com/a2a-protocol".to_string(),
190            }),
191            version: PROTOCOL_VERSION.to_string(),
192            documentation_url: None,
193            capabilities: AgentCapabilities::default(),
194            security_schemes: Default::default(),
195            security_requirements: vec![],
196            default_input_modes: vec!["text/plain".to_string()],
197            default_output_modes: vec!["text/plain".to_string()],
198            skills: vec![AgentSkill {
199                id: "echo".to_string(),
200                name: "Echo".to_string(),
201                description: "Echoes back the user's message".to_string(),
202                tags: vec!["demo".to_string(), "echo".to_string()],
203                examples: vec![],
204                input_modes: vec![],
205                output_modes: vec![],
206                security_requirements: vec![],
207            }],
208            signatures: vec![],
209            icon_url: None,
210        }
211    }
212}
213
214/// Type alias for a boxed handler
215pub type BoxedHandler = Arc<dyn MessageHandler>;