pub struct ConnectionTo<Counterpart: Role> { /* private fields */ }Expand description
Connection context for sending messages and spawning tasks.
This is the primary handle for interacting with the JSON-RPC connection from within handler callbacks. You can use it to:
- Send requests and notifications to the other side
- Spawn concurrent tasks that run alongside the connection
- Respond to requests (via
Responderwhich wraps this)
§Cloning
ConnectionTo is cheaply cloneable - all clones refer to the same underlying connection.
This makes it easy to share across async tasks.
§Event Loop and Concurrency
Handler callbacks run on the event loop, which means the connection cannot process new
messages while your handler is running. Use spawn to offload any
expensive or blocking work to concurrent tasks.
See the Event Loop and Concurrency section for more details.
Implementations§
Source§impl<Counterpart: Role> ConnectionTo<Counterpart>
impl<Counterpart: Role> ConnectionTo<Counterpart>
Sourcepub fn counterpart(&self) -> Counterpart
pub fn counterpart(&self) -> Counterpart
Return the counterpart role this connection is talking to.
Sourcepub async fn incoming_closed(&self)
pub async fn incoming_closed(&self)
Wait until the incoming transport reaches clean EOF.
Transport closure means that no more messages or responses can arrive.
Pending requests are failed first; this completes after registered
Builder::on_close callbacks finish.
It does not automatically cancel the future passed to
Builder::connect_with; use Builder::on_close when the connection
should run application-specific cleanup or terminate that future.
This never completes if the connection ends some other way; use
terminated to observe every termination.
Sourcepub fn is_incoming_closed(&self) -> bool
pub fn is_incoming_closed(&self) -> bool
Return whether clean incoming-EOF processing has completed.
This remains false while Builder::on_close callbacks are running.
Sourcepub async fn terminated(&self) -> ConnectionTermination
pub async fn terminated(&self) -> ConnectionTermination
Wait until the connection driver has terminated, for any reason.
This is the universal terminal event for a connection driven by
Builder::connect_to or Builder::connect_with. Unlike
incoming_closed, it also completes after
transport errors, handler or close-callback errors, foreground
completion, and a dropped driver future.
The termination is published once, synchronously, as the driver future returns (or is dropped). At that point:
- No further handler, close callback, runner, or
spawned task of this connection will run; each has completed or been dropped. - Every request still awaiting a response has been failed. Requests
failed earlier by clean incoming EOF satisfy
is_incoming_transport_closed; the rest satisfyis_connection_terminated, as do requests sent afterwards.
Do not await this from inside the connection’s own handlers, close callbacks, or tasks: termination happens only after they finish.
Termination does not reap child processes owned by the transport, nor drain application-owned queues.
Sourcepub fn termination(&self) -> Option<ConnectionTermination>
pub fn termination(&self) -> Option<ConnectionTermination>
Return the termination reason if the connection driver has terminated.
See terminated.
Sourcepub fn spawn(
&self,
task: impl IntoFuture<Output = Result<(), Error>, IntoFuture: Send + 'static>,
) -> Result<(), Error>
pub fn spawn( &self, task: impl IntoFuture<Output = Result<(), Error>, IntoFuture: Send + 'static>, ) -> Result<(), Error>
Spawns a task that will run so long as the JSON-RPC connection is being served.
This is the primary mechanism for offloading expensive work from handler callbacks to avoid blocking the event loop. Spawned tasks run concurrently with the connection, allowing the server to continue processing messages.
§Event Loop
Handler callbacks run on the event loop, which cannot process new messages while
your handler is running. Use spawn for any expensive operations:
connection.on_receive_request(async |req: ProcessRequest, responder, cx| {
// Clone cx for the spawned task
cx.spawn({
let connection = cx.clone();
async move {
let result = expensive_operation(&req.data).await?;
connection.send_notification(ProcessComplete { result })?;
Ok(())
}
})?;
// Respond immediately
responder.respond(ProcessResponse { result: "started".into() })
}, agent_client_protocol::on_receive_request!())§Errors
If the spawned task returns an error, the entire server will shut down.
Sourcepub fn spawn_connection<R: Role, P: Protocol>(
&self,
builder: Builder<R, impl HandleDispatchFrom<R::Counterpart> + 'static, impl RunWithConnectionTo<R::Counterpart> + 'static, impl HandleConnectionClose<R::Counterpart> + 'static, P>,
transport: impl ConnectTo<R> + 'static,
) -> Result<P::Connection<R::Counterpart>, Error>
pub fn spawn_connection<R: Role, P: Protocol>( &self, builder: Builder<R, impl HandleDispatchFrom<R::Counterpart> + 'static, impl RunWithConnectionTo<R::Counterpart> + 'static, impl HandleConnectionClose<R::Counterpart> + 'static, P>, transport: impl ConnectTo<R> + 'static, ) -> Result<P::Connection<R::Counterpart>, Error>
Spawn a JSON-RPC connection in the background and return the connection context its builder selects.
This is useful for creating multiple connections that communicate with each other, such as implementing proxy patterns or connecting to multiple backend services.
§Arguments
builder: The connection builder with handlers configuredtransport: The transport component to connect to
§Returns
The connection type that builder’s callbacks receive: a raw
ConnectionTo for a Raw builder such as Builder::new, a
v1::ConnectionTo for
Client.v1(), and a v2::ConnectionTo for
Client.v2(). Convert a typed connection back with into_raw.
§Example: Proxying to a backend connection
// Set up a backend connection builder
let backend = UntypedRole.builder()
.on_receive_request(async |req: MyRequest, responder, _cx| {
responder.respond(MyResponse { status: "ok".into() })
}, agent_client_protocol::on_receive_request!());
// Spawn it and get a context to send requests to it
let backend_connection = cx.spawn_connection(backend, MockTransport)?;
// Now you can forward requests to the backend
let response = backend_connection.send_request(MyRequest {}).block_task().await?;§Example: Spawning a v1 client
let child: v1::ConnectionTo<Agent> = connection.spawn_connection(Client.v1(), transport)?;Sourcepub fn protocol_version(&self) -> Option<ProtocolVersion>
pub fn protocol_version(&self) -> Option<ProtocolVersion>
The ACP protocol version of this connection’s wire guard, or None
when its builder applies no guard, as with Builder::new.
Client.v1(), Agent.v1() and Proxy.v1() connections report
ProtocolVersion::V1, and the
v2() entry points report ProtocolVersion::V2.
Sourcepub fn into_protocol<P: Protocol>(
self,
) -> Result<P::Connection<Counterpart>, Error>
pub fn into_protocol<P: Protocol>( self, ) -> Result<P::Connection<Counterpart>, Error>
The same connection, typed for protocol P.
into_protocol::<V1>() returns a v1::ConnectionTo
(and, with unstable_protocol_v2, into_protocol::<V2>() a
v2::ConnectionTo), whose sends return request handles typed for that
version and which carries that version’s session helpers. Only the
Rust API changes: the connection’s wire guard is the one its builder
selected. The versioned connections convert back with into_raw.
§Errors
Returns an Internal error (-32603) when the builder’s guard selected
a different version than P, for example into_protocol::<V2>() on a
connection from Client.v1(). The mismatch is a local programming
error, not a malformed message from the peer. A
connection whose builder applies no guard (see
protocol_version) converts to every
protocol, and into_protocol::<Raw>() never fails.
fn sessions(connection: ConnectionTo<Agent>) -> Result<v1::ConnectionTo<Agent>, Error> {
connection.into_protocol::<V1>()
}Sourcepub fn send_proxied_message<Req: JsonRpcRequest<Response: Send>, Notif: JsonRpcNotification, Q: Protocol>(
&self,
message: Dispatch<Req, Notif, Q>,
) -> Result<(), Error>where
Counterpart: HasPeer<Counterpart>,
pub fn send_proxied_message<Req: JsonRpcRequest<Response: Send>, Notif: JsonRpcNotification, Q: Protocol>(
&self,
message: Dispatch<Req, Notif, Q>,
) -> Result<(), Error>where
Counterpart: HasPeer<Counterpart>,
Send a request/notification and forward the response appropriately.
The request context’s response type matches the request’s response type, enabling type-safe message forwarding.
Sourcepub fn send_proxied_message_to<Peer: Role, Req: JsonRpcRequest<Response: Send>, Notif: JsonRpcNotification, Q: Protocol>(
&self,
peer: Peer,
message: Dispatch<Req, Notif, Q>,
) -> Result<(), Error>where
Counterpart: HasPeer<Peer>,
pub fn send_proxied_message_to<Peer: Role, Req: JsonRpcRequest<Response: Send>, Notif: JsonRpcNotification, Q: Protocol>(
&self,
peer: Peer,
message: Dispatch<Req, Notif, Q>,
) -> Result<(), Error>where
Counterpart: HasPeer<Peer>,
Send a request/notification and forward the response appropriately.
The request context’s response type matches the request’s response type, enabling type-safe message forwarding.
$/cancel_request notifications are not forwarded: their requestId
refers to a request on the connection they arrived over and would be
meaningless to peer. Cancellation instead propagates hop by hop,
because the responders passed to
forward_response_to observe it
and re-issue the cancellation with the forwarded request’s own ID.
Sourcepub fn send_request<Req: JsonRpcRequest>(
&self,
request: Req,
) -> SentRequest<Req::Response>where
Counterpart: HasPeer<Counterpart>,
pub fn send_request<Req: JsonRpcRequest>(
&self,
request: Req,
) -> SentRequest<Req::Response>where
Counterpart: HasPeer<Counterpart>,
Send an outgoing request and return a SentRequest for handling the reply.
The returned SentRequest makes the response-consumption mode explicit:
on_receiving_result- Register a callback and return immediately. If registered before the response is routed during its original dispatch, the loop waits for the callback to complete.block_task- Wait on the current task until the response arrives. This is only safe when that task already runs outside the dispatch loop.
For callback ordering selected before publication, use
prepare_request instead. Even an immediately
chained callback can race with a fast response on a concurrent connection.
§Anti-Footgun Design
The API intentionally makes it difficult to block on the result directly to prevent the common mistake of blocking the event loop while waiting for a response:
// ❌ This doesn't compile - prevents blocking the event loop
let response = cx.send_request(MyRequest {}).await?;// ✅ Option 1: Register an ordered callback (safe in handlers)
cx.send_request(MyRequest {})
.on_receiving_result(async |result| {
// Handle the response
Ok(())
})?;
// ✅ Option 2: Block in spawned task (safe because task is concurrent)
cx.spawn({
let cx = cx.clone();
async move {
let response = cx.send_request(MyRequest {})
.block_task()
.await?;
// Process response...
Ok(())
}
})?;Send an outgoing request to the default counterpart peer.
This is a convenience method that sends to the counterpart role R.
For explicit control over the target peer, use send_request_to.
Sourcepub fn send_request_to<Peer: Role, Req: JsonRpcRequest>(
&self,
peer: Peer,
request: Req,
) -> SentRequest<Req::Response>where
Counterpart: HasPeer<Peer>,
pub fn send_request_to<Peer: Role, Req: JsonRpcRequest>(
&self,
peer: Peer,
request: Req,
) -> SentRequest<Req::Response>where
Counterpart: HasPeer<Peer>,
Send an outgoing request to a specific peer.
The message will be transformed according to the HasPeer
implementation before being sent.
Sourcepub fn prepare_request<Req: JsonRpcRequest>(
&self,
request: Req,
) -> PreparedRequest<Req::Response>where
Counterpart: HasPeer<Counterpart>,
pub fn prepare_request<Req: JsonRpcRequest>(
&self,
request: Req,
) -> PreparedRequest<Req::Response>where
Counterpart: HasPeer<Counterpart>,
Prepare a request without sending it until response handling is selected.
Unlike send_request, this does not register a
pending reply or enqueue the request. A consuming method on the returned
PreparedRequest publishes it synchronously. Callback-style methods
select ordered consumption before publication, closing the race with a
fast peer response even when the connection runs on another task.
Dropping the prepared request sends nothing. See PreparedRequest for
the available consumption modes and their error and cancellation behavior.
§Examples
connection.prepare_request(MyRequest {}).on_receiving_result(async |result| {
let response = result?;
// Apply bounded response work before later inbound messages.
Ok(())
})?;Sourcepub fn prepare_request_to<Peer: Role, Req: JsonRpcRequest>(
&self,
peer: Peer,
request: Req,
) -> PreparedRequest<Req::Response>where
Counterpart: HasPeer<Peer>,
pub fn prepare_request_to<Peer: Role, Req: JsonRpcRequest>(
&self,
peer: Peer,
request: Req,
) -> PreparedRequest<Req::Response>where
Counterpart: HasPeer<Peer>,
Prepare a request to a specific peer without sending it.
The request is serialized now. A consuming method synchronously registers
its pending reply and enqueues the request; peer transformation and
transmission happen later in the connection driver.
See prepare_request.
Sourcepub fn send_notification<N: JsonRpcNotification>(
&self,
notification: N,
) -> Result<(), Error>where
Counterpart: HasPeer<Counterpart>,
pub fn send_notification<N: JsonRpcNotification>(
&self,
notification: N,
) -> Result<(), Error>where
Counterpart: HasPeer<Counterpart>,
Send an outgoing notification to the default counterpart peer (no reply expected).
Notifications are fire-and-forget messages that don’t have IDs and don’t expect responses. This method sends the notification immediately and returns.
This is a convenience method that sends to the counterpart role R.
For explicit control over the target peer, use send_notification_to.
cx.send_notification(StatusUpdate {
message: "Processing...".into(),
})?;Sourcepub fn send_notification_to<Peer: Role, N: JsonRpcNotification>(
&self,
peer: Peer,
notification: N,
) -> Result<(), Error>where
Counterpart: HasPeer<Peer>,
pub fn send_notification_to<Peer: Role, N: JsonRpcNotification>(
&self,
peer: Peer,
notification: N,
) -> Result<(), Error>where
Counterpart: HasPeer<Peer>,
Send an outgoing notification to a specific peer (no reply expected).
The message will be transformed according to the HasPeer
implementation before being sent.
Sourcepub fn send_cancel_request(
&self,
request_id: impl Into<RawJsonRpcId>,
) -> Result<(), Error>where
Counterpart: HasPeer<Counterpart>,
pub fn send_cancel_request(
&self,
request_id: impl Into<RawJsonRpcId>,
) -> Result<(), Error>where
Counterpart: HasPeer<Counterpart>,
Send a $/cancel_request notification for an arbitrary request ID to
the default counterpart peer.
Prefer SentRequest::cancel when you have the request handle: it
already knows the correct request ID. Use this low-level method only
when implementing custom routing with a request ID that is valid on
this connection.
Sourcepub fn send_cancel_request_to<Peer: Role>(
&self,
_peer: Peer,
request_id: impl Into<RawJsonRpcId>,
) -> Result<(), Error>where
Counterpart: HasPeer<Peer>,
pub fn send_cancel_request_to<Peer: Role>(
&self,
_peer: Peer,
request_id: impl Into<RawJsonRpcId>,
) -> Result<(), Error>where
Counterpart: HasPeer<Peer>,
Send a $/cancel_request notification for an arbitrary request ID to a
specific peer.
$/cancel_request is hop-local, so the notification is always sent
plain, never wrapped in proxy/successor, even to a proxy’s
successor. To cancel a request sent to a successor, pass the ID of the
proxy/successor request on this connection.
Prefer SentRequest::cancel when you have the request handle: it
already knows the correct request ID. Use this low-level method only
when implementing custom routing with a request ID that is valid on
this connection.
Sourcepub fn add_dynamic_handler(
&self,
handler: impl HandleDispatchFrom<Counterpart> + 'static,
) -> Result<DynamicHandlerGuard<Counterpart>, Error>
pub fn add_dynamic_handler( &self, handler: impl HandleDispatchFrom<Counterpart> + 'static, ) -> Result<DynamicHandlerGuard<Counterpart>, Error>
Register a dynamic message handler, used to intercept messages specific to a particular session or some similar modal thing.
Dynamic message handlers run after the handlers registered on Builder and before the
role’s default handler. They receive messages that the builder handlers decline.
The handler will stay registered until the returned registration guard is dropped.
Trait Implementations§
Source§impl<Counterpart: Role> From<ConnectionTo<Counterpart>> for ConnectionTo<Counterpart>
impl<Counterpart: Role> From<ConnectionTo<Counterpart>> for ConnectionTo<Counterpart>
Source§fn from(connection: ConnectionTo<Counterpart>) -> Self
fn from(connection: ConnectionTo<Counterpart>) -> Self
The same as ConnectionTo::into_raw. Generic code over
Protocol uses it to turn a P::Connection<Counterpart> back into
the version-neutral connection.
Source§impl<Counterpart: Role> From<ConnectionTo<Counterpart>> for ConnectionTo<Counterpart>
Available on crate feature unstable_protocol_v2 only.
impl<Counterpart: Role> From<ConnectionTo<Counterpart>> for ConnectionTo<Counterpart>
unstable_protocol_v2 only.Source§fn from(connection: ConnectionTo<Counterpart>) -> Self
fn from(connection: ConnectionTo<Counterpart>) -> Self
The same as ConnectionTo::into_raw. Generic code over
Protocol uses it to turn a P::Connection<Counterpart> back into
the version-neutral connection.