From 5830e21460544e795e2af1e6d848c9750c9c7766 Mon Sep 17 00:00:00 2001 From: Andy Adams-Moran Date: Wed, 27 May 2026 15:46:25 +0100 Subject: [PATCH 1/5] Rust SDK: add typed per-session capability controls Adds a typed `SessionCapability` enum and matching `SessionConfig` / `ResumeSessionConfig` fields plus builder methods, so callers can express "enable memory", "disable plan-mode", etc. as a per-session wire parameter rather than a spawn-time CLI flag. - `SessionCapability` is `#[non_exhaustive]`, kebab-case-serialized (via `Display` / `FromStr` / `From<&str>` / `From`), and carries an `Other(String)` escape hatch for forward compatibility with capabilities the runtime adds without requiring an SDK rebuild. - `SessionConfig` and `ResumeSessionConfig` each gain `enabled_capabilities` / `disabled_capabilities` vectors and four builders: `with_enable_capability`, `with_disable_capability`, `with_enabled_capabilities`, `with_disabled_capabilities`. - `SessionConfig::into_wire` and `ResumeSessionConfig::into_wire` convert the vecs to `Option>` and emit them as `enabledCapabilities` / `disabledCapabilities` in the `session.create` and `session.resume` JSON-RPC payloads. Empty vecs are serialised as `None` (field omitted). Disable wins over enable on conflict (the runtime applies enable first, then disable). - Works for every transport -- including `Transport::External` (Desktop app / shared CLI server) -- because it does not rely on CLI spawn arguments. Pairs with github/copilot-agent-runtime#8918 (per-session capability API) and github/agents#981 (Desktop missing memory capability). 10 new unit tests: 3 enum-level tests (Display / FromStr / From conversions) and 7 wire-serialisation tests in a dedicated `capability_tests` module in `types.rs` (empty omitted, single enable, single disable, bulk-replace, Other round-trip, resume empty, resume enable+disable). Pre-existing test breakage: rust/tests/session_test.rs and rust/tests/protocol_version_test.rs reference removed API methods on main and are unrelated to this change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/README.md | 99 +++++++++++++ rust/src/lib.rs | 164 ++++++++++++++++++++++ rust/src/types.rs | 344 +++++++++++++++++++++++++++++++++++++++++++++- rust/src/wire.rs | 20 +++ 4 files changed, 625 insertions(+), 2 deletions(-) diff --git a/rust/README.md b/rust/README.md index 0b5bec1cd..a3ca5fad0 100644 --- a/rust/README.md +++ b/rust/README.md @@ -78,6 +78,97 @@ client.stop().await?; With the default `CliProgram::Resolve`, `Client::start()` resolves the CLI in this order: an explicit `CliProgram::Path(path)`, the `COPILOT_CLI_PATH` env var, then the bundled CLI that was embedded at build time. There is no PATH scanning — if you've opted out of bundling (`default-features = false`) you must supply either `CliProgram::Path` or `COPILOT_CLI_PATH`. +### Session capabilities + +The `SessionCapability` enum lets callers enable or disable named runtime features on a **per-session** basis. Capabilities are sent to the runtime as part of the `session.create` / `session.resume` JSON-RPC calls via two wire fields: + +- `enabledCapabilities` -- capabilities to opt the session into (extends the `SDK_CAPABILITIES` baseline) +- `disabledCapabilities` -- capabilities to opt the session out of (disable wins on overlap) + +This approach works for every transport -- including `Transport::External` (Desktop app / shared CLI server) -- because it does not rely on CLI spawn arguments. + +> **Experimental.** Per-session capability controls are an experimental +> wire-protocol surface and may change or be removed in future SDK or CLI +> releases. The Rust SDK marks the relevant fields and builders with a +> `
**Experimental.**
` rustdoc block (the +> repository convention used in `rust/src/generated/`); there is no +> `#[experimental]` attribute, so the marker is documentation-only. + +> **Runtime dependency.** Per-session capability controls require +> [github/copilot-agent-runtime#8918](https://github.com/github/copilot-agent-runtime/pull/8918) +> or later. On older runtimes the fields are silently ignored. +> Pairs with [github/agents#1081](https://github.com/github/agents/issues/1081) +> (Desktop app missing memory capability). + +Use `SessionConfig::with_enable_capability` / `with_disable_capability` (and their plural counterparts): + +```rust,ignore +use github_copilot_sdk::{SessionCapability, SessionConfig}; + +let session = client.create_session( + SessionConfig::default() + .with_enable_capability(SessionCapability::Memory), + "What is 2 + 2?".into(), +).await?; +``` + +On resume, use the same builders on `ResumeSessionConfig`: + +```rust,ignore +use github_copilot_sdk::{ResumeSessionConfig, SessionCapability}; + +let session = client.resume_session( + ResumeSessionConfig::new(session_id) + .with_enable_capability(SessionCapability::Memory), + None, +).await?; +``` + +**Variants:** + +| Variant | Wire name | Description | +| -------------------- | ----------------------- | ----------------------------------------------------- | +| `TuiHints` | `tui-hints` | TUI keyboard shortcuts | +| `PlanMode` | `plan-mode` | `[[PLAN]]` handling and plan-mode instructions | +| `Memory` | `memory` | `store_memory` tool and `` system-prompt section | +| `CliDocumentation` | `cli-documentation` | `fetch_copilot_cli_documentation` tool and `` section | +| `AskUser` | `ask-user` | `ask_user` tool for interactive clarification | +| `InteractiveMode` | `interactive-mode` | Interactive-CLI identity (vs headless) | +| `SystemNotifications`| `system-notifications` | Automatic batched system notifications to the agent | +| `Elicitation` | `elicitation` | Elicitation prompts (confirm / select / input) | +| `McpApps` | `mcp-apps` | MCP-Apps `ui://` resource passthrough (SEP-1865) | +| `CanvasRenderer` | `canvas-renderer` | Host-rendered extension canvases | +| `Other(String)` | *(verbatim)* | Forward-compat escape hatch for unknown future names | + +**Disable-wins semantics.** If the same capability appears in both +`enabled_capabilities` and `disabled_capabilities`, disable wins. The runtime +starts from an `SDK_CAPABILITIES` baseline; enabled capabilities extend it and +disabled capabilities remove from it, in that order. + +**Forward compatibility.** The enum is `#[non_exhaustive]` and carries an +`Other(String)` variant so callers on older SDK builds can opt into +capabilities that the runtime adds ahead of a new SDK release, without any +recompile-blocking enum-variant additions: + +```rust,ignore +use github_copilot_sdk::{SessionCapability, SessionConfig}; + +// Opt into a capability the SDK doesn't know about yet. +let config = SessionConfig::default() + .with_enable_capability(SessionCapability::Other("future-cap".to_string())); +``` + +`&str` and `String` implement `Into`, so you can also pass +string literals directly to the builders: + +```rust,ignore +use github_copilot_sdk::SessionConfig; + +let config = SessionConfig::default() + .with_enable_capability("memory") // &str coerces to SessionCapability + .with_disable_capability("plan-mode"); +``` + ### Session Created via `Client::create_session` or `Client::resume_session`. Owns an internal event loop that dispatches CLI callbacks to the focused handler traits you install on `SessionConfig`, and broadcasts session events through `subscribe()`. @@ -714,6 +805,14 @@ gets to be Rust here — cross-SDK parity for these is a post-release conversation, not a release blocker. None of these are deprecated and none of them are scheduled for removal. +- **`SessionCapability` enum** -- typed, `#[non_exhaustive]` enum for per-session + capability opt-in / opt-out, with an `Other(String)` escape hatch for + forward compatibility. Sent via `enabledCapabilities` / + `disabledCapabilities` on the `session.create` and `session.resume` wire + calls -- works for all transports including `Transport::External`. See + [Session capabilities](#session-capabilities) above. Marked + **experimental** via the repository's `
` rustdoc + convention. Node/Python/Go/.NET accept stringly-typed flags. - **Typed newtypes** — `SessionId` and `RequestId` are `#[serde(transparent)]` newtypes around `String`, so the type system distinguishes a session identifier from an arbitrary `String` at compile time. Node/Python/Go diff --git a/rust/src/lib.rs b/rust/src/lib.rs index cab34b476..d1400210e 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -402,6 +402,129 @@ impl OtelExporterType { } } +/// A named session capability sent in the `session.create` and +/// `session.resume` wire payloads. +/// +/// Capabilities gate optional CLI features (extra tools, system-prompt +/// sections, host-rendered surfaces). The runtime starts from a +/// hard-coded `SDK_CAPABILITIES` set; use +/// [`SessionConfig::with_enable_capability`] / +/// [`SessionConfig::with_disable_capability`] (and their plural +/// counterparts) to opt individual sessions in or out. +/// +/// > **Not** the same as [`SessionCapabilities`] — that struct is the +/// > *runtime-negotiated* capability descriptor reported by the CLI on +/// > `session.create`. [`SessionCapability`] is the *opt-in / opt-out +/// > toggle name* sent with each `session.create` / `session.resume`. +/// +/// The runtime's overlap semantics are **disable-wins**: if a capability +/// appears in both the enabled and disabled lists, the disable wins. +/// The SDK preserves the order callers add capabilities in so the +/// resulting wire payload is deterministic. +/// +/// The enum is `#[non_exhaustive]` and carries an [`Other`](Self::Other) +/// variant so forward-compat capabilities the runtime grows ahead of an +/// SDK release can still be opted into without waiting for a new +/// enum variant. +/// +/// Requires github/copilot-agent-runtime#8918 or later. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[non_exhaustive] +pub enum SessionCapability { + /// TUI-only prompt hints (keyboard shortcuts). + TuiHints, + /// `[[PLAN]]` handling and plan-mode instructions. + PlanMode, + /// `store_memory` tool and the `` system-prompt section. + Memory, + /// `fetch_copilot_cli_documentation` tool plus the + /// `` system-prompt section. + CliDocumentation, + /// `ask_user` tool for interactive clarification. + AskUser, + /// Interactive-CLI identity (vs non-interactive / headless). + InteractiveMode, + /// Automatic system notifications to the agent (batched, hidden + /// from the user timeline). + SystemNotifications, + /// Elicitation support (confirm / select / input prompts). + Elicitation, + /// MCP-Apps (SEP-1865) `ui://` resource passthrough. + McpApps, + /// Extension-provided canvases rendered by the host. + CanvasRenderer, + /// A capability name the SDK doesn't have a typed variant for yet. + /// + /// Pass any kebab-case capability string here to forward it + /// verbatim to the runtime. + Other(String), +} + +impl SessionCapability { + /// The kebab-case wire string sent in `enabledCapabilities` / + /// `disabledCapabilities` on `session.create` and `session.resume`. + pub fn as_str(&self) -> &str { + match self { + Self::TuiHints => "tui-hints", + Self::PlanMode => "plan-mode", + Self::Memory => "memory", + Self::CliDocumentation => "cli-documentation", + Self::AskUser => "ask-user", + Self::InteractiveMode => "interactive-mode", + Self::SystemNotifications => "system-notifications", + Self::Elicitation => "elicitation", + Self::McpApps => "mcp-apps", + Self::CanvasRenderer => "canvas-renderer", + Self::Other(name) => name.as_str(), + } + } +} + +impl std::fmt::Display for SessionCapability { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +impl std::str::FromStr for SessionCapability { + type Err = std::convert::Infallible; + + /// Parse a kebab-case capability name. Unknown names round-trip + /// through [`SessionCapability::Other`] so old SDK builds stay + /// useful against CLIs that add new capabilities. Always returns + /// `Ok` — the error type is [`Infallible`](std::convert::Infallible). + fn from_str(s: &str) -> std::result::Result { + Ok(match s { + "tui-hints" => Self::TuiHints, + "plan-mode" => Self::PlanMode, + "memory" => Self::Memory, + "cli-documentation" => Self::CliDocumentation, + "ask-user" => Self::AskUser, + "interactive-mode" => Self::InteractiveMode, + "system-notifications" => Self::SystemNotifications, + "elicitation" => Self::Elicitation, + "mcp-apps" => Self::McpApps, + "canvas-renderer" => Self::CanvasRenderer, + other => Self::Other(other.to_owned()), + }) + } +} + +impl From<&str> for SessionCapability { + fn from(s: &str) -> Self { + // FromStr::from_str is Infallible — unwrap is safe. + s.parse() + .expect("SessionCapability::from_str is Infallible") + } +} + +impl From for SessionCapability { + fn from(s: String) -> Self { + s.as_str().into() + } +} + /// OpenTelemetry configuration forwarded to the spawned GitHub Copilot CLI /// process. /// @@ -2379,6 +2502,47 @@ mod tests { assert_eq!(Client::remote_args(&opts), vec!["--remote".to_string()]); } + #[test] + fn session_capability_round_trips_via_str() { + for cap in [ + SessionCapability::TuiHints, + SessionCapability::PlanMode, + SessionCapability::Memory, + SessionCapability::CliDocumentation, + SessionCapability::AskUser, + SessionCapability::InteractiveMode, + SessionCapability::SystemNotifications, + SessionCapability::Elicitation, + SessionCapability::McpApps, + SessionCapability::CanvasRenderer, + ] { + let s = cap.to_string(); + let parsed: SessionCapability = s.parse().unwrap(); + assert_eq!(parsed, cap, "round-trip failed for {s}"); + } + } + + #[test] + fn session_capability_from_str_falls_back_to_other_for_unknown_names() { + let parsed: SessionCapability = "brand-new-cap".parse().unwrap(); + assert_eq!( + parsed, + SessionCapability::Other("brand-new-cap".to_string()) + ); + assert_eq!(parsed.as_str(), "brand-new-cap"); + } + + #[test] + fn session_capability_into_from_str_and_string() { + let from_str: SessionCapability = "memory".into(); + let from_string: SessionCapability = "memory".to_string().into(); + assert_eq!(from_str, SessionCapability::Memory); + assert_eq!(from_string, SessionCapability::Memory); + // Unknown names go to Other + let other: SessionCapability = "future-cap".into(); + assert_eq!(other, SessionCapability::Other("future-cap".to_string())); + } + #[test] fn log_level_args_omitted_when_unset() { let opts = ClientOptions::default(); diff --git a/rust/src/types.rs b/rust/src/types.rs index 8b9b5960a..5698d9535 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -12,6 +12,7 @@ use std::time::Duration; use serde::{Deserialize, Serialize}; use serde_json::Value; +use crate::SessionCapability; use crate::canvas::{CanvasDeclaration, CanvasHandler}; use crate::generated::api_types::OpenCanvasInstance; /// Context window tier for models that support tiered context windows. @@ -1369,6 +1370,33 @@ pub struct SessionConfig { /// `session.options.update` after create/resume. Defaults to `false` in /// [`crate::ClientMode::Empty`] when unset. pub manage_schedule_enabled: Option, + /// Capabilities to opt this session into via `enabledCapabilities` on + /// the `session.create` wire call. The runtime starts from a + /// `SDK_CAPABILITIES` baseline; this list extends it. + /// + ///
+ /// + /// **Experimental.** This is part of an experimental wire-protocol + /// surface and may change or be removed in future SDK or CLI releases. + /// + ///
+ /// + /// Requires github/copilot-agent-runtime#8918 or later. On older + /// runtimes the field is silently ignored. + pub enabled_capabilities: Vec, + /// Capabilities to opt this session out of via `disabledCapabilities` + /// on the `session.create` wire call. Disable wins over enable on + /// overlap. See [`SessionCapability`]. + /// + ///
+ /// + /// **Experimental.** This is part of an experimental wire-protocol + /// surface and may change or be removed in future SDK or CLI releases. + /// + ///
+ /// + /// Requires github/copilot-agent-runtime#8918 or later. + pub disabled_capabilities: Vec, } impl std::fmt::Debug for SessionConfig { @@ -1476,6 +1504,8 @@ impl std::fmt::Debug for SessionConfig { "system_message_transform", &self.system_message_transform.as_ref().map(|_| ""), ) + .field("enabled_capabilities", &self.enabled_capabilities) + .field("disabled_capabilities", &self.disabled_capabilities) .finish() } } @@ -1550,11 +1580,11 @@ impl Default for SessionConfig { custom_agents_local_only: None, coauthor_enabled: None, manage_schedule_enabled: None, + enabled_capabilities: Vec::new(), + disabled_capabilities: Vec::new(), } } } - -/// Runtime-only bundle drained out of a [`SessionConfig`] or /// [`ResumeSessionConfig`] by [`SessionConfig::into_wire`] / /// [`ResumeSessionConfig::into_wire`]. Holds the trait-object handlers, /// session-fs provider, and slash commands so the wire payload struct @@ -1623,6 +1653,26 @@ impl SessionConfig { let wire_canvases = self.canvases.clone(); let canvas_handler = self.canvas_handler.clone(); + let enabled_capabilities = if self.enabled_capabilities.is_empty() { + None + } else { + Some( + self.enabled_capabilities + .iter() + .map(|c| c.to_string()) + .collect(), + ) + }; + let disabled_capabilities = if self.disabled_capabilities.is_empty() { + None + } else { + Some( + self.disabled_capabilities + .iter() + .map(|c| c.to_string()) + .collect(), + ) + }; let wire = crate::wire::SessionCreateWire { session_id, model: self.model, @@ -1679,6 +1729,8 @@ impl SessionConfig { cloud: self.cloud, include_sub_agent_streaming_events: self.include_sub_agent_streaming_events, commands: wire_commands, + enabled_capabilities, + disabled_capabilities, }; let runtime = SessionConfigRuntime { @@ -1889,6 +1941,90 @@ impl SessionConfig { self } + /// Opt this session into a capability via `enabledCapabilities` on + /// `session.create`. Appends to [`Self::enabled_capabilities`], + /// preserving insertion order. + /// + /// See [`SessionCapability`] for the disable-wins overlap rules + /// and the [`SessionCapability::Other`] forward-compat escape hatch. + /// + ///
+ /// + /// **Experimental.** This is part of an experimental wire-protocol + /// surface and may change or be removed in future SDK or CLI releases. + /// + ///
+ /// + /// Requires github/copilot-agent-runtime#8918 or later. + /// + /// # Example + /// + /// ```rust,ignore + /// use github_copilot_sdk::{SessionCapability, SessionConfig}; + /// let config = SessionConfig::default() + /// .with_enable_capability(SessionCapability::Memory); + /// ``` + pub fn with_enable_capability(mut self, capability: impl Into) -> Self { + self.enabled_capabilities.push(capability.into()); + self + } + + /// Opt this session out of a capability via `disabledCapabilities` on + /// `session.create`. Disable wins over enable on overlap. + /// + ///
+ /// + /// **Experimental.** This is part of an experimental wire-protocol + /// surface and may change or be removed in future SDK or CLI releases. + /// + ///
+ /// + /// Requires github/copilot-agent-runtime#8918 or later. + pub fn with_disable_capability(mut self, capability: impl Into) -> Self { + self.disabled_capabilities.push(capability.into()); + self + } + + /// Replace [`Self::enabled_capabilities`] with the given iterable. + /// Insertion order is preserved. + /// + ///
+ /// + /// **Experimental.** This is part of an experimental wire-protocol + /// surface and may change or be removed in future SDK or CLI releases. + /// + ///
+ /// + /// Requires github/copilot-agent-runtime#8918 or later. + pub fn with_enabled_capabilities(mut self, capabilities: I) -> Self + where + I: IntoIterator, + C: Into, + { + self.enabled_capabilities = capabilities.into_iter().map(Into::into).collect(); + self + } + + /// Replace [`Self::disabled_capabilities`] with the given iterable. + /// Insertion order is preserved. Disable wins over enable on overlap. + /// + ///
+ /// + /// **Experimental.** This is part of an experimental wire-protocol + /// surface and may change or be removed in future SDK or CLI releases. + /// + ///
+ /// + /// Requires github/copilot-agent-runtime#8918 or later. + pub fn with_disabled_capabilities(mut self, capabilities: I) -> Self + where + I: IntoIterator, + C: Into, + { + self.disabled_capabilities = capabilities.into_iter().map(Into::into).collect(); + self + } + /// Set stable extension identity metadata for this connection. pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self { self.extension_info = Some(extension_info); @@ -2346,6 +2482,30 @@ pub struct ResumeSessionConfig { pub coauthor_enabled: Option, /// See [`SessionConfig::manage_schedule_enabled`]. pub manage_schedule_enabled: Option, + /// Capabilities to opt this session into via `enabledCapabilities` on + /// the `session.resume` wire call. See [`SessionConfig::enabled_capabilities`]. + /// + ///
+ /// + /// **Experimental.** This is part of an experimental wire-protocol + /// surface and may change or be removed in future SDK or CLI releases. + /// + ///
+ /// + /// Requires github/copilot-agent-runtime#8918 or later. + pub enabled_capabilities: Vec, + /// Capabilities to opt this session out of via `disabledCapabilities` on + /// the `session.resume` wire call. Disable wins over enable on overlap. + /// + ///
+ /// + /// **Experimental.** This is part of an experimental wire-protocol + /// surface and may change or be removed in future SDK or CLI releases. + /// + ///
+ /// + /// Requires github/copilot-agent-runtime#8918 or later. + pub disabled_capabilities: Vec, } impl std::fmt::Debug for ResumeSessionConfig { @@ -2454,6 +2614,8 @@ impl std::fmt::Debug for ResumeSessionConfig { ) .field("suppress_resume_event", &self.suppress_resume_event) .field("continue_pending_work", &self.continue_pending_work) + .field("enabled_capabilities", &self.enabled_capabilities) + .field("disabled_capabilities", &self.disabled_capabilities) .finish() } } @@ -2502,6 +2664,27 @@ impl ResumeSessionConfig { let wire_canvases = self.canvases.clone(); let canvas_handler = self.canvas_handler.clone(); + let enabled_capabilities = if self.enabled_capabilities.is_empty() { + None + } else { + Some( + self.enabled_capabilities + .iter() + .map(|c| c.to_string()) + .collect(), + ) + }; + let disabled_capabilities = if self.disabled_capabilities.is_empty() { + None + } else { + Some( + self.disabled_capabilities + .iter() + .map(|c| c.to_string()) + .collect(), + ) + }; + let wire = crate::wire::SessionResumeWire { session_id: self.session_id, client_name: self.client_name, @@ -2559,6 +2742,8 @@ impl ResumeSessionConfig { commands: wire_commands, suppress_resume_event: self.suppress_resume_event, continue_pending_work: self.continue_pending_work, + enabled_capabilities, + disabled_capabilities, }; let runtime = SessionConfigRuntime { @@ -2648,6 +2833,8 @@ impl ResumeSessionConfig { custom_agents_local_only: None, coauthor_enabled: None, manage_schedule_enabled: None, + enabled_capabilities: Vec::new(), + disabled_capabilities: Vec::new(), } } @@ -2824,6 +3011,76 @@ impl ResumeSessionConfig { self } + /// Opt this resumed session into a capability via `enabledCapabilities` + /// on `session.resume`. See [`SessionConfig::with_enable_capability`]. + /// + ///
+ /// + /// **Experimental.** This is part of an experimental wire-protocol + /// surface and may change or be removed in future SDK or CLI releases. + /// + ///
+ /// + /// Requires github/copilot-agent-runtime#8918 or later. + pub fn with_enable_capability(mut self, capability: impl Into) -> Self { + self.enabled_capabilities.push(capability.into()); + self + } + + /// Opt this resumed session out of a capability. Disable wins on overlap. + /// + ///
+ /// + /// **Experimental.** This is part of an experimental wire-protocol + /// surface and may change or be removed in future SDK or CLI releases. + /// + ///
+ /// + /// Requires github/copilot-agent-runtime#8918 or later. + pub fn with_disable_capability(mut self, capability: impl Into) -> Self { + self.disabled_capabilities.push(capability.into()); + self + } + + /// Replace [`Self::enabled_capabilities`] with the given iterable. + /// + ///
+ /// + /// **Experimental.** This is part of an experimental wire-protocol + /// surface and may change or be removed in future SDK or CLI releases. + /// + ///
+ /// + /// Requires github/copilot-agent-runtime#8918 or later. + pub fn with_enabled_capabilities(mut self, capabilities: I) -> Self + where + I: IntoIterator, + C: Into, + { + self.enabled_capabilities = capabilities.into_iter().map(Into::into).collect(); + self + } + + /// Replace [`Self::disabled_capabilities`] with the given iterable. + /// Disable wins over enable on overlap. + /// + ///
+ /// + /// **Experimental.** This is part of an experimental wire-protocol + /// surface and may change or be removed in future SDK or CLI releases. + /// + ///
+ /// + /// Requires github/copilot-agent-runtime#8918 or later. + pub fn with_disabled_capabilities(mut self, capabilities: I) -> Self + where + I: IntoIterator, + C: Into, + { + self.disabled_capabilities = capabilities.into_iter().map(Into::into).collect(); + self + } + /// Set stable extension identity metadata for this connection on resume. pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self { self.extension_info = Some(extension_info); @@ -5338,3 +5595,86 @@ mod permission_builder_tests { )); } } + +#[cfg(test)] +mod capability_tests { + use super::*; + use crate::SessionCapability; + + fn create_session_wire(config: SessionConfig) -> crate::wire::SessionCreateWire { + let (wire, _) = config + .into_wire(Some(SessionId::new("test-session"))) + .unwrap(); + wire + } + + fn resume_session_wire(config: ResumeSessionConfig) -> crate::wire::SessionResumeWire { + let (wire, _) = config.into_wire().unwrap(); + wire + } + + #[test] + fn session_config_empty_capabilities_omitted_from_wire() { + let wire = create_session_wire(SessionConfig::default()); + assert!(wire.enabled_capabilities.is_none()); + assert!(wire.disabled_capabilities.is_none()); + } + + #[test] + fn session_config_enabled_capabilities_serialized_on_wire() { + let config = SessionConfig::default() + .with_enable_capability(SessionCapability::Memory) + .with_enable_capability(SessionCapability::PlanMode); + let wire = create_session_wire(config); + let enabled = wire.enabled_capabilities.as_ref().unwrap(); + assert_eq!(enabled, &["memory".to_string(), "plan-mode".to_string()]); + assert!(wire.disabled_capabilities.is_none()); + } + + #[test] + fn session_config_disabled_capabilities_serialized_on_wire() { + let config = SessionConfig::default().with_disable_capability(SessionCapability::PlanMode); + let wire = create_session_wire(config); + assert!(wire.enabled_capabilities.is_none()); + let disabled = wire.disabled_capabilities.as_ref().unwrap(); + assert_eq!(disabled, &["plan-mode".to_string()]); + } + + #[test] + fn session_config_with_enabled_capabilities_replaces() { + let config = SessionConfig::default() + .with_enable_capability(SessionCapability::Memory) + .with_enabled_capabilities([SessionCapability::PlanMode]); + let wire = create_session_wire(config); + let enabled = wire.enabled_capabilities.as_ref().unwrap(); + assert_eq!(enabled, &["plan-mode".to_string()]); + } + + #[test] + fn session_config_other_capability_round_trips_through_wire() { + let config = SessionConfig::default() + .with_enable_capability(SessionCapability::Other("custom-cap".to_string())); + let wire = create_session_wire(config); + let enabled = wire.enabled_capabilities.as_ref().unwrap(); + assert_eq!(enabled, &["custom-cap".to_string()]); + } + + #[test] + fn resume_session_config_empty_capabilities_omitted_from_wire() { + let wire = resume_session_wire(ResumeSessionConfig::new("sid".into())); + assert!(wire.enabled_capabilities.is_none()); + assert!(wire.disabled_capabilities.is_none()); + } + + #[test] + fn resume_session_config_capabilities_serialized_on_wire() { + let config = ResumeSessionConfig::new("sid".into()) + .with_enable_capability(SessionCapability::Memory) + .with_disable_capability(SessionCapability::PlanMode); + let wire = resume_session_wire(config); + let enabled = wire.enabled_capabilities.as_ref().unwrap(); + let disabled = wire.disabled_capabilities.as_ref().unwrap(); + assert_eq!(enabled, &["memory".to_string()]); + assert_eq!(disabled, &["plan-mode".to_string()]); + } +} diff --git a/rust/src/wire.rs b/rust/src/wire.rs index de40720b2..b1c982e1c 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -147,6 +147,16 @@ pub(crate) struct SessionCreateWire { pub include_sub_agent_streaming_events: Option, #[serde(skip_serializing_if = "Option::is_none")] pub commands: Option>, + /// Capabilities to opt this session into. Forwarded as + /// `enabledCapabilities` on the `session.create` wire call. + /// Requires github/copilot-agent-runtime#8918. + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled_capabilities: Option>, + /// Capabilities to opt this session out of. Disable wins on overlap. + /// Forwarded as `disabledCapabilities` on the `session.create` wire call. + /// Requires github/copilot-agent-runtime#8918. + #[serde(skip_serializing_if = "Option::is_none")] + pub disabled_capabilities: Option>, } /// The exact JSON shape sent on the `session.resume` JSON-RPC request. @@ -257,4 +267,14 @@ pub(crate) struct SessionResumeWire { pub suppress_resume_event: Option, #[serde(skip_serializing_if = "Option::is_none")] pub continue_pending_work: Option, + /// Capabilities to opt this session into. Forwarded as + /// `enabledCapabilities` on the `session.resume` wire call. + /// Requires github/copilot-agent-runtime#8918. + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled_capabilities: Option>, + /// Capabilities to opt this session out of. Disable wins on overlap. + /// Forwarded as `disabledCapabilities` on the `session.resume` wire call. + /// Requires github/copilot-agent-runtime#8918. + #[serde(skip_serializing_if = "Option::is_none")] + pub disabled_capabilities: Option>, } From 5272857c02dcf4f1f57948df07c9f9f4395867b9 Mon Sep 17 00:00:00 2001 From: Andy Adams-Moran Date: Tue, 9 Jun 2026 15:26:44 +0100 Subject: [PATCH 2/5] Refine session capability enum Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/README.md | 3 +- rust/src/lib.rs | 130 +++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 112 insertions(+), 21 deletions(-) diff --git a/rust/README.md b/rust/README.md index a3ca5fad0..b5870f4e5 100644 --- a/rust/README.md +++ b/rust/README.md @@ -108,7 +108,6 @@ use github_copilot_sdk::{SessionCapability, SessionConfig}; let session = client.create_session( SessionConfig::default() .with_enable_capability(SessionCapability::Memory), - "What is 2 + 2?".into(), ).await?; ``` @@ -120,7 +119,6 @@ use github_copilot_sdk::{ResumeSessionConfig, SessionCapability}; let session = client.resume_session( ResumeSessionConfig::new(session_id) .with_enable_capability(SessionCapability::Memory), - None, ).await?; ``` @@ -136,6 +134,7 @@ let session = client.resume_session( | `InteractiveMode` | `interactive-mode` | Interactive-CLI identity (vs headless) | | `SystemNotifications`| `system-notifications` | Automatic batched system notifications to the agent | | `Elicitation` | `elicitation` | Elicitation prompts (confirm / select / input) | +| `SessionStore` | `session-store` | Cross-session history tools and session-store metadata | | `McpApps` | `mcp-apps` | MCP-Apps `ui://` resource passthrough (SEP-1865) | | `CanvasRenderer` | `canvas-renderer` | Host-rendered extension canvases | | `Other(String)` | *(verbatim)* | Forward-compat escape hatch for unknown future names | diff --git a/rust/src/lib.rs b/rust/src/lib.rs index d1400210e..f9cdc8e77 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -416,6 +416,11 @@ impl OtelExporterType { /// > *runtime-negotiated* capability descriptor reported by the CLI on /// > `session.create`. [`SessionCapability`] is the *opt-in / opt-out /// > toggle name* sent with each `session.create` / `session.resume`. +/// > +/// > This public type is also separate from the generated protocol enum: +/// > unknown generated enum values collapse to `Unknown`, while callers +/// > need [`Other`](Self::Other) to preserve and send capability names +/// > introduced by newer runtimes. /// /// The runtime's overlap semantics are **disable-wins**: if a capability /// appears in both the enabled and disabled lists, the disable wins. @@ -428,8 +433,7 @@ impl OtelExporterType { /// enum variant. /// /// Requires github/copilot-agent-runtime#8918 or later. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum SessionCapability { /// TUI-only prompt hints (keyboard shortcuts). @@ -450,6 +454,8 @@ pub enum SessionCapability { SystemNotifications, /// Elicitation support (confirm / select / input prompts). Elicitation, + /// Cross-session history tools and session-store prompt/tool metadata. + SessionStore, /// MCP-Apps (SEP-1865) `ui://` resource passthrough. McpApps, /// Extension-provided canvases rendered by the host. @@ -474,11 +480,29 @@ impl SessionCapability { Self::InteractiveMode => "interactive-mode", Self::SystemNotifications => "system-notifications", Self::Elicitation => "elicitation", + Self::SessionStore => "session-store", Self::McpApps => "mcp-apps", Self::CanvasRenderer => "canvas-renderer", Self::Other(name) => name.as_str(), } } + + fn from_known_name(name: &str) -> Option { + Some(match name { + "tui-hints" => Self::TuiHints, + "plan-mode" => Self::PlanMode, + "memory" => Self::Memory, + "cli-documentation" => Self::CliDocumentation, + "ask-user" => Self::AskUser, + "interactive-mode" => Self::InteractiveMode, + "system-notifications" => Self::SystemNotifications, + "elicitation" => Self::Elicitation, + "session-store" => Self::SessionStore, + "mcp-apps" => Self::McpApps, + "canvas-renderer" => Self::CanvasRenderer, + _ => return None, + }) + } } impl std::fmt::Display for SessionCapability { @@ -495,33 +519,42 @@ impl std::str::FromStr for SessionCapability { /// useful against CLIs that add new capabilities. Always returns /// `Ok` — the error type is [`Infallible`](std::convert::Infallible). fn from_str(s: &str) -> std::result::Result { - Ok(match s { - "tui-hints" => Self::TuiHints, - "plan-mode" => Self::PlanMode, - "memory" => Self::Memory, - "cli-documentation" => Self::CliDocumentation, - "ask-user" => Self::AskUser, - "interactive-mode" => Self::InteractiveMode, - "system-notifications" => Self::SystemNotifications, - "elicitation" => Self::Elicitation, - "mcp-apps" => Self::McpApps, - "canvas-renderer" => Self::CanvasRenderer, - other => Self::Other(other.to_owned()), - }) + Ok(Self::from(s)) } } impl From<&str> for SessionCapability { fn from(s: &str) -> Self { - // FromStr::from_str is Infallible — unwrap is safe. - s.parse() - .expect("SessionCapability::from_str is Infallible") + Self::from_known_name(s).unwrap_or_else(|| Self::Other(s.to_owned())) } } impl From for SessionCapability { fn from(s: String) -> Self { - s.as_str().into() + if let Some(capability) = Self::from_known_name(&s) { + capability + } else { + Self::Other(s) + } + } +} + +impl Serialize for SessionCapability { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de> Deserialize<'de> for SessionCapability { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let capability = String::deserialize(deserializer)?; + Ok(Self::from(capability)) } } @@ -2513,6 +2546,7 @@ mod tests { SessionCapability::InteractiveMode, SessionCapability::SystemNotifications, SessionCapability::Elicitation, + SessionCapability::SessionStore, SessionCapability::McpApps, SessionCapability::CanvasRenderer, ] { @@ -2543,6 +2577,64 @@ mod tests { assert_eq!(other, SessionCapability::Other("future-cap".to_string())); } + #[test] + fn session_capability_serializes_as_wire_string() { + assert_eq!( + serde_json::to_value(SessionCapability::Memory).unwrap(), + serde_json::json!("memory") + ); + assert_eq!( + serde_json::to_value(SessionCapability::Other("future-cap".to_string())).unwrap(), + serde_json::json!("future-cap") + ); + } + + #[test] + fn session_capability_deserializes_unknown_as_other() { + let parsed: SessionCapability = + serde_json::from_value(serde_json::json!("future-cap")).unwrap(); + assert_eq!(parsed, SessionCapability::Other("future-cap".to_string())); + } + + #[test] + fn generated_session_capabilities_have_public_variants() { + use crate::generated::api_types::SessionCapability as GeneratedSessionCapability; + + fn expected_wire_name(capability: GeneratedSessionCapability) -> Option<&'static str> { + match capability { + GeneratedSessionCapability::TuiHints => Some("tui-hints"), + GeneratedSessionCapability::PlanMode => Some("plan-mode"), + GeneratedSessionCapability::Memory => Some("memory"), + GeneratedSessionCapability::CliDocumentation => Some("cli-documentation"), + GeneratedSessionCapability::AskUser => Some("ask-user"), + GeneratedSessionCapability::InteractiveMode => Some("interactive-mode"), + GeneratedSessionCapability::SystemNotifications => Some("system-notifications"), + GeneratedSessionCapability::Elicitation => Some("elicitation"), + GeneratedSessionCapability::SessionStore => Some("session-store"), + GeneratedSessionCapability::McpApps => Some("mcp-apps"), + GeneratedSessionCapability::CanvasRenderer => Some("canvas-renderer"), + GeneratedSessionCapability::Unknown => None, + } + } + + for generated in [ + GeneratedSessionCapability::TuiHints, + GeneratedSessionCapability::PlanMode, + GeneratedSessionCapability::Memory, + GeneratedSessionCapability::CliDocumentation, + GeneratedSessionCapability::AskUser, + GeneratedSessionCapability::InteractiveMode, + GeneratedSessionCapability::SystemNotifications, + GeneratedSessionCapability::Elicitation, + GeneratedSessionCapability::SessionStore, + GeneratedSessionCapability::McpApps, + GeneratedSessionCapability::CanvasRenderer, + ] { + let wire_name = expected_wire_name(generated).unwrap(); + assert_eq!(SessionCapability::from(wire_name).as_str(), wire_name); + } + } + #[test] fn log_level_args_omitted_when_unset() { let opts = ClientOptions::default(); From 7d2d68660fcd3ec11786f3a5407d5ed92a4fe453 Mon Sep 17 00:00:00 2001 From: Andy Adams-Moran Date: Tue, 9 Jun 2026 15:54:08 +0100 Subject: [PATCH 3/5] Remove internal references from capability docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/README.md | 10 ++++------ rust/src/lib.rs | 2 +- rust/src/types.rs | 34 +++++++++++++++++----------------- rust/src/wire.rs | 8 ++++---- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/rust/README.md b/rust/README.md index b5870f4e5..fd4d84a40 100644 --- a/rust/README.md +++ b/rust/README.md @@ -85,7 +85,7 @@ The `SessionCapability` enum lets callers enable or disable named runtime featur - `enabledCapabilities` -- capabilities to opt the session into (extends the `SDK_CAPABILITIES` baseline) - `disabledCapabilities` -- capabilities to opt the session out of (disable wins on overlap) -This approach works for every transport -- including `Transport::External` (Desktop app / shared CLI server) -- because it does not rely on CLI spawn arguments. +This approach works for every transport -- including `Transport::External` -- because it does not rely on CLI spawn arguments. > **Experimental.** Per-session capability controls are an experimental > wire-protocol surface and may change or be removed in future SDK or CLI @@ -94,11 +94,9 @@ This approach works for every transport -- including `Transport::External` (Desk > repository convention used in `rust/src/generated/`); there is no > `#[experimental]` attribute, so the marker is documentation-only. -> **Runtime dependency.** Per-session capability controls require -> [github/copilot-agent-runtime#8918](https://github.com/github/copilot-agent-runtime/pull/8918) -> or later. On older runtimes the fields are silently ignored. -> Pairs with [github/agents#1081](https://github.com/github/agents/issues/1081) -> (Desktop app missing memory capability). +> **Runtime dependency.** Per-session capability controls require a runtime +> version that supports `enabledCapabilities` and `disabledCapabilities`. +> On older runtimes the fields are silently ignored. Use `SessionConfig::with_enable_capability` / `with_disable_capability` (and their plural counterparts): diff --git a/rust/src/lib.rs b/rust/src/lib.rs index f9cdc8e77..9cb7c8efc 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -432,7 +432,7 @@ impl OtelExporterType { /// SDK release can still be opted into without waiting for a new /// enum variant. /// -/// Requires github/copilot-agent-runtime#8918 or later. +/// Requires runtime support for per-session capability controls. #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum SessionCapability { diff --git a/rust/src/types.rs b/rust/src/types.rs index 5698d9535..a0d448ca9 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -831,7 +831,7 @@ impl CloudSessionOptions { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ExtensionInfo { - /// Extension namespace/source, e.g. `"github-app"`. + /// Extension namespace/source, e.g. `"example-host"`. pub source: String, /// Stable provider name within the source namespace. pub name: String, @@ -1381,7 +1381,7 @@ pub struct SessionConfig { /// ///
/// - /// Requires github/copilot-agent-runtime#8918 or later. On older + /// Requires runtime support for per-session capability controls. On older /// runtimes the field is silently ignored. pub enabled_capabilities: Vec, /// Capabilities to opt this session out of via `disabledCapabilities` @@ -1395,7 +1395,7 @@ pub struct SessionConfig { /// /// /// - /// Requires github/copilot-agent-runtime#8918 or later. + /// Requires runtime support for per-session capability controls. pub disabled_capabilities: Vec, } @@ -1955,7 +1955,7 @@ impl SessionConfig { /// /// /// - /// Requires github/copilot-agent-runtime#8918 or later. + /// Requires runtime support for per-session capability controls. /// /// # Example /// @@ -1979,7 +1979,7 @@ impl SessionConfig { /// /// /// - /// Requires github/copilot-agent-runtime#8918 or later. + /// Requires runtime support for per-session capability controls. pub fn with_disable_capability(mut self, capability: impl Into) -> Self { self.disabled_capabilities.push(capability.into()); self @@ -1995,7 +1995,7 @@ impl SessionConfig { /// /// /// - /// Requires github/copilot-agent-runtime#8918 or later. + /// Requires runtime support for per-session capability controls. pub fn with_enabled_capabilities(mut self, capabilities: I) -> Self where I: IntoIterator, @@ -2015,7 +2015,7 @@ impl SessionConfig { /// /// /// - /// Requires github/copilot-agent-runtime#8918 or later. + /// Requires runtime support for per-session capability controls. pub fn with_disabled_capabilities(mut self, capabilities: I) -> Self where I: IntoIterator, @@ -2492,7 +2492,7 @@ pub struct ResumeSessionConfig { /// /// /// - /// Requires github/copilot-agent-runtime#8918 or later. + /// Requires runtime support for per-session capability controls. pub enabled_capabilities: Vec, /// Capabilities to opt this session out of via `disabledCapabilities` on /// the `session.resume` wire call. Disable wins over enable on overlap. @@ -2504,7 +2504,7 @@ pub struct ResumeSessionConfig { /// /// /// - /// Requires github/copilot-agent-runtime#8918 or later. + /// Requires runtime support for per-session capability controls. pub disabled_capabilities: Vec, } @@ -3021,7 +3021,7 @@ impl ResumeSessionConfig { /// /// /// - /// Requires github/copilot-agent-runtime#8918 or later. + /// Requires runtime support for per-session capability controls. pub fn with_enable_capability(mut self, capability: impl Into) -> Self { self.enabled_capabilities.push(capability.into()); self @@ -3036,7 +3036,7 @@ impl ResumeSessionConfig { /// /// /// - /// Requires github/copilot-agent-runtime#8918 or later. + /// Requires runtime support for per-session capability controls. pub fn with_disable_capability(mut self, capability: impl Into) -> Self { self.disabled_capabilities.push(capability.into()); self @@ -3051,7 +3051,7 @@ impl ResumeSessionConfig { /// /// /// - /// Requires github/copilot-agent-runtime#8918 or later. + /// Requires runtime support for per-session capability controls. pub fn with_enabled_capabilities(mut self, capabilities: I) -> Self where I: IntoIterator, @@ -3071,7 +3071,7 @@ impl ResumeSessionConfig { /// /// /// - /// Requires github/copilot-agent-runtime#8918 or later. + /// Requires runtime support for per-session capability controls. pub fn with_disabled_capabilities(mut self, capabilities: I) -> Self where I: IntoIterator, @@ -4891,7 +4891,7 @@ mod tests { .with_github_token("ghp_test") .with_enable_session_telemetry(false) .with_include_sub_agent_streaming_events(false) - .with_extension_info(ExtensionInfo::new("github-app", "counter")); + .with_extension_info(ExtensionInfo::new("example-host", "counter")); assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1")); assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4")); @@ -4929,7 +4929,7 @@ mod tests { assert_eq!(cfg.include_sub_agent_streaming_events, Some(false)); assert_eq!( cfg.extension_info, - Some(ExtensionInfo::new("github-app", "counter")) + Some(ExtensionInfo::new("example-host", "counter")) ); } @@ -4959,7 +4959,7 @@ mod tests { .with_include_sub_agent_streaming_events(true) .with_suppress_resume_event(true) .with_continue_pending_work(true) - .with_extension_info(ExtensionInfo::new("github-app", "counter")); + .with_extension_info(ExtensionInfo::new("example-host", "counter")); assert_eq!(cfg.session_id.as_str(), "sess-2"); assert_eq!(cfg.client_name.as_deref(), Some("test-app")); @@ -4997,7 +4997,7 @@ mod tests { assert_eq!(cfg.continue_pending_work, Some(true)); assert_eq!( cfg.extension_info, - Some(ExtensionInfo::new("github-app", "counter")) + Some(ExtensionInfo::new("example-host", "counter")) ); } diff --git a/rust/src/wire.rs b/rust/src/wire.rs index b1c982e1c..ee5c87c9e 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -149,12 +149,12 @@ pub(crate) struct SessionCreateWire { pub commands: Option>, /// Capabilities to opt this session into. Forwarded as /// `enabledCapabilities` on the `session.create` wire call. - /// Requires github/copilot-agent-runtime#8918. + /// Requires runtime support for per-session capability controls. #[serde(skip_serializing_if = "Option::is_none")] pub enabled_capabilities: Option>, /// Capabilities to opt this session out of. Disable wins on overlap. /// Forwarded as `disabledCapabilities` on the `session.create` wire call. - /// Requires github/copilot-agent-runtime#8918. + /// Requires runtime support for per-session capability controls. #[serde(skip_serializing_if = "Option::is_none")] pub disabled_capabilities: Option>, } @@ -269,12 +269,12 @@ pub(crate) struct SessionResumeWire { pub continue_pending_work: Option, /// Capabilities to opt this session into. Forwarded as /// `enabledCapabilities` on the `session.resume` wire call. - /// Requires github/copilot-agent-runtime#8918. + /// Requires runtime support for per-session capability controls. #[serde(skip_serializing_if = "Option::is_none")] pub enabled_capabilities: Option>, /// Capabilities to opt this session out of. Disable wins on overlap. /// Forwarded as `disabledCapabilities` on the `session.resume` wire call. - /// Requires github/copilot-agent-runtime#8918. + /// Requires runtime support for per-session capability controls. #[serde(skip_serializing_if = "Option::is_none")] pub disabled_capabilities: Option>, } From 6f3d5b243b5cf729d9dee9cb33d9ce3b95b88424 Mon Sep 17 00:00:00 2001 From: Andy Adams-Moran Date: Tue, 9 Jun 2026 16:15:00 +0100 Subject: [PATCH 4/5] Use generated session capability enum Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/README.md | 35 ++----- rust/src/lib.rs | 256 ---------------------------------------------- rust/src/types.rs | 168 +++++++++++++++--------------- rust/src/wire.rs | 10 +- 4 files changed, 95 insertions(+), 374 deletions(-) diff --git a/rust/README.md b/rust/README.md index fd4d84a40..10e922f92 100644 --- a/rust/README.md +++ b/rust/README.md @@ -135,36 +135,14 @@ let session = client.resume_session( | `SessionStore` | `session-store` | Cross-session history tools and session-store metadata | | `McpApps` | `mcp-apps` | MCP-Apps `ui://` resource passthrough (SEP-1865) | | `CanvasRenderer` | `canvas-renderer` | Host-rendered extension canvases | -| `Other(String)` | *(verbatim)* | Forward-compat escape hatch for unknown future names | +| `Unknown` | *(none)* | Deserialization fallback; rejected for outbound config | **Disable-wins semantics.** If the same capability appears in both `enabled_capabilities` and `disabled_capabilities`, disable wins. The runtime starts from an `SDK_CAPABILITIES` baseline; enabled capabilities extend it and -disabled capabilities remove from it, in that order. - -**Forward compatibility.** The enum is `#[non_exhaustive]` and carries an -`Other(String)` variant so callers on older SDK builds can opt into -capabilities that the runtime adds ahead of a new SDK release, without any -recompile-blocking enum-variant additions: - -```rust,ignore -use github_copilot_sdk::{SessionCapability, SessionConfig}; - -// Opt into a capability the SDK doesn't know about yet. -let config = SessionConfig::default() - .with_enable_capability(SessionCapability::Other("future-cap".to_string())); -``` - -`&str` and `String` implement `Into`, so you can also pass -string literals directly to the builders: - -```rust,ignore -use github_copilot_sdk::SessionConfig; - -let config = SessionConfig::default() - .with_enable_capability("memory") // &str coerces to SessionCapability - .with_disable_capability("plan-mode"); -``` +disabled capabilities remove from it, in that order. `SessionCapability::Unknown` +exists only as a generated deserialization fallback and is rejected if supplied +to the create/resume capability builders. ### Session @@ -802,9 +780,8 @@ gets to be Rust here — cross-SDK parity for these is a post-release conversation, not a release blocker. None of these are deprecated and none of them are scheduled for removal. -- **`SessionCapability` enum** -- typed, `#[non_exhaustive]` enum for per-session - capability opt-in / opt-out, with an `Other(String)` escape hatch for - forward compatibility. Sent via `enabledCapabilities` / +- **`SessionCapability` enum** -- generated typed enum for per-session + capability opt-in / opt-out. Sent via `enabledCapabilities` / `disabledCapabilities` on the `session.create` and `session.resume` wire calls -- works for all transports including `Transport::External`. See [Session capabilities](#session-capabilities) above. Marked diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 9cb7c8efc..cab34b476 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -402,162 +402,6 @@ impl OtelExporterType { } } -/// A named session capability sent in the `session.create` and -/// `session.resume` wire payloads. -/// -/// Capabilities gate optional CLI features (extra tools, system-prompt -/// sections, host-rendered surfaces). The runtime starts from a -/// hard-coded `SDK_CAPABILITIES` set; use -/// [`SessionConfig::with_enable_capability`] / -/// [`SessionConfig::with_disable_capability`] (and their plural -/// counterparts) to opt individual sessions in or out. -/// -/// > **Not** the same as [`SessionCapabilities`] — that struct is the -/// > *runtime-negotiated* capability descriptor reported by the CLI on -/// > `session.create`. [`SessionCapability`] is the *opt-in / opt-out -/// > toggle name* sent with each `session.create` / `session.resume`. -/// > -/// > This public type is also separate from the generated protocol enum: -/// > unknown generated enum values collapse to `Unknown`, while callers -/// > need [`Other`](Self::Other) to preserve and send capability names -/// > introduced by newer runtimes. -/// -/// The runtime's overlap semantics are **disable-wins**: if a capability -/// appears in both the enabled and disabled lists, the disable wins. -/// The SDK preserves the order callers add capabilities in so the -/// resulting wire payload is deterministic. -/// -/// The enum is `#[non_exhaustive]` and carries an [`Other`](Self::Other) -/// variant so forward-compat capabilities the runtime grows ahead of an -/// SDK release can still be opted into without waiting for a new -/// enum variant. -/// -/// Requires runtime support for per-session capability controls. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum SessionCapability { - /// TUI-only prompt hints (keyboard shortcuts). - TuiHints, - /// `[[PLAN]]` handling and plan-mode instructions. - PlanMode, - /// `store_memory` tool and the `` system-prompt section. - Memory, - /// `fetch_copilot_cli_documentation` tool plus the - /// `` system-prompt section. - CliDocumentation, - /// `ask_user` tool for interactive clarification. - AskUser, - /// Interactive-CLI identity (vs non-interactive / headless). - InteractiveMode, - /// Automatic system notifications to the agent (batched, hidden - /// from the user timeline). - SystemNotifications, - /// Elicitation support (confirm / select / input prompts). - Elicitation, - /// Cross-session history tools and session-store prompt/tool metadata. - SessionStore, - /// MCP-Apps (SEP-1865) `ui://` resource passthrough. - McpApps, - /// Extension-provided canvases rendered by the host. - CanvasRenderer, - /// A capability name the SDK doesn't have a typed variant for yet. - /// - /// Pass any kebab-case capability string here to forward it - /// verbatim to the runtime. - Other(String), -} - -impl SessionCapability { - /// The kebab-case wire string sent in `enabledCapabilities` / - /// `disabledCapabilities` on `session.create` and `session.resume`. - pub fn as_str(&self) -> &str { - match self { - Self::TuiHints => "tui-hints", - Self::PlanMode => "plan-mode", - Self::Memory => "memory", - Self::CliDocumentation => "cli-documentation", - Self::AskUser => "ask-user", - Self::InteractiveMode => "interactive-mode", - Self::SystemNotifications => "system-notifications", - Self::Elicitation => "elicitation", - Self::SessionStore => "session-store", - Self::McpApps => "mcp-apps", - Self::CanvasRenderer => "canvas-renderer", - Self::Other(name) => name.as_str(), - } - } - - fn from_known_name(name: &str) -> Option { - Some(match name { - "tui-hints" => Self::TuiHints, - "plan-mode" => Self::PlanMode, - "memory" => Self::Memory, - "cli-documentation" => Self::CliDocumentation, - "ask-user" => Self::AskUser, - "interactive-mode" => Self::InteractiveMode, - "system-notifications" => Self::SystemNotifications, - "elicitation" => Self::Elicitation, - "session-store" => Self::SessionStore, - "mcp-apps" => Self::McpApps, - "canvas-renderer" => Self::CanvasRenderer, - _ => return None, - }) - } -} - -impl std::fmt::Display for SessionCapability { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.as_str()) - } -} - -impl std::str::FromStr for SessionCapability { - type Err = std::convert::Infallible; - - /// Parse a kebab-case capability name. Unknown names round-trip - /// through [`SessionCapability::Other`] so old SDK builds stay - /// useful against CLIs that add new capabilities. Always returns - /// `Ok` — the error type is [`Infallible`](std::convert::Infallible). - fn from_str(s: &str) -> std::result::Result { - Ok(Self::from(s)) - } -} - -impl From<&str> for SessionCapability { - fn from(s: &str) -> Self { - Self::from_known_name(s).unwrap_or_else(|| Self::Other(s.to_owned())) - } -} - -impl From for SessionCapability { - fn from(s: String) -> Self { - if let Some(capability) = Self::from_known_name(&s) { - capability - } else { - Self::Other(s) - } - } -} - -impl Serialize for SessionCapability { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.as_str()) - } -} - -impl<'de> Deserialize<'de> for SessionCapability { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - let capability = String::deserialize(deserializer)?; - Ok(Self::from(capability)) - } -} - /// OpenTelemetry configuration forwarded to the spawned GitHub Copilot CLI /// process. /// @@ -2535,106 +2379,6 @@ mod tests { assert_eq!(Client::remote_args(&opts), vec!["--remote".to_string()]); } - #[test] - fn session_capability_round_trips_via_str() { - for cap in [ - SessionCapability::TuiHints, - SessionCapability::PlanMode, - SessionCapability::Memory, - SessionCapability::CliDocumentation, - SessionCapability::AskUser, - SessionCapability::InteractiveMode, - SessionCapability::SystemNotifications, - SessionCapability::Elicitation, - SessionCapability::SessionStore, - SessionCapability::McpApps, - SessionCapability::CanvasRenderer, - ] { - let s = cap.to_string(); - let parsed: SessionCapability = s.parse().unwrap(); - assert_eq!(parsed, cap, "round-trip failed for {s}"); - } - } - - #[test] - fn session_capability_from_str_falls_back_to_other_for_unknown_names() { - let parsed: SessionCapability = "brand-new-cap".parse().unwrap(); - assert_eq!( - parsed, - SessionCapability::Other("brand-new-cap".to_string()) - ); - assert_eq!(parsed.as_str(), "brand-new-cap"); - } - - #[test] - fn session_capability_into_from_str_and_string() { - let from_str: SessionCapability = "memory".into(); - let from_string: SessionCapability = "memory".to_string().into(); - assert_eq!(from_str, SessionCapability::Memory); - assert_eq!(from_string, SessionCapability::Memory); - // Unknown names go to Other - let other: SessionCapability = "future-cap".into(); - assert_eq!(other, SessionCapability::Other("future-cap".to_string())); - } - - #[test] - fn session_capability_serializes_as_wire_string() { - assert_eq!( - serde_json::to_value(SessionCapability::Memory).unwrap(), - serde_json::json!("memory") - ); - assert_eq!( - serde_json::to_value(SessionCapability::Other("future-cap".to_string())).unwrap(), - serde_json::json!("future-cap") - ); - } - - #[test] - fn session_capability_deserializes_unknown_as_other() { - let parsed: SessionCapability = - serde_json::from_value(serde_json::json!("future-cap")).unwrap(); - assert_eq!(parsed, SessionCapability::Other("future-cap".to_string())); - } - - #[test] - fn generated_session_capabilities_have_public_variants() { - use crate::generated::api_types::SessionCapability as GeneratedSessionCapability; - - fn expected_wire_name(capability: GeneratedSessionCapability) -> Option<&'static str> { - match capability { - GeneratedSessionCapability::TuiHints => Some("tui-hints"), - GeneratedSessionCapability::PlanMode => Some("plan-mode"), - GeneratedSessionCapability::Memory => Some("memory"), - GeneratedSessionCapability::CliDocumentation => Some("cli-documentation"), - GeneratedSessionCapability::AskUser => Some("ask-user"), - GeneratedSessionCapability::InteractiveMode => Some("interactive-mode"), - GeneratedSessionCapability::SystemNotifications => Some("system-notifications"), - GeneratedSessionCapability::Elicitation => Some("elicitation"), - GeneratedSessionCapability::SessionStore => Some("session-store"), - GeneratedSessionCapability::McpApps => Some("mcp-apps"), - GeneratedSessionCapability::CanvasRenderer => Some("canvas-renderer"), - GeneratedSessionCapability::Unknown => None, - } - } - - for generated in [ - GeneratedSessionCapability::TuiHints, - GeneratedSessionCapability::PlanMode, - GeneratedSessionCapability::Memory, - GeneratedSessionCapability::CliDocumentation, - GeneratedSessionCapability::AskUser, - GeneratedSessionCapability::InteractiveMode, - GeneratedSessionCapability::SystemNotifications, - GeneratedSessionCapability::Elicitation, - GeneratedSessionCapability::SessionStore, - GeneratedSessionCapability::McpApps, - GeneratedSessionCapability::CanvasRenderer, - ] { - let wire_name = expected_wire_name(generated).unwrap(); - assert_eq!(SessionCapability::from(wire_name).as_str(), wire_name); - } - } - #[test] fn log_level_args_omitted_when_unset() { let opts = ClientOptions::default(); diff --git a/rust/src/types.rs b/rust/src/types.rs index a0d448ca9..d2ffba027 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -12,7 +12,6 @@ use std::time::Duration; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::SessionCapability; use crate::canvas::{CanvasDeclaration, CanvasHandler}; use crate::generated::api_types::OpenCanvasInstance; /// Context window tier for models that support tiered context windows. @@ -1604,6 +1603,27 @@ pub(crate) struct SessionConfigRuntime { pub commands: Option>, } +fn capability_list_to_wire( + field_name: &str, + capabilities: Vec, +) -> Result>, crate::Error> { + if capabilities.is_empty() { + return Ok(None); + } + + if capabilities + .iter() + .any(|capability| matches!(capability, SessionCapability::Unknown)) + { + return Err(crate::Error::with_message( + crate::ErrorKind::InvalidConfig, + format!("{field_name} cannot include SessionCapability::Unknown"), + )); + } + + Ok(Some(capabilities)) +} + impl SessionConfig { /// Consume this config to produce the [`SessionCreateWire`] payload /// for `session.create` and a [`SessionConfigRuntime`] bundle holding @@ -1653,26 +1673,10 @@ impl SessionConfig { let wire_canvases = self.canvases.clone(); let canvas_handler = self.canvas_handler.clone(); - let enabled_capabilities = if self.enabled_capabilities.is_empty() { - None - } else { - Some( - self.enabled_capabilities - .iter() - .map(|c| c.to_string()) - .collect(), - ) - }; - let disabled_capabilities = if self.disabled_capabilities.is_empty() { - None - } else { - Some( - self.disabled_capabilities - .iter() - .map(|c| c.to_string()) - .collect(), - ) - }; + let enabled_capabilities = + capability_list_to_wire("enabled_capabilities", self.enabled_capabilities)?; + let disabled_capabilities = + capability_list_to_wire("disabled_capabilities", self.disabled_capabilities)?; let wire = crate::wire::SessionCreateWire { session_id, model: self.model, @@ -1945,8 +1949,7 @@ impl SessionConfig { /// `session.create`. Appends to [`Self::enabled_capabilities`], /// preserving insertion order. /// - /// See [`SessionCapability`] for the disable-wins overlap rules - /// and the [`SessionCapability::Other`] forward-compat escape hatch. + /// See [`SessionCapability`] for the available capability names. /// ///
/// @@ -1964,8 +1967,8 @@ impl SessionConfig { /// let config = SessionConfig::default() /// .with_enable_capability(SessionCapability::Memory); /// ``` - pub fn with_enable_capability(mut self, capability: impl Into) -> Self { - self.enabled_capabilities.push(capability.into()); + pub fn with_enable_capability(mut self, capability: SessionCapability) -> Self { + self.enabled_capabilities.push(capability); self } @@ -1980,8 +1983,8 @@ impl SessionConfig { ///
/// /// Requires runtime support for per-session capability controls. - pub fn with_disable_capability(mut self, capability: impl Into) -> Self { - self.disabled_capabilities.push(capability.into()); + pub fn with_disable_capability(mut self, capability: SessionCapability) -> Self { + self.disabled_capabilities.push(capability); self } @@ -1996,12 +1999,11 @@ impl SessionConfig { /// /// /// Requires runtime support for per-session capability controls. - pub fn with_enabled_capabilities(mut self, capabilities: I) -> Self + pub fn with_enabled_capabilities(mut self, capabilities: I) -> Self where - I: IntoIterator, - C: Into, + I: IntoIterator, { - self.enabled_capabilities = capabilities.into_iter().map(Into::into).collect(); + self.enabled_capabilities = capabilities.into_iter().collect(); self } @@ -2016,12 +2018,11 @@ impl SessionConfig { /// /// /// Requires runtime support for per-session capability controls. - pub fn with_disabled_capabilities(mut self, capabilities: I) -> Self + pub fn with_disabled_capabilities(mut self, capabilities: I) -> Self where - I: IntoIterator, - C: Into, + I: IntoIterator, { - self.disabled_capabilities = capabilities.into_iter().map(Into::into).collect(); + self.disabled_capabilities = capabilities.into_iter().collect(); self } @@ -2664,26 +2665,10 @@ impl ResumeSessionConfig { let wire_canvases = self.canvases.clone(); let canvas_handler = self.canvas_handler.clone(); - let enabled_capabilities = if self.enabled_capabilities.is_empty() { - None - } else { - Some( - self.enabled_capabilities - .iter() - .map(|c| c.to_string()) - .collect(), - ) - }; - let disabled_capabilities = if self.disabled_capabilities.is_empty() { - None - } else { - Some( - self.disabled_capabilities - .iter() - .map(|c| c.to_string()) - .collect(), - ) - }; + let enabled_capabilities = + capability_list_to_wire("enabled_capabilities", self.enabled_capabilities)?; + let disabled_capabilities = + capability_list_to_wire("disabled_capabilities", self.disabled_capabilities)?; let wire = crate::wire::SessionResumeWire { session_id: self.session_id, @@ -3022,8 +3007,8 @@ impl ResumeSessionConfig { /// /// /// Requires runtime support for per-session capability controls. - pub fn with_enable_capability(mut self, capability: impl Into) -> Self { - self.enabled_capabilities.push(capability.into()); + pub fn with_enable_capability(mut self, capability: SessionCapability) -> Self { + self.enabled_capabilities.push(capability); self } @@ -3037,8 +3022,8 @@ impl ResumeSessionConfig { /// /// /// Requires runtime support for per-session capability controls. - pub fn with_disable_capability(mut self, capability: impl Into) -> Self { - self.disabled_capabilities.push(capability.into()); + pub fn with_disable_capability(mut self, capability: SessionCapability) -> Self { + self.disabled_capabilities.push(capability); self } @@ -3052,12 +3037,11 @@ impl ResumeSessionConfig { /// /// /// Requires runtime support for per-session capability controls. - pub fn with_enabled_capabilities(mut self, capabilities: I) -> Self + pub fn with_enabled_capabilities(mut self, capabilities: I) -> Self where - I: IntoIterator, - C: Into, + I: IntoIterator, { - self.enabled_capabilities = capabilities.into_iter().map(Into::into).collect(); + self.enabled_capabilities = capabilities.into_iter().collect(); self } @@ -3072,12 +3056,11 @@ impl ResumeSessionConfig { /// /// /// Requires runtime support for per-session capability controls. - pub fn with_disabled_capabilities(mut self, capabilities: I) -> Self + pub fn with_disabled_capabilities(mut self, capabilities: I) -> Self where - I: IntoIterator, - C: Into, + I: IntoIterator, { - self.disabled_capabilities = capabilities.into_iter().map(Into::into).collect(); + self.disabled_capabilities = capabilities.into_iter().collect(); self } @@ -4430,6 +4413,7 @@ pub use crate::generated::api_types::{ Model, ModelBilling, ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision, ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision, PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable, + SessionCapability, }; /// Permission categories the CLI may request approval for. @@ -5624,10 +5608,14 @@ mod capability_tests { fn session_config_enabled_capabilities_serialized_on_wire() { let config = SessionConfig::default() .with_enable_capability(SessionCapability::Memory) - .with_enable_capability(SessionCapability::PlanMode); + .with_enable_capability(SessionCapability::PlanMode) + .with_enable_capability(SessionCapability::CanvasRenderer); let wire = create_session_wire(config); - let enabled = wire.enabled_capabilities.as_ref().unwrap(); - assert_eq!(enabled, &["memory".to_string(), "plan-mode".to_string()]); + let value = serde_json::to_value(&wire).unwrap(); + assert_eq!( + value["enabledCapabilities"], + serde_json::json!(["memory", "plan-mode", "canvas-renderer"]) + ); assert!(wire.disabled_capabilities.is_none()); } @@ -5636,8 +5624,11 @@ mod capability_tests { let config = SessionConfig::default().with_disable_capability(SessionCapability::PlanMode); let wire = create_session_wire(config); assert!(wire.enabled_capabilities.is_none()); - let disabled = wire.disabled_capabilities.as_ref().unwrap(); - assert_eq!(disabled, &["plan-mode".to_string()]); + let value = serde_json::to_value(&wire).unwrap(); + assert_eq!( + value["disabledCapabilities"], + serde_json::json!(["plan-mode"]) + ); } #[test] @@ -5646,17 +5637,24 @@ mod capability_tests { .with_enable_capability(SessionCapability::Memory) .with_enabled_capabilities([SessionCapability::PlanMode]); let wire = create_session_wire(config); - let enabled = wire.enabled_capabilities.as_ref().unwrap(); - assert_eq!(enabled, &["plan-mode".to_string()]); + assert_eq!( + wire.enabled_capabilities.as_ref().unwrap(), + &[SessionCapability::PlanMode] + ); } #[test] - fn session_config_other_capability_round_trips_through_wire() { - let config = SessionConfig::default() - .with_enable_capability(SessionCapability::Other("custom-cap".to_string())); - let wire = create_session_wire(config); - let enabled = wire.enabled_capabilities.as_ref().unwrap(); - assert_eq!(enabled, &["custom-cap".to_string()]); + fn session_config_unknown_capability_is_invalid() { + let config = SessionConfig::default().with_enable_capability(SessionCapability::Unknown); + let err = match config.into_wire(Some(SessionId::new("test-session"))) { + Ok(_) => panic!("expected Unknown to be rejected"), + Err(err) => err, + }; + assert_eq!(err.kind(), &crate::ErrorKind::InvalidConfig); + assert!( + err.to_string() + .contains("enabled_capabilities cannot include SessionCapability::Unknown") + ); } #[test] @@ -5672,9 +5670,11 @@ mod capability_tests { .with_enable_capability(SessionCapability::Memory) .with_disable_capability(SessionCapability::PlanMode); let wire = resume_session_wire(config); - let enabled = wire.enabled_capabilities.as_ref().unwrap(); - let disabled = wire.disabled_capabilities.as_ref().unwrap(); - assert_eq!(enabled, &["memory".to_string()]); - assert_eq!(disabled, &["plan-mode".to_string()]); + let value = serde_json::to_value(&wire).unwrap(); + assert_eq!(value["enabledCapabilities"], serde_json::json!(["memory"])); + assert_eq!( + value["disabledCapabilities"], + serde_json::json!(["plan-mode"]) + ); } } diff --git a/rust/src/wire.rs b/rust/src/wire.rs index ee5c87c9e..4a3f60b26 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -20,7 +20,7 @@ use serde::Serialize; use crate::canvas::CanvasDeclaration; use crate::generated::api_types::{ - ModelCapabilitiesOverride, OpenCanvasInstance, RemoteSessionMode, + ModelCapabilitiesOverride, OpenCanvasInstance, RemoteSessionMode, SessionCapability, }; use crate::generated::session_events::ReasoningSummary; use crate::types::{ @@ -151,12 +151,12 @@ pub(crate) struct SessionCreateWire { /// `enabledCapabilities` on the `session.create` wire call. /// Requires runtime support for per-session capability controls. #[serde(skip_serializing_if = "Option::is_none")] - pub enabled_capabilities: Option>, + pub enabled_capabilities: Option>, /// Capabilities to opt this session out of. Disable wins on overlap. /// Forwarded as `disabledCapabilities` on the `session.create` wire call. /// Requires runtime support for per-session capability controls. #[serde(skip_serializing_if = "Option::is_none")] - pub disabled_capabilities: Option>, + pub disabled_capabilities: Option>, } /// The exact JSON shape sent on the `session.resume` JSON-RPC request. @@ -271,10 +271,10 @@ pub(crate) struct SessionResumeWire { /// `enabledCapabilities` on the `session.resume` wire call. /// Requires runtime support for per-session capability controls. #[serde(skip_serializing_if = "Option::is_none")] - pub enabled_capabilities: Option>, + pub enabled_capabilities: Option>, /// Capabilities to opt this session out of. Disable wins on overlap. /// Forwarded as `disabledCapabilities` on the `session.resume` wire call. /// Requires runtime support for per-session capability controls. #[serde(skip_serializing_if = "Option::is_none")] - pub disabled_capabilities: Option>, + pub disabled_capabilities: Option>, } From 0a783bb65305f104ccf227da95a9320a20cd5587 Mon Sep 17 00:00:00 2001 From: Andy Adams-Moran Date: Tue, 9 Jun 2026 19:44:57 +0100 Subject: [PATCH 5/5] Address review: append-style capability builders, trim docs Make `with_enabled_capabilities` / `with_disabled_capabilities` append to the existing list (via `extend`) instead of replacing it, so the plural builders are consistent with the singular `with_enable_capability` / `with_disable_capability`. Update the builder docs and the round-trip test accordingly. Trim the README capability section to drop implementation details and the hand-maintained variants table, deferring to the enum's own documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/README.md | 33 ++++++--------------------------- rust/src/types.rs | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/rust/README.md b/rust/README.md index 10e922f92..95dea7ed7 100644 --- a/rust/README.md +++ b/rust/README.md @@ -85,14 +85,9 @@ The `SessionCapability` enum lets callers enable or disable named runtime featur - `enabledCapabilities` -- capabilities to opt the session into (extends the `SDK_CAPABILITIES` baseline) - `disabledCapabilities` -- capabilities to opt the session out of (disable wins on overlap) -This approach works for every transport -- including `Transport::External` -- because it does not rely on CLI spawn arguments. - > **Experimental.** Per-session capability controls are an experimental > wire-protocol surface and may change or be removed in future SDK or CLI -> releases. The Rust SDK marks the relevant fields and builders with a -> `
**Experimental.**
` rustdoc block (the -> repository convention used in `rust/src/generated/`); there is no -> `#[experimental]` attribute, so the marker is documentation-only. +> releases. > **Runtime dependency.** Per-session capability controls require a runtime > version that supports `enabledCapabilities` and `disabledCapabilities`. @@ -120,22 +115,8 @@ let session = client.resume_session( ).await?; ``` -**Variants:** - -| Variant | Wire name | Description | -| -------------------- | ----------------------- | ----------------------------------------------------- | -| `TuiHints` | `tui-hints` | TUI keyboard shortcuts | -| `PlanMode` | `plan-mode` | `[[PLAN]]` handling and plan-mode instructions | -| `Memory` | `memory` | `store_memory` tool and `` system-prompt section | -| `CliDocumentation` | `cli-documentation` | `fetch_copilot_cli_documentation` tool and `` section | -| `AskUser` | `ask-user` | `ask_user` tool for interactive clarification | -| `InteractiveMode` | `interactive-mode` | Interactive-CLI identity (vs headless) | -| `SystemNotifications`| `system-notifications` | Automatic batched system notifications to the agent | -| `Elicitation` | `elicitation` | Elicitation prompts (confirm / select / input) | -| `SessionStore` | `session-store` | Cross-session history tools and session-store metadata | -| `McpApps` | `mcp-apps` | MCP-Apps `ui://` resource passthrough (SEP-1865) | -| `CanvasRenderer` | `canvas-renderer` | Host-rendered extension canvases | -| `Unknown` | *(none)* | Deserialization fallback; rejected for outbound config | +**Variants.** See the `SessionCapability` enum's own documentation for the +authoritative set of capabilities and their wire names. **Disable-wins semantics.** If the same capability appears in both `enabled_capabilities` and `disabled_capabilities`, disable wins. The runtime @@ -780,13 +761,11 @@ gets to be Rust here — cross-SDK parity for these is a post-release conversation, not a release blocker. None of these are deprecated and none of them are scheduled for removal. -- **`SessionCapability` enum** -- generated typed enum for per-session +- **`SessionCapability` enum** -- typed enum for per-session capability opt-in / opt-out. Sent via `enabledCapabilities` / `disabledCapabilities` on the `session.create` and `session.resume` wire - calls -- works for all transports including `Transport::External`. See - [Session capabilities](#session-capabilities) above. Marked - **experimental** via the repository's `
` rustdoc - convention. Node/Python/Go/.NET accept stringly-typed flags. + calls. See [Session capabilities](#session-capabilities) above. + Experimental. - **Typed newtypes** — `SessionId` and `RequestId` are `#[serde(transparent)]` newtypes around `String`, so the type system distinguishes a session identifier from an arbitrary `String` at compile time. Node/Python/Go diff --git a/rust/src/types.rs b/rust/src/types.rs index d2ffba027..2b4df1e52 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1988,7 +1988,7 @@ impl SessionConfig { self } - /// Replace [`Self::enabled_capabilities`] with the given iterable. + /// Append the given capabilities to [`Self::enabled_capabilities`]. /// Insertion order is preserved. /// ///
@@ -2003,11 +2003,11 @@ impl SessionConfig { where I: IntoIterator, { - self.enabled_capabilities = capabilities.into_iter().collect(); + self.enabled_capabilities.extend(capabilities); self } - /// Replace [`Self::disabled_capabilities`] with the given iterable. + /// Append the given capabilities to [`Self::disabled_capabilities`]. /// Insertion order is preserved. Disable wins over enable on overlap. /// ///
@@ -2022,7 +2022,7 @@ impl SessionConfig { where I: IntoIterator, { - self.disabled_capabilities = capabilities.into_iter().collect(); + self.disabled_capabilities.extend(capabilities); self } @@ -3027,7 +3027,7 @@ impl ResumeSessionConfig { self } - /// Replace [`Self::enabled_capabilities`] with the given iterable. + /// Append the given capabilities to [`Self::enabled_capabilities`]. /// ///
/// @@ -3041,11 +3041,11 @@ impl ResumeSessionConfig { where I: IntoIterator, { - self.enabled_capabilities = capabilities.into_iter().collect(); + self.enabled_capabilities.extend(capabilities); self } - /// Replace [`Self::disabled_capabilities`] with the given iterable. + /// Append the given capabilities to [`Self::disabled_capabilities`]. /// Disable wins over enable on overlap. /// ///
@@ -3060,7 +3060,7 @@ impl ResumeSessionConfig { where I: IntoIterator, { - self.disabled_capabilities = capabilities.into_iter().collect(); + self.disabled_capabilities.extend(capabilities); self } @@ -5632,14 +5632,14 @@ mod capability_tests { } #[test] - fn session_config_with_enabled_capabilities_replaces() { + fn session_config_with_enabled_capabilities_appends() { let config = SessionConfig::default() .with_enable_capability(SessionCapability::Memory) .with_enabled_capabilities([SessionCapability::PlanMode]); let wire = create_session_wire(config); assert_eq!( wire.enabled_capabilities.as_ref().unwrap(), - &[SessionCapability::PlanMode] + &[SessionCapability::Memory, SessionCapability::PlanMode] ); }