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
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ repository = "https://github.com/agentclientprotocol/rust-sdk"
[workspace.dependencies]
# Internal crates
agent-client-protocol = { path = "src/agent-client-protocol", version = "3.6.0" }
agent-client-protocol-derive = { path = "src/agent-client-protocol-derive", version = "3.6.0" }
# Exact pin: the derive output calls `agent_client_protocol::__private`, which
# only matches the derive crate released with the same core version.
agent-client-protocol-derive = { path = "src/agent-client-protocol-derive", version = "=3.6.0" }
agent-client-protocol-polyfill = { path = "src/agent-client-protocol-polyfill", version = "3.6.0" }
agent-client-protocol-rmcp = { path = "src/agent-client-protocol-rmcp", version = "4.0.6" }
agent-client-protocol-test = { path = "src/agent-client-protocol-test", default-features = false }
Expand Down
100 changes: 98 additions & 2 deletions md/migration-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ and `"1"` stay distinct, and integers keep the full 64-bit signed range.
| Before | After |
| --- | --- |
| `RawJsonRpcMessage::Request(schema::v1::Request<RawJsonRpcParams>)` | `RawJsonRpcMessage::Request(RawJsonRpcRequest)`, with the same `id`, `method` and `params` fields |
| `RawJsonRpcMessage::Notification(schema::v1::Notification<RawJsonRpcParams>)` | `RawJsonRpcMessage::Notification(RawJsonRpcNotification)`, with the same `method` and `params` fields |
| `RawJsonRpcResponse`, an alias of `schema::v1::Response<Value, Box<RawJsonRpcError>>` | `RawJsonRpcResponse`, the SDK's own enum with the same `Result { id, result }` and `Error { id, error }` variants and `new` |
| `RawJsonRpcMessage::response_id()` returned `Option<&v1::RequestId>` | returns `Option<&RawJsonRpcId>` |

Expand Down Expand Up @@ -727,6 +728,76 @@ callbacks receive (`ConnectionTo` for `Raw`, `v1::ConnectionTo` for `V1`,
`v2::ConnectionTo` for `V2`), so code generic over a builder's context changes
`Context: ConnectionContext` to `P: Protocol`.

## Schema enums and SDK helpers

### v1 extension methods keep their `_` prefix

Parsing a v1 dispatch enum (`schema::v1::ClientRequest`, `AgentRequest`,
`ClientNotification` or `AgentNotification`) from an extension method used to
strip the leading `_`: a parsed `_vendor/x` reported `method()` as `vendor/x`
and was sent on as `vendor/x`, which the peer answered with Method not found.
The `ExtMethodRequest` and `ExtNotification` variants now keep the method
exactly as it was on the wire, as v2 already did. Code that matched on
`ext.method` without the prefix matches the full name instead:

```rust,ignore
// Before
v1::ClientRequest::ExtMethodRequest(ext) if &*ext.method == "vendor/x" => ...
// After
v1::ClientRequest::ExtMethodRequest(ext) if &*ext.method == "_vendor/x" => ...
```

### Removed `util` helpers

`util::internal_error(message)` and `util::parse_error(message)` duplicated the
`Error` constructors and are removed. Build the error directly:

```rust,ignore
// Before
Err(util::internal_error(format!("lost {id}")))
result.map_err(util::internal_error)?;

// After
Err(Error::internal_error().data(format!("lost {id}")))
result.map_err(Error::into_internal_error)?; // any `std::error::Error`
```

`util::TypeNotification` is removed. It duplicated
`MatchDispatch::if_notification` for notifications. Wrap the message in
`Dispatch::Notification` and use `MatchDispatch`, which returns a parse error
for malformed params instead of logging and dropping them:

```rust,ignore
// Before
TypeNotification::new(message)
.handle_if(async |notif: SessionNotification| { ... })
.await
.otherwise(async |untyped| { ... })
.await

// After
MatchDispatch::new(Dispatch::Notification(message))
.if_notification(async |notif: SessionNotification| { ... })
.await
.otherwise(async |dispatch| { ... })
.await
```

`agent-client-protocol` now depends on the `agent-client-protocol-derive`
version released with it exactly (`=`), because the derive output calls
hidden SDK internals. Depend on the derives through `agent_client_protocol`
rather than adding the derive crate separately.

