Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ This repository is the official **Rust SDK** for ACP. It provides crates for bui
Native MCP-over-ACP support is currently opt-in through the core crate's
`unstable_mcp_over_acp` feature. Standalone MCP servers need no ACP transport
feature; the rmcp integration exposes a matching passthrough feature when those
servers are attached to ACP.
servers are attached to ACP. MCP attachment and proxy-session helpers currently
use stable protocol v1.

**Proxy orchestration**

Expand All @@ -40,6 +41,9 @@ servers are attached to ACP.

- **API reference** for individual crates is on [docs.rs/agent-client-protocol](https://docs.rs/agent-client-protocol).
- **Design and architecture documentation** lives in the mdbook at [agentclientprotocol.github.io/rust-sdk](https://agentclientprotocol.github.io/rust-sdk/). Source is in [`md/`](./md/).
- **Draft protocol v2** setup, version-typed connections, and high-level
session usage are covered in
[Protocol V2](./md/protocol-v2.md).

## Integrations

Expand Down
141 changes: 140 additions & 1 deletion md/protocol-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ rules.

By default, `Client.builder()` and `Agent.builder()` continue to expose the
stable v1 API and advertise protocol v1. To use the v2 API for a connection,
construct the builder with `Client.v2()` or `Agent.v2()`:
construct the builder with `Client.v2()` or `Agent.v2()`. Fluent typed handlers,
spawned tasks, close callbacks, and `connect_with` receive
`V2ConnectionTo<_>`, so the protocol version is reflected in the high-level
Rust API as well as on the wire:

```rust
use agent_client_protocol::schema::{ProtocolVersion, v2};
Expand Down Expand Up @@ -73,6 +76,142 @@ When v2 mode is enabled, application code should use types from
exports remain the stable v1 schema. This will likely change as v2 gets closer
to release.

## High-level v2 sessions

Stable callbacks receive `ConnectionTo<_>` and expose the protocol v1
`build_session*`, `SessionBuilder`, `ActiveSession`, and `SessionMessage` APIs.
Callbacks installed through `Client.v2()` receive `V2ConnectionTo<_>` and expose
the v2 `build_session*` and `resume_session*` helpers. The shared names describe
the same lifecycle operations while the connection type selects their schema
and return types at compile time.

Low-level custom `with_handler` and `with_runner` implementations continue to
receive the protocol-neutral `ConnectionTo<_>`, and generic `send_request`
remains schema-agnostic. Runtime compatibility checks remain at those explicit
escape hatches. Dynamic handlers registered through
`V2ConnectionTo::add_dynamic_handler` use the same low-level
`HandleDispatchFrom` interface and therefore also receive `ConnectionTo<_>`.

Nested connections preserve the stable `ConnectionTo` API while still typing
the child implementation's callbacks. On a raw `ConnectionTo<_>`,
`spawn_connection(Client.v2(), transport)` returns a raw `ConnectionTo<_>`;
callbacks installed on that v2 child builder still receive
`V2ConnectionTo<_>`. Existing `spawn_connection::<Role>` calls therefore remain
source-compatible.

When a raw parent also needs a typed handle to the v2 child, the
`unstable_protocol_v2` feature exposes
`ConnectionTo::spawn_connection_with_context`, which returns the context
selected by the child builder. `V2ConnectionTo::spawn_connection` likewise
follows the child builder naturally, so spawning `Client.v2()` through an
already-typed v2 connection returns another `V2ConnectionTo<_>`.

```rust,ignore
use agent_client_protocol::schema::{ProtocolVersion, v2};
use agent_client_protocol::{Client, Responder};

Client
.v2()
.on_receive_notification(
async |update: v2::UpdateSessionNotification, _cx| {
apply_session_update(update)?;
Ok(())
},
agent_client_protocol::on_receive_notification!(),
)
.on_receive_request(
async |request: v2::RequestPermissionRequest,
responder: Responder<v2::RequestPermissionResponse>,
_cx| {
// Transfer the responder to application-owned permission handling
// without waiting for user input in the dispatch callback.
queue_permission_request(request, responder)?;
Ok(())
},
agent_client_protocol::on_receive_request!(),
)
.connect_with(agent_transport, async |cx| {
let initialize = cx
.send_request(v2::InitializeRequest::new(
ProtocolVersion::V2,
v2::Implementation::new("example", "0.1.0"),
))
.block_task()
.await?;
assert!(initialize.capabilities.session.is_some());

let opened = cx
.build_session_cwd()?
.start_session()
.block_task()
.await?;
let (session, new_session_response) = opened.into_parts();
assert_eq!(session.session_id(), &new_session_response.session_id);

session
.send_prompt("What is 2 + 2?")
.block_task()
.await?;
println!("prompt accepted");

Ok(())
})
.await?;
```

Here `apply_session_update` updates application-owned state, while
`queue_permission_request` transfers the request and its responder to a
separate permission workflow.

V2 deliberately separates prompt submission from session observation:

- `session/prompt` returns a `PromptResponse` as soon as the agent accepts the
prompt. `V2Session::send_prompt` returns that request as a
`SentRequest<PromptResponse>`; callers must explicitly await it, register a
response callback, or detach it.
- `V2SessionBuilder::start_session` likewise returns a mapped `SentRequest`.
Its `OpenedV2Session` result keeps the command handle separate from the
complete `NewSessionResponse` represented by the linked schema, rather than
reconstructing a selected subset of its fields.
- `V2Session` is a cloneable command handle containing only the session ID and
connection. It does not own, buffer, or unregister inbound messages.
- Register typed `UpdateSessionNotification` and `RequestPermissionRequest`
handlers on `Client.v2()` before connecting. Updates and interactive requests
are separate protocol lanes; permission handlers should transfer responders
to application-owned work rather than waiting for user input inside the
connection dispatch loop. Without matching handlers, unhandled v2
notifications are ignored and unhandled requests receive a method-not-found
response; they are not retained for a later per-session receiver.
- `session/update` events can arrive before, during, or after a prompt request.
They carry a session ID and entity IDs, but no prompt or turn ID. The SDK
therefore does not attribute intervening events to a locally submitted
prompt or provide a prompt-scoped text accumulator.
- `state_update` describes the session-wide foreground state. `idle` means the
session can accept ordinary new foreground work; it is not a wire-level
boundary assigning previous events to one prompt, and background updates may
continue while idle.
- `cancel_active_work` sends session-wide `session/cancel`. Cancellation
completes after the required `idle` update with stop reason `cancelled`. The
client should immediately mark unfinished tool calls for the active work as
cancelled and must resolve every pending permission request with the
cancelled outcome. Cancelling or dropping the prompt's `SentRequest` is the
separate JSON-RPC request-cancellation mechanism.
- `set_config_option` returns the authoritative replacement option set, and
`close` returns the complete close response. Mutable configuration is not
cached on the command handle.

Install connection handlers before `session/new` and `session/resume` requests.
This is especially important for `resume_session_from`: replay updates
precede the resume response on the wire, so preinstalled typed handlers observe
them in order. If a handler forwards updates to another task, the application
is responsible for any additional projection-drained barrier it needs before
treating replay as locally applied.

Dropping command handles has no network or inbound-routing side effect. If an
application wants stream ergonomics, it can fan typed updates out from the
connection handler with an explicit buffering and subscriber policy. MCP
attachment and proxy-session helpers are still v1-only.

The SDK handles the `initialize` negotiation at the JSON-RPC boundary:

- A v2 client advertises protocol v2 as its latest supported version.
Expand Down
21 changes: 21 additions & 0 deletions src/agent-client-protocol/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,32 @@
`unstable_tool_call_name` feature.
- *(unstable)* Expose v1 and draft-v2 plan operations through the
`unstable_plan_operations` feature.
- *(unstable-v2)* Add high-level protocol v2 session builders and handles with
independent prompt-acceptance requests, create and resume setup responses,
cloneable command handles, configuration, close, and session-wide
cancellation. `Client.v2()` and `Agent.v2()` callbacks receive a
`V2ConnectionTo` whose unversioned `build_session*` and `resume_session*`
methods expose only the v2 lifecycle. Session updates and interactive
requests remain on typed connection handlers; MCP attachment and
proxy-session helpers remain v1-only.
- *(unstable-v2)* Expose protocol-neutral dynamic handler registration on
`V2ConnectionTo`.
- *(unstable-v2)* Add `ConnectionTo::spawn_connection_with_context` for raw
parents that need the context selected by a child builder.
`V2ConnectionTo::spawn_connection` also returns that natural typed context.
The existing `ConnectionTo::spawn_connection::<Role>` API remains
source-compatible and returns a raw `ConnectionTo`, including when the v2
child builder's callbacks receive `V2ConnectionTo`.

### Fixed

- *(unstable-v2)* Preserve unknown initialize fields when the protocol router
hands a same-version connection to its selected implementation.
- *(unstable-v2)* Do not retain unhandled v2 session messages for a dynamic v1
session route that will never be installed.
- Register framework-owned ordered response handling before outbound requests
become visible to the peer, preventing fast responses from overtaking session
routing or proxy forwarding.
- Allow the portable protocol engine and transport abstractions to build for
the `wasm32-wasip1` and `wasm32-wasip2` targets. The native process-backed
`AcpAgent`, thread-backed `Stdio`, and associated `LineDirection` type are not
Expand Down
11 changes: 10 additions & 1 deletion src/agent-client-protocol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ enabling features like tool use, permission requests, and streaming responses.

## Quick Start: Connecting to an Agent

The most common use case is connecting to an existing ACP agent as a client:
The most common use case is connecting to an existing ACP agent as a client.
This quick start uses stable protocol v1:

```rust,no_run
use agent_client_protocol::{AcpAgent, Client, Result};
Expand All @@ -39,6 +40,13 @@ Client.builder()
# }
```

Draft protocol v2 is opt-in through `unstable_protocol_v2`. `Client.v2()` and
`Agent.v2()` callbacks receive a version-typed `V2ConnectionTo` with
high-level, command-only session helpers because prompt acceptance and inbound
traffic are independent. Session updates and interactive requests use typed
connection handlers. See [Protocol V2](https://agentclientprotocol.github.io/rust-sdk/protocol-v2.html#high-level-v2-sessions).
MCP attachment and proxy-session helpers remain v1-only.

## MCP Server Attachment

The runtime-agnostic `mcp_server` module can build and directly serve standalone
Expand All @@ -47,6 +55,7 @@ the `with_mcp_server` builder methods requires `unstable_mcp_over_acp`.
Attached servers are advertised with native `McpServer::Acp` declarations and
communicate through `mcp/connect`, `mcp/message`, and `mcp/disconnect`. Use
`agent-client-protocol-polyfill` immediately before an HTTP-capable agent.
These ACP attachment and proxy paths currently use stable protocol v1.

## Learning More

Expand Down
7 changes: 7 additions & 0 deletions src/agent-client-protocol/src/concepts/sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
//! session, you can send prompts, receive responses, and the agent maintains
//! context across turns.
//!
//! The examples below use the stable protocol v1 `SessionBuilder` and
//! `ActiveSession`. With the `unstable_protocol_v2` feature, callbacks created
//! through `Client.v2()` receive `V2ConnectionTo` and its `build_session*`,
//! `V2SessionBuilder`, and command-only `V2Session` APIs. V2
//! prompt responses acknowledge acceptance independently; receive session-wide
//! updates and interactive requests through typed connection handlers.
//!
//! # Creating a Session
//!
//! Use the session builder to create a new session:
Expand Down
Loading