agent_client_protocol/concepts/connections.rs
1//! Establishing connections using role types and connection builders.
2//!
3//! To communicate over ACP, you need to establish a connection. This involves
4//! choosing a **role type** that matches your role and using a **connection builder**
5//! to configure and run the connection.
6//!
7//! # Choosing a Role Type
8//!
9//! Your role type determines what messages you can send and who you can send them to.
10//! Choose based on what you're building:
11//!
12//! | You are building... | Use this role type |
13//! |---------------------|-------------------|
14//! | A client that talks to an agent | [`Client`] |
15//! | An agent that responds to clients | [`Agent`] |
16//! | A proxy in a conductor chain | [`Proxy`] |
17//!
18//! # The Connection Builder Pattern
19//!
20//! Every role type has a `builder()` method that returns a connection builder.
21//! The builder lets you configure handlers, then connect to a transport:
22//!
23//! ```
24//! # use agent_client_protocol::{Client, Agent, ConnectTo};
25//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
26//! Client.builder()
27//! .name("my-client")
28//! .connect_with(transport, async |cx| {
29//! // Use `cx` to send requests and handle responses
30//! Ok(())
31//! })
32//! .await?;
33//! # Ok(())
34//! # }
35//! ```
36//!
37//! # The Connection Context
38//!
39//! Inside `connect_with`, you receive a [`ConnectionTo`] (connection context) that
40//! lets you interact with the remote peer:
41//!
42//! ```
43//! # use agent_client_protocol::{Client, Agent, ConnectTo};
44//! # use agent_client_protocol::schema::{ProtocolVersion, v1::InitializeRequest};
45//! # use agent_client_protocol_test::StatusUpdate;
46//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
47//! # Client.builder().connect_with(transport, async |cx| {
48//! // Send a request and wait for the response
49//! let response = cx.send_request(InitializeRequest::new(ProtocolVersion::V1))
50//! .block_task()
51//! .await?;
52//!
53//! // Send a notification (fire-and-forget)
54//! cx.send_notification(StatusUpdate { message: "hello".into() })?;
55//! # Ok(())
56//! # }).await?;
57//! # Ok(())
58//! # }
59//! ```
60//!
61//! # Clean Incoming EOF
62//!
63//! [`Builder::connect_to`](crate::Builder::connect_to) is reactive: it returns
64//! `Ok(())` when the incoming transport reaches clean EOF, after draining
65//! responses and notifications already accepted by its outgoing queue through
66//! the transport sink.
67//! [`Builder::connect_with`](crate::Builder::connect_with) is foreground-owned:
68//! EOF fails pending requests, but does not cancel unrelated work in its
69//! closure. This avoids dropping application futures at an arbitrary await
70//! point.
71//!
72//! Use [`ConnectionTo::incoming_closed`](crate::ConnectionTo::incoming_closed)
73//! to await EOF directly, or [`Builder::on_close`](crate::Builder::on_close)
74//! for cleanup and application-specific shutdown policy:
75//!
76//! ```
77//! # use agent_client_protocol::{Client, ConnectTo, Error};
78//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), Error> {
79//! Client.builder()
80//! .on_close(async |_cx| {
81//! // Notify application-owned work here. Returning an error also
82//! // terminates a still-running connect_with foreground.
83//! Ok(())
84//! })
85//! .connect_with(transport, async |cx| {
86//! cx.incoming_closed().await;
87//! Ok(())
88//! })
89//! .await?;
90//! # Ok(())
91//! # }
92//! ```
93//!
94//! Every request still waiting for a response at EOF is completed with an
95//! internal error whose data contains
96//! `{"reason":"incoming_transport_closed","method":"..."}`. Requests made
97//! after EOF fail the same way; use
98//! [`is_incoming_transport_closed`](crate::is_incoming_transport_closed) to
99//! identify this error. In `connect_with`, notification and response sends
100//! remain available so applications can choose their own half-close policy;
101//! reactive `connect_to` stops accepting them when its final drain begins.
102//!
103//! Pending requests are failed before close callbacks begin. The close signal
104//! is published after callbacks finish, so a callback must not await
105//! [`ConnectionTo::incoming_closed`](crate::ConnectionTo::incoming_closed)
106//! itself.
107//!
108//! # Connection Termination
109//!
110//! Clean EOF is only one way a connection ends. Close callbacks do not run,
111//! and `incoming_closed` never completes, after a transport or handler error,
112//! foreground completion, or a dropped driver.
113//! [`ConnectionTo::terminated`](crate::ConnectionTo::terminated) completes for
114//! all of them with a [`ConnectionTermination`](crate::ConnectionTermination)
115//! reason, once the driver future has returned or been dropped:
116//!
117//! ```
118//! # use agent_client_protocol::{Client, ConnectTo, ConnectionTermination, Error};
119//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), Error> {
120//! let (connection_tx, connection_rx) = futures::channel::oneshot::channel();
121//! let driver = Client.builder().connect_with(transport, async |cx| {
122//! drop(connection_tx.send(cx.clone()));
123//! // ... drive the session ...
124//! Ok(())
125//! });
126//! let observer = async {
127//! let Ok(cx) = connection_rx.await else { return };
128//! match cx.terminated().await {
129//! ConnectionTermination::IncomingClosed => { /* peer hung up */ }
130//! termination => {
131//! let _error = termination.error();
132//! // Run the same disconnect cleanup for every other reason.
133//! }
134//! }
135//! };
136//! let (result, ()) = futures::join!(driver, observer);
137//! result
138//! # }
139//! ```
140//!
141//! By then no handler, close callback, runner, or spawned task of the
142//! connection runs again, and every request still awaiting a response has
143//! failed. Requests failed by clean EOF keep the `incoming_transport_closed`
144//! error; the rest, and any sent afterwards, satisfy
145//! [`is_connection_terminated`](crate::is_connection_terminated). Dropping the
146//! driver performs only this synchronous bookkeeping: no asynchronous cleanup
147//! runs after an arbitrary drop. Termination is not ordered with application
148//! queues and does not reap child processes owned by the transport.
149//!
150//! # Sending Requests
151//!
152//! When you call `send_request()`, you get back a [`SentRequest`] that represents
153//! the pending response. You have two main ways to handle it:
154//!
155//! ## Option 1: Block and wait
156//!
157//! Use `block_task()` when you need the response before continuing:
158//!
159//! ```
160//! # use agent_client_protocol::{Client, Agent, ConnectTo};
161//! # use agent_client_protocol_test::MyRequest;
162//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
163//! # Client.builder().connect_with(transport, async |cx| {
164//! let response = cx.send_request(MyRequest {})
165//! .block_task()
166//! .await?;
167//! // Use response here
168//! # Ok(())
169//! # }).await?;
170//! # Ok(())
171//! # }
172//! ```
173//!
174//! ## Option 2: Schedule a callback
175//!
176//! Use `prepare_request(...).on_receiving_result(...)` to register an ordered
177//! response callback before sending:
178//!
179//! ```
180//! # use agent_client_protocol::{Client, Agent, ConnectTo};
181//! # use agent_client_protocol_test::MyRequest;
182//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
183//! # Client.builder().connect_with(transport, async |cx| {
184//! cx.prepare_request(MyRequest {})
185//! .on_receiving_result(async |result| {
186//! match result {
187//! Ok(response) => { /* handle success */ }
188//! Err(error) => { /* handle error */ }
189//! }
190//! Ok(())
191//! })?;
192//! // Continues immediately, callback runs when response arrives
193//! # Ok(())
194//! # }).await?;
195//! # Ok(())
196//! # }
197//! ```
198//!
199//! See [Ordering](super::ordering) for important details about how these differ.
200//! Eager `send_request(...).on_receiving_result(...)` remains available, but can
201//! race with a fast response when called outside the connection future.
202//! `prepare_request(...).block_task()` and `.detach()` publish during their
203//! method calls without selecting ordered consumption. Dropping an unconsumed
204//! [`PreparedRequest`](crate::PreparedRequest) sends nothing.
205//!
206//! ## Dropping a `SentRequest`
207//!
208//! Dropping a [`SentRequest`] before the SDK has received the response sends a
209//! `$/cancel_request` notification asking the peer to cancel the request, then
210//! discards the response when it arrives. For a request whose eventual response
211//! should be ignored, but which should keep running on the peer, call
212//! [`SentRequest::detach`] instead. If no response is expected at all, use a
213//! notification. See the request cancellation chapter
214//! ([`concepts::cancellation`](super::cancellation)) for details.
215//!
216//! # Next Steps
217//!
218//! - [Sessions](super::sessions) - Create multi-turn conversations
219//! - [Callbacks](super::callbacks) - Handle incoming requests from the remote peer
220//!
221//! [`Client`]: crate::Client
222//! [`Agent`]: crate::Agent
223//! [`Proxy`]: crate::Proxy
224//! [`ConnectionTo`]: crate::ConnectionTo
225//! [`SentRequest`]: crate::SentRequest
226//! [`SentRequest::detach`]: crate::SentRequest::detach