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(&self, _base_url: &str, _auth: &AuthContext) -> Option<AgentCard> {
107        None
108    }
109}
110
111/// A simple echo handler for testing and demos
112pub struct EchoHandler {
113    pub prefix: String,
114    pub agent_name: String,
115}
116
117impl Default for EchoHandler {
118    fn default() -> Self {
119        Self {
120            prefix: "echo:".to_string(),
121            agent_name: "Echo Agent".to_string(),
122        }
123    }
124}
125
126#[async_trait]
127impl MessageHandler for EchoHandler {
128    async fn handle_message(
129        &self,
130        message: Message,
131        _auth: Option<AuthContext>,
132    ) -> HandlerResult<SendMessageResponse> {
133        use a2a_rs_core::{now_iso8601, Part, Role, TaskState, TaskStatus};
134        use uuid::Uuid;
135
136        let text = message
137            .parts
138            .iter()
139            .filter_map(|p| p.as_text())
140            .collect::<Vec<_>>()
141            .join("\n");
142
143        let task_id = Uuid::new_v4().to_string();
144        let context_id = message.context_id.clone().unwrap_or_default();
145
146        let response = Message {
147            kind: "message".to_string(),
148            message_id: Uuid::new_v4().to_string(),
149            context_id: message.context_id.clone(),
150            task_id: None,
151            role: Role::Agent,
152            parts: vec![Part::text(format!("{} {}", self.prefix, text))],
153            metadata: None,
154            extensions: vec![],
155            reference_task_ids: None,
156        };
157
158        Ok(SendMessageResponse::Task(Task {
159            kind: "task".to_string(),
160            id: task_id,
161            context_id,
162            status: TaskStatus {
163                state: TaskState::Completed,
164                message: None,
165                timestamp: Some(now_iso8601()),
166            },
167            history: Some(vec![message, response]),
168            artifacts: None,
169            metadata: None,
170        }))
171    }
172
173    fn agent_card(&self, base_url: &str) -> AgentCard {
174        use a2a_rs_core::{
175            AgentCapabilities, AgentInterface, AgentProvider, AgentSkill, PROTOCOL_VERSION,
176        };
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>;