agent_client_protocol/concepts/ordering.rs
1//! Message ordering, concurrency, and the dispatch loop.
2//!
3//! Understanding how agent-client-protocol processes messages is key to writing correct code.
4//! This chapter explains the dispatch loop and the ordering guarantees you
5//! can rely on.
6//!
7//! # The Dispatch Loop
8//!
9//! Each connection has a central **dispatch loop** that processes incoming
10//! messages one at a time. When a message arrives, it is passed to your
11//! handlers in order until one claims it.
12//!
13//! The key property: **the dispatch loop waits for each handler to complete
14//! before processing the next message.** This gives you sequential ordering
15//! guarantees within a single connection.
16//!
17//! # Ordered Callbacks Hold the Loop
18//!
19//! Request and notification callbacks registered with [`on_receive_request`]
20//! and [`on_receive_notification`] run inside the dispatch loop. The loop is
21//! blocked until the callback completes.
22//!
23//! Registering [`on_receiving_result`] or [`on_receiving_ok_result`] returns
24//! immediately. If registration happens before a peer response is routed
25//! during its original dispatch, response handling then holds an ordering
26//! barrier until the callback completes. A pending-request failure delivered
27//! without an incoming response, or a response routed later, does not carry
28//! that barrier.
29//!
30//! Session-start helpers are two-phase: [`on_session_start`] and
31//! [`on_proxy_session_start`] perform framework-owned session setup under this
32//! ordering guarantee, then invoke the user callback in a spawned task so it
33//! can consume later session traffic. No user callback code runs under the
34//! session-setup ordering guarantee.
35//!
36//! While a callback holds the dispatch loop, this means:
37//! - No other messages are processed while your callback runs
38//! - You can safely do setup before "releasing" control back to the loop
39//! - Messages are processed in the order they arrive
40//!
41//! # Deadlock Risk
42//!
43//! Because callbacks can hold the dispatch loop, it's easy to create deadlocks.
44//! The most common pattern:
45//!
46//! ```ignore
47//! // DEADLOCK: This blocks the loop waiting for a response,
48//! // but the response can't arrive because the loop is blocked!
49//! builder.on_receive_request(async |request: MyRequest, responder, cx| {
50//! let response = cx.send_request(SomeRequest { ... })
51//! .block_task() // <-- Waits for response
52//! .await?; // <-- But response can never arrive!
53//! responder.respond(response)
54//! }, on_receive_request!());
55//! ```
56//!
57//! The response can never arrive because the dispatch loop is blocked waiting
58//! for your callback to complete.
59//!
60//! # `block_task` vs `on_receiving_result`
61//!
62//! When you send a request, you get a [`SentRequest`] with two ways to handle it:
63//!
64//! ## `block_task()` - Does not hold dispatch while you process
65//!
66//! Use this from a task that already runs outside the dispatch loop, such as a
67//! foreground `connect_with` future or a spawned task:
68//!
69//! ```
70//! # use agent_client_protocol::{Client, Agent, ConnectTo};
71//! # use agent_client_protocol_test::MyRequest;
72//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
73//! # Client.builder().connect_with(transport, async |cx| {
74//! cx.spawn({
75//! let cx = cx.clone();
76//! async move {
77//! // Safe: we're in a spawned task, not blocking the dispatch loop
78//! let response = cx.send_request(MyRequest {})
79//! .block_task()
80//! .await?;
81//! // Process response...
82//! Ok(())
83//! }
84//! })?;
85//! # Ok(())
86//! # }).await?;
87//! # Ok(())
88//! # }
89//! ```
90//!
91//! The dispatch loop continues immediately after delivering the response.
92//! Your code receives the response and can take as long as it wants.
93//!
94//! ## `on_receiving_result()` - A peer response callback can hold the loop
95//!
96//! Use this when you need ordering guarantees:
97//!
98//! ```
99//! # use agent_client_protocol::{Client, Agent, ConnectTo};
100//! # use agent_client_protocol_test::MyRequest;
101//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
102//! # Client.builder().connect_with(transport, async |cx| {
103//! cx.send_request(MyRequest {})
104//! .on_receiving_result(async |result| {
105//! // A timely peer response holds dispatch until this completes
106//! let response = result?;
107//! // Do something with response...
108//! Ok(())
109//! })?;
110//! # Ok(())
111//! # }).await?;
112//! # Ok(())
113//! # }
114//! ```
115//!
116//! Register the callback before a peer response is routed to select this
117//! ordered mode. The dispatch loop then waits for your callback before
118//! processing the next message. A pending-request failure delivered without
119//! an incoming response (such as EOF), a response that was already routed, or
120//! one that an interceptor retains and routes after its original dispatch does
121//! not carry the barrier.
122//!
123//! An ordered callback must not wait for later inbound traffic on the same
124//! connection. Spawn that follow-up work and return from the callback so the
125//! loop can continue.
126//!
127//! # Escaping the Loop: `spawn`
128//!
129//! Use [`spawn`] to run work outside the dispatch loop:
130//!
131//! ```ignore
132//! builder.on_receive_request(async |request: MyRequest, responder, cx| {
133//! cx.spawn(async move {
134//! // This runs outside the loop - other messages may be processed
135//! let response = cx.send_request(SomeRequest { ... })
136//! .block_task()
137//! .await?;
138//! // ...
139//! Ok(())
140//! })?;
141//! responder.respond(MyResponse { ... }) // Return immediately
142//! }, on_receive_request!());
143//! ```
144//!
145//! # Blocking Session Methods
146//!
147//! `SessionBuilder::run_until` does not spawn its caller. It waits for the
148//! session response on the current task, so call it only when that task already
149//! runs outside the dispatch loop—for example, from the foreground future
150//! passed to `connect_with` or from a task created with [`spawn`]. Awaiting it
151//! from a message handler deadlocks just like awaiting `block_task()` there.
152//!
153//! ```
154//! # use agent_client_protocol::{Client, Agent, ConnectTo};
155//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
156//! # Client.builder().connect_with(transport, async |cx| {
157//! cx.build_session_cwd()?
158//! .block_task()
159//! .run_until(async |mut session| {
160//! // Safe: connect_with's foreground future runs outside the dispatch loop
161//! session.send_prompt("Hello")?;
162//! let response = session.read_to_string().await?;
163//! Ok(())
164//! })
165//! .await?;
166//! # Ok(())
167//! # }).await?;
168//! # Ok(())
169//! # }
170//! ```
171//!
172//! # Summary
173//!
174//! | Pattern | Blocks Loop? | Use When |
175//! |---------|--------------|----------|
176//! | `on_receive_*` callback | Yes | Handle one incoming message |
177//! | `on_receiving_*` callback | For a timely peer response | Bounded response work that needs ordering |
178//! | `on_session_start` / `on_proxy_session_start` | Setup only | Install session routing, then run session work concurrently |
179//! | `block_task()` | If awaited in a handler | Wait for a response from outside the dispatch loop |
180//! | `spawn(...)` | No | Long-running work, don't need ordering |
181//! | `block_task().run_until(...)` | If called in a handler | Session-scoped work from outside the dispatch loop |
182//!
183//! # Next Steps
184//!
185//! - [Proxies and Conductors](super::proxies) - Building message interceptors
186//!
187//! [`on_receive_request`]: crate::Builder::on_receive_request
188//! [`on_receive_notification`]: crate::Builder::on_receive_notification
189//! [`on_receiving_result`]: crate::SentRequest::on_receiving_result
190//! [`on_receiving_ok_result`]: crate::SentRequest::on_receiving_ok_result
191//! [`on_session_start`]: crate::SessionBuilder::on_session_start
192//! [`on_proxy_session_start`]: crate::SessionBuilder::on_proxy_session_start
193//! [`SentRequest`]: crate::SentRequest
194//! [`spawn`]: crate::ConnectionTo::spawn