Skip to content

Commit 97607cc

Browse files
committed
docs: Add migration guide for next release
1 parent ec9ceae commit 97607cc

1 file changed

Lines changed: 300 additions & 0 deletions

File tree

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
# Migrating from `agent-client-protocol` `0.10.x` to `0.11`
2+
3+
This guide explains how to move existing code to the programming model planned for `agent-client-protocol` `0.11`.
4+
5+
Throughout this guide:
6+
7+
- **old API** = `agent-client-protocol` `0.10.x`
8+
- **new API** = the planned `agent-client-protocol` `0.11` API
9+
10+
All code snippets below use the intended `0.11` import paths.
11+
12+
## 1. Move message types under `schema`
13+
14+
The current `0.10.x` crate exports most protocol message types at the crate root.
15+
16+
The `0.11` API moves most ACP request, response, and notification types under `schema`.
17+
18+
```rust
19+
// Old (0.10.x)
20+
use agent_client_protocol as acp;
21+
use acp::{InitializeRequest, NewSessionRequest, PromptRequest, ProtocolVersion};
22+
23+
// New (0.11)
24+
use agent_client_protocol as acp;
25+
use acp::schema::{
26+
InitializeRequest, NewSessionRequest, PromptRequest, ProtocolVersion,
27+
};
28+
```
29+
30+
Most ACP request, response, and notification types live under `schema` in `0.11`
31+
32+
## 2. Replace connection construction
33+
34+
The main construction changes are:
35+
36+
- `ClientSideConnection::new(handler, outgoing, incoming, spawn)`
37+
- becomes `Client.builder().connect_with(ByteStreams::new(outgoing, incoming), async |cx| { ... })`
38+
- `AgentSideConnection::new(handler, outgoing, incoming, spawn)`
39+
- becomes `Agent.builder().connect_to(ByteStreams::new(outgoing, incoming)).await?`
40+
- custom `spawn` function + `handle_io` future
41+
- becomes builder-managed connection execution
42+
43+
If you already have stdin/stdout or socket-like byte streams, wrap them with `ByteStreams::new(outgoing, incoming)`.
44+
45+
If you are spawning subprocess agents, prefer `agent-client-protocol-tokio` over hand-rolled process wiring.
46+
47+
If you already have a reason to stay at the raw request/response level, you can still send `PromptRequest` directly with `cx.send_request(...)`; the session helpers are just the default migration path for most client code.
48+
49+
## 3. Replace outbound trait-style calls with `send_request` and `send_notification`
50+
51+
In the old API, the connection itself implemented the remote trait, so calling the other side looked like this:
52+
53+
```rust
54+
conn.initialize(InitializeRequest::new(ProtocolVersion::V1)).await?;
55+
```
56+
57+
In `0.11`, you send a typed request through `ConnectionTo<Peer>`:
58+
59+
```rust
60+
cx.send_request(InitializeRequest::new(ProtocolVersion::V1))
61+
.block_task()
62+
.await?;
63+
```
64+
65+
The main replacements are:
66+
67+
| Old style | New style |
68+
| ---------------------------------- | ------------------------------------------------------------ |
69+
| `conn.initialize(req).await?` | `cx.send_request(req).block_task().await?` |
70+
| `conn.new_session(req).await?` | usually `cx.build_session(...)` or `cx.build_session_cwd()?` |
71+
| `conn.prompt(req).await?` | usually `session.send_prompt(...)` on an `ActiveSession` |
72+
| `conn.cancel(notification).await?` | `cx.send_notification(notification)?` |
73+
74+
A few behavioral differences matter during migration:
75+
76+
- `send_request(...)` returns a `SentRequest`, not the response directly
77+
- call `.block_task().await?` when you want to wait for the response in a top-level task
78+
- inside `on_receive_*` callbacks, prefer `on_receiving_result(...)`, `on_session_start(...)`, or `cx.spawn(...)` instead of blocking the dispatch loop
79+
80+
## 4. Replace manual session management with `SessionBuilder`
81+
82+
One of the biggest user-facing changes is session handling.
83+
84+
Old code typically looked like this:
85+
86+
```rust
87+
let session = conn
88+
.new_session(NewSessionRequest::new(cwd))
89+
.await?;
90+
91+
conn.prompt(PromptRequest::new(
92+
session.session_id.clone(),
93+
vec!["Hello".into()],
94+
))
95+
.await?;
96+
```
97+
98+
New code usually starts from the connection and uses a session builder:
99+
100+
```rust
101+
cx.build_session_cwd()?
102+
.block_task()
103+
.run_until(async |mut session| {
104+
session.send_prompt("Hello")?;
105+
let output = session.read_to_string().await?;
106+
println!("{output}");
107+
Ok(())
108+
})
109+
.await?;
110+
```
111+
112+
Useful replacements:
113+
114+
| Old pattern | New pattern |
115+
| --------------------------------------------------------- | ------------------------------------------------------------------- |
116+
| `new_session(NewSessionRequest::new(cwd))` | `build_session(cwd)` |
117+
| `new_session(NewSessionRequest::new(current_dir))` | `build_session_cwd()?` |
118+
| store `session_id` and pass it into every `PromptRequest` | let `ActiveSession` manage the session lifecycle |
119+
| `subscribe()` to observe streamed session output | `ActiveSession::read_update()` or `ActiveSession::read_to_string()` |
120+
| intercept and rewrite a `session/new` request in a proxy | `build_session_from(request)` |
121+
122+
Also note:
123+
124+
- use `start_session()` when you want an `ActiveSession<'static, _>` you can keep around
125+
- use `on_session_start(...)` inside `on_receive_*` callbacks when you need to start a session without manually blocking the current task
126+
- use `on_proxy_session_start(...)` or `start_session_proxy(...)` for proxy-style session startup and forwarding
127+
128+
## 5. Replace `Client` trait impls with builder callbacks
129+
130+
In `0.10.x`, your client behavior lived in an `impl acp::Client for T` block.
131+
132+
In `0.11`, register typed handlers on `Client.builder()` instead.
133+
134+
### Common client-side method mapping
135+
136+
- `request_permission` -> `.on_receive_request(|req: RequestPermissionRequest, responder, cx| ...)`
137+
- `write_text_file` -> `.on_receive_request(|req: WriteTextFileRequest, responder, cx| ...)`
138+
- `read_text_file` -> `.on_receive_request(|req: ReadTextFileRequest, responder, cx| ...)`
139+
- `create_terminal` -> `.on_receive_request(|req: CreateTerminalRequest, responder, cx| ...)`
140+
- `terminal_output` -> `.on_receive_request(|req: TerminalOutputRequest, responder, cx| ...)`
141+
- `release_terminal` -> `.on_receive_request(|req: ReleaseTerminalRequest, responder, cx| ...)`
142+
- `wait_for_terminal_exit` -> `.on_receive_request(|req: WaitForTerminalExitRequest, responder, cx| ...)`
143+
- `kill_terminal` -> `.on_receive_request(|req: KillTerminalRequest, responder, cx| ...)`
144+
- `session_notification` -> `.on_receive_notification(|notif: SessionNotification, cx| ...)`
145+
- `ext_method` / `ext_notification` -> your own derived `JsonRpcRequest` / `JsonRpcNotification` types, or a catch-all `on_receive_dispatch(...)`
146+
147+
A small client-side translation looks like this:
148+
149+
```rust
150+
use agent_client_protocol as acp;
151+
use acp::schema::{
152+
RequestPermissionOutcome, RequestPermissionRequest, RequestPermissionResponse,
153+
SessionNotification,
154+
};
155+
156+
#[tokio::main]
157+
async fn main() -> acp::Result<()> {
158+
let transport = todo!("create the transport that connects to your agent");
159+
160+
acp::Client
161+
.builder()
162+
.on_receive_request(
163+
async move |_: RequestPermissionRequest, responder, _cx| {
164+
responder.respond(RequestPermissionResponse::new(
165+
RequestPermissionOutcome::Cancelled,
166+
))
167+
},
168+
acp::on_receive_request!(),
169+
)
170+
.on_receive_notification(
171+
async move |notification: SessionNotification, _cx| {
172+
println!("{:?}", notification.update);
173+
Ok(())
174+
},
175+
acp::on_receive_notification!(),
176+
)
177+
.connect_with(transport, async |cx: acp::ConnectionTo<acp::Agent>| {
178+
// send requests here
179+
Ok(())
180+
})
181+
.await
182+
}
183+
```
184+
185+
## 6. Replace `Agent` trait impls with builder callbacks
186+
187+
The same shift applies on the agent side.
188+
189+
### Common agent-side method mapping
190+
191+
- `initialize` -> `.on_receive_request(|req: InitializeRequest, responder, cx| ...)`
192+
- `authenticate` -> `.on_receive_request(|req: AuthenticateRequest, responder, cx| ...)`
193+
- `new_session` -> `.on_receive_request(|req: NewSessionRequest, responder, cx| ...)`
194+
- `prompt` -> `.on_receive_request(|req: PromptRequest, responder, cx| ...)`
195+
- `cancel` -> `.on_receive_notification(|notif: CancelNotification, cx| ...)`
196+
- `load_session` -> `.on_receive_request(|req: LoadSessionRequest, responder, cx| ...)`
197+
- `set_session_mode` -> `.on_receive_request(|req: SetSessionModeRequest, responder, cx| ...)`
198+
- `set_session_config_option` -> `.on_receive_request(|req: SetSessionConfigOptionRequest, responder, cx| ...)`
199+
- `list_sessions` and other unstable session methods -> request handlers for the corresponding schema type
200+
- `ext_method` / `ext_notification` -> your own derived `JsonRpcRequest` / `JsonRpcNotification` types, or a catch-all `on_receive_dispatch(...)`
201+
202+
A minimal agent skeleton now looks like this:
203+
204+
```rust
205+
use agent_client_protocol as acp;
206+
use acp::schema::{
207+
AgentCapabilities, CancelNotification, InitializeRequest, InitializeResponse,
208+
PromptRequest, PromptResponse, StopReason,
209+
};
210+
211+
#[tokio::main]
212+
async fn main() -> acp::Result<()> {
213+
let outgoing = todo!("create the agent's outgoing byte stream");
214+
let incoming = todo!("create the agent's incoming byte stream");
215+
216+
acp::Agent
217+
.builder()
218+
.name("my-agent")
219+
.on_receive_request(
220+
async move |request: InitializeRequest, responder, _cx| {
221+
responder.respond(
222+
InitializeResponse::new(request.protocol_version)
223+
.agent_capabilities(AgentCapabilities::new()),
224+
)
225+
},
226+
acp::on_receive_request!(),
227+
)
228+
.on_receive_request(
229+
async move |_request: PromptRequest, responder, _cx: acp::ConnectionTo<acp::Client>| {
230+
responder.respond(PromptResponse::new(StopReason::EndTurn))
231+
},
232+
acp::on_receive_request!(),
233+
)
234+
.on_receive_notification(
235+
async move |_notification: CancelNotification, _cx| {
236+
Ok(())
237+
},
238+
acp::on_receive_notification!(),
239+
)
240+
.connect_to(acp::ByteStreams::new(outgoing, incoming))
241+
.await
242+
}
243+
```
244+
245+
If you need a catch-all handler, use `on_receive_dispatch(...)`.
246+
247+
## 7. Replace `subscribe()` with session readers or explicit callbacks
248+
249+
There is no direct connection-level replacement for `ClientSideConnection::subscribe()`.
250+
251+
Choose the replacement based on what you were using it for:
252+
253+
- if you were reading prompt output for one session, prefer `ActiveSession::read_update()` or `ActiveSession::read_to_string()`
254+
- if you were observing inbound notifications generally, register `on_receive_notification(...)`
255+
- if you were forwarding or inspecting raw messages in a proxy, use `on_receive_dispatch(...)` plus `send_proxied_message(...)`
256+
257+
## 8. Remove `LocalSet`, `spawn_local`, and manual I/O tasks
258+
259+
The old crate examples needed a `LocalSet` because the connection futures were `!Send`.
260+
261+
Most migrations to the `0.11` API can remove:
262+
263+
- `tokio::task::LocalSet`
264+
- `tokio::task::spawn_local(...)`
265+
- the custom `spawn` closure passed into connection construction
266+
- the separate `handle_io` future you had to drive manually
267+
268+
When you need concurrency from a handler in `0.11`, use `cx.spawn(...)`.
269+
270+
## 9. Prefer `agent-client-protocol-tokio` for subprocess agents
271+
272+
If your old client code spawned an agent with `tokio::process::Command`, the new stack has a higher-level helper for that:
273+
274+
```rust
275+
use agent_client_protocol as acp;
276+
use agent_client_protocol_tokio::AcpAgent;
277+
use std::str::FromStr;
278+
279+
#[tokio::main]
280+
async fn main() -> acp::Result<()> {
281+
let agent = AcpAgent::from_str("python my_agent.py")?;
282+
283+
acp::Client
284+
.builder()
285+
.name("my-client")
286+
.connect_to(agent)
287+
.await
288+
}
289+
```
290+
291+
You can still use `ByteStreams::new(...)` when you already own the byte streams and do not want the extra helper crate.
292+
293+
## 10. Common gotchas
294+
295+
- `Client` and `Agent` are role markers now, not traits you implement.
296+
- The response type for `send_request(...)` is inferred from the request type.
297+
- `send_request(...)` does not wait by itself; use `.block_task().await?` or `on_receiving_result(...)`.
298+
- Be careful about calling blocking operations from `on_receive_*` callbacks. Those callbacks run in the dispatch loop and preserve message ordering.
299+
- If your old code used `subscribe()` as a global message tap, plan a new strategy around `ActiveSession`, notification callbacks, or proxy dispatch handlers.
300+
- For reusable ACP components, implement `ConnectTo<Role>` instead of trying to recreate the old monolithic trait pattern.

0 commit comments

Comments
 (0)