pub struct SentRequest<T> { /* private fields */ }Expand description
Represents a pending response of type R from an outgoing request.
Returned by ConnectionTo::send_request, this type provides methods for handling
the response without blocking the event loop. The API is intentionally designed to make
it difficult to accidentally block.
§Anti-Footgun Design
You cannot directly .await a SentRequest. Instead, you must choose how to handle
the response:
§Option 1: Schedule a Callback (Safe in Handlers)
Use on_receiving_result to schedule a task
that runs when the response arrives. This doesn’t block the event loop:
cx.send_request(MyRequest {})
.on_receiving_result(async |result| {
match result {
Ok(response) => {
// Handle successful response
Ok(())
}
Err(error) => {
// Handle error
Err(error)
}
}
})?;§Option 2: Block in a Spawned Task (Safe Only in spawn)
Use block_task to block until the response arrives, but only
in a spawned task (never in a handler):
// ✅ Safe: Spawned task runs concurrently
cx.spawn({
let cx = cx.clone();
async move {
let response = cx.send_request(MyRequest {})
.block_task()
.await?;
// Process response...
Ok(())
}
})?;// ❌ NEVER do this in a handler - blocks the event loop!
connection.on_receive_request(async |req: MyRequest, responder, cx| {
let response = cx.send_request(MyRequest {})
.block_task() // This will deadlock!
.await?;
responder.respond(response)
}, agent_client_protocol::on_receive_request!())§Why This Design?
If you block the event loop while waiting for a response, the connection cannot process the incoming response message, creating a deadlock. This API design prevents that footgun by making blocking explicit and encouraging non-blocking patterns.
§Drop Behavior
By default, dropping a SentRequest without consuming it discards the
response when it arrives. When the unstable_cancel_request feature is
enabled, dropping a SentRequest before the SDK has received the response
additionally sends a $/cancel_request notification asking the peer to
cancel the request; requests whose eventual response should be ignored, but
which should keep running on the peer, should use detach
instead.
Implementations§
Source§impl<T> SentRequest<T>
impl<T> SentRequest<T>
Sourcepub fn detach(self)
pub fn detach(self)
Detach this request handle without waiting for its response.
The response will be discarded when it arrives. When the
unstable_cancel_request feature is enabled, this also disarms the
drop-time automatic cancellation described in
Drop Behavior, so use it for requests whose
eventual response should be ignored, but which should keep running on the
peer. The peer is still expected to answer the JSON-RPC request
eventually; use a notification instead when no response is expected at
all.
To ask the peer to stop the request, enable unstable_cancel_request
and call cancel instead, or drop the handle while automatic
cancellation is enabled.
Sourcepub fn cancel(&self) -> Result<(), Error>
Available on crate feature unstable_cancel_request only.
pub fn cancel(&self) -> Result<(), Error>
unstable_cancel_request only.Send a $/cancel_request notification for this outgoing request.
This uses the same peer and message wrapping that were used to send the
original request, so it is the preferred way to cancel a SentRequest
when the request handle is still available.
At most one $/cancel_request is ever sent per request: the first
cancel call sends it (and also prevents the drop-time automatic
cancellation described in Drop Behavior), while
later calls return Ok(()) without sending anything. Likewise, once
the SDK has routed the response to this handle, cancel becomes a
no-op: there is nothing left to cancel.
Errors are only reported by the call that attempts to send the notification.
Sourcepub fn forward_cancellation_from(self, source: RequestCancellation) -> Self
Available on crate feature unstable_cancel_request only.
pub fn forward_cancellation_from(self, source: RequestCancellation) -> Self
unstable_cancel_request only.Forward cancellation of another request to this one.
When the request that source belongs to is cancelled by its peer,
a $/cancel_request for this request is sent to its peer, using the
same wrapping as the original request. The response is still awaited
and delivered as usual (normal data or a cancellation error), so this
composes with block_task and
on_receiving_result.
This is the building block for proxies that forward a request with
custom logic instead of forward_response_to
(which wires this up automatically from its responder). Without it,
custom forwarding absorbs cancellation: the upstream marker is still
set, but nothing is sent downstream.
backend
.send_request(request)
.forward_cancellation_from(responder.cancellation())
.on_receiving_result(async move |result| {
// Custom result handling, e.g. bookkeeping or rewriting.
responder.respond_with_result(result)
})?;May be called multiple times; cancellation of any registered source
triggers the forwarding (at most one $/cancel_request is ever sent
per request). Sources are observed while the response is being
awaited — that is, once the handle is consumed with
block_task,
on_receiving_result, or
forward_response_to; a source that was
already cancelled by then is honored immediately.
Source§impl<T: JsonRpcResponse> SentRequest<T>
impl<T: JsonRpcResponse> SentRequest<T>
Sourcepub fn map<U>(
self,
map_fn: impl Fn(T) -> Result<U, Error> + 'static + Send,
) -> SentRequest<U>
pub fn map<U>( self, map_fn: impl Fn(T) -> Result<U, Error> + 'static + Send, ) -> SentRequest<U>
Create a new response that maps the result of the response to a new type.
Sourcepub fn forward_response_to(self, responder: Responder<T>) -> Result<(), Error>where
T: Send,
pub fn forward_response_to(self, responder: Responder<T>) -> Result<(), Error>where
T: Send,
Forward the response (success or error) to a request context when it arrives.
This is a convenience method for proxying messages between connections. When the response arrives, it will be automatically sent to the provided request context, whether it’s a successful response or an error.
§Example: Proxying requests
// Set up 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 backend and get a context to send to it
let backend_connection = cx.spawn_connection(backend, MockTransport)?;
// Set up proxy that forwards requests to backend
UntypedRole.builder()
.on_receive_request({
let backend_connection = backend_connection.clone();
async move |req: MyRequest, responder, cx| {
// Forward the request to backend and proxy the response back
backend_connection.send_request(req)
.forward_response_to(responder)?;
Ok(())
}
}, agent_client_protocol::on_receive_request!());§Type Safety
The request context’s response type must match the request’s response type, ensuring type-safe message forwarding.
§When to Use
Use this when:
- You’re implementing a proxy or gateway pattern
- You want to forward responses without processing them
- The response types match between the outgoing request and incoming request
This is equivalent to calling on_receiving_result and manually forwarding
the result, with two proxy-specific additions:
- If the pending response is dropped without ever being delivered (for example, the downstream connection closed), the incoming request is answered with an internal error instead of being left unanswered.
- When the
unstable_cancel_requestfeature is enabled and the peer cancels the incoming request, the cancellation is forwarded to the outgoing request, and the downstream response (normal data or a cancellation error) is still forwarded back. This is equivalent to registering the responder’s marker withforward_cancellation_from.
Sourcepub async fn block_task(self) -> Result<T, Error>where
T: Send,
pub async fn block_task(self) -> Result<T, Error>where
T: Send,
Block the current task until the response is received.
Warning: This method blocks the current async task. It is only safe to use
in spawned tasks created with ConnectionTo::spawn. Using it directly in a
handler callback will deadlock the connection.
§Safe Usage (in spawned tasks)
connection.on_receive_request(async |req: MyRequest, responder, cx| {
// Spawn a task to handle the request
cx.spawn({
let connection = cx.clone();
async move {
// Safe: We're in a spawned task, not blocking the event loop
let response = connection.send_request(OtherRequest {})
.block_task()
.await?;
// Process the response...
Ok(())
}
})?;
// Respond immediately
responder.respond(MyResponse { status: "ok".into() })
}, agent_client_protocol::on_receive_request!())§Unsafe Usage (in handlers - will deadlock!)
connection.on_receive_request(async |req: MyRequest, responder, cx| {
// ❌ DEADLOCK: Handler blocks event loop, which can't process the response
let response = cx.send_request(OtherRequest {})
.block_task()
.await?;
responder.respond(MyResponse { status: response.value })
}, agent_client_protocol::on_receive_request!())§When to Use
Use this method when:
- You’re in a spawned task (via
ConnectionTo::spawn) - You need the response value to proceed with your logic
- Linear control flow is more natural than callbacks
For handler callbacks, use on_receiving_result instead.
Sourcepub fn on_receiving_ok_result<F>(
self,
responder: Responder<T>,
task: impl FnOnce(T, Responder<T>) -> F + 'static + Send,
) -> Result<(), Error>
pub fn on_receiving_ok_result<F>( self, responder: Responder<T>, task: impl FnOnce(T, Responder<T>) -> F + 'static + Send, ) -> Result<(), Error>
Schedule an async task to run when a successful response is received.
This is a convenience wrapper around on_receiving_result
for the common pattern of forwarding errors to a request context while only processing
successful responses.
§Behavior
- If the response is
Ok(value), your task receives the value and the request context - If the response is
Err(error), the error is automatically sent toresponderand your task is not called
§Example: Chaining requests
connection.on_receive_request(async |req: ValidateRequest, responder, cx| {
// Send initial request
cx.send_request(ValidateRequest { data: req.data.clone() })
.on_receiving_ok_result(responder, async |validation, responder| {
// Only runs if validation succeeded
if validation.is_valid {
// Respond to original request
responder.respond(ValidateResponse { is_valid: true, error: None })
} else {
responder.respond_with_error(agent_client_protocol::util::internal_error("validation failed"))
}
})?;
Ok(())
}, agent_client_protocol::on_receive_request!())§Ordering
Like on_receiving_result, the callback blocks the
dispatch loop until it completes. See the ordering module
for details.
§When to Use
Use this when:
- You need to respond to a request based on another request’s result
- You want errors to automatically propagate to the request context
- You only care about the success case
For more control over error handling, use on_receiving_result.
Sourcepub fn on_receiving_result<F>(
self,
task: impl FnOnce(Result<T, Error>) -> F + 'static + Send,
) -> Result<(), Error>
pub fn on_receiving_result<F>( self, task: impl FnOnce(Result<T, Error>) -> F + 'static + Send, ) -> Result<(), Error>
Schedule an async task to run when the response is received.
This is the recommended way to handle responses in handler callbacks, as it doesn’t block the event loop. The task will be spawned automatically when the response arrives.
§Example: Handle response in callback
connection.on_receive_request(async |req: MyRequest, responder, cx| {
// Send a request and schedule a callback for the response
cx.send_request(QueryRequest { id: 22 })
.on_receiving_result({
let connection = cx.clone();
async move |result| {
match result {
Ok(response) => {
println!("Got response: {:?}", response);
// Can send more messages here
connection.send_notification(QueryComplete {})?;
Ok(())
}
Err(error) => {
eprintln!("Request failed: {}", error);
Err(error)
}
}
}
})?;
// Handler continues immediately without waiting
responder.respond(MyResponse { status: "processing".into() })
}, agent_client_protocol::on_receive_request!())§Ordering
The callback runs as a spawned task, but the dispatch loop waits for it to complete before processing the next message. This gives you ordering guarantees: no other messages will be processed while your callback runs.
This differs from block_task, which signals completion immediately
upon receiving the response (before your code processes it).
See the ordering module for details on ordering guarantees
and how to avoid deadlocks.
§Error Handling
If the scheduled task returns Err, the entire server will shut down. Make sure to handle
errors appropriately within your task.
§When to Use
Use this method when:
- You’re in a handler callback (not a spawned task)
- You want ordering guarantees (no other messages processed during your callback)
- You need to do async work before “releasing” control back to the dispatch loop
For spawned tasks where you don’t need ordering guarantees, consider block_task.