Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,96 @@ 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
> `<div class="warning">**Experimental.**</div>` 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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid referring to closed-source repos anywhere in the open-source code.

@Morabbin Morabbin Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dang it! Will excise, apologies.


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),
).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),
).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 `<memories>` system-prompt section |
| `CliDocumentation` | `cli-documentation` | `fetch_copilot_cli_documentation` tool and `<self_documentation>` 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 |
| `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<SessionCapability>`, 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()`.
Expand Down Expand Up @@ -714,6 +804,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 `<div class="warning">` 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
Expand Down
256 changes: 256 additions & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,162 @@ 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 github/copilot-agent-runtime#8918 or later.

@SteveSandersonMS SteveSandersonMS Jun 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid referring to closed source. There are a few comments like this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix incoming; removing all mentions from all parts of the PR

#[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 `<memories>` system-prompt section.
Memory,
/// `fetch_copilot_cli_documentation` tool plus the
/// `<self_documentation>` 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<Self> {
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<Self, std::convert::Infallible> {
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<String> 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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}

impl<'de> Deserialize<'de> for SessionCapability {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let capability = String::deserialize(deserializer)?;
Ok(Self::from(capability))
}
}

/// OpenTelemetry configuration forwarded to the spawned GitHub Copilot CLI
/// process.
///
Expand Down Expand Up @@ -2379,6 +2535,106 @@ 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();
Expand Down
Loading
Loading