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
106 changes: 106 additions & 0 deletions rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,105 @@ 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, disable, or declare provider support for 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 three 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)
- `capabilityProviders` -- capabilities this client actively *provides* (e.g. renders a canvas, drives elicitation)

This approach works for every transport -- including `Transport::External` (Desktop app / shared CLI server) -- because it does not rely on CLI spawn arguments.

> **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#981](https://github.com/github/agents/issues/981)
> (Desktop app missing memory capability).

Use `SessionConfig::with_enable_capability` / `with_disable_capability` / `with_capability_provider` (and their plural counterparts):

```rust,ignore
use github_copilot_sdk::{SessionCapability, SessionConfig};

let session = client.create_session(
SessionConfig::default()
.with_enable_capability(SessionCapability::Memory)
// Declare that this client provides the canvas renderer.
.with_capability_provider(SessionCapability::CanvasRenderer),
"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)
.with_capability_provider(SessionCapability::Elicitation),
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 `<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) |
| `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.

**Capability providers vs enable/disable.** `enabledCapabilities` and
`disabledCapabilities` control which server-side capabilities are active.
`capabilityProviders` is different: it tells the runtime that *this client*
provides a capability end-to-end (for example, renders a canvas, drives the
elicitation flow). The runtime uses the provider list to negotiate the
capability handshake with the model.

> **Note:** `requestCanvasRenderer` and `requestElicitation` are legacy field
> aliases for provider registration and remain supported for backward
> compatibility. The `capabilityProviders` field is the forward-looking API.

**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 @@ -716,6 +815,13 @@ 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 / provider declaration, with an `Other(String)` escape
hatch for forward compatibility. Sent via `enabledCapabilities` /
`disabledCapabilities` / `capabilityProviders` on the `session.create` and
`session.resume` wire calls -- works for all transports including
`Transport::External`. See [Session capabilities](#session-capabilities) above.
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
164 changes: 164 additions & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,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 `<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,
/// 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<Self, std::convert::Infallible> {
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<String> for SessionCapability {
fn from(s: String) -> Self {
s.as_str().into()
}
}

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