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: 4 additions & 0 deletions src/agent-client-protocol/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- *(unstable)* Add JSON-RPC support for elicitation requests and notifications.

## [0.13.1](https://github.com/agentclientprotocol/rust-sdk/compare/v0.13.0...v0.13.1) - 2026-06-01

### Added
Expand Down
2 changes: 2 additions & 0 deletions src/agent-client-protocol/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ default = []
unstable = [
"unstable_auth_methods",
"unstable_boolean_config",
"unstable_elicitation",
"unstable_mcp_over_acp",
"unstable_message_id",
"unstable_session_delete",
Expand All @@ -30,6 +31,7 @@ unstable = [
]
unstable_auth_methods = ["agent-client-protocol-schema/unstable_auth_methods"]
unstable_boolean_config = ["agent-client-protocol-schema/unstable_boolean_config"]
unstable_elicitation = ["agent-client-protocol-schema/unstable_elicitation"]
unstable_mcp_over_acp = ["agent-client-protocol-schema/unstable_mcp_over_acp"]
unstable_message_id = ["agent-client-protocol-schema/unstable_message_id"]
unstable_session_delete = ["agent-client-protocol-schema/unstable_session_delete"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#[cfg(feature = "unstable_elicitation")]
use crate::schema::CompleteElicitationNotification;
use crate::schema::SessionNotification;

impl_jsonrpc_notification!(SessionNotification, "session/update");
#[cfg(feature = "unstable_elicitation")]
impl_jsonrpc_notification!(CompleteElicitationNotification, "elicitation/complete");
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#[cfg(feature = "unstable_elicitation")]
use crate::schema::{CreateElicitationRequest, CreateElicitationResponse};
use crate::schema::{
CreateTerminalRequest, CreateTerminalResponse, KillTerminalRequest, KillTerminalResponse,
ReadTextFileRequest, ReadTextFileResponse, ReleaseTerminalRequest, ReleaseTerminalResponse,
Expand Down Expand Up @@ -42,3 +44,9 @@ impl_jsonrpc_request!(
"terminal/wait_for_exit"
);
impl_jsonrpc_request!(KillTerminalRequest, KillTerminalResponse, "terminal/kill");
#[cfg(feature = "unstable_elicitation")]
impl_jsonrpc_request!(
CreateElicitationRequest,
CreateElicitationResponse,
"elicitation/create"
);
6 changes: 6 additions & 0 deletions src/agent-client-protocol/src/schema/enum_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ impl_jsonrpc_request_enum!(AgentRequest {
ReleaseTerminalRequest => "terminal/release",
WaitForTerminalExitRequest => "terminal/wait_for_exit",
KillTerminalRequest => "terminal/kill",
#[cfg(feature = "unstable_elicitation")]
CreateElicitationRequest => "elicitation/create",
#[cfg(feature = "unstable_mcp_over_acp")]
ConnectMcpRequest => "mcp/connect",
#[cfg(feature = "unstable_mcp_over_acp")]
Expand All @@ -90,6 +92,8 @@ impl_jsonrpc_response_enum!(ClientResponse {
ReleaseTerminalResponse => "terminal/release",
WaitForTerminalExitResponse => "terminal/wait_for_exit",
KillTerminalResponse => "terminal/kill",
#[cfg(feature = "unstable_elicitation")]
CreateElicitationResponse => "elicitation/create",
#[cfg(feature = "unstable_mcp_over_acp")]
ConnectMcpResponse => "mcp/connect",
#[cfg(feature = "unstable_mcp_over_acp")]
Expand All @@ -101,6 +105,8 @@ impl_jsonrpc_response_enum!(ClientResponse {

impl_jsonrpc_notification_enum!(AgentNotification {
SessionNotification => "session/update",
#[cfg(feature = "unstable_elicitation")]
CompleteElicitationNotification => "elicitation/complete",
#[cfg(feature = "unstable_mcp_over_acp")]
MessageMcpNotification => "mcp/message",
[ext] ExtNotification,
Expand Down
14 changes: 14 additions & 0 deletions src/agent-client-protocol/src/schema/v2_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,12 @@ impl_v2_jsonrpc_request!(
v2::KillTerminalResponse,
"terminal/kill"
);
#[cfg(feature = "unstable_elicitation")]
impl_v2_jsonrpc_request!(
v2::CreateElicitationRequest,
v2::CreateElicitationResponse,
"elicitation/create"
);
#[cfg(feature = "unstable_mcp_over_acp")]
impl_v2_jsonrpc_request!(v2::ConnectMcpRequest, v2::ConnectMcpResponse, "mcp/connect");
#[cfg(feature = "unstable_mcp_over_acp")]
Expand All @@ -312,6 +318,8 @@ impl_v2_jsonrpc_request!(
);

impl_v2_jsonrpc_notification!(v2::SessionNotification, "session/update");
#[cfg(feature = "unstable_elicitation")]
impl_v2_jsonrpc_notification!(v2::CompleteElicitationNotification, "elicitation/complete");

impl_v2_jsonrpc_request_enum!(v2::ClientRequest {
InitializeRequest => "initialize",
Expand Down Expand Up @@ -369,6 +377,8 @@ impl_v2_jsonrpc_request_enum!(v2::AgentRequest {
ReleaseTerminalRequest => "terminal/release",
WaitForTerminalExitRequest => "terminal/wait_for_exit",
KillTerminalRequest => "terminal/kill",
#[cfg(feature = "unstable_elicitation")]
CreateElicitationRequest => "elicitation/create",
#[cfg(feature = "unstable_mcp_over_acp")]
ConnectMcpRequest => "mcp/connect",
#[cfg(feature = "unstable_mcp_over_acp")]
Expand All @@ -387,6 +397,8 @@ impl_v2_jsonrpc_response_enum!(v2::ClientResponse {
ReleaseTerminalResponse => "terminal/release",
WaitForTerminalExitResponse => "terminal/wait_for_exit",
KillTerminalResponse => "terminal/kill",
#[cfg(feature = "unstable_elicitation")]
CreateElicitationResponse => "elicitation/create",
#[cfg(feature = "unstable_mcp_over_acp")]
ConnectMcpResponse => "mcp/connect",
#[cfg(feature = "unstable_mcp_over_acp")]
Expand All @@ -398,6 +410,8 @@ impl_v2_jsonrpc_response_enum!(v2::ClientResponse {

impl_v2_jsonrpc_notification_enum!(v2::AgentNotification {
SessionNotification => "session/update",
#[cfg(feature = "unstable_elicitation")]
CompleteElicitationNotification => "elicitation/complete",
#[cfg(feature = "unstable_mcp_over_acp")]
MessageMcpNotification => "mcp/message",
[ext] ExtNotification,
Expand Down
269 changes: 269 additions & 0 deletions src/agent-client-protocol/tests/schema_elicitation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
#![cfg(feature = "unstable_elicitation")]

use agent_client_protocol::schema::{
AgentNotification, AgentRequest, ClientCapabilities, ClientResponse,
CompleteElicitationNotification, CreateElicitationRequest, CreateElicitationResponse,
ElicitationAction, ElicitationCapabilities, ElicitationFormCapabilities, ElicitationFormMode,
ElicitationSchema, ElicitationSessionScope, ElicitationUrlCapabilities, Error, ErrorCode,
UrlElicitationRequiredData, UrlElicitationRequiredItem,
};
use agent_client_protocol::{JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse};
use serde::Serialize;
use serde_json::{Value, json};

fn json_value(value: impl Serialize) -> Result<Value, Error> {
serde_json::to_value(value).map_err(Error::into_internal_error)
}

fn form_request() -> CreateElicitationRequest {
CreateElicitationRequest::new(
ElicitationFormMode::new(
ElicitationSessionScope::new("sess_abc123"),
ElicitationSchema::new().string("name", true),
),
"Please enter your name",
)
}

fn assert_request_response_pair<T: JsonRpcRequest<Response = CreateElicitationResponse>>() {}
fn assert_notification<T: JsonRpcNotification>() {}

#[test]
fn create_elicitation_request_has_jsonrpc_metadata() {
let request = form_request();

assert_eq!(request.method(), "elicitation/create");
assert!(CreateElicitationRequest::matches_method(
"elicitation/create"
));
assert!(!CreateElicitationRequest::matches_method("session/prompt"));

let untyped = request.to_untyped_message().unwrap();
assert_eq!(untyped.method, "elicitation/create");
assert_eq!(untyped.params["mode"], "form");
assert_eq!(untyped.params["sessionId"], "sess_abc123");

let parsed =
CreateElicitationRequest::parse_message("elicitation/create", &untyped.params).unwrap();
assert!(matches!(
parsed.mode,
agent_client_protocol::schema::ElicitationMode::Form(_)
));

assert_request_response_pair::<CreateElicitationRequest>();
}

#[test]
fn elicitation_participates_in_agent_request_enum() {
let request = AgentRequest::CreateElicitationRequest(form_request());

assert_eq!(request.method(), "elicitation/create");
assert!(AgentRequest::matches_method("elicitation/create"));

let parsed =
AgentRequest::parse_message("elicitation/create", &json_value(form_request()).unwrap())
.unwrap();
assert!(matches!(parsed, AgentRequest::CreateElicitationRequest(_)));
}

#[test]
fn create_elicitation_response_round_trips_json() {
let value = CreateElicitationResponse::new(ElicitationAction::Decline)
.into_json("elicitation/create")
.unwrap();
assert_eq!(value, json!({ "action": "decline" }));

let parsed = CreateElicitationResponse::from_value("elicitation/create", value).unwrap();
assert!(matches!(parsed.action, ElicitationAction::Decline));

let enum_response =
ClientResponse::from_value("elicitation/create", json!({ "action": "cancel" })).unwrap();
assert!(matches!(
enum_response,
ClientResponse::CreateElicitationResponse(_)
));
}

#[test]
fn complete_elicitation_notification_has_jsonrpc_metadata() {
assert_notification::<CompleteElicitationNotification>();

let notification = CompleteElicitationNotification::new("elicit_1");
assert_eq!(notification.method(), "elicitation/complete");
assert!(CompleteElicitationNotification::matches_method(
"elicitation/complete"
));
assert!(!CompleteElicitationNotification::matches_method(
"session/update"
));

let untyped = notification.to_untyped_message().unwrap();
assert_eq!(untyped.method, "elicitation/complete");
assert_eq!(untyped.params, json!({ "elicitationId": "elicit_1" }));

let parsed = AgentNotification::parse_message("elicitation/complete", &untyped.params).unwrap();
assert!(matches!(
parsed,
AgentNotification::CompleteElicitationNotification(_)
));
}

#[test]
fn client_capabilities_can_declare_elicitation_modes() {
let capabilities = ClientCapabilities::new().elicitation(
ElicitationCapabilities::new()
.form(ElicitationFormCapabilities::new())
.url(ElicitationUrlCapabilities::new()),
);

let value = json_value(capabilities).unwrap();
assert_eq!(value["elicitation"], json!({ "form": {}, "url": {} }));

let parsed: ClientCapabilities = serde_json::from_value(json!({ "elicitation": {} })).unwrap();
assert!(parsed.elicitation.is_some());
}

#[test]
fn url_elicitation_required_error_helper_is_available() {
let data = UrlElicitationRequiredData::new(vec![UrlElicitationRequiredItem::new(
"elicit_1",
"https://example.com/connect",
"Connect your account",
)]);
let error = Error::url_elicitation_required().data(json_value(data).unwrap());

assert_eq!(error.code, ErrorCode::UrlElicitationRequired);
assert_eq!(
error.data.unwrap(),
json!({
"elicitations": [{
"mode": "url",
"elicitationId": "elicit_1",
"url": "https://example.com/connect",
"message": "Connect your account"
}]
})
);
}

#[cfg(feature = "unstable_protocol_v2")]
#[test]
fn protocol_v2_elicitation_variants_are_jsonrpc_mapped() -> Result<(), Error> {
use agent_client_protocol::schema::v2;

let request = v2::CreateElicitationRequest::new(
v2::ElicitationFormMode::new(
v2::ElicitationSessionScope::new("sess_abc123"),
v2::ElicitationSchema::new().string("name", true),
),
"Please enter your name",
);

let parsed_request =
v2::AgentRequest::parse_message("elicitation/create", &json_value(request.clone())?)?;
assert!(matches!(
parsed_request,
v2::AgentRequest::CreateElicitationRequest(_)
));

let parsed_response =
v2::ClientResponse::from_value("elicitation/create", json!({ "action": "decline" }))?;
assert!(matches!(
parsed_response,
v2::ClientResponse::CreateElicitationResponse(_)
));

let notification = v2::CompleteElicitationNotification::new("elicit_1");
let parsed_notification =
v2::AgentNotification::parse_message("elicitation/complete", &json_value(notification)?)?;
assert!(matches!(
parsed_notification,
v2::AgentNotification::CompleteElicitationNotification(_)
));

Ok(())
}

#[cfg(feature = "unstable_protocol_v2")]
#[tokio::test(flavor = "current_thread")]
async fn v2_agent_can_elicit_from_v1_client() -> Result<(), Error> {
use agent_client_protocol::schema::{self, ProtocolVersion, v2};
use agent_client_protocol::{Agent, Client};
use std::collections::BTreeMap;

let agent = Agent
.v2()
.on_receive_request(
async |initialize: v2::InitializeRequest, responder, _cx| {
assert_eq!(initialize.protocol_version, ProtocolVersion::V2);
responder.respond(v2::InitializeResponse::new(ProtocolVersion::V2))
},
agent_client_protocol::on_receive_request!(),
)
.on_receive_request(
async |_prompt: v2::PromptRequest, responder, cx| {
let request = v2::CreateElicitationRequest::new(
v2::ElicitationFormMode::new(
v2::ElicitationSessionScope::new("sess_abc123"),
v2::ElicitationSchema::new().string("name", true),
),
"Please enter your name",
);

cx.send_request(request)
.on_receiving_result(async move |result| {
let response = result?;
let v2::ElicitationAction::Accept(action) = response.action else {
return Err(Error::invalid_params().data("expected accept action"));
};
let content = action.content.ok_or_else(|| {
Error::invalid_params().data("expected response content")
})?;
assert_eq!(
content.get("name"),
Some(&v2::ElicitationContentValue::String("Ada".into()))
);
responder.respond(v2::PromptResponse::new(v2::StopReason::EndTurn))
})?;

Ok(())
},
agent_client_protocol::on_receive_request!(),
);

Client
.builder()
.on_receive_request(
async |request: CreateElicitationRequest, responder, _cx| {
assert_eq!(request.method(), "elicitation/create");
assert!(matches!(
request.mode,
schema::ElicitationMode::Form(schema::ElicitationFormMode { .. })
));

let content = BTreeMap::from([("name".to_string(), "Ada".into())]);
responder.respond(CreateElicitationResponse::new(ElicitationAction::Accept(
schema::ElicitationAcceptAction::new().content(content),
)))
},
agent_client_protocol::on_receive_request!(),
)
.connect_with(agent, async |cx| {
let initialize = cx
.send_request(schema::InitializeRequest::new(ProtocolVersion::V1))
.block_task()
.await?;
assert_eq!(initialize.protocol_version, ProtocolVersion::V1);

let prompt = cx
.send_request(schema::PromptRequest::new(
"sess_abc123",
vec!["continue".into()],
))
.block_task()
.await?;
assert_eq!(prompt.stop_reason, schema::StopReason::EndTurn);
Ok(())
})
.await
}