Expand description
Establishing connections using role types and connection builders.
To communicate over ACP, you need to establish a connection. This involves choosing a role type that matches your role and using a connection builder to configure and run the connection.
§Choosing a Role Type
Your role type determines what messages you can send and who you can send them to. Choose based on what you’re building:
| You are building… | Use this role type |
|---|---|
| A client that talks to an agent | Client |
| An agent that responds to clients | Agent |
| A proxy in a conductor chain | Proxy |
§The Connection Builder Pattern
Every role type has a builder() method that returns a connection builder.
The builder lets you configure handlers, then connect to a transport:
Client.builder()
.name("my-client")
.connect_with(transport, async |cx| {
// Use `cx` to send requests and handle responses
Ok(())
})
.await?;§The Connection Context
Inside connect_with, you receive a ConnectionTo (connection context) that
lets you interact with the remote peer:
// Send a request and wait for the response
let response = cx.send_request(InitializeRequest::new(ProtocolVersion::V1))
.block_task()
.await?;
// Send a notification (fire-and-forget)
cx.send_notification(StatusUpdate { message: "hello".into() })?;§Clean Incoming EOF
Builder::connect_to is reactive: it returns
Ok(()) when the incoming transport reaches clean EOF, after draining
responses and notifications already accepted by its outgoing queue through
the transport sink.
Builder::connect_with is foreground-owned:
EOF fails pending requests, but does not cancel unrelated work in its
closure. This avoids dropping application futures at an arbitrary await
point.
Use ConnectionTo::incoming_closed
to await EOF directly, or Builder::on_close
for cleanup and application-specific shutdown policy:
Client.builder()
.on_close(async |_cx| {
// Notify application-owned work here. Returning an error also
// terminates a still-running connect_with foreground.
Ok(())
})
.connect_with(transport, async |cx| {
cx.incoming_closed().await;
Ok(())
})
.await?;Every request still waiting for a response at EOF is completed with an
internal error whose data contains
{"reason":"incoming_transport_closed","method":"..."}. Requests made
after EOF fail the same way; use
is_incoming_transport_closed to
identify this error. In connect_with, notification and response sends
remain available so applications can choose their own half-close policy;
reactive connect_to stops accepting them when its final drain begins.
Pending requests are failed before close callbacks begin. The close signal
is published after callbacks finish, so a callback must not await
ConnectionTo::incoming_closed
itself.
§Sending Requests
When you call send_request(), you get back a SentRequest that represents
the pending response. You have two main ways to handle it:
§Option 1: Block and wait
Use block_task() when you need the response before continuing:
let response = cx.send_request(MyRequest {})
.block_task()
.await?;
// Use response here§Option 2: Schedule a callback
Use on_receiving_result() when you want to handle the response asynchronously:
cx.send_request(MyRequest {})
.on_receiving_result(async |result| {
match result {
Ok(response) => { /* handle success */ }
Err(error) => { /* handle error */ }
}
Ok(())
})?;
// Continues immediately, callback runs when response arrivesSee Ordering for important details about how these differ.
§Dropping a SentRequest
Dropping a SentRequest before the SDK has received the response sends a
$/cancel_request notification asking the peer to cancel the request, then
discards the response when it arrives. For a request whose eventual response
should be ignored, but which should keep running on the peer, call
SentRequest::detach instead. If no response is expected at all, use a
notification. See the request cancellation chapter
(concepts::cancellation) for details.