From 7cefaf2873dcf474ecd1508ef7c32df878266f3a Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 20 Aug 2026 15:51:52 -0700 Subject: [PATCH] feat(acp): update schema to 1.7 --- Cargo.lock | 23 +--- Cargo.toml | 2 +- md/migration_v2.0.md | 4 +- md/protocol-v2.md | 10 +- md/testy.md | 8 +- src/agent-client-protocol-test/CHANGELOG.md | 2 +- src/agent-client-protocol-test/src/testy.rs | 101 +++++--------- src/agent-client-protocol-test/tests/testy.rs | 80 +++++++++-- src/agent-client-protocol/Cargo.toml | 6 +- .../src/mcp_server/server.rs | 2 +- src/agent-client-protocol/src/role/acp.rs | 2 - .../schema/agent_to_client/notifications.rs | 2 - .../src/schema/agent_to_client/requests.rs | 2 - .../src/schema/enum_impls.rs | 3 - .../src/schema/v2_impls.rs | 5 - src/agent-client-protocol/src/session/v2.rs | 6 +- .../tests/protocol_v2.rs | 10 +- .../tests/schema_elicitation.rs | 2 - .../tests/schema_session_compaction.rs | 129 ++++++++++++++++++ 19 files changed, 258 insertions(+), 141 deletions(-) create mode 100644 src/agent-client-protocol/tests/schema_session_compaction.rs diff --git a/Cargo.lock b/Cargo.lock index 5cca7202..dc2314ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,13 +139,12 @@ dependencies = [ [[package]] name = "agent-client-protocol-schema" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c216d4b350cbc1b13f2b7d752af6698c8ba347be274275cb5a5c735da147066" +checksum = "ca98360c7bb8cc97d7acd49e2a8a851c3f7bee6b2f0535036d8ab86b5fcd223d" dependencies = [ "anyhow", "derive_more", - "diffy", "schemars 1.2.2", "serde", "serde_json", @@ -876,15 +875,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "diffy" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10aec8f7f9393bd6a4f2762be0ceb012d3cbe2478987258cc9960de148561914" -dependencies = [ - "hashbrown 0.17.1", -] - [[package]] name = "digest" version = "0.10.7" @@ -1010,12 +1000,6 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1205,9 +1189,6 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "foldhash", -] [[package]] name = "heck" diff --git a/Cargo.toml b/Cargo.toml index 867564c1..c5b8a21e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ agent-client-protocol-trace-viewer = { path = "src/agent-client-protocol-trace-v yopo = { package = "agent-client-protocol-yopo", path = "src/yopo" } # Protocol -agent-client-protocol-schema = { version = "=1.6.0", features = ["tracing"] } +agent-client-protocol-schema = { version = "=1.7.0", features = ["tracing"] } # Core async runtime tokio = { version = "1.52", default-features = false } diff --git a/md/migration_v2.0.md b/md/migration_v2.0.md index d0938970..3f6146cc 100644 --- a/md/migration_v2.0.md +++ b/md/migration_v2.0.md @@ -342,8 +342,8 @@ Construct `Lines` and `ByteStreams` with `Lines::new(outgoing, incoming)` and ## Draft v2 schema updates The optional `unstable_protocol_v2` surface now tracks -`agent-client-protocol-schema` 1.6. The changes accumulated across schema 1.5 -and 1.6 are included in the SDK 2.0 migration because this API is explicitly +`agent-client-protocol-schema` 1.7. The changes accumulated across schema 1.5 +through 1.7 are included in the SDK 2.0 migration because this API is explicitly unstable, rather than treated as stable-v1 wire changes. - Many values that were plain `String` or `PathBuf` fields are semantic newtypes, including diff --git a/md/protocol-v2.md b/md/protocol-v2.md index 951727af..624a39b1 100644 --- a/md/protocol-v2.md +++ b/md/protocol-v2.md @@ -437,12 +437,12 @@ The reuse probe is conservative: if parsing and serializing the raw v2 request would change any parameter, reuse is disabled and fallback opens a fresh connection. That does not turn an otherwise valid v2 request into an error. -## Draft schema changes in schema 1.5 and 1.6 +## Draft schema changes in schema 1.5 through 1.7 The `unstable_protocol_v2` API follows the moving draft schema. Schema 1.5 added semantic newtypes for paths, media types, IDs, and cursors; renamed `DiffPatch.diff` to `DiffPatch.text`; and added terminal state and output update -types. Schema 1.6 removed the former schema-wide v1/v2 conversion API: +types. Schema 1.7 removed the former schema-wide v1/v2 conversion API: versioned implementations should remain separate, with purpose-specific adapters at runtime boundaries where the required state and policy are available. These are draft API changes rather than stable v1 wire @@ -455,3 +455,9 @@ the separate `unstable_tool_call_name` feature. Draft v2 users must enable both `unstable_protocol_v2` and `unstable_tool_call_name`. In v2, an omitted name leaves the existing value unchanged, `null` clears it, and a string replaces it. V1 cannot express the explicit v2 `null` clear operation. + +Schema 1.7 stabilizes elicitation and terminal authentication, so neither +surface requires its former SDK feature flag. It also adds context compaction +updates behind `unstable_session_compaction`; the SDK carries them through its +existing typed `session/update` routing in both protocol versions. V1 clients +advertise compaction support through `ClientSessionCapabilities::compaction`. diff --git a/md/testy.md b/md/testy.md index 95440049..15f1fe02 100644 --- a/md/testy.md +++ b/md/testy.md @@ -42,11 +42,11 @@ Plain-text commands: - `content` emits prompt/content-focused updates, including every stable `ContentBlock` variant. - `tool_calls` emits tool call create and update flows. - `callbacks` sends every stable agent-to-client request. -- `elicitations` sends only unstable elicitation requests when built with default features. +- `elicitations` sends only elicitation requests. - `cancel_status` reports whether `session/cancel` has been received. - `full` runs all stable scenarios in deterministic order. -With default features, `callbacks` and `full` also run unstable protocol coverage. +`callbacks` and `full` also run the elicitation coverage. JSON command form: @@ -69,8 +69,10 @@ The `full` scenario sends every stable agent-to-client callback request: It also emits the stable session update variants, including message chunks, tool calls, plans, available commands, mode/config/session info, and usage. -With default features, `elicitations`, `callbacks`, and `full` cover `elicitation/create` form mode, +`elicitations`, `callbacks`, and `full` cover `elicitation/create` form mode, URL mode, session scope, request scope, accept, decline, cancel, and `elicitation/complete`. +If the client does not advertise form elicitation, the scenario returns a deterministic +invalid-params prompt error before sending an elicitation request. If the client advertises form elicitation but not URL elicitation, the URL part returns a deterministic invalid-params prompt error. diff --git a/src/agent-client-protocol-test/CHANGELOG.md b/src/agent-client-protocol-test/CHANGELOG.md index f4d9df4e..9d63009d 100644 --- a/src/agent-client-protocol-test/CHANGELOG.md +++ b/src/agent-client-protocol-test/CHANGELOG.md @@ -8,5 +8,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Expand the `testy` binary into a deterministic ACP test agent that can exercise stable v1 agent methods, notifications, session updates, and client callbacks. -- Add default `testy` coverage through the `unstable` cargo feature for elicitation form/URL requests, session/request scopes, response actions, completion notifications, URL-required prompt errors, and a direct `elicitations` prompt trigger. +- Add default `testy` coverage for elicitation form/URL requests, session/request scopes, response actions, completion notifications, capability-negotiation prompt errors, and a direct `elicitations` prompt trigger. - Add an opt-in native protocol v2 Testy agent and a dual-version stdio router covering the baseline session lifecycle, independent prompt acceptance, cancellation completion, and resume replay. diff --git a/src/agent-client-protocol-test/src/testy.rs b/src/agent-client-protocol-test/src/testy.rs index 786324aa..9ae4b604 100644 --- a/src/agent-client-protocol-test/src/testy.rs +++ b/src/agent-client-protocol-test/src/testy.rs @@ -31,7 +31,6 @@ use agent_client_protocol::schema::v1::{ ToolCallLocation, ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields, ToolKind, UnstructuredCommandInput, UsageUpdate, WriteTextFileRequest, }; -#[cfg(feature = "unstable")] use agent_client_protocol::schema::v1::{ CompleteElicitationNotification, CreateElicitationRequest, ElicitationAction, ElicitationCapabilities, ElicitationFormMode, ElicitationRequestScope, ElicitationSchema, @@ -43,7 +42,6 @@ use agent_client_protocol::{ }; use anyhow::Result; use serde::{Deserialize, Serialize}; -#[cfg(feature = "unstable")] use std::collections::BTreeMap; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; @@ -103,16 +101,15 @@ pub enum TestyScenario { Content, /// Emits tool-call create/update notifications with content, diff, and locations. ToolCalls, - /// Sends every stable agent-to-client request and any enabled unstable callback coverage. + /// Sends every stable agent-to-client request. Callbacks, - /// Runs unstable elicitation coverage without the stable callback requests. - #[cfg(feature = "unstable")] + /// Runs elicitation coverage without the other stable callback requests. Elicitations, /// Reports whether the session has received a `session/cancel` notification. CancelStatus, /// Waits for `session/cancel` before completing. WaitForCancel, - /// Runs all stable scenarios and any enabled unstable coverage in a deterministic order. + /// Runs all stable scenarios in a deterministic order. Full, } @@ -123,7 +120,6 @@ impl TestyScenario { Self::Content, Self::ToolCalls, Self::Callbacks, - #[cfg(feature = "unstable")] Self::Elicitations, Self::CancelStatus, Self::WaitForCancel, @@ -137,7 +133,6 @@ impl TestyScenario { "content" => Some(Self::Content), "tool_calls" | "tool calls" | "tools" => Some(Self::ToolCalls), "callbacks" | "client_callbacks" | "client callbacks" => Some(Self::Callbacks), - #[cfg(feature = "unstable")] "elicitations" | "elicitation" | "elicit" => Some(Self::Elicitations), "cancel_status" | "cancel status" | "cancel" => Some(Self::CancelStatus), "wait_for_cancel" | "wait for cancel" => Some(Self::WaitForCancel), @@ -152,7 +147,6 @@ impl TestyScenario { Self::Content => "content", Self::ToolCalls => "tool_calls", Self::Callbacks => "callbacks", - #[cfg(feature = "unstable")] Self::Elicitations => "elicitations", Self::CancelStatus => "cancel_status", Self::WaitForCancel => "wait_for_cancel", @@ -184,7 +178,6 @@ struct TestyState { next_message_id: u64, next_tool_call_id: u64, authenticated_methods: HashSet, - #[cfg(feature = "unstable")] client_elicitation_capabilities: Option, } @@ -196,7 +189,6 @@ impl Default for TestyState { next_message_id: 1, next_tool_call_id: 1, authenticated_methods: HashSet::new(), - #[cfg(feature = "unstable")] client_elicitation_capabilities: None, } } @@ -431,12 +423,9 @@ impl Testy { request: InitializeRequest, responder: Responder, ) -> Result<(), agent_client_protocol::Error> { - #[cfg(feature = "unstable")] - { - self.lock_state() - .client_elicitation_capabilities - .clone_from(&request.client_capabilities.elicitation); - } + self.lock_state() + .client_elicitation_capabilities + .clone_from(&request.client_capabilities.elicitation); responder.respond( InitializeResponse::new(request.protocol_version) @@ -445,12 +434,18 @@ impl Testy { ) } - #[cfg(feature = "unstable")] + fn client_supports_form_elicitation(&self) -> bool { + self.lock_state() + .client_elicitation_capabilities + .as_ref() + .is_some_and(ElicitationCapabilities::supports_form) + } + fn client_supports_url_elicitation(&self) -> bool { self.lock_state() .client_elicitation_capabilities .as_ref() - .is_some_and(|capabilities| capabilities.url.is_some()) + .is_some_and(ElicitationCapabilities::supports_url) } fn handle_authenticate( @@ -734,13 +729,11 @@ impl Testy { TestyScenario::Callbacks => { self.exercise_client_callbacks(session_id, connection, report) .await?; - #[cfg(feature = "unstable")] if !self.is_cancelled(session_id) { self.exercise_elicitations(session_id, connection, report) .await?; } } - #[cfg(feature = "unstable")] TestyScenario::Elicitations => { if self.is_cancelled(session_id) { report.push("elicitations: cancelled".to_string()); @@ -799,7 +792,6 @@ impl Testy { } self.exercise_client_callbacks(session_id, connection, report) .await?; - #[cfg(feature = "unstable")] if !self.is_cancelled(session_id) { self.exercise_elicitations(session_id, connection, report) .await?; @@ -1151,13 +1143,16 @@ impl Testy { } } - #[cfg(feature = "unstable")] async fn exercise_elicitations( &self, session_id: &SessionId, connection: &ConnectionTo, report: &mut Vec, ) -> Result<(), agent_client_protocol::Error> { + if !self.client_supports_form_elicitation() { + return Err(form_elicitation_unsupported_error()); + } + let tool_call_id = self.next_tool_call_id("testy-elicit-tool"); let form_accept = CreateElicitationRequest::new( ElicitationFormMode::new( @@ -1277,7 +1272,6 @@ impl Testy { Ok(()) } - #[cfg(feature = "unstable")] async fn request_elicitation_report_until_cancelled( &self, session_id: &SessionId, @@ -1688,61 +1682,34 @@ fn parse_command(input_text: &str) -> TestyCommand { } fn help_text() -> String { - let unstable = cfg!(feature = "unstable"); let scenarios = TestyScenario::all() .into_iter() .map(TestyScenario::name) .collect::>() .join(", "); - let mut text = format!( + format!( "Testy commands: help, greet, echo , {scenarios}. JSON command form: {}", TestyCommand::RunScenario { scenario: TestyScenario::Full } .to_prompt() - ); - if unstable { - text.push_str( - ". Unstable mode is enabled: callbacks and full include unstable elicitation coverage", - ); - } - text + ) } fn available_commands() -> Vec { - let unstable = cfg!(feature = "unstable"); - let full_description = if unstable { - "Run every stable and enabled unstable Testy scenario" - } else { - "Run every stable Testy scenario" - }; - let callbacks_description = if unstable { - "Exercise stable and enabled unstable agent-to-client requests" - } else { - "Exercise agent-to-client requests" - }; - - let full_command = - AvailableCommand::new("full", full_description).input(AvailableCommandInput::Unstructured( - UnstructuredCommandInput::new("optional scenario arguments"), - )); - let callbacks_command = AvailableCommand::new("callbacks", callbacks_description); + let full_command = AvailableCommand::new("full", "Run every stable Testy scenario").input( + AvailableCommandInput::Unstructured(UnstructuredCommandInput::new( + "optional scenario arguments", + )), + ); - #[cfg(feature = "unstable")] - { - vec![ - full_command, - callbacks_command, - AvailableCommand::new("elicitations", "Exercise unstable elicitation requests"), - ] - } - #[cfg(not(feature = "unstable"))] - { - vec![full_command, callbacks_command] - } + vec![ + full_command, + AvailableCommand::new("callbacks", "Exercise agent-to-client requests"), + AvailableCommand::new("elicitations", "Exercise elicitation requests"), + ] } -#[cfg(feature = "unstable")] fn testy_elicitation_schema() -> ElicitationSchema { ElicitationSchema::new() .title("Testy elicitation form") @@ -1775,7 +1742,6 @@ fn testy_elicitation_schema() -> ElicitationSchema { ) } -#[cfg(feature = "unstable")] fn elicitation_action_summary(action: &ElicitationAction) -> String { match action { ElicitationAction::Accept(action) => { @@ -1788,7 +1754,6 @@ fn elicitation_action_summary(action: &ElicitationAction) -> String { } } -#[cfg(feature = "unstable")] fn elicitation_cancelled(agent: &Testy, session_id: &SessionId, report: &mut Vec) -> bool { if agent.is_cancelled(session_id) { report.push("elicitations: cancelled".to_string()); @@ -1798,7 +1763,10 @@ fn elicitation_cancelled(agent: &Testy, session_id: &SessionId, report: &mut Vec } } -#[cfg(feature = "unstable")] +fn form_elicitation_unsupported_error() -> agent_client_protocol::Error { + invalid_params("client does not support form elicitation") +} + fn url_elicitation_unsupported_error() -> agent_client_protocol::Error { invalid_params("client does not support URL elicitation") } @@ -2120,7 +2088,6 @@ mod tests { } } - #[cfg(feature = "unstable")] #[test] fn parse_command_accepts_elicitation_prompt_aliases() { for input in ["elicitations", "elicitation", "elicit"] { diff --git a/src/agent-client-protocol-test/tests/testy.rs b/src/agent-client-protocol-test/tests/testy.rs index cf06437e..7c24b30d 100644 --- a/src/agent-client-protocol-test/tests/testy.rs +++ b/src/agent-client-protocol-test/tests/testy.rs @@ -1,4 +1,3 @@ -#[cfg(feature = "unstable")] use std::collections::BTreeMap; use std::{ collections::{HashMap, HashSet}, @@ -7,7 +6,6 @@ use std::{ time::Duration, }; -#[cfg(feature = "unstable")] use agent_client_protocol::schema::v1::{ CompleteElicitationNotification, CreateElicitationRequest, CreateElicitationResponse, ElicitationAcceptAction, ElicitationAction, ElicitationCapabilities, ElicitationContentValue, @@ -1306,9 +1304,7 @@ async fn testy_full_scenario_exercises_updates_and_callbacks() let created_terminal_ids = Arc::new(Mutex::new(Vec::::new())); let released_terminal_ids = Arc::new(Mutex::new(Vec::::new())); let terminal_content_ids = Arc::new(Mutex::new(Vec::::new())); - #[cfg(feature = "unstable")] let elicitation_requests = Arc::new(Mutex::new(Vec::<&'static str>::new())); - #[cfg(feature = "unstable")] let completed_elicitations = Arc::new(Mutex::new(Vec::::new())); let builder = Client @@ -1463,7 +1459,6 @@ async fn testy_full_scenario_exercises_updates_and_callbacks() agent_client_protocol::on_receive_request!(), ); - #[cfg(feature = "unstable")] let builder = builder .on_receive_notification( { @@ -1520,7 +1515,6 @@ async fn testy_full_scenario_exercises_updates_and_callbacks() builder .connect_with(Testy::new(), async |cx| { let initialize = InitializeRequest::new(ProtocolVersion::V1); - #[cfg(feature = "unstable")] let initialize = initialize.client_capabilities( ClientCapabilities::new().elicitation( ElicitationCapabilities::new() @@ -1589,7 +1583,6 @@ async fn testy_full_scenario_exercises_updates_and_callbacks() assert!(messages.contains("scenario: full")); assert!(messages.contains("terminal/release_for_tool_call: ok")); assert!(messages.contains("terminal/release: ok")); - #[cfg(feature = "unstable")] for expected in [ "elicitation/form_session_accept: ok accept content_fields=5", "elicitation/form_session_decline: ok decline", @@ -1605,7 +1598,6 @@ async fn testy_full_scenario_exercises_updates_and_callbacks() ); } - #[cfg(feature = "unstable")] { assert_eq!( elicitation_requests.lock().unwrap().as_slice(), @@ -1641,7 +1633,6 @@ async fn testy_full_scenario_exercises_updates_and_callbacks() Ok(()) } -#[cfg(feature = "unstable")] #[tokio::test] async fn testy_elicitations_prompt_exercises_all_elicitation_create_and_complete_paths() -> Result<(), agent_client_protocol::Error> { @@ -1838,9 +1829,75 @@ async fn testy_elicitations_prompt_exercises_all_elicitation_create_and_complete Ok(()) } -#[cfg(feature = "unstable")] #[tokio::test] -async fn testy_callbacks_with_unstable_feature_returns_invalid_params_when_url_elicitation_is_unsupported() +async fn testy_elicitations_prompt_rejects_missing_elicitation_capability_before_sending_requests() +-> Result<(), agent_client_protocol::Error> { + assert_form_elicitation_unsupported(None).await +} + +#[tokio::test] +async fn testy_elicitations_prompt_rejects_url_only_capability_before_sending_requests() +-> Result<(), agent_client_protocol::Error> { + assert_form_elicitation_unsupported(Some( + ElicitationCapabilities::new().url(ElicitationUrlCapabilities::new()), + )) + .await +} + +async fn assert_form_elicitation_unsupported( + elicitation_capabilities: Option, +) -> Result<(), agent_client_protocol::Error> { + let requests = Arc::new(Mutex::new(0_usize)); + + Client + .builder() + .on_receive_request( + { + let requests = Arc::clone(&requests); + async move |_request: CreateElicitationRequest, responder, _cx| { + *requests.lock().unwrap() += 1; + responder.respond(CreateElicitationResponse::new(ElicitationAction::Cancel)) + } + }, + agent_client_protocol::on_receive_request!(), + ) + .connect_with(Testy::new(), async move |cx| { + let mut initialize = InitializeRequest::new(ProtocolVersion::V1); + if let Some(capabilities) = elicitation_capabilities { + initialize = initialize + .client_capabilities(ClientCapabilities::new().elicitation(capabilities)); + } + cx.send_request(initialize).block_task().await?; + let session = cx + .send_request(NewSessionRequest::new(PathBuf::from("/tmp"))) + .block_task() + .await?; + + let error = cx + .send_request(PromptRequest::new( + session.session_id, + vec!["elicitations".to_string().into()], + )) + .block_task() + .await + .expect_err("unsupported form elicitation should fail the prompt request"); + assert_eq!(error.code, ErrorCode::InvalidParams); + assert_eq!( + error.data, + Some(serde_json::json!( + "client does not support form elicitation" + )) + ); + Ok(()) + }) + .await?; + + assert_eq!(*requests.lock().unwrap(), 0); + Ok(()) +} + +#[tokio::test] +async fn testy_callbacks_return_invalid_params_when_url_elicitation_is_unsupported() -> Result<(), agent_client_protocol::Error> { let requests = Arc::new(Mutex::new(Vec::<&'static str>::new())); @@ -1979,7 +2036,6 @@ async fn testy_callbacks_with_unstable_feature_returns_invalid_params_when_url_e Ok(()) } -#[cfg(feature = "unstable")] fn testy_elicitation_request_label(request: &CreateElicitationRequest) -> &'static str { match request.message.as_str() { "Accept the Testy session-scoped form elicitation" => { diff --git a/src/agent-client-protocol/Cargo.toml b/src/agent-client-protocol/Cargo.toml index 51352ed0..1eb857aa 100644 --- a/src/agent-client-protocol/Cargo.toml +++ b/src/agent-client-protocol/Cargo.toml @@ -29,21 +29,19 @@ default = [] # Forward unstable features from agent-client-protocol-schema. # Enable these to get support for the corresponding unstable ACP surfaces. unstable = [ - "unstable_auth_methods", - "unstable_elicitation", "unstable_end_turn_token_usage", "unstable_llm_providers", "unstable_mcp_over_acp", "unstable_plan_operations", + "unstable_session_compaction", "unstable_session_fork", "unstable_tool_call_name", ] -unstable_auth_methods = ["agent-client-protocol-schema/unstable_auth_methods"] -unstable_elicitation = ["agent-client-protocol-schema/unstable_elicitation"] unstable_end_turn_token_usage = ["agent-client-protocol-schema/unstable_end_turn_token_usage"] unstable_llm_providers = ["agent-client-protocol-schema/unstable_llm_providers"] unstable_mcp_over_acp = ["agent-client-protocol-schema/unstable_mcp_over_acp"] unstable_plan_operations = ["agent-client-protocol-schema/unstable_plan_operations"] +unstable_session_compaction = ["agent-client-protocol-schema/unstable_session_compaction"] unstable_session_fork = ["agent-client-protocol-schema/unstable_session_fork"] unstable_tool_call_name = ["agent-client-protocol-schema/unstable_tool_call_name"] unstable_protocol_v2 = ["agent-client-protocol-schema/unstable_protocol_v2"] diff --git a/src/agent-client-protocol/src/mcp_server/server.rs b/src/agent-client-protocol/src/mcp_server/server.rs index cd12d10f..2b34859f 100644 --- a/src/agent-client-protocol/src/mcp_server/server.rs +++ b/src/agent-client-protocol/src/mcp_server/server.rs @@ -198,7 +198,7 @@ where fn declaration(&self) -> crate::schema::v2::McpServer { crate::schema::v2::McpServer::Acp(crate::schema::v2::McpServerAcp::new( self.connect.name(), - crate::schema::v2::McpServerAcpId::from(self.server_id.clone()), + crate::schema::v2::McpServerAcpId::from(self.server_id.0.clone()), )) } diff --git a/src/agent-client-protocol/src/role/acp.rs b/src/agent-client-protocol/src/role/acp.rs index e2c6f6c5..f99c1ecc 100644 --- a/src/agent-client-protocol/src/role/acp.rs +++ b/src/agent-client-protocol/src/role/acp.rs @@ -777,7 +777,6 @@ mod initialize_normalization_tests { assert_eq!(normalized, params); } - #[cfg(feature = "unstable_auth_methods")] #[test] fn null_v2_terminal_marker_meta_is_omitted_before_v1_normalization() { let mut params = v2_initialize_params(); @@ -800,7 +799,6 @@ mod initialize_normalization_tests { ); } - #[cfg(feature = "unstable_auth_methods")] #[test] fn terminal_marker_metadata_disables_reuse_but_not_v1_routing() { let mut params = v2_initialize_params(); diff --git a/src/agent-client-protocol/src/schema/agent_to_client/notifications.rs b/src/agent-client-protocol/src/schema/agent_to_client/notifications.rs index e68e9569..0484765e 100644 --- a/src/agent-client-protocol/src/schema/agent_to_client/notifications.rs +++ b/src/agent-client-protocol/src/schema/agent_to_client/notifications.rs @@ -1,7 +1,5 @@ -#[cfg(feature = "unstable_elicitation")] use crate::schema::v1::CompleteElicitationNotification; use crate::schema::v1::SessionNotification; impl_jsonrpc_notification!(SessionNotification, "session/update"); -#[cfg(feature = "unstable_elicitation")] impl_jsonrpc_notification!(CompleteElicitationNotification, "elicitation/complete"); diff --git a/src/agent-client-protocol/src/schema/agent_to_client/requests.rs b/src/agent-client-protocol/src/schema/agent_to_client/requests.rs index c6b8ab2f..d3b0ae19 100644 --- a/src/agent-client-protocol/src/schema/agent_to_client/requests.rs +++ b/src/agent-client-protocol/src/schema/agent_to_client/requests.rs @@ -1,4 +1,3 @@ -#[cfg(feature = "unstable_elicitation")] use crate::schema::v1::{CreateElicitationRequest, CreateElicitationResponse}; use crate::schema::v1::{ CreateTerminalRequest, CreateTerminalResponse, KillTerminalRequest, KillTerminalResponse, @@ -44,7 +43,6 @@ impl_jsonrpc_request!( "terminal/wait_for_exit" ); impl_jsonrpc_request!(KillTerminalRequest, KillTerminalResponse, "terminal/kill"); -#[cfg(feature = "unstable_elicitation")] impl_jsonrpc_request!( CreateElicitationRequest, CreateElicitationResponse, diff --git a/src/agent-client-protocol/src/schema/enum_impls.rs b/src/agent-client-protocol/src/schema/enum_impls.rs index 25397ae0..8a937c43 100644 --- a/src/agent-client-protocol/src/schema/enum_impls.rs +++ b/src/agent-client-protocol/src/schema/enum_impls.rs @@ -82,7 +82,6 @@ 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", @@ -102,7 +101,6 @@ 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", @@ -115,7 +113,6 @@ 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", diff --git a/src/agent-client-protocol/src/schema/v2_impls.rs b/src/agent-client-protocol/src/schema/v2_impls.rs index eb971f75..8cae486b 100644 --- a/src/agent-client-protocol/src/schema/v2_impls.rs +++ b/src/agent-client-protocol/src/schema/v2_impls.rs @@ -276,7 +276,6 @@ impl_v2_jsonrpc_request!( v2::RequestPermissionResponse, "session/request_permission" ); -#[cfg(feature = "unstable_elicitation")] impl_v2_jsonrpc_request!( v2::CreateElicitationRequest, v2::CreateElicitationResponse, @@ -292,7 +291,6 @@ impl_v2_jsonrpc_request!( ); impl_v2_jsonrpc_notification!(v2::UpdateSessionNotification, "session/update"); -#[cfg(feature = "unstable_elicitation")] impl_v2_jsonrpc_notification!(v2::CompleteElicitationNotification, "elicitation/complete"); impl_jsonrpc_protocol_level_notification_enum!(v2::ProtocolLevelNotification { @@ -356,7 +354,6 @@ impl_v2_jsonrpc_notification_enum!(v2::ClientNotification { impl_v2_jsonrpc_request_enum!(v2::AgentRequest { RequestPermissionRequest => "session/request_permission", - #[cfg(feature = "unstable_elicitation")] CreateElicitationRequest => "elicitation/create", #[cfg(feature = "unstable_mcp_over_acp")] ConnectMcpRequest => "mcp/connect", @@ -369,7 +366,6 @@ impl_v2_jsonrpc_request_enum!(v2::AgentRequest { impl_v2_jsonrpc_response_enum!(v2::ClientResponse { RequestPermissionResponse => "session/request_permission", - #[cfg(feature = "unstable_elicitation")] CreateElicitationResponse => "elicitation/create", #[cfg(feature = "unstable_mcp_over_acp")] ConnectMcpResponse => "mcp/connect", @@ -382,7 +378,6 @@ impl_v2_jsonrpc_response_enum!(v2::ClientResponse { impl_v2_jsonrpc_notification_enum!(v2::AgentNotification { UpdateSessionNotification => "session/update", - #[cfg(feature = "unstable_elicitation")] CompleteElicitationNotification => "elicitation/complete", #[cfg(feature = "unstable_mcp_over_acp")] MessageMcpNotification => "mcp/message", diff --git a/src/agent-client-protocol/src/session/v2.rs b/src/agent-client-protocol/src/session/v2.rs index dea34aa5..4d78c2fc 100644 --- a/src/agent-client-protocol/src/session/v2.rs +++ b/src/agent-client-protocol/src/session/v2.rs @@ -356,7 +356,7 @@ where let session_id = response.session_id.clone(); let raw_connection = session_connection.raw_connection(); let route = match raw_connection.add_dynamic_handler(ProxySessionMessages::new( - crate::schema::v1::SessionId::from(session_id.clone()), + crate::schema::v1::SessionId::from(session_id.0.clone()), )) { Ok(route) => route, Err(error) => return responder.respond_with_error(error), @@ -526,7 +526,7 @@ where let session_id = response.session_id.clone(); let raw_connection = session_connection.raw_connection(); let route = match raw_connection.add_dynamic_handler(ProxySessionMessages::new( - crate::schema::v1::SessionId::from(session_id.clone()), + crate::schema::v1::SessionId::from(session_id.0.clone()), )) { Ok(route) => route, Err(error) => return responder.respond_with_error(error), @@ -694,7 +694,7 @@ where session_connection .raw_connection() .add_dynamic_handler(ProxySessionMessages::new( - crate::schema::v1::SessionId::from(session_id.clone()), + crate::schema::v1::SessionId::from(session_id.0.clone()), ))?, ); diff --git a/src/agent-client-protocol/tests/protocol_v2.rs b/src/agent-client-protocol/tests/protocol_v2.rs index 1eb24668..ca95d3ad 100644 --- a/src/agent-client-protocol/tests/protocol_v2.rs +++ b/src/agent-client-protocol/tests/protocol_v2.rs @@ -939,7 +939,6 @@ fn sdk_supported_v2_method_surface_is_jsonrpc_mapped() -> Result<(), Error> { ) })?; - #[cfg(feature = "unstable_elicitation")] { assert_agent_request!( CreateElicitationRequest, @@ -1972,7 +1971,6 @@ async fn protocol_router_downgrades_v2_initialize_metadata_to_v1() -> Result<(), .and_then(|meta| meta.get("request")), Some(&Value::Bool(true)) ); - #[cfg(feature = "unstable_auth_methods")] assert!(initialize.client_capabilities.auth.terminal); responder.respond(v1::InitializeResponse::new(initialize.protocol_version)) }, @@ -1986,12 +1984,8 @@ async fn protocol_router_downgrades_v2_initialize_metadata_to_v1() -> Result<(), "source".into(), Value::String("v2".into()), )])); - #[cfg(feature = "unstable_auth_methods")] - { - capabilities = capabilities.auth( - v2::AuthCapabilities::new().terminal(v2::TerminalAuthCapabilities::new()), - ); - } + capabilities = capabilities + .auth(v2::AuthCapabilities::new().terminal(v2::TerminalAuthCapabilities::new())); let request = v2::InitializeRequest::new( ProtocolVersion::V2, v2::Implementation::new("v2-metadata-client", "9.9.9").title("V2 Metadata Client"), diff --git a/src/agent-client-protocol/tests/schema_elicitation.rs b/src/agent-client-protocol/tests/schema_elicitation.rs index afaf2c30..9da4c25f 100644 --- a/src/agent-client-protocol/tests/schema_elicitation.rs +++ b/src/agent-client-protocol/tests/schema_elicitation.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "unstable_elicitation")] - use agent_client_protocol::schema::v1::{ AgentNotification, AgentRequest, ClientCapabilities, ClientResponse, CompleteElicitationNotification, CreateElicitationRequest, CreateElicitationResponse, diff --git a/src/agent-client-protocol/tests/schema_session_compaction.rs b/src/agent-client-protocol/tests/schema_session_compaction.rs new file mode 100644 index 00000000..56d56615 --- /dev/null +++ b/src/agent-client-protocol/tests/schema_session_compaction.rs @@ -0,0 +1,129 @@ +#![cfg(feature = "unstable_session_compaction")] + +use agent_client_protocol::JsonRpcMessage; +use agent_client_protocol::schema::v1::{ + AgentNotification, ClientCapabilities, ClientSessionCapabilities, CompactionCapabilities, + CompactionStatus, CompactionSummaryChunk, CompactionUpdate, ContentBlock, SessionNotification, + SessionUpdate, TextContent, +}; +use serde_json::json; + +#[test] +fn v1_compaction_capability_and_updates_pass_through_session_notifications() { + let capabilities = ClientCapabilities::new() + .session(ClientSessionCapabilities::new().compaction(CompactionCapabilities::new())); + assert_eq!( + serde_json::to_value(capabilities).unwrap()["session"]["compaction"], + json!({}) + ); + + let notification = SessionNotification::new( + "session-1", + SessionUpdate::CompactionUpdate(CompactionUpdate::new( + "compaction-1", + CompactionStatus::InProgress, + )), + ); + let untyped = notification.to_untyped_message().unwrap(); + assert_eq!(untyped.method, "session/update"); + assert_eq!( + untyped.params["update"], + json!({ + "sessionUpdate": "compaction_update", + "compactionId": "compaction-1", + "status": "in_progress" + }) + ); + + let parsed = AgentNotification::parse_message("session/update", &untyped.params).unwrap(); + assert!(matches!( + parsed, + AgentNotification::SessionNotification(SessionNotification { + update: SessionUpdate::CompactionUpdate(_), + .. + }) + )); + + let notification = SessionNotification::new( + "session-1", + SessionUpdate::CompactionSummaryChunk(CompactionSummaryChunk::new( + "compaction-1", + ContentBlock::Text(TextContent::new("retained context")), + )), + ); + let untyped = notification.to_untyped_message().unwrap(); + assert_eq!( + untyped.params["update"], + json!({ + "sessionUpdate": "compaction_summary_chunk", + "compactionId": "compaction-1", + "content": { "type": "text", "text": "retained context" } + }) + ); + let parsed = AgentNotification::parse_message("session/update", &untyped.params).unwrap(); + assert!(matches!( + parsed, + AgentNotification::SessionNotification(SessionNotification { + update: SessionUpdate::CompactionSummaryChunk(_), + .. + }) + )); +} + +#[cfg(feature = "unstable_protocol_v2")] +#[test] +fn draft_v2_compaction_updates_pass_through_session_notifications() { + use agent_client_protocol::schema::v2; + + let notification = v2::UpdateSessionNotification::new( + "session-1", + v2::SessionUpdate::CompactionUpdate(v2::CompactionUpdate::new( + "compaction-1", + v2::CompactionStatus::Completed, + )), + ); + let untyped = notification.to_untyped_message().unwrap(); + assert_eq!(untyped.method, "session/update"); + assert_eq!( + untyped.params["update"], + json!({ + "sessionUpdate": "compaction_update", + "compactionId": "compaction-1", + "status": "completed" + }) + ); + + let parsed = v2::AgentNotification::parse_message("session/update", &untyped.params).unwrap(); + let v2::AgentNotification::UpdateSessionNotification(parsed) = parsed else { + panic!("expected a v2 session update notification"); + }; + assert!(matches!( + parsed.update, + v2::SessionUpdate::CompactionUpdate(_) + )); + + let notification = v2::UpdateSessionNotification::new( + "session-1", + v2::SessionUpdate::CompactionSummaryChunk(v2::CompactionSummaryChunk::new( + "compaction-1", + v2::ContentBlock::Text(v2::TextContent::new("retained context")), + )), + ); + let untyped = notification.to_untyped_message().unwrap(); + assert_eq!( + untyped.params["update"], + json!({ + "sessionUpdate": "compaction_summary_chunk", + "compactionId": "compaction-1", + "content": { "type": "text", "text": "retained context" } + }) + ); + let parsed = v2::AgentNotification::parse_message("session/update", &untyped.params).unwrap(); + let v2::AgentNotification::UpdateSessionNotification(parsed) = parsed else { + panic!("expected a v2 session update notification"); + }; + assert!(matches!( + parsed.update, + v2::SessionUpdate::CompactionSummaryChunk(_) + )); +}