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}