1use async_trait::async_trait;
14use buffa::Enumeration;
15use std::pin::Pin;
16
17use crate::{
18 application::{SendOptions, TaskService},
19 domain::{
20 A2AError, AgentCard, SendCompletion, Task, TaskArtifactUpdateEvent, TaskId,
21 TaskPushNotificationConfig, TaskStatusUpdateEvent,
22 generated::{
23 A2aService, CancelTaskRequestView, DeleteTaskPushNotificationConfigRequestView,
24 GetExtendedAgentCardRequestView, GetTaskPushNotificationConfigRequestView,
25 GetTaskRequestView, ListTaskPushNotificationConfigsRequestView,
26 ListTaskPushNotificationConfigsResponse, ListTasksRequest, ListTasksRequestView,
27 ListTasksResponse, SendMessageRequestView, SendMessageResponse, StreamResponse,
28 SubscribeToTaskRequestView, TaskArtifactUpdateEvent as GenTaskArtifactUpdateEvent,
29 TaskPushNotificationConfigView, TaskState,
30 TaskStatusUpdateEvent as GenTaskStatusUpdateEvent, send_message_response,
31 stream_response,
32 },
33 },
34 port::{
35 AsyncMessageHandler, AsyncNotificationManager, AsyncStreamingHandler, AsyncTaskLifecycle,
36 AsyncTaskQuery, SeqEvent, UpdateEvent, streaming_handler::Subscriber,
37 },
38 services::server::AgentInfoProvider,
39};
40
41#[derive(Clone)]
47pub struct ConnectRpcAdapter {
48 service: TaskService,
49}
50
51impl ConnectRpcAdapter {
52 pub fn new(
57 message_handler: impl AsyncMessageHandler + 'static,
58 tasks: impl AsyncTaskLifecycle + AsyncTaskQuery + 'static,
59 notification_manager: impl AsyncNotificationManager + 'static,
60 agent_info: impl AgentInfoProvider + 'static,
61 ) -> Self {
62 Self {
63 service: TaskService::new(
64 message_handler,
65 tasks,
66 notification_manager,
67 agent_info,
68 NoopStreamingHandler,
69 crate::port::NoopPushNotifier,
70 ),
71 }
72 }
73
74 pub fn with_handler(
77 handler: impl AsyncMessageHandler
78 + AsyncTaskLifecycle
79 + AsyncTaskQuery
80 + AsyncNotificationManager
81 + 'static,
82 agent_info: impl AgentInfoProvider + 'static,
83 ) -> Self {
84 Self {
85 service: TaskService::with_handler(
86 handler,
87 agent_info,
88 NoopStreamingHandler,
89 crate::port::NoopPushNotifier,
90 ),
91 }
92 }
93
94 pub fn with_streaming_handler(
96 self,
97 streaming_handler: impl AsyncStreamingHandler + 'static,
98 ) -> Self {
99 Self {
100 service: self.service.with_streaming_handler(streaming_handler),
101 }
102 }
103
104 pub fn with_push_notifier(
106 self,
107 push_notifier: impl crate::port::AsyncPushNotifier + 'static,
108 ) -> Self {
109 Self {
110 service: self.service.with_push_notifier(push_notifier),
111 }
112 }
113}
114
115fn map_err(e: A2AError) -> ::connectrpc::ConnectError {
117 match e {
118 A2AError::TaskNotFound(msg) => {
119 ::connectrpc::ConnectError::new(::connectrpc::ErrorCode::NotFound, msg)
120 }
121 A2AError::InvalidParams(msg) => {
122 ::connectrpc::ConnectError::new(::connectrpc::ErrorCode::InvalidArgument, msg)
123 }
124 A2AError::ValidationError { field, message } => ::connectrpc::ConnectError::new(
125 ::connectrpc::ErrorCode::InvalidArgument,
126 format!("{}: {}", field, message),
127 ),
128 A2AError::UnsupportedOperation(msg) => {
129 ::connectrpc::ConnectError::new(::connectrpc::ErrorCode::Unimplemented, msg)
130 }
131 A2AError::AuthenticatedExtendedCardNotConfigured => ::connectrpc::ConnectError::new(
132 ::connectrpc::ErrorCode::FailedPrecondition,
133 "Authenticated extended card not configured".to_string(),
134 ),
135 A2AError::MethodNotFound(msg) => {
136 ::connectrpc::ConnectError::new(::connectrpc::ErrorCode::Unimplemented, msg)
137 }
138 _ => ::connectrpc::ConnectError::new(::connectrpc::ErrorCode::Internal, e.to_string()),
139 }
140}
141
142fn map_metadata(
144 opt: Option<serde_json::Map<String, serde_json::Value>>,
145) -> ::buffa::MessageField<::buffa_types::google::protobuf::Struct> {
146 if let Some(map) = opt {
147 let val = serde_json::Value::Object(map);
148 if let Ok(struc) = serde_json::from_value::<::buffa_types::google::protobuf::Struct>(val) {
149 return ::buffa::MessageField::some(struc);
150 }
151 }
152 ::buffa::MessageField::none()
153}
154
155fn map_status_update(
156 evt: crate::domain::events::TaskStatusUpdateEvent,
157) -> GenTaskStatusUpdateEvent {
158 GenTaskStatusUpdateEvent {
159 task_id: evt.task_id,
160 context_id: evt.context_id,
161 status: ::buffa::MessageField::some(evt.status),
162 metadata: map_metadata(evt.metadata),
163 ..Default::default()
164 }
165}
166
167fn map_artifact_update(
168 evt: crate::domain::events::TaskArtifactUpdateEvent,
169) -> GenTaskArtifactUpdateEvent {
170 GenTaskArtifactUpdateEvent {
171 task_id: evt.task_id,
172 context_id: evt.context_id,
173 artifact: ::buffa::MessageField::some(evt.artifact),
174 append: evt.append.unwrap_or(false),
175 last_chunk: evt.last_chunk.unwrap_or(false),
176 metadata: map_metadata(evt.metadata),
177 ..Default::default()
178 }
179}
180
181pub(super) fn map_update_event(evt: UpdateEvent) -> StreamResponse {
186 match evt {
187 UpdateEvent::StatusUpdate(event) => StreamResponse {
188 payload: Some(stream_response::Payload::StatusUpdate(Box::new(
189 map_status_update(event),
190 ))),
191 ..Default::default()
192 },
193 UpdateEvent::ArtifactUpdate(event) => StreamResponse {
194 payload: Some(stream_response::Payload::ArtifactUpdate(Box::new(
195 map_artifact_update(event),
196 ))),
197 ..Default::default()
198 },
199 }
200}
201
202impl A2aService for ConnectRpcAdapter {
203 async fn send_message(
204 &self,
205 ctx: ::connectrpc::Context,
206 request: ::buffa::view::OwnedView<SendMessageRequestView<'static>>,
207 ) -> Result<(SendMessageResponse, ::connectrpc::Context), ::connectrpc::ConnectError> {
208 let req = request.to_owned_message();
209 let message = req.message.into_option().ok_or_else(|| {
210 ::connectrpc::ConnectError::new(
211 ::connectrpc::ErrorCode::InvalidArgument,
212 "Missing message".to_string(),
213 )
214 })?;
215 let config = req.configuration.into_option();
216
217 let task_id = message.task_id.clone();
218 let session_id = if message.context_id.is_empty() {
219 None
220 } else {
221 Some(message.context_id.as_str())
222 };
223
224 let task = self
225 .service
226 .send_message(&task_id, &message, session_id, decode_send_config(config))
227 .await
228 .map_err(map_err)?;
229
230 let response = SendMessageResponse {
231 payload: Some(send_message_response::Payload::Task(Box::new(task))),
232 ..Default::default()
233 };
234
235 Ok((response, ctx))
236 }
237
238 #[allow(clippy::result_large_err)]
239 async fn send_streaming_message(
240 &self,
241 ctx: ::connectrpc::Context,
242 request: ::buffa::view::OwnedView<SendMessageRequestView<'static>>,
243 ) -> Result<
244 (
245 ::std::pin::Pin<
246 Box<
247 dyn ::futures::Stream<Item = Result<StreamResponse, ::connectrpc::ConnectError>>
248 + Send,
249 >,
250 >,
251 ::connectrpc::Context,
252 ),
253 ::connectrpc::ConnectError,
254 > {
255 let req = request.to_owned_message();
256 let message = req.message.into_option().ok_or_else(|| {
257 ::connectrpc::ConnectError::new(
258 ::connectrpc::ErrorCode::InvalidArgument,
259 "Missing message".to_string(),
260 )
261 })?;
262 let config = req.configuration.into_option();
263
264 let task_id = message.task_id.clone();
265 let session_id = if message.context_id.is_empty() {
266 None
267 } else {
268 Some(message.context_id.as_str())
269 };
270
271 let opts = decode_send_config(config);
276
277 let (task, update_stream) = self
278 .service
279 .send_streaming_message(
280 &task_id,
281 &message,
282 session_id,
283 opts.push_config,
284 opts.history_limit,
285 )
286 .await
287 .map_err(map_err)?;
288
289 use futures::StreamExt;
290
291 let initial_response = StreamResponse {
292 payload: Some(stream_response::Payload::Task(Box::new(task))),
293 ..Default::default()
294 };
295
296 let mapped_stream =
297 update_stream.map(|item| item.map(|seq| map_update_event(seq.event)).map_err(map_err));
298
299 let chained_stream =
300 futures::stream::once(async { Ok(initial_response) }).chain(mapped_stream);
301
302 Ok((Box::pin(chained_stream), ctx))
303 }
304
305 async fn get_task(
306 &self,
307 ctx: ::connectrpc::Context,
308 request: ::buffa::view::OwnedView<GetTaskRequestView<'static>>,
309 ) -> Result<(Task, ::connectrpc::Context), ::connectrpc::ConnectError> {
310 let req = request.to_owned_message();
311 let history_length = req.history_length.map(|l| l as u32);
312 let id: TaskId = req.id.parse().map_err(map_err)?;
313 let task = self
314 .service
315 .get(&id, history_length)
316 .await
317 .map_err(map_err)?;
318 Ok((task, ctx))
319 }
320
321 async fn list_tasks(
322 &self,
323 ctx: ::connectrpc::Context,
324 request: ::buffa::view::OwnedView<ListTasksRequestView<'static>>,
325 ) -> Result<(ListTasksResponse, ::connectrpc::Context), ::connectrpc::ConnectError> {
326 let req = request.to_owned_message();
327 let params = list_request_to_params(req);
328
329 let result = self.service.list(¶ms).await.map_err(map_err)?;
330
331 let response = ListTasksResponse {
332 tasks: result.tasks,
333 next_page_token: result.next_page_token,
334 page_size: result.page_size,
335 total_size: result.total_size,
336 ..Default::default()
337 };
338
339 Ok((response, ctx))
340 }
341
342 async fn cancel_task(
343 &self,
344 ctx: ::connectrpc::Context,
345 request: ::buffa::view::OwnedView<CancelTaskRequestView<'static>>,
346 ) -> Result<(Task, ::connectrpc::Context), ::connectrpc::ConnectError> {
347 let req = request.to_owned_message();
348 let id: TaskId = req.id.parse().map_err(map_err)?;
349 let task = self.service.cancel(&id).await.map_err(map_err)?;
350 Ok((task, ctx))
351 }
352
353 #[allow(clippy::result_large_err)]
354 async fn subscribe_to_task(
355 &self,
356 ctx: ::connectrpc::Context,
357 request: ::buffa::view::OwnedView<SubscribeToTaskRequestView<'static>>,
358 ) -> Result<
359 (
360 ::std::pin::Pin<
361 Box<
362 dyn ::futures::Stream<Item = Result<StreamResponse, ::connectrpc::ConnectError>>
363 + Send,
364 >,
365 >,
366 ::connectrpc::Context,
367 ),
368 ::connectrpc::ConnectError,
369 > {
370 let req = request.to_owned_message();
371
372 let (initial_task, update_stream) = self
373 .service
374 .subscribe(&req.id, None)
375 .await
376 .map_err(map_err)?;
377
378 use futures::StreamExt;
379
380 let mapped_stream =
381 update_stream.map(|item| item.map(|seq| map_update_event(seq.event)).map_err(map_err));
382
383 if let Some(task) = initial_task {
384 let initial_response = StreamResponse {
385 payload: Some(stream_response::Payload::Task(Box::new(task))),
386 ..Default::default()
387 };
388 let chained_stream =
389 futures::stream::once(async { Ok(initial_response) }).chain(mapped_stream);
390 Ok((Box::pin(chained_stream), ctx))
391 } else {
392 Ok((Box::pin(mapped_stream), ctx))
393 }
394 }
395
396 async fn create_task_push_notification_config(
397 &self,
398 ctx: ::connectrpc::Context,
399 request: ::buffa::view::OwnedView<TaskPushNotificationConfigView<'static>>,
400 ) -> Result<(TaskPushNotificationConfig, ::connectrpc::Context), ::connectrpc::ConnectError>
401 {
402 let config = request.to_owned_message();
403 let created_config = self
404 .service
405 .set_push_config(&config)
406 .await
407 .map_err(map_err)?;
408 Ok((created_config, ctx))
409 }
410
411 async fn get_task_push_notification_config(
412 &self,
413 ctx: ::connectrpc::Context,
414 request: ::buffa::view::OwnedView<GetTaskPushNotificationConfigRequestView<'static>>,
415 ) -> Result<(TaskPushNotificationConfig, ::connectrpc::Context), ::connectrpc::ConnectError>
416 {
417 let req = request.to_owned_message();
418 let params = crate::domain::GetTaskPushNotificationConfigParams {
419 id: req.task_id,
420 push_notification_config_id: Some(req.id),
421 metadata: None,
422 };
423 let config = self
424 .service
425 .get_push_config(¶ms)
426 .await
427 .map_err(map_err)?;
428 Ok((config, ctx))
429 }
430
431 async fn list_task_push_notification_configs(
432 &self,
433 ctx: ::connectrpc::Context,
434 request: ::buffa::view::OwnedView<ListTaskPushNotificationConfigsRequestView<'static>>,
435 ) -> Result<
436 (
437 ListTaskPushNotificationConfigsResponse,
438 ::connectrpc::Context,
439 ),
440 ::connectrpc::ConnectError,
441 > {
442 let req = request.to_owned_message();
443 let params = crate::domain::ListTaskPushNotificationConfigsParams {
444 id: req.task_id,
445 metadata: None,
446 };
447 let configs = self
448 .service
449 .list_push_configs(¶ms)
450 .await
451 .map_err(map_err)?;
452 let response = ListTaskPushNotificationConfigsResponse {
453 configs,
454 ..Default::default()
455 };
456 Ok((response, ctx))
457 }
458
459 async fn get_extended_agent_card(
460 &self,
461 ctx: ::connectrpc::Context,
462 request: ::buffa::view::OwnedView<GetExtendedAgentCardRequestView<'static>>,
463 ) -> Result<(AgentCard, ::connectrpc::Context), ::connectrpc::ConnectError> {
464 let _req = request.to_owned_message();
465 let card = self.service.extended_agent_card().await.map_err(map_err)?;
466 Ok((card, ctx))
467 }
468
469 async fn delete_task_push_notification_config(
470 &self,
471 ctx: ::connectrpc::Context,
472 request: ::buffa::view::OwnedView<DeleteTaskPushNotificationConfigRequestView<'static>>,
473 ) -> Result<
474 (
475 ::buffa_types::google::protobuf::Empty,
476 ::connectrpc::Context,
477 ),
478 ::connectrpc::ConnectError,
479 > {
480 let req = request.to_owned_message();
481 let params = crate::domain::DeleteTaskPushNotificationConfigParams {
482 id: req.task_id,
483 push_notification_config_id: req.id,
484 metadata: None,
485 };
486 self.service
487 .delete_push_config(¶ms)
488 .await
489 .map_err(map_err)?;
490 Ok((::buffa_types::google::protobuf::Empty::default(), ctx))
491 }
492}
493
494pub(super) fn list_request_to_params(req: ListTasksRequest) -> crate::domain::ListTasksParams {
497 crate::domain::ListTasksParams {
498 context_id: if req.context_id.is_empty() {
499 None
500 } else {
501 Some(req.context_id)
502 },
503 status: match req.status.to_i32() {
504 0 => None,
505 val => Some(TaskState::from_i32(val).unwrap_or(TaskState::TASK_STATE_UNSPECIFIED)),
506 },
507 page_size: req.page_size,
508 page_token: if req.page_token.is_empty() {
509 None
510 } else {
511 Some(req.page_token)
512 },
513 history_length: req.history_length,
514 include_artifacts: req.include_artifacts,
515 status_timestamp_after: req.status_timestamp_after.as_option().map(|t| {
516 let dt = chrono::DateTime::<chrono::Utc>::from_timestamp(t.seconds, t.nanos as u32)
517 .unwrap_or_default();
518 dt.to_rfc3339()
519 }),
520 metadata: None,
521 }
522}
523
524pub(super) fn decode_send_config(
536 config: Option<crate::domain::generated::SendMessageConfiguration>,
537) -> SendOptions {
538 let Some(c) = config else {
539 return SendOptions::default();
540 };
541 SendOptions {
542 push_config: c.task_push_notification_config.into_option(),
543 history_limit: c.history_length.map(|limit| limit as u32),
544 completion: if c.return_immediately {
545 SendCompletion::WhenCreated
546 } else {
547 SendCompletion::WhenSettled
548 },
549 }
550}
551
552#[derive(Clone, Debug, Default)]
555pub struct NoopStreamingHandler;
556
557#[async_trait]
558impl AsyncStreamingHandler for NoopStreamingHandler {
559 async fn add_status_subscriber(
560 &self,
561 _task_id: &str,
562 _subscriber: Box<dyn Subscriber<TaskStatusUpdateEvent> + Send + Sync>,
563 ) -> Result<String, A2AError> {
564 Err(A2AError::UnsupportedOperation(
565 "Streaming not supported by this processor".to_string(),
566 ))
567 }
568
569 async fn add_artifact_subscriber(
570 &self,
571 _task_id: &str,
572 _subscriber: Box<dyn Subscriber<TaskArtifactUpdateEvent> + Send + Sync>,
573 ) -> Result<String, A2AError> {
574 Err(A2AError::UnsupportedOperation(
575 "Streaming not supported by this processor".to_string(),
576 ))
577 }
578
579 async fn remove_subscription(&self, _subscription_id: &str) -> Result<(), A2AError> {
580 Ok(())
581 }
582
583 async fn remove_task_subscribers(&self, _task_id: &str) -> Result<(), A2AError> {
584 Ok(())
585 }
586
587 async fn get_subscriber_count(&self, _task_id: &str) -> Result<usize, A2AError> {
588 Ok(0)
589 }
590
591 async fn broadcast_status_update(
592 &self,
593 _task_id: &str,
594 _update: TaskStatusUpdateEvent,
595 ) -> Result<(), A2AError> {
596 Ok(())
597 }
598
599 async fn broadcast_artifact_update(
600 &self,
601 _task_id: &str,
602 _update: TaskArtifactUpdateEvent,
603 ) -> Result<(), A2AError> {
604 Ok(())
605 }
606
607 async fn status_update_stream(
608 &self,
609 _task_id: &str,
610 ) -> Result<
611 Pin<Box<dyn ::futures::Stream<Item = Result<TaskStatusUpdateEvent, A2AError>> + Send>>,
612 A2AError,
613 > {
614 Err(A2AError::UnsupportedOperation(
615 "Streaming not supported by this processor".to_string(),
616 ))
617 }
618
619 async fn artifact_update_stream(
620 &self,
621 _task_id: &str,
622 ) -> Result<
623 Pin<Box<dyn ::futures::Stream<Item = Result<TaskArtifactUpdateEvent, A2AError>> + Send>>,
624 A2AError,
625 > {
626 Err(A2AError::UnsupportedOperation(
627 "Streaming not supported by this processor".to_string(),
628 ))
629 }
630
631 async fn combined_update_stream(
632 &self,
633 _task_id: &str,
634 _from_event_id: Option<u64>,
635 ) -> Result<Pin<Box<dyn ::futures::Stream<Item = Result<SeqEvent, A2AError>> + Send>>, A2AError>
636 {
637 Err(A2AError::UnsupportedOperation(
638 "Streaming not supported by this processor".to_string(),
639 ))
640 }
641}