diff --git a/Cargo.lock b/Cargo.lock index d1a56af1..ef3a6d83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,9 +142,9 @@ dependencies = [ [[package]] name = "agent-client-protocol-schema" -version = "1.10.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3ffd489b0e2f96a134d7966c2f43d874d91299e951b66093b91b54b00b7f8f" +checksum = "3d9a84a76a4f44721e0d8dabcd85b219893c43d6171a4a033d84844d85880823" dependencies = [ "anyhow", "derive_more", diff --git a/Cargo.toml b/Cargo.toml index c1e28626..4d8f90db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,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.10.2", default-features = false, features = ["tracing"] } +agent-client-protocol-schema = { version = "=1.11.0", default-features = false, features = ["tracing"] } # Core async runtime tokio = { version = "1.52", default-features = false } diff --git a/md/features.md b/md/features.md index 6b4532b0..21770938 100644 --- a/md/features.md +++ b/md/features.md @@ -115,6 +115,12 @@ agent-client-protocol = { version = "3", features = ["unstable_protocol_v2", "un The `unstable` aggregate does not select draft v2; enable `unstable_protocol_v2` explicitly when using that version. +Session notices and context compaction are stable in schema 1.11 and available +without feature flags in v1. Draft v2 only needs `unstable_protocol_v2` for these +surfaces. The former `unstable_session_notices` and `unstable_session_compaction` +SDK features have been removed. Applications still negotiate support through +the corresponding protocol capabilities. + The `unstable_subagents` feature exposes the schema's subagent capabilities, updates, and session-message types. It is also included in `unstable`. The existing `session/update` notification route carries subagent updates in v1 and, diff --git a/md/protocol-v2.md b/md/protocol-v2.md index 72fd3832..d243e8c4 100644 --- a/md/protocol-v2.md +++ b/md/protocol-v2.md @@ -446,7 +446,7 @@ 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. -## Schema changes in schema 1.5 through 1.8 +## Schema changes in schema 1.5 through 1.11 The `unstable_protocol_v2` API follows the moving draft schema. Schema 1.5 added semantic newtypes for paths, media types, IDs, and cursors; renamed @@ -462,7 +462,7 @@ Schema 1.6 adds `Cancelled` tool-call and plan-entry statuses to draft v2. 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 +updates (stable since schema 1.11); the SDK carries them through its existing typed `session/update` routing in both protocol versions. V1 clients advertise compaction support through `ClientSessionCapabilities::compaction`. @@ -472,3 +472,23 @@ versions. No tool-name-specific feature is required; draft v2 users only need value unchanged on updates. In v2, omission leaves it unchanged, `null` clears it, and a string replaces it. V1 cannot express the explicit v2 `null` clear operation. + +Schema 1.11 stabilizes session notices and compaction. No surface-specific +feature is required; draft v2 users still need `unstable_protocol_v2`. + +In draft v2, failures after a prompt's user message is inserted end active work +with `StopReason::Error(ErrorStopReason)` on the idle state update, optionally +including a JSON-RPC error object. Failures before insertion remain error +responses to `session/prompt`. `StopReason` now serializes as an object tagged +by `stopReason`, flattened into `IdleStateUpdate`; its `Other` variant takes +`OtherStopReason` rather than a string and preserves unknown payload fields. +V1 prompt responses are unchanged and do not accept an `error` stop reason. + +Schema 1.11 also rejects malformed payloads that receivers act on rather than +silently dropping invalid entries or substituting defaults. This includes +session setup's MCP servers and additional directories, terminal launch +arguments and environment, and v2 replay cursors. Typed request decoding +reports invalid parameters (`-32602`) without invoking the handler. `null` +still means empty for the defaultable acted-on lists; omission is accepted +only where the field is optional. Capability and display metadata retain their +lenient decoding. diff --git a/src/agent-client-protocol/Cargo.toml b/src/agent-client-protocol/Cargo.toml index 21e43b57..92e1ce21 100644 --- a/src/agent-client-protocol/Cargo.toml +++ b/src/agent-client-protocol/Cargo.toml @@ -55,18 +55,14 @@ unstable = [ "unstable_llm_providers", "unstable_mcp_over_acp", "unstable_plan_operations", - "unstable_session_compaction", "unstable_session_fork", - "unstable_session_notices", "unstable_subagents", ] 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_session_notices = ["agent-client-protocol-schema/unstable_session_notices"] unstable_subagents = ["agent-client-protocol-schema/unstable_subagents"] unstable_protocol_v2 = ["agent-client-protocol-schema/unstable_protocol_v2"] diff --git a/src/agent-client-protocol/tests/schema_1_11.rs b/src/agent-client-protocol/tests/schema_1_11.rs new file mode 100644 index 00000000..d6b9e90a --- /dev/null +++ b/src/agent-client-protocol/tests/schema_1_11.rs @@ -0,0 +1,134 @@ +use agent_client_protocol::{ErrorCode, JsonRpcMessage, schema::v1}; +use serde_json::json; + +#[test] +fn stable_notices_pass_through_typed_session_notifications() { + let capabilities = v1::ClientCapabilities::new() + .session(v1::ClientSessionCapabilities::new().notices(v1::NoticeCapabilities::new())); + assert_eq!( + serde_json::to_value(capabilities).unwrap()["session"]["notices"], + json!({}) + ); + + let params = json!({ + "sessionId": "session-1", + "update": { + "sessionUpdate": "notice", + "severity": "warning", + "title": "Provider degraded", + "description": "Retry later", + "_meta": { "source": "provider" } + } + }); + let parsed = v1::AgentNotification::parse_message("session/update", ¶ms).unwrap(); + let v1::AgentNotification::SessionNotification(notification) = &parsed else { + panic!("expected a session notification"); + }; + assert!(matches!(notification.update, v1::SessionUpdate::Notice(_))); + assert_eq!(parsed.to_untyped_message().unwrap().params, params); + + #[cfg(feature = "unstable_protocol_v2")] + { + use agent_client_protocol::schema::v2; + let parsed = v2::AgentNotification::parse_message("session/update", ¶ms).unwrap(); + let v2::AgentNotification::UpdateSessionNotification(notification) = &parsed else { + panic!("expected a v2 session notification"); + }; + assert!(matches!(notification.update, v2::SessionUpdate::Notice(_))); + assert_eq!(parsed.to_untyped_message().unwrap().params, params); + } +} + +#[test] +fn acted_on_session_setup_lists_reject_invalid_params() { + for (field, malformed) in [ + ("additionalDirectories", json!(["/repo/lib", 42])), + ("mcpServers", json!([{"name": "incomplete"}])), + ] { + let mut params = json!({ "cwd": "/repo", "mcpServers": [] }); + params[field] = malformed; + let error = v1::ClientRequest::parse_message("session/new", ¶ms).unwrap_err(); + assert_eq!(error.code, ErrorCode::InvalidParams); + + #[cfg(feature = "unstable_protocol_v2")] + { + use agent_client_protocol::schema::v2; + let error = v2::ClientRequest::parse_message("session/new", ¶ms).unwrap_err(); + assert_eq!(error.code, ErrorCode::InvalidParams); + } + } + + let params = json!({ + "cwd": "/repo", "mcpServers": null, "additionalDirectories": null + }); + let parsed = v1::NewSessionRequest::parse_message("session/new", ¶ms).unwrap(); + assert_eq!(parsed.mcp_servers, []); + assert_eq!( + parsed.additional_directories, + Vec::::new() + ); + + #[cfg(feature = "unstable_protocol_v2")] + { + use agent_client_protocol::schema::v2; + let parsed = v2::NewSessionRequest::parse_message("session/new", ¶ms).unwrap(); + assert_eq!(parsed.mcp_servers, []); + assert_eq!(parsed.additional_directories, []); + + let error = v2::ResumeSessionRequest::parse_message( + "session/resume", + &json!({ "sessionId": "session-1", "cwd": "/repo", "replayFrom": "start" }), + ) + .unwrap_err(); + assert_eq!(error.code, ErrorCode::InvalidParams); + } +} + +#[cfg(feature = "unstable_protocol_v2")] +#[test] +fn v2_error_and_custom_stop_reasons_pass_through_typed_notifications() { + use agent_client_protocol::schema::v2; + + let reason = v2::StopReason::Error( + v2::ErrorStopReason::new() + .error(v2::Error::internal_error().data(json!({ "provider": "unavailable" }))), + ); + let notification = v2::UpdateSessionNotification::new( + "session-1", + v2::SessionUpdate::StateUpdate(v2::StateUpdate::Idle( + v2::IdleStateUpdate::new().stop_reason(reason.clone()), + )), + ); + let mut params = notification.to_untyped_message().unwrap().params; + assert_eq!( + params["update"], + json!({ + "sessionUpdate": "state_update", + "state": "idle", + "stopReason": "error", + "error": { + "code": -32603, + "message": "Internal error", + "data": { "provider": "unavailable" } + } + }) + ); + let parsed = v2::UpdateSessionNotification::parse_message("session/update", ¶ms).unwrap(); + let v2::SessionUpdate::StateUpdate(v2::StateUpdate::Idle(idle)) = parsed.update else { + panic!("expected an idle state update"); + }; + assert_eq!(idle.stop_reason, Some(reason)); + + params["update"] = json!({ + "sessionUpdate": "state_update", "state": "idle", + "stopReason": "_paused", "resumeAfter": 30, + "_meta": { "source": "provider" } + }); + let parsed = v2::UpdateSessionNotification::parse_message("session/update", ¶ms).unwrap(); + assert_eq!(parsed.to_untyped_message().unwrap().params, params); + + // V1 still reports prompt failures through the JSON-RPC response error. + assert!( + serde_json::from_value::(json!({ "stopReason": "error" })).is_err() + ); +} diff --git a/src/agent-client-protocol/tests/schema_session_compaction.rs b/src/agent-client-protocol/tests/schema_session_compaction.rs index 56d56615..c1c5c6f9 100644 --- a/src/agent-client-protocol/tests/schema_session_compaction.rs +++ b/src/agent-client-protocol/tests/schema_session_compaction.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "unstable_session_compaction")] - use agent_client_protocol::JsonRpcMessage; use agent_client_protocol::schema::v1::{ AgentNotification, ClientCapabilities, ClientSessionCapabilities, CompactionCapabilities,