### MCP-over-ACP error codes are `ErrorCode`s

`mcp_server::MCP_SERVER_UNAVAILABLE` (`-33001`) and
`mcp_server::MCP_BACKEND_FAILURE` (`-33002`) are now `ErrorCode` constants
instead of `i32`, and `mcp_server::MCP_RESOURCE_LIMIT_EXCEEDED` (`-33000`) is
new. `Error::new(MCP_BACKEND_FAILURE, ..)` still compiles. Compare with
`error.code == MCP_BACKEND_FAILURE` directly instead of through
`i32::from(error.code)`, and use `.value()` where you need an `i32` (for
example, against a JSON `code`).

## Proxy method names

The proxy protocol now uses the method names defined by the
Expand All @@ -740,8 +811,9 @@ extensions.
| `_proxy/successor` | `proxy/successor` |

The message shapes are unchanged. `proxy/successor` still flattens the inner
ACP message into its params and sends optional metadata as `_meta` (`meta` is
also accepted on receive).
ACP message into its params and sends optional metadata as `_meta`. The
non-spec `meta` key is no longer read on receive; see
[`SuccessorMessage` and `InitializeProxyRequest` are non-exhaustive](#successormessage-and-initializeproxyrequest-are-non-exhaustive).

### Upgrade conductors and proxies together

Expand All @@ -757,6 +829,30 @@ Code that uses the SDK types (`schema::v1::InitializeProxyRequest`, formerly
the `Proxy` and `Conductor` roles) needs no changes for the rename. Update any hard-coded method
strings, such as in raw JSON-RPC handlers, test fixtures, or trace filters.

### `SuccessorMessage` and `InitializeProxyRequest` are non-exhaustive

`schema::SuccessorMessage`, `schema::v1::InitializeProxyRequest` and
`schema::v2::InitializeProxyRequest` are `#[non_exhaustive]`, so the proxy
protocol can add fields in a minor release. Their fields stay public for
reading, but outside the SDK you build them with constructors:

```rust,ignore
// Before
let message = SuccessorMessage { message: request, meta: None };
let init = v1::InitializeProxyRequest { initialize };

// After
let message = SuccessorMessage::new(request);
let message = SuccessorMessage::new(request).meta(meta);
let init = v1::InitializeProxyRequest::new(initialize); // or initialize.into()
```

`SuccessorMessage::meta` is now `Option<serde_json::Map<String, Value>>`
instead of `Option<Value>`, because ACP `_meta` is always an object. A
`proxy/successor` request whose `_meta` is not an object is accepted and
its `_meta` is ignored, and the non-spec `meta` key is ignored instead of
being read as `_meta`.

### `ConductorHostRole` is sealed

`agent-client-protocol-conductor::ConductorHostRole` can no longer be
Expand Down
38 changes: 19 additions & 19 deletions src/agent-client-protocol-conductor/src/conductor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,9 +461,9 @@ impl<Host: ConductorHostRole> ConductorImpl<Host> {
conductor_tx: conductor_tx.clone(),
initialization: InitializationState::Pending(self.instantiator),
proxies: Vec::default(),
successor: Arc::new(agent_client_protocol::util::internal_error(
"successor not initialized",
)),
successor: Arc::new(
agent_client_protocol::Error::internal_error().data("successor not initialized"),
),
trace_handle,
host: self.host.clone(),
};
Expand Down Expand Up @@ -496,7 +496,7 @@ impl<Host: ConductorHostRole> ConductorImpl<Host> {
message,
})
.await
.map_err(agent_client_protocol::util::internal_error)
.map_err(agent_client_protocol::Error::into_internal_error)
}

async fn incoming_message_from_agent(
Expand All @@ -509,7 +509,7 @@ impl<Host: ConductorHostRole> ConductorImpl<Host> {
message,
})
.await
.map_err(agent_client_protocol::util::internal_error)
.map_err(agent_client_protocol::Error::into_internal_error)
}
}

Expand Down Expand Up @@ -753,10 +753,7 @@ where
if source_component_index == 0 {
client_connection.send_request_to(Client, request)
} else {
self.proxies[source_component_index - 1].send_request(SuccessorMessage {
message: request,
meta: None,
})
self.proxies[source_component_index - 1].send_request(SuccessorMessage::new(request))
}
}

