Skip to main content

a2a_rs/port/
client.rs

1//! The client-side `Transport` port.
2//!
3//! [`Transport`] is the outbound port a client uses to talk to a remote A2A
4//! agent: the application names the capability it needs ("send a message", "get a
5//! task", "subscribe to updates"), and a concrete transport **adapter**
6//! (ConnectRPC, JSON-RPC 2.0, …) fulfils it over the wire. This is the mirror of
7//! the inbound server ports — same hexagonal shape, opposite direction.
8//!
9//! Each adapter reports its wire protocol via [`Transport::protocol`] so a
10//! card-driven negotiator can pick the right one from an agent card's
11//! `supported_interfaces`.
12//!
13//! The port carries no feature gate (hex rule 5 — gate adapters, not ports); it
14//! depends only on the always-available `async-trait`/`futures` and domain types.
15
16use async_trait::async_trait;
17use futures::Stream;
18use std::pin::Pin;
19
20use crate::domain::{
21    A2AError, ListTasksParams, ListTasksResult, Message, SendCompletion, Task,
22    TaskArtifactUpdateEvent, TaskPushNotificationConfig, TaskStatusUpdateEvent,
23};
24
25/// The capability a client needs from a remote A2A agent, independent of wire
26/// protocol. Implemented by each transport adapter (`HttpClient` for ConnectRPC,
27/// `JsonRpcClient` for JSON-RPC 2.0, …).
28#[async_trait]
29pub trait Transport: Send + Sync {
30    /// The wire protocol this transport speaks, matching an agent interface's
31    /// `protocol_binding` (e.g. `"JSONRPC"`, `"CONNECTRPC"`, `"GRPC"`).
32    fn protocol(&self) -> &str;
33
34    /// Send a message, continuing `task_id` or — with `None` — starting a task
35    /// the server names.
36    ///
37    /// The wire makes the id optional, so a caller with nothing to continue
38    /// passes `None` and reads the assigned id off the returned task's `id`
39    /// rather than inventing one client-side. Pass `Some` only to continue a
40    /// task the caller already holds.
41    ///
42    /// `completion` maps to the wire's
43    /// `SendMessageConfiguration.return_immediately`, inverted:
44    /// [`SendCompletion::WhenSettled`] (the spec default) asks the server to
45    /// hold the response until the task settles, so the returned task carries
46    /// the agent's actual answer rather than an acknowledgement.
47    ///
48    /// Pass [`SendCompletion::WhenCreated`] when the caller runs its own
49    /// follow-up — a poll loop or a subscription with its own deadline —
50    /// otherwise it inherits the server's wait on top of its own, and whichever
51    /// is longer wins.
52    async fn send_task_message(
53        &self,
54        task_id: Option<&str>,
55        message: &Message,
56        session_id: Option<&str>,
57        history_length: Option<u32>,
58        completion: SendCompletion,
59    ) -> Result<Task, A2AError>;
60
61    /// Get a task by ID
62    async fn get_task(&self, task_id: &str, history_length: Option<u32>) -> Result<Task, A2AError>;
63
64    /// Cancel a task
65    async fn cancel_task(&self, task_id: &str) -> Result<Task, A2AError>;
66
67    /// Set up push notifications for a task
68    async fn set_task_push_notification(
69        &self,
70        config: &TaskPushNotificationConfig,
71    ) -> Result<TaskPushNotificationConfig, A2AError>;
72
73    /// Get push notification configuration for a task
74    async fn get_task_push_notification(
75        &self,
76        task_id: &str,
77    ) -> Result<TaskPushNotificationConfig, A2AError>;
78
79    /// List tasks with filtering and pagination (v1.0.0)
80    async fn list_tasks(&self, params: &ListTasksParams) -> Result<ListTasksResult, A2AError>;
81
82    /// List all push notification configs for a task (v1.0.0)
83    async fn list_push_notification_configs(
84        &self,
85        task_id: &str,
86    ) -> Result<Vec<TaskPushNotificationConfig>, A2AError>;
87
88    /// Get a specific push notification config by ID (v1.0.0)
89    async fn get_push_notification_config(
90        &self,
91        task_id: &str,
92        config_id: &str,
93    ) -> Result<TaskPushNotificationConfig, A2AError>;
94
95    /// Delete a specific push notification config (v1.0.0)
96    async fn delete_push_notification_config(
97        &self,
98        task_id: &str,
99        config_id: &str,
100    ) -> Result<(), A2AError>;
101
102    /// Subscribe to task updates (for streaming).
103    ///
104    /// Passing `last_event_id = None` is the spec-compliant subscribe: it maps
105    /// to the A2A `SubscribeToTask` call and streams from the task's current
106    /// state — exactly what a spec client expects.
107    ///
108    /// `last_event_id = Some(..)` opts into the a2a-rs **`Last-Event-ID`
109    /// resumption enhancement** (not part of the A2A v1.0 spec): a resumable
110    /// transport sends it as the `Last-Event-ID` request header so an a2a-rs
111    /// server replays the events after that id before streaming live. A
112    /// spec-compliant server ignores the header and simply streams from current
113    /// state, so this stays interoperable either way.
114    async fn subscribe_to_task(
115        &self,
116        task_id: &str,
117        history_length: Option<u32>,
118        last_event_id: Option<&str>,
119    ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent, A2AError>> + Send>>, A2AError>;
120}
121
122/// A streamed [`StreamItem`] tagged with the server's SSE event id (when the
123/// transport supports it). A resilient client records the most recent `event_id`
124/// and echoes it as `Last-Event-ID` on reconnect to resume without gaps.
125///
126/// The `event_id` is part of the a2a-rs resumption enhancement (see
127/// [`subscribe_to_task`](Transport::subscribe_to_task)); spec clients that only
128/// read `item` are unaffected.
129#[derive(Debug, Clone)]
130pub struct StreamEvent {
131    /// The server-assigned per-task event id: the SSE `id:` field on the
132    /// JSON-RPC and REST transports, a namespaced metadata key on ConnectRPC,
133    /// which has no protocol-level field for it. `None` for the initial task
134    /// snapshot and for a spec-compliant server, which sends neither.
135    pub event_id: Option<u64>,
136    /// The update payload.
137    pub item: StreamItem,
138}
139
140impl StreamEvent {
141    /// Construct a stream event.
142    #[inline]
143    pub fn new(event_id: Option<u64>, item: StreamItem) -> Self {
144        Self { event_id, item }
145    }
146
147    /// A stream event with no id (initial snapshot / id-less transport).
148    #[inline]
149    pub fn untagged(item: StreamItem) -> Self {
150        Self {
151            event_id: None,
152            item,
153        }
154    }
155}
156
157/// Items that can be streamed from the server during task subscriptions.
158///
159/// When subscribing to streaming updates for a task, the server can send
160/// different types of items:
161/// - `Task`: The complete initial task state when subscription starts
162/// - `StatusUpdate`: Updates to the task's status (state changes, progress)
163/// - `ArtifactUpdate`: Notifications about new or updated artifacts
164///
165/// This allows clients to receive real-time updates about task progress
166/// and results as they become available.
167#[derive(Debug, Clone)]
168pub enum StreamItem {
169    /// The initial task state
170    Task(Task),
171    /// A task status update
172    StatusUpdate(TaskStatusUpdateEvent),
173    /// A task artifact update
174    ArtifactUpdate(TaskArtifactUpdateEvent),
175}