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//! # `on_*` Methods Block the Loop
18//!
19//! Methods whose names begin with `on_` register callbacks that run inside
20//! the dispatch loop. When your callback is invoked, the loop is blocked
21//! until your callback completes.
22//!
23//! This includes:
24//! - [`on_receive_request`] and [`on_receive_notification`]
25//! - [`on_receiving_result`] and [`on_receiving_ok_result`]
26//! - [`on_session_start`] and [`on_proxy_session_start`]
27//!
28//! This means:
29//! - No other messages are processed while your callback runs
30//! - You can safely do setup before "releasing" control back to the loop
31//! - Messages are processed in the order they arrive
32//!
33//! # Deadlock Risk
34//!
35//! Because `on_*` callbacks block the dispatch loop, it's easy to create
36//! deadlocks. The most common pattern:
37//!
38//! ```ignore
39//! // DEADLOCK: This blocks the loop waiting for a response,
40//! // but the response can't arrive because the loop is blocked!
41//! builder.on_receive_request(async |request: MyRequest, responder, cx| {
42//! let response = cx.send_request(SomeRequest { ... })
43//! .block_task() // <-- Waits for response
44//! .await?; // <-- But response can never arrive!
45//! responder.respond(response)
46//! }, on_receive_request!());
47//! ```
48//!
49//! The response can never arrive because the dispatch loop is blocked waiting
50//! for your callback to complete.
51//!
52//! # `block_task` vs `on_receiving_result`
53//!
54//! When you send a request, you get a [`SentRequest`] with two ways to handle it:
55//!
56//! ## `block_task()` - Acks immediately, you process later
57//!
58//! Use this in spawned tasks where you need to wait for the response:
59//!
60//! ```
61//! # use agent_client_protocol::{Client, Agent, ConnectTo};
62//! # use agent_client_protocol_test::MyRequest;
63//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
64//! # Client.builder().connect_with(transport, async |cx| {
65//! cx.spawn({
66//! let cx = cx.clone();
67//! async move {
68//! // Safe: we're in a spawned task, not blocking the dispatch loop
69//! let response = cx.send_request(MyRequest {})
70//! .block_task()
71//! .await?;
72//! // Process response...
73//! Ok(())
74//! }
75//! })?;
76//! # Ok(())
77//! # }).await?;
78//! # Ok(())
79//! # }
80//! ```
81//!
82//! The dispatch loop continues immediately after delivering the response.
83//! Your code receives the response and can take as long as it wants.
84//!
85//! ## `on_receiving_result()` - Your callback blocks the loop
86//!
87//! Use this when you need ordering guarantees:
88//!
89//! ```
90//! # use agent_client_protocol::{Client, Agent, ConnectTo};
91//! # use agent_client_protocol_test::MyRequest;
92//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
93//! # Client.builder().connect_with(transport, async |cx| {
94//! cx.send_request(MyRequest {})
95//! .on_receiving_result(async |result| {
96//! // Dispatch loop is blocked until this completes
97//! let response = result?;
98//! // Do something with response...
99//! Ok(())
100//! })?;
101//! # Ok(())
102//! # }).await?;
103//! # Ok(())
104//! # }
105//! ```
106//!
107//! The dispatch loop waits for your callback to complete before processing
108//! the next message. Use this when you need to ensure no other messages
109//! are processed until you've handled the response.
110//!
111//! # Escaping the Loop: `spawn`
112//!
113//! Use [`spawn`] to run work outside the dispatch loop:
114//!
115//! ```ignore
116//! builder.on_receive_request(async |request: MyRequest, responder, cx| {
117//! cx.spawn(async move {
118//! // This runs outside the loop - other messages may be processed
119//! let response = cx.send_request(SomeRequest { ... })
120//! .block_task()
121//! .await?;
122//! // ...
123//! Ok(())
124//! })?;
125//! responder.respond(MyResponse { ... }) // Return immediately
126//! }, on_receive_request!());
127//! ```
128//!
129//! # `run_until` Methods
130//!
131//! Methods named `run_until` (like on session builders) run in a spawned task,
132//! so awaiting them won't cause deadlocks:
133//!
134//! ```
135//! # use agent_client_protocol::{Client, Agent, ConnectTo};
136//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
137//! # Client.builder().connect_with(transport, async |cx| {
138//! cx.build_session_cwd()?
139//! .block_task()
140//! .run_until(async |mut session| {
141//! // Safe to await here - we're in a spawned task
142//! session.send_prompt("Hello")?;
143//! let response = session.read_to_string().await?;
144//! Ok(())
145//! })
146//! .await?;
147//! # Ok(())
148//! # }).await?;
149//! # Ok(())
150//! # }
151//! ```
152//!
153//! # Summary
154//!
155//! | Pattern | Blocks Loop? | Use When |
156//! |---------|--------------|----------|
157//! | `on_*` callback | Yes | Quick decisions, need ordering |
158//! | `on_receiving_result` | Yes | Need to process response before next message |
159//! | `block_task()` | No | In spawned tasks, need response value |
160//! | `spawn(...)` | No | Long-running work, don't need ordering |
161//! | `block_task().run_until(...)` | No | Session-scoped work |
162//!
163//! # Next Steps
164//!
165//! - [Proxies and Conductors](super::proxies) - Building message interceptors
166//!
167//! [`on_receive_request`]: crate::Builder::on_receive_request
168//! [`on_receive_notification`]: crate::Builder::on_receive_notification
169//! [`on_receiving_result`]: crate::SentRequest::on_receiving_result
170//! [`on_receiving_ok_result`]: crate::SentRequest::on_receiving_ok_result
171//! [`on_session_start`]: crate::SessionBuilder::on_session_start
172//! [`on_proxy_session_start`]: crate::SessionBuilder::on_proxy_session_start
173//! [`SentRequest`]: crate::SentRequest
174//! [`spawn`]: crate::ConnectionTo::spawn