Expand Down Expand Up @@ -789,10 +786,8 @@ where
target_proxy = source_component_index - 1,
"Sending notification wrapped as SuccessorMessage to proxy"
);
self.proxies[source_component_index - 1].send_notification(SuccessorMessage {
message: notification,
meta: None,
})
self.proxies[source_component_index - 1]
.send_notification(SuccessorMessage::new(notification))
}
}

Expand Down Expand Up @@ -1040,7 +1035,10 @@ where
message: dispatch.map(|r, cx| (r.message, cx), |n| n.message),
})
.await
.map_err(agent_client_protocol::util::internal_error)
.map_err(|e| {
agent_client_protocol::Error::internal_error()
.data(e.to_string())
})
})
.await
.otherwise(async |dispatch| {
Expand All @@ -1064,10 +1062,9 @@ where
),
message: dispatch,
};
conductor_tx
.send(message)
.await
.map_err(agent_client_protocol::util::internal_error)
conductor_tx.send(message).await.map_err(|e| {
agent_client_protocol::Error::internal_error().data(e.to_string())
})
})
.await
},
Expand Down Expand Up @@ -1554,7 +1551,10 @@ impl ConductorHostRole for Agent {
message: dispatch,
})
.await
.map_err(agent_client_protocol::util::internal_error)
.map_err(|e| {
agent_client_protocol::Error::internal_error()
.data(e.to_string())
})
}
},
agent_client_protocol::on_receive_dispatch!(),
Expand Down
4 changes: 1 addition & 3 deletions src/agent-client-protocol-conductor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,7 @@ impl InstantiateProxiesAndAgent for CommandLineComponents {
}
}

Err(agent_client_protocol::util::internal_error(
"no agent component in list",
))
Err(agent_client_protocol::Error::internal_error().data("no agent component in list"))
})
}
}
Expand Down
10 changes: 5 additions & 5 deletions src/agent-client-protocol-conductor/src/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ use std::time::Instant;

use agent_client_protocol::schema::SuccessorMessage;
use agent_client_protocol::schema::v1::{
MessageMcpNotification, MessageMcpRequest, MessageMcpResponse, Notification as RpcNotification,
MessageMcpNotification, MessageMcpRequest, MessageMcpResponse,
};
use agent_client_protocol::{
DynConnectTo, JsonRpcMessage, RawJsonRpcId, RawJsonRpcMessage, RawJsonRpcParams,
RawJsonRpcRequest, RawJsonRpcResponse as RpcResponse, Role, UntypedMessage,
DynConnectTo, JsonRpcMessage, RawJsonRpcId, RawJsonRpcMessage, RawJsonRpcNotification,
RawJsonRpcParams, RawJsonRpcRequest, RawJsonRpcResponse as RpcResponse, Role, UntypedMessage,
};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -479,7 +479,7 @@ impl TraceHandle {
incoming,
message: message.clone(),
})
.map_err(agent_client_protocol::util::internal_error)
.map_err(agent_client_protocol::Error::into_internal_error)
}

/// Create a tracing bridge that wraps a proxy component.
Expand Down Expand Up @@ -732,7 +732,7 @@ impl MessageInfo {
Self::from_untyped_request(Successor(false), Some(req.id), Protocol::Acp, untyped)
}

fn from_notification(notification: RpcNotification<RawJsonRpcParams>) -> Self {
fn from_notification(notification: RawJsonRpcNotification) -> Self {
let untyped = UntypedMessage::parse_message(
&notification.method,
&params_from_transport(notification.params),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -512,11 +512,10 @@ async fn prompt_cancellation_cascades_through_real_proxy_chain() -> Result<(), E
if i32::from(permission_error.code) == -32800 {
responder.respond_with_result(Err(Error::request_cancelled()))
} else {
responder.respond_with_result(Err(
agent_client_protocol::util::internal_error(format!(
"unexpected permission error: {permission_error:?}"
)),
))
responder
.respond_with_result(Err(agent_client_protocol::Error::internal_error(
)
.data(format!("unexpected permission error: {permission_error:?}"))))
}
})?;
Ok(())
Expand Down
Loading
Loading