1#[cfg(feature = "mcp-client")]
7use crate::core::McpClientManager;
8use crate::core::config::{AgentConfig, ConfigError, StorageConfig};
9use crate::core::runtime::AgentRuntime;
10use a2a_rs::domain::{
11 A2AError, Task, TaskArtifactUpdateEvent, TaskPushNotificationConfig, TaskState,
12 TaskStatusUpdateEvent,
13};
14use a2a_rs::port::{
15 AsyncMessageHandler, AsyncNotificationManager, AsyncStreamingHandler, AsyncTaskManager,
16 StreamingSubscriber, UpdateEvent,
17};
18use a2a_rs::{HttpPushNotificationSender, InMemoryTaskStorage};
19use async_trait::async_trait;
20use futures::Stream;
21use std::path::Path;
22use std::pin::Pin;
23use std::sync::Arc;
24#[cfg(feature = "mcp-client")]
25use tracing::info;
26
27#[cfg(feature = "sqlx")]
28use a2a_rs::adapter::storage::SqlxTaskStorage;
29
30#[derive(Clone)]
33pub enum AutoStorage {
34 InMemory(InMemoryTaskStorage),
35 #[cfg(feature = "sqlx")]
36 Sqlx(SqlxTaskStorage),
37}
38
39#[async_trait]
40impl AsyncTaskManager for AutoStorage {
41 async fn create_task(&self, task_id: &str, context_id: &str) -> Result<Task, A2AError> {
42 match self {
43 AutoStorage::InMemory(s) => s.create_task(task_id, context_id).await,
44 #[cfg(feature = "sqlx")]
45 AutoStorage::Sqlx(s) => s.create_task(task_id, context_id).await,
46 }
47 }
48
49 async fn get_task(&self, task_id: &str, history_length: Option<u32>) -> Result<Task, A2AError> {
50 match self {
51 AutoStorage::InMemory(s) => s.get_task(task_id, history_length).await,
52 #[cfg(feature = "sqlx")]
53 AutoStorage::Sqlx(s) => s.get_task(task_id, history_length).await,
54 }
55 }
56
57 async fn update_task_status(
58 &self,
59 task_id: &str,
60 state: TaskState,
61 message: Option<a2a_rs::domain::Message>,
62 ) -> Result<Task, A2AError> {
63 match self {
64 AutoStorage::InMemory(s) => s.update_task_status(task_id, state, message).await,
65 #[cfg(feature = "sqlx")]
66 AutoStorage::Sqlx(s) => s.update_task_status(task_id, state, message).await,
67 }
68 }
69
70 async fn cancel_task(&self, task_id: &str) -> Result<Task, A2AError> {
71 match self {
72 AutoStorage::InMemory(s) => s.cancel_task(task_id).await,
73 #[cfg(feature = "sqlx")]
74 AutoStorage::Sqlx(s) => s.cancel_task(task_id).await,
75 }
76 }
77
78 async fn task_exists(&self, task_id: &str) -> Result<bool, A2AError> {
79 match self {
80 AutoStorage::InMemory(s) => s.task_exists(task_id).await,
81 #[cfg(feature = "sqlx")]
82 AutoStorage::Sqlx(s) => s.task_exists(task_id).await,
83 }
84 }
85}
86
87#[async_trait]
88impl AsyncNotificationManager for AutoStorage {
89 async fn set_task_notification(
90 &self,
91 config: &TaskPushNotificationConfig,
92 ) -> Result<TaskPushNotificationConfig, A2AError> {
93 match self {
94 AutoStorage::InMemory(s) => s.set_task_notification(config).await,
95 #[cfg(feature = "sqlx")]
96 AutoStorage::Sqlx(s) => s.set_task_notification(config).await,
97 }
98 }
99
100 async fn get_task_notification(
101 &self,
102 task_id: &str,
103 ) -> Result<TaskPushNotificationConfig, A2AError> {
104 match self {
105 AutoStorage::InMemory(s) => s.get_task_notification(task_id).await,
106 #[cfg(feature = "sqlx")]
107 AutoStorage::Sqlx(s) => s.get_task_notification(task_id).await,
108 }
109 }
110
111 async fn remove_task_notification(&self, task_id: &str) -> Result<(), A2AError> {
112 match self {
113 AutoStorage::InMemory(s) => s.remove_task_notification(task_id).await,
114 #[cfg(feature = "sqlx")]
115 AutoStorage::Sqlx(s) => s.remove_task_notification(task_id).await,
116 }
117 }
118}
119
120impl AutoStorage {
121 pub async fn from_config(config: &StorageConfig) -> Result<Self, BuildError> {
123 match config {
124 StorageConfig::InMemory => {
125 let push_sender = a2a_rs::adapter::HttpPushNotificationSender::new()
126 .with_timeout(30)
127 .with_max_retries(3);
128 let storage = a2a_rs::InMemoryTaskStorage::with_push_sender(push_sender);
129 Ok(AutoStorage::InMemory(storage))
130 }
131 #[cfg(feature = "sqlx")]
132 StorageConfig::Sqlx {
133 url,
134 enable_logging,
135 ..
136 } => {
137 if *enable_logging {
138 tracing::info!("SQL query logging enabled");
139 }
140
141 let storage = a2a_rs::adapter::storage::SqlxTaskStorage::new(url)
142 .await
143 .map_err(|e| {
144 BuildError::StorageError(format!("Failed to create SQLx storage: {}", e))
145 })?;
146
147 Ok(AutoStorage::Sqlx(storage))
148 }
149 #[cfg(not(feature = "sqlx"))]
150 StorageConfig::Sqlx { .. } => Err(BuildError::StorageError(
151 "SQLx storage requested but 'sqlx' feature is not enabled".to_string(),
152 )),
153 }
154 }
155}
156
157#[async_trait]
158impl AsyncStreamingHandler for AutoStorage {
159 async fn add_status_subscriber(
160 &self,
161 task_id: &str,
162 subscriber: Box<dyn StreamingSubscriber<TaskStatusUpdateEvent> + Send + Sync>,
163 ) -> Result<String, A2AError> {
164 match self {
165 AutoStorage::InMemory(s) => s.add_status_subscriber(task_id, subscriber).await,
166 #[cfg(feature = "sqlx")]
167 AutoStorage::Sqlx(s) => s.add_status_subscriber(task_id, subscriber).await,
168 }
169 }
170
171 async fn add_artifact_subscriber(
172 &self,
173 task_id: &str,
174 subscriber: Box<dyn StreamingSubscriber<TaskArtifactUpdateEvent> + Send + Sync>,
175 ) -> Result<String, A2AError> {
176 match self {
177 AutoStorage::InMemory(s) => s.add_artifact_subscriber(task_id, subscriber).await,
178 #[cfg(feature = "sqlx")]
179 AutoStorage::Sqlx(s) => s.add_artifact_subscriber(task_id, subscriber).await,
180 }
181 }
182
183 async fn remove_subscription(&self, subscription_id: &str) -> Result<(), A2AError> {
184 match self {
185 AutoStorage::InMemory(s) => s.remove_subscription(subscription_id).await,
186 #[cfg(feature = "sqlx")]
187 AutoStorage::Sqlx(s) => s.remove_subscription(subscription_id).await,
188 }
189 }
190
191 async fn remove_task_subscribers(&self, task_id: &str) -> Result<(), A2AError> {
192 match self {
193 AutoStorage::InMemory(s) => s.remove_task_subscribers(task_id).await,
194 #[cfg(feature = "sqlx")]
195 AutoStorage::Sqlx(s) => s.remove_task_subscribers(task_id).await,
196 }
197 }
198
199 async fn get_subscriber_count(&self, task_id: &str) -> Result<usize, A2AError> {
200 match self {
201 AutoStorage::InMemory(s) => s.get_subscriber_count(task_id).await,
202 #[cfg(feature = "sqlx")]
203 AutoStorage::Sqlx(s) => s.get_subscriber_count(task_id).await,
204 }
205 }
206
207 async fn broadcast_status_update(
208 &self,
209 task_id: &str,
210 update: TaskStatusUpdateEvent,
211 ) -> Result<(), A2AError> {
212 match self {
213 AutoStorage::InMemory(s) => s.broadcast_status_update(task_id, update).await,
214 #[cfg(feature = "sqlx")]
215 AutoStorage::Sqlx(s) => s.broadcast_status_update(task_id, update).await,
216 }
217 }
218
219 async fn broadcast_artifact_update(
220 &self,
221 task_id: &str,
222 update: TaskArtifactUpdateEvent,
223 ) -> Result<(), A2AError> {
224 match self {
225 AutoStorage::InMemory(s) => s.broadcast_artifact_update(task_id, update).await,
226 #[cfg(feature = "sqlx")]
227 AutoStorage::Sqlx(s) => s.broadcast_artifact_update(task_id, update).await,
228 }
229 }
230
231 async fn status_update_stream(
232 &self,
233 task_id: &str,
234 ) -> Result<Pin<Box<dyn Stream<Item = Result<TaskStatusUpdateEvent, A2AError>> + Send>>, A2AError>
235 {
236 match self {
237 AutoStorage::InMemory(s) => s.status_update_stream(task_id).await,
238 #[cfg(feature = "sqlx")]
239 AutoStorage::Sqlx(s) => s.status_update_stream(task_id).await,
240 }
241 }
242
243 async fn artifact_update_stream(
244 &self,
245 task_id: &str,
246 ) -> Result<
247 Pin<Box<dyn Stream<Item = Result<TaskArtifactUpdateEvent, A2AError>> + Send>>,
248 A2AError,
249 > {
250 match self {
251 AutoStorage::InMemory(s) => s.artifact_update_stream(task_id).await,
252 #[cfg(feature = "sqlx")]
253 AutoStorage::Sqlx(s) => s.artifact_update_stream(task_id).await,
254 }
255 }
256
257 async fn combined_update_stream(
258 &self,
259 task_id: &str,
260 ) -> Result<Pin<Box<dyn Stream<Item = Result<UpdateEvent, A2AError>> + Send>>, A2AError> {
261 match self {
262 AutoStorage::InMemory(s) => s.combined_update_stream(task_id).await,
263 #[cfg(feature = "sqlx")]
264 AutoStorage::Sqlx(s) => s.combined_update_stream(task_id).await,
265 }
266 }
267}
268
269pub struct AgentBuilder<H = (), S = ()> {
271 config: AgentConfig,
272 handler: Option<H>,
273 storage: Option<S>,
274}
275
276impl AgentBuilder<(), ()> {
277 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
279 let config = AgentConfig::from_file(path)?;
280 Ok(Self {
281 config,
282 handler: None,
283 storage: None,
284 })
285 }
286
287 pub fn from_toml(toml: &str) -> Result<Self, ConfigError> {
289 let config = AgentConfig::from_toml(toml)?;
290 Ok(Self {
291 config,
292 handler: None,
293 storage: None,
294 })
295 }
296
297 pub fn new(config: AgentConfig) -> Self {
299 Self {
300 config,
301 handler: None,
302 storage: None,
303 }
304 }
305}
306
307impl<H, S> AgentBuilder<H, S> {
308 pub fn with_handler<NewH>(self, handler: NewH) -> AgentBuilder<NewH, S>
310 where
311 NewH: AsyncMessageHandler + Clone + Send + Sync + 'static,
312 {
313 AgentBuilder {
314 config: self.config,
315 handler: Some(handler),
316 storage: self.storage,
317 }
318 }
319
320 pub fn with_storage<NewS>(self, storage: NewS) -> AgentBuilder<H, NewS>
322 where
323 NewS: AsyncTaskManager + AsyncNotificationManager + Clone + Send + Sync + 'static,
324 {
325 AgentBuilder {
326 config: self.config,
327 handler: self.handler,
328 storage: Some(storage),
329 }
330 }
331
332 pub fn config(&self) -> &AgentConfig {
334 &self.config
335 }
336
337 pub fn with_config<F>(mut self, f: F) -> Self
339 where
340 F: FnOnce(&mut AgentConfig),
341 {
342 f(&mut self.config);
343 self
344 }
345}
346
347impl<H, S> AgentBuilder<H, S>
348where
349 H: AsyncMessageHandler + Clone + Send + Sync + 'static,
350 S: AsyncTaskManager + AsyncNotificationManager + Clone + Send + Sync + 'static,
351{
352 pub fn build(self) -> Result<AgentRuntime<H, S>, BuildError> {
354 let handler = self.handler.ok_or(BuildError::MissingHandler)?;
355 let storage = self.storage.ok_or(BuildError::MissingStorage)?;
356
357 Ok(AgentRuntime::new(
358 self.config,
359 Arc::new(handler),
360 Arc::new(storage),
361 ))
362 }
363}
364
365impl<H> AgentBuilder<H, ()>
366where
367 H: AsyncMessageHandler + Clone + Send + Sync + 'static,
368{
369 pub async fn build_with_auto_storage(self) -> Result<AgentRuntime<H, AutoStorage>, BuildError> {
373 let handler = self.handler.ok_or(BuildError::MissingHandler)?;
374
375 let storage = match &self.config.server.storage {
376 StorageConfig::InMemory => {
377 let push_sender = HttpPushNotificationSender::new()
378 .with_timeout(30)
379 .with_max_retries(3);
380 let storage = InMemoryTaskStorage::with_push_sender(push_sender);
381 AutoStorage::InMemory(storage)
382 }
383 #[cfg(feature = "sqlx")]
384 StorageConfig::Sqlx {
385 url,
386 enable_logging,
387 ..
388 } => {
389 if *enable_logging {
390 tracing::info!("SQL query logging enabled");
391 }
392
393 let storage = SqlxTaskStorage::new(url).await.map_err(|e| {
394 BuildError::StorageError(format!("Failed to create SQLx storage: {}", e))
395 })?;
396
397 AutoStorage::Sqlx(storage)
398 }
399 #[cfg(not(feature = "sqlx"))]
400 StorageConfig::Sqlx { .. } => {
401 return Err(BuildError::StorageError(
402 "SQLx storage requested but 'sqlx' feature is not enabled".to_string(),
403 ));
404 }
405 };
406
407 #[cfg(feature = "mcp-client")]
409 if self.config.features.mcp_client.enabled {
410 info!("Initializing MCP client...");
411 let mcp_client = McpClientManager::new();
412
413 if let Err(e) = mcp_client
415 .initialize(&self.config.features.mcp_client)
416 .await
417 {
418 return Err(BuildError::RuntimeError(format!(
419 "Failed to initialize MCP client: {}",
420 e
421 )));
422 }
423
424 return Ok(AgentRuntime::with_mcp_client(
425 self.config,
426 Arc::new(handler),
427 Arc::new(storage),
428 mcp_client,
429 ));
430 }
431
432 Ok(AgentRuntime::new(
433 self.config,
434 Arc::new(handler),
435 Arc::new(storage),
436 ))
437 }
438
439 #[cfg(feature = "sqlx")]
442 pub async fn build_with_auto_storage_and_migrations(
443 self,
444 migrations: &'static [&'static str],
445 ) -> Result<AgentRuntime<H, AutoStorage>, BuildError> {
446 let handler = self.handler.ok_or(BuildError::MissingHandler)?;
447
448 let storage = match &self.config.server.storage {
449 StorageConfig::InMemory => {
450 tracing::warn!(
451 "Migrations provided but using in-memory storage - migrations ignored"
452 );
453 let push_sender = HttpPushNotificationSender::new()
454 .with_timeout(30)
455 .with_max_retries(3);
456 let storage = InMemoryTaskStorage::with_push_sender(push_sender);
457 AutoStorage::InMemory(storage)
458 }
459 StorageConfig::Sqlx {
460 url,
461 enable_logging,
462 ..
463 } => {
464 if *enable_logging {
465 tracing::info!("SQL query logging enabled");
466 }
467
468 let storage = SqlxTaskStorage::with_migrations(url, migrations)
469 .await
470 .map_err(|e| {
471 BuildError::StorageError(format!("Failed to create SQLx storage: {}", e))
472 })?;
473
474 AutoStorage::Sqlx(storage)
475 }
476 };
477
478 #[cfg(feature = "mcp-client")]
480 if self.config.features.mcp_client.enabled {
481 info!("Initializing MCP client...");
482 let mcp_client = McpClientManager::new();
483
484 if let Err(e) = mcp_client
486 .initialize(&self.config.features.mcp_client)
487 .await
488 {
489 return Err(BuildError::RuntimeError(format!(
490 "Failed to initialize MCP client: {}",
491 e
492 )));
493 }
494
495 return Ok(AgentRuntime::with_mcp_client(
496 self.config,
497 Arc::new(handler),
498 Arc::new(storage),
499 mcp_client,
500 ));
501 }
502
503 Ok(AgentRuntime::new(
504 self.config,
505 Arc::new(handler),
506 Arc::new(storage),
507 ))
508 }
509}
510
511#[derive(Debug, thiserror::Error)]
513pub enum BuildError {
514 #[error("Handler must be set before building")]
515 MissingHandler,
516
517 #[error("Storage must be set before building")]
518 MissingStorage,
519
520 #[error("Configuration error: {0}")]
521 ConfigError(#[from] ConfigError),
522
523 #[error("Storage error: {0}")]
524 StorageError(String),
525
526 #[error("Runtime error: {0}")]
527 RuntimeError(String),
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 #[test]
535 fn test_builder_from_toml() {
536 let toml = r#"
537 [agent]
538 name = "Test Agent"
539
540 [server]
541 http_port = 9000
542 "#;
543
544 let builder = AgentBuilder::from_toml(toml).unwrap();
545 assert_eq!(builder.config().agent.name, "Test Agent");
546 assert_eq!(builder.config().server.http_port, 9000);
547 }
548
549 #[test]
550 fn test_builder_config_modification() {
551 let toml = r#"
552 [agent]
553 name = "Test Agent"
554 "#;
555
556 let builder = AgentBuilder::from_toml(toml)
557 .unwrap()
558 .with_config(|config| {
559 config.server.http_port = 7000;
560 });
561
562 assert_eq!(builder.config().server.http_port, 7000);
563 }
564}