|
| 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