Add Rust SDK (technical preview) - #1164
Conversation
Adds the Copilot Rust SDK (`copilot-sdk` crate) under `rust/`, alongside Rust codegen plumbed into `scripts/codegen/` and CI under `.github/workflows/rust-sdk-tests.yml`. The crate ships a JSON-RPC client, session lifecycle management, system message transforms, permission policy helpers, the `define_tool` adapter, and per-event `SessionHandler`/`SessionHooks` traits. Includes: - 14 ported E2E scenarios under `rust/tests/` driving the replay-proxy harness, plus a hand-curated set of unit tests. - A rust-coding-skill (`.github/skills/rust-coding-skill/`) capturing conventions for error handling, async/concurrency, tracing, and the intentional trait exceptions in the SDK's public API. - Release tooling: `rust-publish-release.yml`, `RELEASING.md`, and protocol-version generation wired into the existing automation. - `PermissionResult` extended with `Deferred` and `Custom` variants for richer permission decisions. Public API is held at 0.1.0-pre. Marked protocol-evolving public enums `#[non_exhaustive]` so additive variants stay non-breaking. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Christopher Schleiden <cschleiden@github.com> Co-authored-by: David Dossett <25163139+daviddossett@users.noreply.github.com> Co-authored-by: Devraj Mehta <devm33@github.com> Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Co-authored-by: Evan Boyle <EvanBoyle@users.noreply.github.com> Co-authored-by: Jeremy Moseley <jemoseley@microsoft.com> Co-authored-by: Steve Sanderson <SteveSandersonMS@users.noreply.github.com>
- **Broadcast subscriptions for lifecycle and session events.** `Client::subscribe_lifecycle()` and `Session::subscribe()` return `tokio::sync::broadcast::Receiver`; dropping the receiver unsubscribes. Replaces the prior callback-based `Client::on`, `Client::on_event_type`, `Session::on`, and `Unsubscribe` API. Spawned consumer tasks isolate panics naturally. - **`PermissionResult` gains `Deferred` and `Custom` variants.** `Deferred` lets handlers resolve a request asynchronously via `session.permissions.handlePendingPermissionRequest` (notification path only — falls back to `Approved` on the direct RPC path). `Custom(Value)` lets handlers send arbitrary response payloads beyond the standard `approve-once` / `reject` shapes. - **`#[non_exhaustive]` on protocol-evolving public enums** (`PermissionResult`, `SessionLifecycleEventType`, `GitHubReferenceType`, others) so additive variants stay non-breaking. - **`ToolHandlerRouter` overrides per-event `SessionHandler` methods** so consumers can call `router.on_external_tool(...)` directly without unwrapping `HandlerResponse`. - **`define_tool` accepts bare `async fn` items** in addition to closures, matching `tower::service_fn` / `hyper::service::service_fn` conventions. Documented in rustdoc. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ypes Generated code emitted `pub session_id: String` for every schema field named `sessionId` and likewise for `requestId`, leaving consumers with mixed types: `Session::id()` returned `SessionId` but `session.events_subscribe()` events exposed `session_id: String`. Same papercut for request IDs in permission and elicitation event payloads. The newtypes are `#[serde(transparent)]` so the wire format is unchanged. This adds a property-name override map to `scripts/codegen/rust.ts` that maps `sessionId`, `remoteSessionId`, and `requestId` to the hand-authored types in `crate::types`, and emits the matching `use` statement in both generated modules. `mc_session_id` (MCP protocol metadata, not a Copilot session) stays as `String`. After regeneration: 27 fields converted to `SessionId` (including the handoff event's `remoteSessionId`) and 25 to `RequestId`. The existing `PartialEq<str>` / `PartialEq<String>` impls on both newtypes mean test code like Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
define_tool's Fn(P) -> Fut bound gave closures only the deserialized
arguments, leaving session_id, tool_call_id, and tool_name unreachable.
That blocked the helper for any tool that needs to scope DB lookups to
a session, emit per-tool-call telemetry, or stream UI updates back to
the originating session — patterns that hit dozens of sites across
realistic tool suites.
Change the closure bound to Fn(ToolInvocation, P) -> Fut. The arguments
are moved out via mem::take before deserialization, so there is no
clone cost on the hot path. Closures that don't need the metadata
write |_inv, params|.
Also add ToolInvocation::params<P>() so long-form impl ToolHandler
blocks can deserialize without naming serde_json directly:
async fn call(&self, inv: ToolInvocation) -> Result<ToolResult, Error> {
let params: MyParams = inv.params()?;
// …use inv.session_id alongside params…
}
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Node, Python, and .NET all expose ping with an optional message. Go requires it only because Go has no Option type — Rust has one, so the API should match the languages with the same expressive power rather than the one without. Change ping(&self, message: &str) to ping(&self, message: Option<&str>). When None, the message field is omitted from the request payload rather than sent as an empty string. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new Rust SDK crate (copilot-sdk, library name copilot) to the monorepo, mirroring the existing SDKs’ JSON-RPC client/session model, and wires it into repo workflows and scenario coverage.
Changes:
- Introduces the Rust SDK crate with protocol types, JSON-RPC transport, session/handler abstractions, examples, and tests.
- Extends scenario samples and verification scripts to build/run Rust implementations alongside TS/Python/Go/C#.
- Updates repo automation: codegen,
justtasks, scenario-build CI, Rust SDK CI, and release/publish workflows.
Show a summary per file
| File | Description |
|---|---|
| test/scenarios/verify.sh | Shows Rust status in the aggregated scenario runner UI. |
| test/scenarios/transport/tcp/verify.sh | Builds/runs the Rust TCP transport scenario. |
| test/scenarios/transport/tcp/rust/Cargo.toml | Rust TCP scenario crate manifest. |
| test/scenarios/transport/tcp/rust/src/main.rs | Rust TCP sample using external host:port CLI server. |
| test/scenarios/transport/stdio/verify.sh | Builds/runs the Rust stdio transport scenario. |
| test/scenarios/transport/stdio/rust/Cargo.toml | Rust stdio scenario crate manifest. |
| test/scenarios/transport/stdio/rust/src/main.rs | Rust stdio sample spawning the CLI child process. |
| test/scenarios/transport/stdio/README.md | Documents Rust sample location/package name. |
| test/scenarios/tools/tool-overrides/verify.sh | Builds/runs Rust tool override scenario. |
| test/scenarios/tools/tool-overrides/rust/Cargo.toml | Rust tool-overrides scenario crate manifest (+derive). |
| test/scenarios/tools/tool-overrides/rust/src/main.rs | Rust sample overriding built-in tool behavior. |
| test/scenarios/tools/tool-filtering/verify.sh | Builds/runs Rust tool filtering scenario. |
| test/scenarios/tools/tool-filtering/rust/Cargo.toml | Rust tool-filtering scenario crate manifest. |
| test/scenarios/tools/tool-filtering/rust/src/main.rs | Rust sample limiting available tools. |
| test/scenarios/tools/skills/verify.sh | Builds/runs Rust skills scenario. |
| test/scenarios/tools/skills/rust/Cargo.toml | Rust skills scenario crate manifest. |
| test/scenarios/tools/skills/rust/src/main.rs | Rust sample configuring skill directories + hooks. |
| test/scenarios/tools/no-tools/verify.sh | Builds/runs Rust no-tools scenario. |
| test/scenarios/tools/no-tools/rust/Cargo.toml | Rust no-tools scenario crate manifest. |
| test/scenarios/tools/no-tools/rust/src/main.rs | Rust sample disabling tools + replacing system prompt. |
| test/scenarios/tools/mcp-servers/verify.sh | Builds/runs Rust MCP servers scenario. |
| test/scenarios/tools/mcp-servers/rust/Cargo.toml | Rust mcp-servers scenario crate manifest. |
| test/scenarios/tools/mcp-servers/rust/src/main.rs | Rust sample passing MCP servers config to CLI. |
| test/scenarios/tools/custom-agents/verify.sh | Builds/runs Rust custom agents scenario. |
| test/scenarios/tools/custom-agents/rust/Cargo.toml | Rust custom-agents scenario crate manifest (+derive). |
| test/scenarios/tools/custom-agents/rust/src/main.rs | Rust sample defining custom agents + custom tool. |
| test/scenarios/sessions/streaming/verify.sh | Builds/runs Rust streaming scenario. |
| test/scenarios/sessions/streaming/rust/Cargo.toml | Rust streaming scenario crate manifest. |
| test/scenarios/sessions/streaming/rust/src/main.rs | Rust sample counting streaming delta events. |
| test/scenarios/sessions/session-resume/verify.sh | Builds/runs Rust session resume scenario. |
| test/scenarios/sessions/session-resume/rust/Cargo.toml | Rust session-resume scenario crate manifest. |
| test/scenarios/sessions/session-resume/rust/src/main.rs | Rust sample creating + resuming session by ID. |
| test/scenarios/sessions/infinite-sessions/verify.sh | Builds/runs Rust infinite sessions scenario. |
| test/scenarios/sessions/infinite-sessions/rust/Cargo.toml | Rust infinite-sessions scenario crate manifest. |
| test/scenarios/sessions/infinite-sessions/rust/src/main.rs | Rust sample exercising infinite session thresholds. |
| test/scenarios/sessions/concurrent-sessions/verify.sh | Builds/runs Rust concurrent sessions scenario. |
| test/scenarios/sessions/concurrent-sessions/rust/Cargo.toml | Rust concurrent-sessions scenario crate manifest. |
| test/scenarios/sessions/concurrent-sessions/rust/src/main.rs | Rust sample running two sessions concurrently. |
| test/scenarios/prompts/system-message/verify.sh | Builds/runs Rust system-message scenario. |
| test/scenarios/prompts/system-message/rust/Cargo.toml | Rust system-message scenario crate manifest. |
| test/scenarios/prompts/system-message/rust/src/main.rs | Rust sample replacing system message. |
| test/scenarios/prompts/reasoning-effort/verify.sh | Builds/runs Rust reasoning-effort scenario. |
| test/scenarios/prompts/reasoning-effort/rust/Cargo.toml | Rust reasoning-effort scenario crate manifest. |
| test/scenarios/prompts/reasoning-effort/rust/src/main.rs | Rust sample setting reasoning_effort. |
| test/scenarios/modes/default/verify.sh | Builds/runs Rust default mode scenario. |
| test/scenarios/modes/default/rust/Cargo.toml | Rust default-mode scenario crate manifest. |
| test/scenarios/modes/default/rust/src/main.rs | Rust sample using default tool-enabled mode. |
| test/scenarios/callbacks/user-input/verify.sh | Builds/runs Rust user-input callback scenario. |
| test/scenarios/callbacks/user-input/rust/Cargo.toml | Rust user-input scenario crate manifest. |
| test/scenarios/callbacks/user-input/rust/src/main.rs | Rust sample handling ask_user prompts. |
| test/scenarios/callbacks/permissions/verify.sh | Builds/runs Rust permission callback scenario. |
| test/scenarios/callbacks/permissions/rust/Cargo.toml | Rust permissions scenario crate manifest. |
| test/scenarios/callbacks/permissions/rust/src/main.rs | Rust sample logging/approving permissions. |
| test/scenarios/callbacks/hooks/verify.sh | Builds/runs Rust hooks callback scenario. |
| test/scenarios/callbacks/hooks/rust/Cargo.toml | Rust hooks scenario crate manifest. |
| test/scenarios/callbacks/hooks/rust/src/main.rs | Rust sample implementing SessionHooks logging. |
| test/scenarios/RUST_COVERAGE.md | Documents Rust scenario parity coverage/gaps. |
| scripts/codegen/package.json | Adds Rust generation to codegen scripts. |
| nodejs/scripts/update-protocol-version.ts | Generates Rust SDK protocol version constant. |
| justfile | Adds Rust format/lint/test/codegen tasks. |
| .gitignore | Ignores Rust scenario build artifacts/lockfiles. |
| .github/workflows/scenario-builds.yml | Adds CI job building all Rust scenario crates. |
| .github/workflows/rust-sdk-tests.yml | Adds Rust SDK CI (fmt/clippy/doc/test/semver-checks). |
| .github/workflows/rust-release-pr.yml | Adds release-plz workflow to open Rust release PRs. |
| .github/workflows/rust-publish-release.yml | Adds release-plz workflow to publish Rust crate. |
| .github/workflows/codegen-check.yml | Ensures Rust codegen + protocol version regen in CI. |
| .github/skills/rust-coding-skill/SKILL.md | Adds repo-specific Rust engineering guidance. |
| .github/skills/rust-coding-skill/examples.md | Adds Rust SDK examples/patterns for contributors. |
| .github/copilot-instructions.md | Updates repo guidance to include Rust SDK + Rust skill. |
| rust/Cargo.toml | Defines the new copilot-sdk crate (features, deps, MSRV). |
| rust/Cargo.lock | Locks Rust dependencies for deterministic builds. |
| rust/README.md | Documents Rust SDK usage, architecture, and API surface. |
| rust/CHANGELOG.md | Establishes initial changelog and release-plz plan. |
| rust/RELEASING.md | Documents release/publish operations for maintainers. |
| rust/LICENSE | Rust crate license file. |
| rust/rust-toolchain.toml | Pins Rust toolchain version/components for the crate. |
| rust/release-plz.toml | Configures release-plz behavior for the Rust crate. |
| rust/clippy.toml | Configures Rust clippy rules (e.g., disallowed macros). |
| rust/.rustfmt.toml | Stable rustfmt config (edition 2024). |
| rust/.rustfmt.nightly.toml | Nightly rustfmt config enabling unstable formatting opts. |
| rust/.gitignore | Ignores Rust target dir and backup lock files. |
| rust/build.rs | Build-time CLI bundling/extraction codegen support. |
| rust/src/sdk_protocol_version.rs | Generated SDK protocol version constant for Rust. |
| rust/src/generated/mod.rs | Rust generated-type module root + re-exports. |
| rust/src/jsonrpc.rs | Content-Length framed JSON-RPC transport implementation. |
| rust/src/router.rs | Per-session routing of notifications/requests. |
| rust/src/handler.rs | Session handler traits/events + default handlers. |
| rust/src/permission.rs | Permission policy wrappers over SessionHandler. |
| rust/src/transforms.rs | System message transform extension point + dispatcher. |
| rust/src/session.rs | Session lifecycle/event loop plumbing (core runtime). |
| rust/tests/jsonrpc_test.rs | Tests for JSON-RPC framing/routing (feature-gated). |
| rust/tests/protocol_version_test.rs | Tests protocol version negotiation behavior. |
| rust/tests/integration_test.rs | Ignored integration tests against real CLI. |
| rust/examples/chat.rs | Interactive streaming chat example. |
| rust/examples/hooks.rs | Hooks example for logging/auditing. |
| rust/examples/tool_server.rs | Tool server example (feature-gated on derive). |
| rust/examples/lifecycle_observer.rs | Observer example for lifecycle/session event streams. |
Copilot's findings
- Files reviewed: 102/107 changed files
- Comments generated: 5
cargo doc was running with --features test-support, which left the derive feature off and made intra-doc links to define_tool and schema_for resolve to nothing — failing under the crate's deny(rustdoc::broken_intra_doc_links). docs.rs already uses all-features (see Cargo.toml's [package.metadata.docs.rs]); align CI with that so the docs job matches what users will see on docs.rs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
emitted from the loop correlate to a session in traces. Matches
the pattern documented in the rust-coding-skill.
- README.md / embeddedcli.rs: correct the embedded-CLI documentation
to match what build.rs and embeddedcli.rs actually do — archives
come from the github/copilot-cli GitHub Releases, integrity is
SHA-256 against SHA256SUMS.txt, and the runtime cache path is
~/.cache/copilot-sdk-{version}/copilot.
- test/scenarios/sessions/streaming/verify.sh: drop a duplicate
'# Go: build' comment.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Picks up the new model.call_failure session event (with its ModelCallFailureData payload and ModelCallFailureSource enum) and the new optional 'tip' field on session_info. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Removes path triggers and the regenerate step for other languages' protocol-version files. Those drift checks are a pre-existing gap on main and out of scope for the Rust SDK port. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 23-line setup checklist duplicated content already in rust/RELEASING.md. One-line pointer is enough. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two scenarios still used the old `Fn(P) -> Fut` shape and broke when the SDK switched to `Fn(ToolInvocation, P) -> Fut`. They don't use the invocation field, so just bind it as `_inv`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1164 · ● 2.5M
Cross-SDK consistency: every other SDK (Node, Python, Go, .NET) uses `send`/`Send`/`SendAsync` plus `MessageOptions` as the public parameter type. Rust was the outlier with `send_message` and `SendOptions`, and the asymmetry with the existing `send_and_wait` method made it read awkwardly. - Rename `Session::send_message` -> `Session::send` (and the private helper `send_message_inner` -> `send_inner`). - Rename the public `SendOptions` type -> `MessageOptions`. - Delete the previous wire-level `MessageOptions` struct: it had no internal callers (the wire payload is hand-rolled in send_inner) and freeing the name was the cleanest path to parity. Pre-1.0 type rename, no protocol or behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Thanks for getting this up. It'd be helpful to run an agent over the other SDKs and this new one to look for any inconsistencies / gaps / divergences, so that we can then evaluate each and decide whether it's acceptable or should be addressed. The more consistent we can be across the SDKs, the easier it'll be to maintain them moving forward, the more information / docs about one will translate to consumption of the others, the better we'll be able to evolve with reduced concerns for how something we want to add may not fit well in a particular SDK, etc. |
This comment has been minimized.
This comment has been minimized.
Previously Session::subscribe and Client::subscribe_lifecycle returned
raw tokio::sync::broadcast::Receiver<T> values. A survey of mature Rust
crates (tonic, lapin, rdkafka, redis-rs, tokio-tungstenite, iroh-gossip,
tokio-stream's BroadcastStream itself) found that none of them expose a
raw broadcast::Receiver in their public API; the dominant pattern is a
named newtype implementing futures::Stream, with overflow surfaced
explicitly in the item type.
Introduce a copilot::subscription module with:
- EventSubscription / LifecycleSubscription newtypes
- Inherent recv() returning Result<T, RecvError> for existing
while-let loop ergonomics
- Stream impl yielding Result<T, Lagged> so callers can use
tokio_stream::StreamExt or futures::StreamExt combinators
- Lagged / RecvError types owned by the SDK so consumers no longer
import tokio's broadcast error types
Net effect: the channel choice is now an internal implementation detail.
We can swap broadcast for async-broadcast / flume / a custom backpressure
policy, or convert lag into an Event::Lagged variant, without a breaking
change to the public surface.
Existing while-let loops in tests and examples continue to compile and
behave identically: close and lag both exit the loop, matching
tokio::sync::broadcast::Receiver.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Local cargo +nightly fmt --check passed without `--config-path .rustfmt.nightly.toml`, but CI runs with the explicit config and flagged two diffs: import group flattening and test-mod import order. Applied with the same flags CI uses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1164 · ● 1.9M
| /// Force-fail resume if the session does not exist on disk, instead of | ||
| /// silently starting a new session. | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub disable_resume: Option<bool>, |
There was a problem hiding this comment.
Cross-SDK consistency gap: continue_pending_work missing from ResumeSessionConfig
All other SDKs expose a continuePendingWork / continue_pending_work / ContinuePendingWork field on their resume config, but the Rust ResumeSessionConfig doesn't include it:
| SDK | Field |
|---|---|
| Node.js | continuePendingWork?: boolean |
| Python | continue_pending_work: bool |
| Go | ContinuePendingWork bool |
| .NET | bool? ContinuePendingWork |
| Rust | ❌ missing |
This field is important for the "pending work resume" pattern — when a session is abandoned mid-tool-execution (e.g. via force_stop), setting continue_pending_work: true on resume instructs the CLI to replay and complete those pending tool/permission requests.
Suggested addition (after disable_resume):
/// When `true`, instructs the runtime to continue any pending tool calls
/// or permission requests that were outstanding when the session was
/// last disconnected. Use in combination with [`Client::force_stop`] to
/// hand off a live session to a new client without losing in-flight work.
/// See [`SessionConfig`] for the equivalent field on initial session creation.
#[serde(skip_serializing_if = "Option::is_none")]
pub continue_pending_work: Option<bool>,And a corresponding builder method in impl ResumeSessionConfig:
pub fn with_continue_pending_work(mut self, value: bool) -> Self {
self.continue_pending_work = Some(value);
self
}Don't forget to include it in ResumeSessionConfig::new (as None) and in the Debug impl.
Cross-SDK parity gap caught by the SDK consistency reviewer. All four other SDKs (Node, Python, Go, .NET) expose this field; Rust omitted it. The field opts the runtime into continuing any tool calls or permission requests that were pending when the previous connection was dropped — it's the key enabler of the pending-work-handoff pattern used together with `Client::force_stop` to migrate a session from one process to another without losing in-flight work. Plumbing: - `pub continue_pending_work: Option<bool>` on `ResumeSessionConfig` with `#[serde(skip_serializing_if = "Option::is_none")]`. Container `rename_all = "camelCase"` covers the wire-name `continuePendingWork`. No manual payload-construction code is needed; resume_session serializes config -> wire via `serde_json::to_value(&config)`. - `with_continue_pending_work(bool)` builder. - Default impl: `None`. - Debug impl: includes the field. Tests: - `resume_session_config_builder_composes` extended to cover the new field. - `resume_session_config_serializes_continue_pending_work_to_camel_case` asserts the wire shape (`continuePendingWork: true`) and that unset values are omitted (skip_serializing_if). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1164 · ● 2.8M
| `AutoModeSwitchResponse::{Yes, YesAlways, No}`. Default impl declines. | ||
| Cross-SDK parity is post-release follow-up — Node / Python / Go / .NET | ||
| consumers currently observe the request as a raw event and must drive | ||
| the wire response themselves. |
There was a problem hiding this comment.
The "Rust-only API" section documents on_auto_mode_switch as a Rust-only typed handler with a note that "Cross-SDK parity is post-release follow-up." The on_exit_plan_mode handler on SessionHandler is in the same situation — it's a typed Rust-only callback for exit_plan_mode.requested (other SDKs expose this only as a raw SessionEvent with no typed dispatch path), but it isn't mentioned here.
Worth adding a parallel bullet for on_exit_plan_mode so consumers have the same explicit heads-up that they would for on_auto_mode_switch:
- **`SessionHandler::on_exit_plan_mode`** — typed handler for the CLI's
plan-mode exit prompt (`exit_plan_mode.requested`). Returns
`ExitPlanModeResult` (default: approved with no action). Cross-SDK parity
is post-release follow-up — Node / Python / Go / .NET consumers currently
observe the request as a raw event and must drive the wire response
themselves.Minor documentation gap only — no code change needed.
Closes three cross-SDK parity gaps surfaced by the post-merge audit against upstream commits 662f270, d3abfa2, and 180ca47. ClientOptions::copilot_home: Option<PathBuf> - Override the CLI's data directory. Exported as COPILOT_HOME to the spawned CLI process. Mirrors Node's copilotHome / Python's copilot_home. ClientOptions::tcp_connection_token: Option<String> - Optional auth token for TCP transport. Sent in the new `connect` JSON-RPC handshake (with backward-compat fall-back to `ping` for legacy CLI servers) and exported as COPILOT_CONNECTION_TOKEN to spawned CLI processes. When the SDK spawns its own CLI in TCP mode and this is unset, a UUID is generated so the loopback listener is safe by default. Combining with Transport::Stdio returns Error::InvalidConfig from Client::start. Mirrors Node's tcpConnectionToken / Python's tcp_connection_token / .NET's TcpConnectionToken. - New `connect` handshake: verify_protocol_version now calls `connect` first, falling back to `ping` on -32601 MethodNotFound. The handshake carries the effective connection token. Matches Node's internalRpc.connect({ token }) sequence in client.ts. - Adds `uuid` build-dep (1.x with v4 feature) for the auto-generated loopback token. Cryptographically-secure source via getrandom (already in the transitive dep tree). SessionConfig::instruction_directories + ResumeSessionConfig:: instruction_directories: Option<Vec<PathBuf>> - Additional directories the CLI searches for custom instruction files, distinct from skill_directories. Pure passthrough on the wire. Mirrors Node/Python instructionDirectories. Error::InvalidConfig(String) - New #[non_exhaustive] variant for client-construction errors that surface from Client::start (token + stdio, empty token, etc). ClientInner gains effective_connection_token: Option<String> populated in start(); from_transport now takes the token through (8 args -> suppressed clippy::too_many_arguments on the internal helper). Tests: - protocol_version_test: existing tests updated for the new connect-then-ping fallback flow (server responds MethodNotFound on connect to exercise the legacy compat path). - protocol_version_test::connect_handshake_supplies_protocol_version: new positive-path test — server responds to connect with a protocolVersion. - lib::tests::build_command_sets_copilot_home_env_when_configured - lib::tests::build_command_sets_connection_token_env_when_configured - lib::tests::start_rejects_token_with_stdio_transport - lib::tests::start_rejects_empty_connection_token Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
…rage Two related changes: 1. Drop the `uuid = "1"` dep added in 02780f0; replace with a `generate_connection_token()` helper that pulls 16 bytes from `getrandom::getrandom` and hex-encodes them. Same 128 bits of CSPRNG entropy, no semantic mismatch (the connection token is an opaque secret, not an identifier — typing it as a UUID would conflict with the existing pre-1.0 review consensus that schema-shaped IDs stay `String`). Output is a 32-char lowercase hex string, distinguishable from UUIDs at a glance. Rustdoc on the helper documents the decision so a future reader doesn't re-litigate it. 2. Address four reviewer items flagged on the github-app sync PR (#4399), all of which routed back upstream because src/ and tests/ are mirrored from copilot-sdk: a. `session_config_serializes_instruction_directories_to_camel_case` — wire-shape test that `with_instruction_directories(...)` populates the field and serializes to `instructionDirectories`. b. `resume_session_config_serializes_instruction_directories_to_camel_case` — same on the resume path. c. `connect_handshake_forwards_explicit_token` and `connect_handshake_forwards_auto_generated_token` — positive coverage that `tcp_connection_token` actually reaches the outbound `connect` request's `token` param. Auto-generated case also asserts the shape (32-char lowercase hex) so a regression in the helper can't silently weaken loopback authentication. d. Rewrote the rustdoc on `Client::verify_protocol_version`. It claimed "sends a `ping` RPC", but the implementation now tries `connect` first and falls back to `ping` only on -32601. New docs describe the full handshake sequence and the token- forwarding semantics. Test-support surface: adds two test-only entry points on `Client` (`from_streams_with_connection_token`, `generate_connection_token_for_test`) gated behind `cfg(any(test, feature = "test-support"))`, mirroring the existing `from_streams_with_trace_provider` shape. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codegen was crashing on the post-1.0.41-0 schema with
`TypeError: s.split is not a function` at toPascalCase. Root cause:
the new `connect` JSON-RPC method's `ConnectResult.ok` field is
declared as `{ "type": "boolean", "enum": [true] }` (a single-value
boolean enum used as a "must-be-true" discriminant). The Rust
generator's `emitRustStringEnum` assumes string values, fed a
boolean, and panicked.
Python and Go generators already pre-process the schema with
`stripBooleanLiterals` (utils.ts:146) for exactly this reason —
upstream PR #1176 added the helper precisely because quicktype's
Python/Go renderers crashed on the same input. The TypeScript and
C# generators handle it natively. Rust's generator wasn't pre-
processing, so it inherited the same crash.
Fix: apply `stripBooleanLiterals` to both `apiSchema` and
`sessionEventsSchema` in `rust.ts` before postProcessSchema /
emit. Mirrors the Python/Go invocation pattern. Boolean
literal narrowing isn't expressible in Rust enums anyway —
the field stays `pub ok: bool`.
Regen produces two new types from the post-1.0.41-0 schema:
- `ConnectRequest` (token: Option<String>)
- `ConnectResult` (ok: bool, protocolVersion: u32)
- rpc_methods::CONNECT constant
These reflect the `connect` JSON-RPC method our hand-coded
verify_protocol_version handshake already invokes via
`client.call("connect", ...)`. We don't switch the call site to
the typed RPC because the existing untyped call matches how
`ping` is invoked next door, and the handshake needs the special
MethodNotFound fallback that doesn't fit the typed RPC abstraction.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Now that codegen produces ConnectRequest / ConnectResult types (b9b4d2b regen output), the hand-coded JSON-building in Client::connect_handshake is just untyped boilerplate. Switches the implementation to the typed `client.rpc().connect(ConnectRequest { token })` call. Same wire shape; same MethodNotFound fallback at the verify_protocol_version call site (lifted-out fallback, not swallowed by the handshake helper); typed extraction of protocol_version replaces the manual `value.get("protocolVersion")` JSON walk. ping() stays as a public hand-coded wrapper because the `Option<&str>` ergonomics don't translate to the typed PingRequest struct, and removing the public function would be a breaking API change. Test fixtures: extended the mock-server `connect` response stubs in protocol_version_test.rs to include the now-required `version` field on ConnectResult. Schema declares it required; deserialization through the typed ConnectResult now enforces it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Pre-1.0 audit caught that `Client::send_telemetry` and `Session::send_telemetry` were Rust-only public surface for an RPC method (`sendTelemetry` / `session.sendTelemetry`) that: - Is not described in `api.schema.json`. - Is not exposed by the Node, Python, Go, or .NET SDKs (zero matches in their source trees). - Has been moving around on the CLI side (the impl had a METHOD_NOT_FOUND fallback between top-level `sendTelemetry` and namespaced `server.sendTelemetry` to compensate). A schema-undocumented + cross-SDK-absent public method is the shape of an internal CLI knob, not a stable consumer API. Consumers' canonical telemetry surface is the spawn-time env injection via `ClientOptions::telemetry` (`TelemetryConfig`). That stays. Changes: - Removed `Client::send_telemetry`, the cached `ServerTelemetryRpcMethod` enum, and the `ClientInner::server_telemetry_method` field that backed its fallback caching. - Removed `Session::send_telemetry`. - Removed `ServerTelemetryEvent` and `SessionTelemetryEvent` types from `types.rs`. - Updated three integration tests in `tests/session_test.rs` (`send_telemetry_injects_payload_and_session_id`, `server_send_telemetry_sends_correct_payload`, `server_send_telemetry_falls_back_to_namespaced_method_and_caches_it`) by removing them, plus a fourth case in the dispatch-table test. - Dropped now-unused local `METHOD_NOT_FOUND` const from the test file. - Removed the "Rust-only API" README bullet describing the shortcut. - Updated CHANGELOG to drop the type and method enumerations. Net surface for telemetry: `ClientOptions::telemetry` + `with_telemetry()` builder, plus the env-var injection in `Client::build_command`. That's the documented, schema-described, cross-SDK-consistent way to forward telemetry config to the CLI; emitting individual telemetry events from consumer code is intentionally out of scope. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Same audit shape as the send_telemetry removal: a sweep of public
methods that exist as Rust convenience wrappers on top of a typed
RPC, with no equivalent surface in the Node, Python, Go, or .NET
SDKs (zero matches across all four SDK source trees).
The methods are reachable cross-SDK through the typed `rpc()`
namespace — Rust callers simply switch from
`session.method()` to `session.rpc().<namespace>().<method>()`,
mirroring how Node/Python/Go/.NET consumers already drive these.
Removed from `Session`:
- get_model
- set_mode / get_mode
- set_name / get_name
- read_plan / update_plan / delete_plan
- list_workspace_files / read_workspace_file / create_workspace_file
- start_fleet
- set_approve_all_permissions
- call_rpc (generic forwarder; the typed rpc() namespace replaces it)
Removed from `Client`:
- get_quota (typed `client.rpc().account().get_quota()` already
available everywhere including Rust)
Kept on Session: send, send_and_wait, abort, set_model, log,
disconnect/destroy, subscribe, capabilities, cancellation_token,
stop_event_loop, ui (and the field accessors id/cwd/workspace_path/
remote_url). These are either present in every SDK already (abort,
set_model, send/send_and_wait, log, disconnect) or are Rust-shape
helpers that don't have a typed RPC equivalent (subscribe,
cancellation_token, ui sub-API).
Updated:
- README "Rust-only API" section: dropped the "First-class Session
convenience methods" bullet and the Client::get_quota bullet.
What remains as Rust-only is now strictly language-shape items
(newtypes, Transport enum, permission builders, from_streams,
on_auto_mode_switch).
- CHANGELOG: dropped enumeration of the removed methods; restated
what stays.
- 5 integration tests removed (get_name, set_name, list_workspace_files,
read_workspace_file, create_workspace_file) plus the dispatch-table
case for session.plan.delete.
- Unused imports cleaned up in session.rs.
Migration for the github-app consumer (the only known caller):
- session.set_approve_all_permissions(b)
-> session.rpc().permissions().set_approve_all(
PermissionsSetApproveAllRequest { enabled: b })
- session.set_mode("plan")
-> session.rpc().mode().set(ModeSetRequest { mode: ... })
- session.read_plan()
-> session.rpc().plan().read()
- (etc — all typed RPC namespace calls)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Were these recently added in runtime? We haven't added anything for them yet in the other SDKs. Could we remove them from rust for now?
Same here. This isn't exposed in any of the other SDKs. If the desire ends up being to have this, we can add it across all of them, including Rust, in one fell swoop.
Ditto. I'm not even sure what this is :) |
Brought in 12 commits from origin/main, including CLI bumps to 1.0.41-0 and 1.0.41-1, plus upstream PR #966 ("Add provider model and token limit overrides to ProviderConfig"). One trivial codegen diff (single doc-comment update on `CustomAgentsUpdatedAgent.tools` for the new "or null when all tools are available" semantics). PR #966 added four new fields to ProviderConfig across all SDKs: - `model_id: Option<String>` (well-known model ID for agent config + token limit lookup; falls back to SessionConfig::model) - `wire_model: Option<String>` (model name sent to provider API for inference; falls back to model_id, then to SessionConfig::model) - `max_prompt_tokens: Option<i64>` (overrides resolved model's default max prompt tokens; triggers compaction) - `max_output_tokens: Option<i64>` (overrides resolved model's default max output tokens; truncates response) Plus matching `with_*` builders. Wire-shape: camelCase (`modelId`/`wireModel`/`maxPromptTokens`/`maxOutputTokens`), skip_serializing_if when unset. Extended `provider_config_builder_composes` test to exercise all four fields and assert their wire shape (camelCase + omission). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
(We will follow-up in a subsequent PR addressing the remaining feedback.) |
Cross-SDK Consistency Review — PR #1164 (Add Rust SDK)This PR adds a brand-new Rust SDK. I reviewed it for cross-language API consistency with the Node.js, Python, Go, and .NET SDKs. ✅ Core API: well-alignedThe fundamental surface is consistent across all five SDKs:
The intentional divergences (typed newtypes,
|
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1164 · ● 2.8M
| // Model management | ||
| let model = session.get_model().await?; | ||
| session.set_model("claude-sonnet-4.5", None).await?; | ||
|
|
||
| // Mode management (interactive, plan, autopilot) | ||
| let mode = session.get_mode().await?; | ||
| session.set_mode("autopilot").await?; | ||
|
|
||
| // Workspace files | ||
| let files = session.list_workspace_files().await?; | ||
| let content = session.read_workspace_file("plan.md").await?; | ||
|
|
||
| // Plan management | ||
| let (exists, content) = session.read_plan().await?; | ||
| session.update_plan("Updated plan content").await?; | ||
|
|
||
| // Fleet (sub-agents) | ||
| session.start_fleet(Some("Implement the auth module")).await?; | ||
|
|
||
| // Cleanup (preserves on-disk session state for later resume) | ||
| session.disconnect().await?; |
There was a problem hiding this comment.
Documentation bug: several session.X() calls shown here don't exist on Session
Lines 108–124 document the following as direct convenience methods on Session, but none of them are actually implemented on the struct (only set_model and get_messages / abort are):
| Shown in README | Actual API |
|---|---|
session.get_model() |
session.rpc().model().get_current().await? |
session.get_mode() |
session.rpc().mode().get().await? |
session.set_mode("autopilot") |
session.rpc().mode().set(ModeSetRequest { mode: "autopilot".into() }).await? |
session.list_workspace_files() |
session.rpc().workspaces().list_files().await? |
session.read_workspace_file("plan.md") |
session.rpc().workspaces().read_file(WorkspacesReadFileRequest { path: "plan.md".into() }).await? |
session.read_plan() |
session.rpc().plan().read().await? |
session.update_plan("...") |
session.rpc().plan().update(PlanUpdateRequest { ... }).await? |
session.start_fleet(Some("...")) |
session.rpc().fleet().start(FleetStartRequest { ... }).await? |
Because the code block uses rust,ignore, this isn't caught by cargo test --doc. A user copying these snippets from the README will get a compile error like no method named 'get_model' found for struct 'Session'.
Suggestion: Either implement these as ergonomic wrappers on Session (thin delegations to rpc()), or update this section to use session.rpc().* calls and add a note that the typed RPC namespace is the primary access point for these operations. If the wrappers are planned as a follow-up, it would be worth adding a // TODO: convenience wrapper note and linking to the "Typed RPC namespace" section so readers know the right path today.
* Add Rust SDK
Adds the Copilot Rust SDK (`copilot-sdk` crate) under `rust/`,
alongside Rust codegen plumbed into `scripts/codegen/` and CI under
`.github/workflows/rust-sdk-tests.yml`. The crate ships a JSON-RPC
client, session lifecycle management, system message transforms,
permission policy helpers, the `define_tool` adapter, and per-event
`SessionHandler`/`SessionHooks` traits.
Includes:
- 14 ported E2E scenarios under `rust/tests/` driving the replay-proxy
harness, plus a hand-curated set of unit tests.
- A rust-coding-skill (`.github/skills/rust-coding-skill/`) capturing
conventions for error handling, async/concurrency, tracing, and the
intentional trait exceptions in the SDK's public API.
- Release tooling: `rust-publish-release.yml`, `RELEASING.md`, and
protocol-version generation wired into the existing automation.
- `PermissionResult` extended with `Deferred` and `Custom` variants
for richer permission decisions.
Public API is held at 0.1.0-pre. Marked protocol-evolving public enums
`#[non_exhaustive]` so additive variants stay non-breaking.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Christopher Schleiden <cschleiden@github.com>
Co-authored-by: David Dossett <25163139+daviddossett@users.noreply.github.com>
Co-authored-by: Devraj Mehta <devm33@github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Co-authored-by: Evan Boyle <EvanBoyle@users.noreply.github.com>
Co-authored-by: Jeremy Moseley <jemoseley@microsoft.com>
Co-authored-by: Steve Sanderson <SteveSandersonMS@users.noreply.github.com>
* Polish public API for 0.1.0 release
- **Broadcast subscriptions for lifecycle and session events.**
`Client::subscribe_lifecycle()` and `Session::subscribe()` return
`tokio::sync::broadcast::Receiver`; dropping the receiver
unsubscribes. Replaces the prior callback-based `Client::on`,
`Client::on_event_type`, `Session::on`, and `Unsubscribe` API.
Spawned consumer tasks isolate panics naturally.
- **`PermissionResult` gains `Deferred` and `Custom` variants.**
`Deferred` lets handlers resolve a request asynchronously via
`session.permissions.handlePendingPermissionRequest` (notification
path only — falls back to `Approved` on the direct RPC path).
`Custom(Value)` lets handlers send arbitrary response payloads
beyond the standard `approve-once` / `reject` shapes.
- **`#[non_exhaustive]` on protocol-evolving public enums**
(`PermissionResult`, `SessionLifecycleEventType`,
`GitHubReferenceType`, others) so additive variants stay
non-breaking.
- **`ToolHandlerRouter` overrides per-event `SessionHandler` methods**
so consumers can call `router.on_external_tool(...)` directly
without unwrapping `HandlerResponse`.
- **`define_tool` accepts bare `async fn` items** in addition to
closures, matching `tower::service_fn` /
`hyper::service::service_fn` conventions. Documented in rustdoc.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Route generated SessionId/RequestId fields through hand-authored newtypes
Generated code emitted `pub session_id: String` for every schema field named
`sessionId` and likewise for `requestId`, leaving consumers with mixed types:
`Session::id()` returned `SessionId` but `session.events_subscribe()` events
exposed `session_id: String`. Same papercut for request IDs in permission and
elicitation event payloads.
The newtypes are `#[serde(transparent)]` so the wire format is unchanged. This
adds a property-name override map to `scripts/codegen/rust.ts` that maps
`sessionId`, `remoteSessionId`, and `requestId` to the hand-authored types in
`crate::types`, and emits the matching `use` statement in both generated
modules. `mc_session_id` (MCP protocol metadata, not a Copilot session) stays
as `String`.
After regeneration: 27 fields converted to `SessionId` (including the handoff
event's `remoteSessionId`) and 25 to `RequestId`. The existing `PartialEq<str>`
/ `PartialEq<String>` impls on both newtypes mean test code like
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Pass ToolInvocation to define_tool closures
define_tool's Fn(P) -> Fut bound gave closures only the deserialized
arguments, leaving session_id, tool_call_id, and tool_name unreachable.
That blocked the helper for any tool that needs to scope DB lookups to
a session, emit per-tool-call telemetry, or stream UI updates back to
the originating session — patterns that hit dozens of sites across
realistic tool suites.
Change the closure bound to Fn(ToolInvocation, P) -> Fut. The arguments
are moved out via mem::take before deserialization, so there is no
clone cost on the hot path. Closures that don't need the metadata
write |_inv, params|.
Also add ToolInvocation::params<P>() so long-form impl ToolHandler
blocks can deserialize without naming serde_json directly:
async fn call(&self, inv: ToolInvocation) -> Result<ToolResult, Error> {
let params: MyParams = inv.params()?;
// …use inv.session_id alongside params…
}
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Make ping message argument optional
Node, Python, and .NET all expose ping with an optional message.
Go requires it only because Go has no Option type — Rust has one,
so the API should match the languages with the same expressive power
rather than the one without.
Change ping(&self, message: &str) to ping(&self, message: Option<&str>).
When None, the message field is omitted from the request payload
rather than sent as an empty string.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Build Rust docs with all features in CI
cargo doc was running with --features test-support, which left the
derive feature off and made intra-doc links to define_tool and
schema_for resolve to nothing — failing under the crate's
deny(rustdoc::broken_intra_doc_links).
docs.rs already uses all-features (see Cargo.toml's
[package.metadata.docs.rs]); align CI with that so the docs job
matches what users will see on docs.rs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address PR #1164 review feedback
emitted from the loop correlate to a session in traces. Matches
the pattern documented in the rust-coding-skill.
- README.md / embeddedcli.rs: correct the embedded-CLI documentation
to match what build.rs and embeddedcli.rs actually do — archives
come from the github/copilot-cli GitHub Releases, integrity is
SHA-256 against SHA256SUMS.txt, and the runtime cache path is
~/.cache/copilot-sdk-{version}/copilot.
- test/scenarios/sessions/streaming/verify.sh: drop a duplicate
'# Go: build' comment.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Regenerate Rust types for @github/copilot 1.0.39-0
Picks up the new model.call_failure session event (with its
ModelCallFailureData payload and ModelCallFailureSource enum) and
the new optional 'tip' field on session_info.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Scope codegen-check workflow changes to Rust only
Removes path triggers and the regenerate step for other languages'
protocol-version files. Those drift checks are a pre-existing gap on
main and out of scope for the Rust SDK port.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Point rust-publish-release workflow header to RELEASING.md
The 23-line setup checklist duplicated content already in
rust/RELEASING.md. One-line pointer is enough.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Update Rust scenario binaries for new define_tool signature
Two scenarios still used the old `Fn(P) -> Fut` shape and broke when
the SDK switched to `Fn(ToolInvocation, P) -> Fut`. They don't use
the invocation field, so just bind it as `_inv`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Rename Session::send_message -> send and align MessageOptions
Cross-SDK consistency: every other SDK (Node, Python, Go, .NET) uses
`send`/`Send`/`SendAsync` plus `MessageOptions` as the public
parameter type. Rust was the outlier with `send_message` and
`SendOptions`, and the asymmetry with the existing `send_and_wait`
method made it read awkwardly.
- Rename `Session::send_message` -> `Session::send` (and the private
helper `send_message_inner` -> `send_inner`).
- Rename the public `SendOptions` type -> `MessageOptions`.
- Delete the previous wire-level `MessageOptions` struct: it had no
internal callers (the wire payload is hand-rolled in send_inner) and
freeing the name was the cleanest path to parity.
Pre-1.0 type rename, no protocol or behavior change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Wrap subscribe() in EventSubscription / LifecycleSubscription newtypes
Previously Session::subscribe and Client::subscribe_lifecycle returned
raw tokio::sync::broadcast::Receiver<T> values. A survey of mature Rust
crates (tonic, lapin, rdkafka, redis-rs, tokio-tungstenite, iroh-gossip,
tokio-stream's BroadcastStream itself) found that none of them expose a
raw broadcast::Receiver in their public API; the dominant pattern is a
named newtype implementing futures::Stream, with overflow surfaced
explicitly in the item type.
Introduce a copilot::subscription module with:
- EventSubscription / LifecycleSubscription newtypes
- Inherent recv() returning Result<T, RecvError> for existing
while-let loop ergonomics
- Stream impl yielding Result<T, Lagged> so callers can use
tokio_stream::StreamExt or futures::StreamExt combinators
- Lagged / RecvError types owned by the SDK so consumers no longer
import tokio's broadcast error types
Net effect: the channel choice is now an internal implementation detail.
We can swap broadcast for async-broadcast / flume / a custom backpressure
policy, or convert lag into an Event::Lagged variant, without a breaking
change to the public surface.
Existing while-let loops in tests and examples continue to compile and
behave identically: close and lag both exit the loop, matching
tokio::sync::broadcast::Receiver.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply nightly rustfmt to subscription module
Local cargo +nightly fmt --check passed without `--config-path
.rustfmt.nightly.toml`, but CI runs with the explicit config and
flagged two diffs: import group flattening and test-mod import order.
Applied with the same flags CI uses.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix workspaces RPC method names (was singular `workspace.*`)
`Session::list_workspace_files`, `read_workspace_file`, and
`create_workspace_file` were calling the singular `session.workspace.*`
RPC methods. The CLI exposes these under the plural namespace
`session.workspaces.*` (see Node `session.rpc.workspaces`, Go
`session.RPC.Workspaces`, .NET `session.Rpc.Workspaces`, Python
`session.rpc.workspaces`, and the generated `SESSION_WORKSPACES_*`
constants).
Route the three calls through the typed constants in
`generated::api_types::rpc_methods` so the bug class is structurally
gone — a stale string literal can no longer drift from the schema.
Add three integration tests against the mock server in
`session_test.rs` asserting the wire method name and request shape for
each helper. This mirrors the
`session_rpc_methods_send_correct_method_names` table-driven test that
already covers the rest of the session RPCs.
Also fold the missing `create_workspace_file` into the workspace
helpers bullet in CHANGELOG.md.
The bug never shipped — the SDK is pre-publish — so no migration
notes. Heads-up filed to the github-app sister copy.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* rust: add typed RPC namespace, route helpers through it
Phase 4 § 4.6.A.5 — generate a fully-typed `client.rpc.*` /
`session.rpc.*` namespace from the Copilot CLI schema, mirroring the
Node/Python/Go/.NET SDKs. All hand-authored ergonomic helpers
(`list_workspace_files`, `read_plan`, `set_mode`, `list_models`,
`get_quota`, etc.) become one-line delegations over the namespace —
their public signatures are unchanged, but wire-method strings now
exist in exactly one place (`generated/rpc.rs`).
This makes the `session.workspace.*` → `session.workspaces.*` typo
bug class structurally impossible: helpers can't drift from the
schema because they no longer reference wire strings, and new RPCs
land in the namespace immediately as the schema regenerates.
- scripts/codegen/rust.ts: emit rust/src/generated/rpc.rs alongside
api_types.rs. Builds a namespace tree from `rpcMethod` paths,
resolves $ref/title/inline schemas for params + results, injects
sessionId for session methods, dispatches via rpc_methods constants.
- rust/src/generated/rpc.rs: new ~1370 LOC generated file.
- rust/src/lib.rs: Client::rpc() accessor; ping/list_models/get_quota
rewritten as delegations.
- rust/src/session.rs: Session::rpc() accessor; workspaces/plan/mode/
model/name/log/fleet/permissions helpers rewritten as delegations.
- rust/tests/session_test.rs: add 3 namespace-only tests
(session.rpc().agent().list, session.rpc().tasks().list,
client.rpc().models().list); update session.log mock response to
match typed LogResult shape (eventId required).
- rust/CHANGELOG.md, rust/README.md: document the new namespace.
cargo +nightly-2026-04-14 fmt --check, cargo clippy
--all-features --all-targets -- -D warnings, and cargo test
--all-features all green.
No breaking changes for SDK or github-app consumers — public helper
signatures are preserved by construction.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Rename crate to `github-copilot-sdk`
Per formal naming decision, the published crate name on crates.io will
be `github-copilot-sdk` and the import path will be
`use github_copilot_sdk::...`.
Changes:
- `Cargo.toml`: `name = "github-copilot-sdk"`, `[lib] name = "github_copilot_sdk"`,
`documentation = "https://docs.rs/github-copilot-sdk"`. Repository and
homepage URLs continue to point at `github/copilot-sdk` (the repo
name is unchanged).
- All `use copilot::` / `copilot::` references migrated to
`use github_copilot_sdk::` / `github_copilot_sdk::` across `src/`,
`tests/`, `examples/`, README, CHANGELOG, RELEASING.md, and codegen
scripts.
- Embedded-CLI cache directory renamed from `~/.cache/copilot-sdk-*` to
`~/.cache/github-copilot-sdk-*` for naming consistency. Pre-release
caches will be orphaned; acceptable given the crate is unpublished.
- `release-plz.toml` package name updated.
Verified: `cargo check`, `cargo clippy -D warnings`, `cargo test`,
nightly `cargo fmt --check` all green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add typed wrappers for filter/MCP/permission shapes (Bucket A.1, A.3, A.4)
Replaces the remaining `serde_json::Value` blobs on the public surface
with typed structs/enums, mirroring the wire shapes documented in
Node/Go/Python/.NET. Helper signatures already changed from `Value` to
typed equivalents in the same call-sites; this lands the type
definitions and constructor-site updates.
A.3 — typed wrappers
* `SessionListFilter { cwd, git_root, repository, branch }` for
`Client::list_sessions`. Was `Option<serde_json::Value>`.
* `McpServerConfig` tagged enum (Stdio/Http/Sse) with
`McpStdioServerConfig` / `McpHttpServerConfig` payloads.
`SessionConfig::mcp_servers`, `ResumeSessionConfig::mcp_servers`, and
`CustomAgentConfig::mcp_servers` are now
`Option<HashMap<String, McpServerConfig>>`. Accepts `type: "local"`
alias on deserialize for back-compat with the legacy CLI shape.
* `PermissionRequestData` gains `kind: Option<PermissionRequestKind>`
and `tool_call_id: Option<String>` fields. The eight CLI permission
categories (shell/write/read/url/mcp/custom-tool/memory/hook) are
enumerated; unknown kinds fall through to `Unknown`. Existing
`extra: Value` flatten is preserved so `data.extra["command"]` etc.
keep working.
A.4 — PermissionResult variants
* Adds `UserNotAvailable` (encodes as `{kind: "user-not-available"}`)
and `NoResult` (encodes as `{kind: "no-result"}`). Both paths —
notification (`handlePendingPermissionRequest`) and direct RPC
(`permission.request`) — emit the correct kind string.
* `pending_permission_result_kind` updated to handle `NoResult`
explicitly; `direct_permission_payload` updated to bypass
ApproveOnce/Reject conversion for these new variants.
A.1 — disable_resume
* `ResumeSessionConfig::disable_resume: Option<bool>`. Mirrors Node's
`ResumeSessionConfig.disableResume` and Go's `*bool DisableResume`.
Forces resume to fail if the session is missing on disk rather than
silently starting a new one.
Tests
* `list_sessions_serializes_typed_filter` — confirms the typed filter
serializes camelCase, omits None fields, and dispatches via
`session.list`.
* `mcp_server_config_roundtrips_through_tagged_enum` — confirms
`type: "stdio"` serialize, `type: "local"` alias on deserialize, and
HashMap roundtrip.
* `permission_request_data_extracts_typed_kind` — confirms typed kind
extraction including the kebab-case `custom-tool` rename and the
forward-compatible `Unknown` fallthrough.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Document infinite_sessions parity + Client::stop deferral (Bucket A.2/A.6)
Notes-only CHANGELOG entries closing out the remaining Bucket A items:
* A.2 (infinite_sessions) — already wired on both SessionConfig and
ResumeSessionConfig as `Option<InfiniteSessionConfig>` with
default-omit-on-the-wire semantics. Matches Node/Go (CLI-applied
defaults when the field is absent). No code change; document the
parity decision so it doesn't get re-flagged.
* A.6 (Client::stop error aggregation) — defer behind a Client-level
session registry. Real aggregation requires iterating Session handles
to disconnect-then-kill, but the Rust Client only tracks per-session
channel senders today (router::SessionSenders), not Session instances.
A cosmetic Result<(), Vec<Error>> change with no behavior shift would
be pure pass-through wrapping. Tracked for Bucket B alongside the
registry refactor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Aggregate Client::stop errors across active sessions (Bucket B / A.6)
Rewrite `Client::stop` to cooperatively shut down every still-registered
session before terminating the CLI child:
1. Snapshot active session IDs from the router (no lock held across
the destroy RPCs).
2. For each, send `session.destroy` and unregister.
3. Take + terminate the child.
4. Collect any errors from steps 2 and 3 into a new `StopErrors`
aggregate and return `Result<(), StopErrors>`.
`StopErrors` is a public newtype around `Vec<Error>` implementing
`std::error::Error`; `errors()` and `into_errors()` expose the
underlying errors. Mirrors Node's `Error[]` shutdown return shape and
closes Bucket A.6 (deferred from the prior batch since true aggregation
required iterating active sessions, not just changing the signature).
Implementation detail: rather than introducing a parallel
`Weak<Session>` registry on the Client, this leans on the router's
existing session-ID HashMap. The router already tracks every session
the client has registered, so a `session_ids()` snapshot helper is the
only new state. No new Arc<Session> ceremony, no Drop-impl back-pointer,
no public API change to Session.
This is a breaking change to `Client::stop`'s return type. README's
quickstart example switches from `client.stop().await?` to
`client.stop().await.ok();` (best-effort shutdown) — callers that
care about per-session destroy errors can match on `StopErrors` and
inspect `errors()`.
Tests:
- `client_stop_sends_session_destroy_for_each_active_session` — two
registered sessions, both destroys observed on the wire, returns Ok.
- `client_stop_aggregates_session_destroy_errors` — destroy returns
a JSON-RPC error; `StopErrors` carries it and Display includes the
underlying message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Bucket B.1 SessionConfig fields
Mirrors the six knobs the Node SDK exposes on `SessionConfig` /
`ResumeSessionConfig` that the Rust SDK was missing:
- session_id: Option<SessionId> on SessionConfig (custom session ID;
remains required on ResumeSessionConfig as the existing field).
- working_directory: Option<PathBuf> (per-session cwd override,
independent of ClientOptions::cwd).
- config_dir: Option<PathBuf> (override default config dir for this
session).
- model_capabilities: Option<ModelCapabilitiesOverride> (per-property
capability overrides, deep-merged at session create / resume time;
the type was already used by SetModelOptions).
- github_token: Option<String> (per-session GitHub token, distinct
from ClientOptions::github_token which authenticates the CLI process
itself). Redacted from Debug output.
- include_sub_agent_streaming_events: Option<bool>.
Wire format: snake_case round-trips to camelCase via
#[serde(rename_all = "camelCase")] except gitHubToken, which uses an
explicit #[serde(rename = "gitHubToken")] to match Node's mixed-case
spelling.
Adds two regression tests in tests/session_test.rs verifying serde
output for the wire field names + Debug-redaction of the token.
CHANGELOG: documents the six new fields under "Configuration parity".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Bucket B.2 ClientOptions fields (log_level + idle timeout)
Mirrors two of the five Node CopilotClientOptions knobs the Rust SDK
was missing:
- log_level: Option<LogLevel> with a new typed enum (None / Error /
Warning / Info / Debug / All), serialized lowercase to match the
CLI's --log-level argument values. Replaces the previously hardcoded
--log-level info in spawn_stdio / spawn_tcp; when unset the SDK still
passes "info" for parity with prior behavior.
- session_idle_timeout_seconds: Option<u64>. When Some(n) with n > 0,
the SDK passes --session-idle-timeout <n>. None or Some(0) leaves
sessions running indefinitely (CLI default). Implementation lives
in a new Client::session_idle_timeout_args helper, mirroring
auth_args.
Adds four lib unit tests covering the helper plus LogLevel serde
round-trip.
Two of the five Node knobs are intentionally not ported and are
documented in the CHANGELOG as N/A:
- isChildProcess: requires a parent-stdio transport variant the Rust
SDK does not yet support; tracked as a future addition rather than
a Bucket B item.
- autoStart: does not apply to the Rust SDK's API shape — Client::start
is a single explicit constructor rather than the deferred-init
pattern Node uses.
The remaining onListModels (BYOK callback) is tracked separately;
its callback type design is non-trivial and warrants its own commit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Bucket B.2 on_list_models BYOK callback override
Mirrors Node's `CopilotClientOptions.onListModels`: when set,
`Client::list_models` returns the handler's result without making
a `models.list` RPC. This is the BYOK escape hatch for environments
where the model catalog is provisioned separately from the CLI
(e.g. external inference servers selected via Transport::External).
API additions:
- `ListModelsHandler` async_trait with a single
`async fn list_models(&self) -> Result<Vec<Model>, Error>` method.
Mirrors the shape of `SessionHandler` / `SessionHooks` for
consistency.
- `ClientOptions::on_list_models: Option<Arc<dyn ListModelsHandler>>`.
- `ClientOptions` switches from `#[derive(Debug)]` to a manual Debug
impl that prints the handler as `<set>` / `None`. Same precedent
as `SessionConfig::handler` and the redacted `github_token` field.
Plumbing:
- `ClientInner` gains an `on_list_models` field carrying the handler.
- `Client::from_transport` takes the handler as a new parameter;
threaded through all 3 transport call sites in `Client::start`
(External / Tcp / Stdio). `Client::from_streams` (no
ClientOptions) passes None.
- `Client::list_models` consults the handler before falling back
to the RPC.
Tests:
- `client_options_debug_redacts_handler`: confirms manual Debug
prints `<set>` for the handler and continues redacting
`github_token`.
- `list_models_uses_on_list_models_handler_when_set`: end-to-end
override path with a counting handler.
Bucket B.2 is now complete: log_level, session_idle_timeout_seconds,
and on_list_models all ported. is_child_process and auto_start
remain documented as N/A (transport variant Rust doesn't have / API
shape mismatch).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add MessageOptions.request_headers (Phase 4 § 4.5)
Mirrors Node's `MessageOptions.requestHeaders` and Go's
`MessageOptions.RequestHeaders`: custom per-turn HTTP headers
forwarded to the CLI on the `session.send` request, used by the
CLI to inject headers into outbound model API calls for that turn.
API additions:
- `MessageOptions::request_headers: Option<HashMap<String, String>>`
(already `#[non_exhaustive]` with builder pattern, so the new field
is fully additive for existing callers).
- `MessageOptions::with_request_headers(headers)` chainable builder.
Wire format:
- `session.send` request payload gains a `requestHeaders` object
when the field is `Some(map)` and the map is non-empty.
- Omitted entirely when `None` or empty — same `omitempty` semantics
as Node's optional field (per Node types.ts:1521-1524).
Tests:
- `send_serializes_request_headers` — multi-header case verifies
field name (camelCase `requestHeaders`) and value pass-through.
- `send_omits_request_headers_when_unset_or_empty` — covers both
the unset (None) and empty-map cases, ensuring neither sends
`"requestHeaders": {}` on the wire.
Bucket scope: this is the smallest of Phase 4's four 1.0-blocking
gaps. 4.1 (slash commands) and 4.2 (SessionFsProvider, ADR-worthy)
remain. 4.3 / 4.4 are pending team review.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add slash command registration (Phase 4 § 4.1)
Introduces the consumer-facing surface for `/cmdname` slash commands,
matching Node's `SessionConfig.commands`, Python's, and Go's:
- `CommandHandler` async trait — single `on_command(ctx)` async method.
Mirrors the SDK's existing `SessionHandler` / `ToolHandler` pattern
(named trait, navigable in stack traces) over a boxed-closure shape.
- `CommandDefinition { name, description?, handler }` — `#[non_exhaustive]`
with `new` + `with_description` builders. Manual `Serialize` emits only
`{name, description?}` on the wire; handlers stay client-side. Manual
`Debug` prints the handler as `<set>` (same precedent as
`SessionConfig::handler`).
- `CommandContext { session_id, command, command_name, args }` —
`#[non_exhaustive]`, what the handler receives.
- `SessionConfig::commands` and `ResumeSessionConfig::commands` —
`Option<Vec<CommandDefinition>>` with `with_commands` builders. Field
is `skip_deserializing` since `CommandDefinition` carries an opaque
handler.
Wire-up:
- `Client::create_session` / `Client::resume_session` drain
`config.commands` into an `Arc<HashMap<String, Arc<dyn CommandHandler>>>`
via a new `build_command_handler_map` helper, threaded through
`spawn_event_loop` to `handle_notification`.
- New `SessionEventType::CommandExecute` arm dispatches incoming
`command.execute` notifications: looks up the handler by `commandName`,
invokes it on a spawned task, then acks via
`session.commands.handlePendingCommand` — no error on success,
`error: <handler message>` on `Err`, and
`error: "Unknown command: <name>"` when the name is unregistered
(matches Node's behavior verbatim).
Tests (4 new in `rust/tests/session_test.rs`, all green):
- `create_serializes_commands_strips_handler` — wire payload contains
only `{name, description?}`, no `handler` key.
- `command_execute_dispatches_to_registered_handler_and_acks_success`
— handler invoked with right `CommandContext`, ack has no error.
- `command_execute_unknown_command_acks_with_error` — unregistered
command name produces `Unknown command: <name>` ack.
- `command_execute_handler_error_propagates_to_ack` — handler `Err` is
surfaced verbatim in the ack's `error` field.
Schema-side types (`CommandExecuteData`,
`SessionRpcCommands::handle_pending_command`, `RPC_METHOD_*`) were
already in `rust/src/generated/` — no codegen changes needed.
Cross-repo impact (additive): github-app's `SessionConfig { ... }`
literal at `core.rs:710` will need `commands: None,` added at sync time,
same mechanical pattern as Bucket B.1 fields.
CHANGELOG entry added under "Configuration parity".
Gates:
- cargo +nightly-2026-04-14 fmt --check ✅
- cargo clippy --all-features --all-targets -- -D warnings ✅
- cargo test --all-features ✅ (84 lib + 5 + 3 + 68 + 17 = 177 tests)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add ADR 0001: SessionFsProvider trait and plumbing (Phase 4 § 4.2)
Drafts the architecture decision record for the last 1.0 parity gap —
SessionFsProvider — before any implementation lands. Per the global
"ADRs for major decisions" convention and tclem's explicit guidance
on §4.2.
Establishes `rust/docs/adr/` as the home for Rust SDK ADRs (this is
the first one) with a short index in `rust/docs/adr/README.md`.
ADR scope:
- **Methodology** — explicit verify-before-drafting audit, citing the
consistent wins from A.2 / A.6 / 4.5 / 4.1. Captures the cross-SDK
source map (Node + Python + Go + generated Rust types) so reviewers
can verify the proposed shape against actual upstream code.
- **Trait shape** — async_trait with 10 methods mirroring Node's
provider, returning `Result<T, FsError>`. Sync alternative and
trait-erased boxed-closure alternative both rejected with rationale.
- **Method signatures** — Rust-idiomatic `Result<T, FsError>` with the
SDK adapting to the schema's `{ ..., error: Option<SessionFsError> }`
payload. `FsError::NotFound` → `ENOENT`, `FsError::Other` → `UNKNOWN`.
`From<io::Error>` provided so `tokio::fs`-backed handlers can `?`.
- **Concurrency model** — concurrent dispatch (each `sessionFs.*`
request on its own spawned task), `Send + Sync` providers must be
re-entrant. Per-session sequential dispatch rejected with rationale
(CLI parallelism assumption).
- **Plumbing** — direct `Arc<dyn SessionFsProvider>` registration on
SessionConfig, NOT a factory closure like Node/Python/Go. Sidesteps
the "lambdas as fn args" rule, idiomatic Rust, callers can carry
session-id refs themselves. Trade-off documented in "Differences
from other SDKs" call-out planned for the README. Future
factory-closure form can land additively if needed.
- **Inbound dispatch** — new arms in `handle_request` for all 10
`sessionFs.*` methods, dispatched through a per-session
`Arc<HashMap<SessionId, Arc<dyn SessionFsProvider>>>` map mirroring
the §4.1 `command_handlers` shape.
- **Naming + module layout** — `rust/src/session_fs.rs` for public
surface, `rust/src/session_fs_dispatch.rs` (`pub(crate)`) for the
request adapters. `SessionFsConventions { Posix, Windows }` is
hand-authored to avoid the generated enum's catch-all `Unknown`
variant on the consumer-input side.
- **Forward compat** — `#[non_exhaustive]` on `SessionFsConfig`,
`FsError`, `FileInfo`, `DirEntry`, `DirEntryKind`. Future trait
methods land with default impls returning `Err(FsError::Other(...))`
so existing impls continue to compile.
- **Tauri-app non-impact** — audited; `src-tauri/` does not register
a virtual filesystem provider today. `ClientOptions { ... }` literal
in `cli.rs` will need `session_fs: None,` added at sync time, same
mechanical pattern as Bucket B.2.
- **Implementation order** — 11-step ordered plan, from trait
authoring through CHANGELOG entry, so the actual implementation
commit can follow ADR approval without sequencing surprises.
This commit ships the ADR only — no implementation code, no public
API changes, no test changes. The ADR is `Status: Proposed` until
tclem and the Rust SDK working group sign off.
ASCII-only diagrams per repo style. No external markdown rendering
dependencies.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* rust: implement SessionFsProvider (Phase 4 § 4.2)
Adds a virtualizable filesystem provider that lets host applications
sandbox sessions, project files into in-memory or remote storage, and
apply permission policies before bytes move. Last 1.0-blocking parity
gap before public release.
Public API additions (in `crate::session_fs`, re-exported from
`crate::types`):
- `SessionFsProvider` async trait with 10 methods (`read_file`,
`write_file`, `append_file`, `exists`, `stat`, `mkdir`, `readdir`,
`readdir_with_types`, `rm`, `rename`). All methods have default impls
returning `Err(FsError::Other("not supported"))` so providers only
override what they need and forward-compatible schema additions land
without breaking implementors.
- `SessionFsConfig` (initial_cwd, session_state_path, conventions),
`SessionFsConventions` (Posix/Windows), `FsError` (NotFound/Other),
`FileInfo`, `DirEntry`, `DirEntryKind`. All `#[non_exhaustive]`.
- `ClientOptions::session_fs: Option<SessionFsConfig>` — when set,
`Client::start` calls `sessionFs.setProvider` after protocol-version
verification.
- `SessionConfig::with_session_fs_provider` /
`ResumeSessionConfig::with_session_fs_provider` builders for
registering an `Arc<dyn SessionFsProvider>` per session.
- `From<std::io::Error>` on `FsError` (NotFound→NotFound, anything
else→Other) so handlers backed by `std::fs` / `tokio::fs` can use `?`.
Wire dispatch:
- `pub(crate) crate::session_fs_dispatch` module bridges the trait to
the schema. `FsError::NotFound` maps to wire `ENOENT`; all other
errors map to `UNKNOWN` with the message preserved for diagnostics.
- The session event loop forwards inbound `sessionFs.*` requests to
the dispatch module; non-fs methods continue to the existing
`unknown method` error path.
- New `SessionError::SessionFsProviderRequired` and
`SessionError::InvalidSessionFsConfig` variants surface
configuration errors at `Client::start` / `create_session` time.
Divergence from Node/Python/Go:
- This SDK accepts `Arc<dyn SessionFsProvider>` directly, rather than a
factory closure that builds a provider per session. There is no
`Session` value to thread into a factory at config time, and the SDK
already prefers traits over boxed closures for handler-shaped APIs
(`SessionHandler`, `SessionHooks`, `ToolHandler`). See
`rust/docs/adr/0001-session-fs-provider.md` for the rejected-factory
rationale and a forward-compat escape hatch
(`with_session_fs_provider_factory`) that can be added additively
post-1.0 if a real factory use case emerges. ADR status flipped from
Proposed to Accepted in this commit.
Tests:
- 7 mock-server tests in `rust/tests/session_test.rs` covering
read_file dispatch, NotFound→ENOENT mapping, Other→UNKNOWN mapping,
write_file with mode, readdir_with_types, rm with force, and
validation rejecting empty `initial_cwd`.
- Inline unit tests in `session_fs.rs` for the io::Error→FsError
conversion.
Docs / examples:
- `rust/examples/session_fs.rs` — in-memory provider example.
- `rust/CHANGELOG.md` — entry under "Configuration parity".
- `rust/README.md` — new "Differences From Other SDKs" section
linking to the ADR.
Verified:
- `cargo +nightly-2026-04-14 fmt --check`
- `cargo clippy --all-features --all-targets -- -D warnings`
- `cargo test --all-features` (190 tests pass; 3 ignored require CLI)
Cross-repo impact (additive only):
- `src-tauri/src/session/cli.rs:337` `ClientOptions { ... }` literal —
adds `session_fs: None`.
- `src-tauri/src/session/core.rs:710` `SessionConfig { ... }` literal —
adds `session_fs_provider: None`.
- `src-tauri/src/session/core.rs:749` `ResumeSessionConfig { ... }`
literal — adds `session_fs_provider: None`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add W3C Trace Context propagation (Phase 4 § 4.3)
Implements the cross-SDK 1.0-blocking `traceparent` / `tracestate` plumbing
across the three RPC injection points and the inbound tool-invocation read
path. Hybrid shape combines Node's callback ergonomics with Go's per-turn
override:
- New `TraceContext` struct (`#[non_exhaustive]`) and `TraceContextProvider`
async trait in `crate::trace_context`, re-exported from `crate::types`.
- `ClientOptions::on_get_trace_context: Option<Arc<dyn TraceContextProvider>>`
supplies an ambient provider invoked on `session.create`, `session.resume`,
and `session.send`. Manual `Debug` impl prints `<set>` / `None` matching
the `on_list_models` precedent.
- `MessageOptions` gains `traceparent` / `tracestate: Option<String>` plus
three builders — `with_trace_context`, `with_traceparent`,
`with_tracestate`. Per-turn values override the callback (provider is not
invoked when MessageOptions already carries trace headers).
- `ToolInvocation` flipped to `#[non_exhaustive]` and exposes inbound
`traceparent` / `tracestate` populated from `external_tool.requested`
events, plus a `trace_context()` helper. Wire fields are omitted when
unset, matching the schema's `omitempty` semantics.
- New test-only constructor `Client::from_streams_with_trace_provider`
(gated on `cfg(test)` / `feature = "test-support"`) so integration tests
can exercise the callback path.
Tests: 7 new mock-server scenarios in `tests/session_test.rs` covering
provider invocation on create/resume/send, MessageOptions-overrides-callback,
MessageOptions-without-callback, ToolInvocation read path, and wire-omits-
when-unset. Full suite: 88 lib + 83 integration green; clippy and nightly
fmt clean.
No formal ADR — `ListModelsHandler` and `MessageOptions` precedents already
cover both shapes. CHANGELOG entry under "Configuration parity" documents
the cross-SDK rationale.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Implement Default on ToolInvocation for test ergonomics
Adds `#[derive(Default)]` to `ToolInvocation` (and to `SessionId`, which
the derive depends on). Pure additive: production code never constructs
`ToolInvocation` literals (it's a CLI-emitted read-only type), so the
"meaningless empty defaults" cost is genuinely zero in practice.
The win is downstream test scaffolding. With `..Default::default()`
available, test sites that build `ToolInvocation` literals to drive
handler tests can collapse boilerplate and absorb future
`#[non_exhaustive]` field additions automatically — no need to
re-touch every test scaffold every time the schema grows. Aligns with
the rust-coding-skill's "use `..Default::default()` in tests to reduce
boilerplate when adding fields" guidance, and mirrors `MessageOptions`
which already derives `Default`.
`SessionId` is a transparent newtype around `String`, so its `Default`
is `SessionId(String::new())` — already the natural identity for an
unset session ID.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add TelemetryConfig env-var passthrough on ClientOptions (Phase 4 § 4.4)
Closes the last 1.0-blocking gap in Phase 4 of the Rust SDK release prep.
Adds opt-in OpenTelemetry configuration mirroring Node/Python/Go.
## Surface
- `TelemetryConfig` (`#[non_exhaustive]`, `Debug + Clone + Default`) with
five `Option`-typed fields: `otlp_endpoint`, `file_path`,
`exporter_type`, `source_name`, `capture_content`. No `Serialize` /
`Deserialize` derives — this type is not wire-serialized; it's pure
spawn-time env-var injection.
- `OtelExporterType` enum (`#[non_exhaustive]`, `Serialize`/`Deserialize`
with `rename_all = "kebab-case"`): `OtlpHttp`, `File`. Typed rather than
`Option<String>` to match the `LogLevel` precedent (B.2) for finite,
enumerated CLI knobs.
- `ClientOptions::telemetry: Option<TelemetryConfig>`, defaulting to
`None`. Field added to the manual `Debug` impl.
## Wire behavior
When `ClientOptions::telemetry` is `Some(...)`, `Client::build_command`
sets `COPILOT_OTEL_ENABLED=true` plus, for each populated field:
- `otlp_endpoint` -> `OTEL_EXPORTER_OTLP_ENDPOINT`
- `file_path` -> `COPILOT_OTEL_FILE_EXPORTER_PATH`
- `exporter_type` -> `COPILOT_OTEL_EXPORTER_TYPE` (`"otlp-http"` / `"file"`)
- `source_name` -> `COPILOT_OTEL_SOURCE_NAME`
- `capture_content` -> `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`
(`"true"` / `"false"`)
Env-var names are byte-for-byte identical to the Node/Python/Go SDKs.
`capture_content` serializes as lowercase `"true"`/`"false"` matching
Node's `String(t.captureContent)` and Python's `str(...).lower()`.
## Env-var precedence
`build_command` env order: auth token -> telemetry -> `options.env`
(user override) -> `options.env_remove`. User-supplied env always wins
over both auth and telemetry, by ordering. Tests cover this.
## Design notes
- Pure env-var passthrough: zero new dependencies. The CLI itself owns
the OpenTelemetry exporter; the SDK is just a config conduit. All
three reference SDKs (Node, Python, Go) work this way — verified
before drafting (verify-before-drafting tally now at 8 wins).
- No ADR. Plan-doc explicitly endorsed skipping the ADR since this
isn't an architectural decision: the shape is fully determined by
cross-SDK precedent. ADR directory is reserved for non-obvious
choices; precedent verification belongs in CHANGELOG + commit body.
- `#[non_exhaustive]` on both `TelemetryConfig` and `OtelExporterType`
to absorb future CLI-side telemetry knobs (sampling, additional
exporters) without breaking changes.
- `PathBuf` (not `String`) for `file_path` for type safety;
`Command::env` accepts `AsRef<OsStr>` so no conversion needed.
## Tests
Five new tests in `lib.rs`:
- `build_command_sets_otel_env_when_telemetry_enabled` — full
TelemetryConfig set; asserts all six expected env vars propagate with
correct values.
- `build_command_omits_otel_env_when_telemetry_none` — default
`ClientOptions`; asserts none of the six env vars are present.
- `build_command_omits_unset_telemetry_fields` — only `otlp_endpoint`
set; asserts that single field plus the implicit enabled flag are
set, and the four others are absent.
- `build_command_lets_user_env_override_telemetry` — telemetry sets
`OTEL_EXPORTER_OTLP_ENDPOINT="http://from-config:..."`, user env
sets it to `"http://from-user-env:..."`; asserts user env wins.
- `telemetry_config_capture_content_serializes_as_lowercase_bool` —
asserts both `Some(true)` and `Some(false)` serialize as the bare
lowercase boolean strings.
All tests pass; `cargo fmt --check`, `cargo clippy -- -D warnings`,
`cargo test --all-features` all green.
## Cross-repo impact
Existing `ClientOptions { ... }` literal in github-app `cli.rs:337`
gains one new field: `telemetry: None`. No other consumer-side
changes. The Tauri app's existing telemetry pipeline is for the host
app and doesn't interact with the SDK config — this is purely about
forwarding telemetry env vars to the spawned CLI child.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Document Rust-only API surface (Phase 4 § 4.7)
Doc-only follow-up to the Phase 4 parity work. Two pieces of content,
no API changes.
## Changes
1. **`Client::get_quota` rustdoc** — flags the method as Rust-only as
of 0.1.0. The underlying `account.getQuota` JSON-RPC endpoint is
exposed only by the Rust SDK in this release; Node, Python, Go, and
.NET don't surface it. Brief, factual, no backport-issue links.
2. **"Rust-only API" subsection in `rust/README.md`** — restructures
the existing "Differences From Other SDKs" section into two
subsections: "Shape divergence" (the existing `SessionFsProvider`
factory-vs-direct discussion) and a new "Rust-only API" listing the
surface that exists only on the Rust SDK as of 0.1.0:
- `Client::get_quota`
- First-class `Session` convenience methods (`set_mode`/`get_mode`,
`set_name`/`get_name`, `read_plan`/`update_plan`/`delete_plan`,
`start_fleet`, `list_workspace_files`/`read_workspace_file`/
`create_workspace_file`)
- Typed newtypes (`SessionId`, `RequestId`)
- Permission policy builders (`permission::approve_all`,
`permission::deny_all`, `permission::approve_if`)
- `Client::from_streams` (arbitrary `AsyncRead`/`AsyncWrite`)
- `enum Transport { Stdio, Tcp, External }`
- Split `prefix_args` / `extra_args`
Tone is "Rust gets to be Rust" — not apologetic, no backport
promises. Cross-SDK parity for these is explicitly framed as a
post-release conversation, not a release blocker.
## Bonus: drive-by intra-doc link fix
Fixed a pre-existing broken intra-doc link in `session_fs.rs` (`[`tokio::fs`]`
-> explicit URL link). Caught by `cargo doc --no-deps --all-features`,
which now builds clean. Doing it here rather than as a separate commit
since the testing-pass that comes next is going to require clean docs
output anyway, and the fix is one line.
`cargo fmt --check`, `cargo clippy -- -D warnings`, `cargo test --doc`,
and `cargo doc --no-deps --all-features` all green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Broaden skills discovery wording in copilot-instructions.md
Skills under .github/skills/ are auto-discovered by Copilot tooling
broadly (CLI, Copilot Coding Agent, etc.), not just Copilot CLI.
Per stephentoub's review on PR #1164.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix ConnectionState::Errored wire form to match Go ("error" not "errored")
Caught by stephentoub on PR #1164. ConnectionState's serde
`rename_all = "lowercase"` was producing "errored" for the Errored
variant, but Go's source-of-truth uses "error". Add explicit
`#[serde(rename = "error")]` on the variant so the wire form matches
Go's ConnectionState string. Variant name stays `Errored` to avoid
shadowing the std `Error` trait and the crate's own `Error` type.
Adds two unit tests locking in the wire format for all four variants.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Rename ConnectionState::Errored to ConnectionState::Error
Per follow-up on stephentoub's review of PR #1164: prefer renaming the
variant to drop the `#[serde(rename = "error")]` attribute. The variant
is unused outside types.rs (the Client transitions Disconnected /
Connecting / Connected today; Error is reserved for future use), so
renaming has no consumer impact and produces a cleaner enum surface.
`ConnectionState::Error` does not collide with anything in scope:
`types.rs` does not import `crate::Error` (it uses fully-qualified
`crate::Error` at use sites) and `std::error::Error` is unimported. The
variant lives inside a typed enum, so no shadowing concern.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address PR #1164 cross-SDK consistency review
Three review-feedback fixes folded into a single commit:
- `session.ui()` sub-API: move `elicitation`, `confirm`, `select`,
`input` onto a new `SessionUi<'a>` view returned by `Session::ui()`.
Mirrors .NET `session.UI`, Python `session.ui`, Go `session.UI()`.
Wire-method names unchanged. Per stephentoub on
https://github.com/github/copilot-sdk/pull/1164#discussion_r3161986035
and the bot duplicate at #discussion_r3163241529.
- Typed `Client::get_status` and `Client::get_auth_status` returns:
introduce `GetStatusResponse { version, protocol_version }` and
`GetAuthStatusResponse { is_authenticated, auth_type, host, login,
status_message }`, both `#[non_exhaustive]`. Matches Node / Go /
Python typed shapes. Per the bot at
https://github.com/github/copilot-sdk/pull/1164#discussion_r3162629019.
- `Session::set_model` now returns `Result<(), Error>` instead of
`Result<Option<String>, Error>`. Mirrors Node / Python / Go / .NET
void-on-success semantics. Per the bot at
https://github.com/github/copilot-sdk/pull/1164#discussion_r3162816817.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Type MessageOptions::mode as DeliveryMode enum
Promote MessageOptions::mode from Option<String> to Option<DeliveryMode>,
where DeliveryMode is a #[non_exhaustive] enum with Enqueue (default) and
Immediate variants serializing to "enqueue" / "immediate". Mirrors Node
types.ts:1519 (mode?: "enqueue" | "immediate") and Go types.go:858
(MessageOptions.Mode is the message delivery mode (default: "enqueue")).
The prior rustdoc on MessageOptions::mode incorrectly described the field
as a permission mode. The field has always controlled message delivery
relative to in-flight session work; the doc lied. Strings other than
"enqueue" / "immediate" were silently no-ops at the CLI, so the typed enum
turns wrong-string-at-call-site from a runtime nothing-happens into a
compile error.
Precedent for typed enums on enumerated CLI knobs: B.2 LogLevel
(c4132c2), 4.4 OtelExporterType (aefb108), SessionFsConventions, and
DirEntryKind. The asymmetric request_headers (4.5) stayed
HashMap<String, String> because that value space is open; mode is finite
and CLI-controlled.
with_mode signature shrinks from impl Into<String> to DeliveryMode and
the wire injection in session.rs uses serde_json::to_value(m). Added a
roundtrip test in types.rs covering both variants.
CHANGELOG entry under Configuration parity.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Default permission-flow flags to Some(true)
SessionConfig::default() and ResumeSessionConfig::new() now set the four
permission-flow flags to Some(true):
- request_user_input
- request_permission
- request_exit_plan_mode
- request_elicitation
Mirrors Node's client.ts:749-751 which always sets requestPermission: true
and derives requestUserInput/requestElicitation from handler presence. In
Rust, SessionHandler is trait-based so a handler is always installed
(DenyAllHandler is the default) — opt-in defaults of None meant a Rust
caller could install a SessionHandler and forget to flip the flags,
silently breaking permission flow vs. the equivalent Node code.
The default DenyAllHandler refuses all permission requests so the wire
surface is safe out-of-the-box. Callers that want the wire surface fully
disabled set the flags explicitly to Some(false).
SessionConfig drops its derived Default in favor of a manual impl that
enumerates every field. ResumeSessionConfig::new() switches the four
field assignments from None to Some(true). Field rustdoc updated on each
of the four fields to call out the default. Two unit tests added in
types.rs covering both constructors.
CHANGELOG entry under Configuration parity.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Mark remaining public config types non_exhaustive
Adds #[non_exhaustive] to the 10 remaining public configuration types
that didn't already carry the attribute:
- SessionConfig
- ResumeSessionConfig
- ClientOptions
- ProviderConfig
- McpServerConfig
- Tool
- CustomAgentConfig
- InfiniteSessionConfig
- SystemMessageConfig
- ConnectionState
HookEvent, HookOutput, MessageOptions, TelemetryConfig, SessionFsConfig,
FsError, FileInfo, DirEntry, ToolInvocation, Error, Transport, and the
new DeliveryMode were already marked. Closing the asymmetry now means
adding fields to any of these post-1.0 is non-breaking on consumers that
construct via Default::default() plus field assignment or the with_*
builders.
Tradeoff: external crates can no longer use struct-literal syntax for
these types -- not even with ..Default::default(), which only works
inside the defining crate. Tests, examples, and the tool_parameters
doctest are migrated to the let-mut + field-assignment pattern. Callers
porting from 0.1.0-* will see the same compile error and apply the same
mechanical transform.
CHANGELOG entry under Configuration parity.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix InputOptions doc-link to SessionUi::input
The UI-grouping commit f4aa8d9 moved input/select/confirm/elicitation off
Session into SessionUi, but the rustdoc on InputOptions still referenced
the old crate::session::Session::input path. Cargo doc with
-D rustdoc::broken_intra_doc_links rejected the link.
Repoints the link to crate::session::SessionUi::input.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Drop cross-SDK comparisons from Rust source comments
Per-symbol "Mirrors Node's Foo / Go's Bar / Python's Baz" rustdoc is
unscalable and drifts as the other SDKs evolve. The Rust SDK seeks parity
with Node/Python/Go/.NET; that fact is now stated once at the top of
rust/README.md, and intentional divergences live in the README's
"Differences From Other SDKs" section.
- Strip "Mirrors X" / "Unlike Y" / cross-SDK file:line citations from
rustdoc across lib.rs, session.rs, types.rs, session_fs.rs,
trace_context.rs. Replace with API-shape descriptions where the dropped
text carried real information (e.g. wire-string variants).
- README: add a one-line parity statement up top that points to the
existing Differences section.
- Update .github/skills/rust-coding-skill/SKILL.md to forbid cross-SDK
references in code comments and rustdoc going forward, with explicit
guidance that intra-SDK self-references ("Mirrors `from_streams`") are
still fine.
cargo doc -D warnings clean. cargo clippy clean. cargo fmt clean.
18 doctests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Move SessionFs ADR out of public crate
ADR 0001 (SessionFsProvider trait and plumbing) captured the design
rationale for choosing direct Arc<dyn SessionFsProvider> registration
over the factory-closure pattern that Node/Python/Go use. That
rationale is internal release-engineering history, not consumer-facing
API documentation; the README's "Differences From Other SDKs" section
already explains what consumers need to know about the divergence.
The full ADR is being relocated to the private SDK release-plan
documentation in github/github-app PR #3166 (docs/copilot/2026-04-14-sdk-release/).
- Delete rust/docs/ entirely (only contained ADR 0001 + index README).
- Strip ADR links from rustdoc, README, CHANGELOG, and the
session_fs example. The README's existing inline rationale (factory
pattern doesn't cleanly express in Rust at the session-config call
site, no `Session` value to thread in, SDK already prefers traits
over closures for handler-shaped APIs) is sufficient on its own.
cargo doc -D warnings clean. cargo clippy clean. cargo fmt clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix SessionUi::elicitation wire field name
The hand-authored `SessionUi::elicitation` convenience layer was
sending the JSON Schema payload as `"schema"` on the wire, but the
`session.ui.elicitation` request shape expects `"requestedSchema"`.
This is the field name in:
- this crate's own generated UIElicitationRequest type
(rust/src/generated/api_types.rs:1721, serde renames to
`requestedSchema` via #[serde(rename_all = "camelCase")])
- the generated typed RPC wrapper SessionRpcUi::elicitation
(rust/src/generated/rpc.rs:1245-1257), which is correct
- and the same wire field used by every other SDK we ship
So every elicitation call from the SessionUi convenience layer was
effectively dead — the CLI saw a missing required `requestedSchema`
field. The `confirm` / `select` / `input` helpers all delegate to
`elicitation`, so they were dead too.
The mock-server test for elicitation round-tripped through the same
misnamed field on both ends, so the bug slipped past unit tests
(`assert_eq!(request["params"]["schema"], schema)` matched the buggy
implementation). The fix is a one-line rename in session.rs plus a
test update that now asserts on `requestedSchema` and explicitly
rejects a stray `schema` key, so we can't regress the same way.
207 tests pass. doc / clippy / fmt clean.
Caught by the gap-analysis re-run before 0.1.0 cut. Wire-shape
divergence between hand-authored and generated layers — same class
of bug as the workspace RPC fix earlier in this stack.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add typed on_auto_mode_switch handler for rate-limit recovery
Wires the CLI's `autoModeSwitch.request` JSON-RPC callback (added by
copilot-agent-runtime PR #7024 — github/copilot-agent-runtime#7024) into
the SDK's typed handler surface. When an eligible rate limit is hit,
the runtime asks the SDK whether to switch the session to auto model;
this commit gives consumers a typed entry point for that prompt
matching the shape of the existing exit-plan-mode / user-input /
elicitation handshakes.
Background: the github-app's vendored copilot-sdk crate has carried a
private version of this handler since Apr 27 (github-app commit
02ec73588). The plumbing was never upstreamed because no other SDK had
a typed handler for it, and the wire-protocol gap on the runtime side
hadn't yet closed. With #7024 merged, the wire path is well-defined
(`autoModeSwitch.request` → `{ response: "yes" | "yes_always" | "no" }`)
and the typed handler can ship.
Cross-SDK divergence: typed handler exists only in the Rust SDK as of
0.1.0. Node, Python, Go, and .NET observe the request as a raw
JSON-RPC callback today; parity ports for those SDKs are post-release
follow-up work and are noted in the README's "Rust-only API" section
and in the changelog.
Wire shape and types:
- `handler::AutoModeSwitchResponse` — typed enum (`Yes`, `YesAlways`,
`No`) with `#[serde(rename_all = "snake_case")]`. Wire values are
byte-identical to the runtime's schema. `#[non_exhaustive]` so future
variants are additive. Ships as a typed enum rather than the
`{ response: String }` shape used in the github-app vendored copy —
consistent with the recently-landed `DeliveryMode` enum and the
general convention that finite, enumerated wire values get typed at
the API surface (LogLevel, OtelExporterType, ConnectionState,
PermissionRequestKind, etc.).
- `handler::HandlerEvent::AutoModeSwitch { session_id, error_code,
retry_after_seconds }` — new event variant. `error_code` is the
rate-limit kind (e.g. `user_weekly_rate_limited`). `retry_after_seconds`
is the RFC 9110 `Retry-After` `delta-seconds` value when the runtime
knows it; consumers can surface a humanized reset time alongside the
prompt.
- `handler::HandlerResponse::AutoModeSwitch(AutoModeSwitchResponse)` —
new response variant.
- `SessionHandler::on_auto_mode_switch` — new trait method with safe
default (`No`). Default `on_event` dispatches to it.
- `types::SessionConfig::request_auto_mode_switch: Option<bool>` and
`types::ResumeSessionConfig::request_auto_mode_switch: Option<bool>`
— new opt-in flags, both default to `Some(true)` via
`SessionConfig::default()` and `ResumeSessionConfig::new()` to match
the convention already established by the other four `request_*`
flags. Without the flag the runtime doesn't dispatch the callback.
- `session.rs` `handle_request` — new arm for `autoModeSwitch.request`
alongside the existing `permission.request` / `userInput.request` /
`exitPlanMode.request` cases. Extracts `errorCode` /
`retryAfterSeconds` from params, dispatches via
`handler.on_event(HandlerEvent::AutoModeSwitch { ... })`, serializes
the typed response back as `{ "response": "yes" | "yes_always" |
"no" }`. Falls through to `No` if the handler returns an unexpected
response variant.
Tests:
- `auto_mode_switch_dispatches_to_handler_and_serializes_response` —
asserts the inbound `autoModeSwitch.request` reaches the typed
handler with the expected `error_code` / `retry_after_seconds`, and
that returning `AutoModeSwitchResponse::YesAlways` serializes as
`{ "response": "yes_always" }` on the wire.
- `auto_mode_switch_default_handler_replies_no` — asserts the trait's
default impl replies `{ "response": "no" }` when no override is
provided.
- Updated `session_config_default_enables_permission_flow_flags` and
`resume_session_config_new_enables_permission_flow_flags` to assert
the new flag is `Some(true)` by default.
Documentation:
- README: new bullet under "Rust-only API" section.
- CHANGELOG: new entry under `### Added > Handlers + helpers`
documenting the wire path, the typed enum, and the cross-SDK
divergence with a note that Node / Python / Go / .NET parity is
post-release work.
- `request_auto_mode_switch` field rustdoc cross-references
`SessionHandler::on_auto_mode_switch`.
Validation:
- 209 tests pass (was 207 — 2 new auto-mode-switch tests).
- `cargo doc -D warnings` clean.
- `cargo +nightly-2026-04-14 fmt --check` clean.
- `cargo clippy --all-features --all-targets -- -D warnings` clean.
Migration note for the github-app sync session: the typed
`AutoModeSwitchResponse` enum replaces github-app's vendored
`AutoModeSwitchResponse { response: String }`. github-app's WebSocket
relay continues to receive the user's choice as a `String`; map to the
typed enum at the boundary
(`match s.as_str() { "yes" => Yes, "yes_always" => YesAlways, _ => No }`).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix Client::list_sessions wire shape — wrap filter under params.filter
The hand-authored `Client::list_sessions` was serializing the optional
`SessionListFilter` directly onto the JSON-RPC `params` object,
flattening fields like `repository` / `branch` / `cwd` / `gitRoot` to
the top level. The `session.list` request shape that the runtime accepts
puts the filter under `params.filter` — and that's what every other SDK
sends:
- Node `nodejs/src/client.ts:1178-1180`: `sendRequest("session.list", { filter })`
- Go `go/types.go`: `listSessionsRequest { Filter *SessionListFilter }`
- Python `python/copilot/client.py:1907-1911`: `payload["filter"] = ...`
- .NET `dotnet/src/Client.cs`: `record ListSessionsRequest(SessionListFilter? Filter)`
Because the runtime silently ignores unknown top-level keys on
`session.list`, calling `list_sessions(Some(filter))` was functionally
equivalent to `list_sessions(None)` in 0.0.x — every filter field was
discarded by the runtime, returning an unfiltered session list. No
runtime error, no log, just silently broken. Functionally dead on the
wire, same class as the elicitation `requestedSchema` fix in `c58e2f2`.
The mock-server test `list_sessions_serializes_typed_filter` asserted on
the flat shape it observed (`request["params"]["repository"]`) rather
than the schema's wrapped shape, so the bug round-tripped through both
ends — the implementation produced the wrong shape, the test verified
the wrong shape. Same root cause as the elicitation test gap.
Fix:
- `Client::list_sessions` now wraps the filter: `Some(f) ->
serde_json::json!({ "filter": f })`, `None -> serde_json::json!({})`.
`None` omits the filter key entirely (matches Go's `omitempty`
behavior; Node's `{ filter: undefined }` also omits via JSON-stringify).
- Mock-server test now asserts on the wrapped path
(`params.filter.repository`, `params.filter.branch`) AND explicitly
asserts the flattened fallback is gone (`params.get("repository")`
must return `None`). Same regression-prevention pattern as the
elicitation fix at `session_test.rs:1248-1251`.
- CHANGELOG entry under `### Fixed` documenting the wire-shape fix and
the test gap that masked it.
Validation:
- 209 tests pass (no count change — same test, stricter assertions).
- `cargo doc -D warnings` clean.
- `cargo +nightly-2026-04-14 fmt --check` clean.
- `cargo clippy --all-features --all-targets -- -D warnings` clean.
Caught by the gap-analysis structural-correctness pass walking every
hand-authored `client.call("...")` site against the schema and the four
other SDKs. This is the second wire-shape bug found by that pass; the
first was the `SessionUi::elicitation` `schema` -> `requestedSchema`
fix in `c58e2f2`. The audit confirms `session.list` is the only other
new bug — three Rust-unique surfaces (`session.respondToUserInput`,
`session.sendTelemetry`, top-level `sendTelemetry` /
`server.sendTelemetry`) are uncheckable cross-SDK and queued as
post-0.1.0 runtime-acceptance verification.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Bump @github/copilot pin to ^1.0.39 + regen Rust types
Per the auto-mode-switch CLI hunt session's verification, the
`@github/copilot@1.0.39-0` pin we were on does NOT contain
copilot-agent-runtime PR #7024 (auto-mode-switch wire support).
The runtime PR merged after `1.0.39-0` was cut as a release-candidate
and shipped in `1.0.39` final, with `1.0.40-0` after.
This commit:
- Bumps `nodejs/package.json` from `^1.0.39-0` to `^1.0.39` (final).
`npm install` resolved to `1.0.39`. The package-lock.json is
regenerated accordingly.
- Regenerates Rust t…
* Add Rust SDK
Adds the Copilot Rust SDK (`copilot-sdk` crate) under `rust/`,
alongside Rust codegen plumbed into `scripts/codegen/` and CI under
`.github/workflows/rust-sdk-tests.yml`. The crate ships a JSON-RPC
client, session lifecycle management, system message transforms,
permission policy helpers, the `define_tool` adapter, and per-event
`SessionHandler`/`SessionHooks` traits.
Includes:
- 14 ported E2E scenarios under `rust/tests/` driving the replay-proxy
harness, plus a hand-curated set of unit tests.
- A rust-coding-skill (`.github/skills/rust-coding-skill/`) capturing
conventions for error handling, async/concurrency, tracing, and the
intentional trait exceptions in the SDK's public API.
- Release tooling: `rust-publish-release.yml`, `RELEASING.md`, and
protocol-version generation wired into the existing automation.
- `PermissionResult` extended with `Deferred` and `Custom` variants
for richer permission decisions.
Public API is held at 0.1.0-pre. Marked protocol-evolving public enums
`#[non_exhaustive]` so additive variants stay non-breaking.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Christopher Schleiden <cschleiden@github.com>
Co-authored-by: David Dossett <25163139+daviddossett@users.noreply.github.com>
Co-authored-by: Devraj Mehta <devm33@github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Co-authored-by: Evan Boyle <EvanBoyle@users.noreply.github.com>
Co-authored-by: Jeremy Moseley <jemoseley@microsoft.com>
Co-authored-by: Steve Sanderson <SteveSandersonMS@users.noreply.github.com>
* Polish public API for 0.1.0 release
- **Broadcast subscriptions for lifecycle and session events.**
`Client::subscribe_lifecycle()` and `Session::subscribe()` return
`tokio::sync::broadcast::Receiver`; dropping the receiver
unsubscribes. Replaces the prior callback-based `Client::on`,
`Client::on_event_type`, `Session::on`, and `Unsubscribe` API.
Spawned consumer tasks isolate panics naturally.
- **`PermissionResult` gains `Deferred` and `Custom` variants.**
`Deferred` lets handlers resolve a request asynchronously via
`session.permissions.handlePendingPermissionRequest` (notification
path only — falls back to `Approved` on the direct RPC path).
`Custom(Value)` lets handlers send arbitrary response payloads
beyond the standard `approve-once` / `reject` shapes.
- **`#[non_exhaustive]` on protocol-evolving public enums**
(`PermissionResult`, `SessionLifecycleEventType`,
`GitHubReferenceType`, others) so additive variants stay
non-breaking.
- **`ToolHandlerRouter` overrides per-event `SessionHandler` methods**
so consumers can call `router.on_external_tool(...)` directly
without unwrapping `HandlerResponse`.
- **`define_tool` accepts bare `async fn` items** in addition to
closures, matching `tower::service_fn` /
`hyper::service::service_fn` conventions. Documented in rustdoc.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Route generated SessionId/RequestId fields through hand-authored newtypes
Generated code emitted `pub session_id: String` for every schema field named
`sessionId` and likewise for `requestId`, leaving consumers with mixed types:
`Session::id()` returned `SessionId` but `session.events_subscribe()` events
exposed `session_id: String`. Same papercut for request IDs in permission and
elicitation event payloads.
The newtypes are `#[serde(transparent)]` so the wire format is unchanged. This
adds a property-name override map to `scripts/codegen/rust.ts` that maps
`sessionId`, `remoteSessionId`, and `requestId` to the hand-authored types in
`crate::types`, and emits the matching `use` statement in both generated
modules. `mc_session_id` (MCP protocol metadata, not a Copilot session) stays
as `String`.
After regeneration: 27 fields converted to `SessionId` (including the handoff
event's `remoteSessionId`) and 25 to `RequestId`. The existing `PartialEq<str>`
/ `PartialEq<String>` impls on both newtypes mean test code like
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Pass ToolInvocation to define_tool closures
define_tool's Fn(P) -> Fut bound gave closures only the deserialized
arguments, leaving session_id, tool_call_id, and tool_name unreachable.
That blocked the helper for any tool that needs to scope DB lookups to
a session, emit per-tool-call telemetry, or stream UI updates back to
the originating session — patterns that hit dozens of sites across
realistic tool suites.
Change the closure bound to Fn(ToolInvocation, P) -> Fut. The arguments
are moved out via mem::take before deserialization, so there is no
clone cost on the hot path. Closures that don't need the metadata
write |_inv, params|.
Also add ToolInvocation::params<P>() so long-form impl ToolHandler
blocks can deserialize without naming serde_json directly:
async fn call(&self, inv: ToolInvocation) -> Result<ToolResult, Error> {
let params: MyParams = inv.params()?;
// …use inv.session_id alongside params…
}
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Make ping message argument optional
Node, Python, and .NET all expose ping with an optional message.
Go requires it only because Go has no Option type — Rust has one,
so the API should match the languages with the same expressive power
rather than the one without.
Change ping(&self, message: &str) to ping(&self, message: Option<&str>).
When None, the message field is omitted from the request payload
rather than sent as an empty string.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Build Rust docs with all features in CI
cargo doc was running with --features test-support, which left the
derive feature off and made intra-doc links to define_tool and
schema_for resolve to nothing — failing under the crate's
deny(rustdoc::broken_intra_doc_links).
docs.rs already uses all-features (see Cargo.toml's
[package.metadata.docs.rs]); align CI with that so the docs job
matches what users will see on docs.rs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address PR #1164 review feedback
emitted from the loop correlate to a session in traces. Matches
the pattern documented in the rust-coding-skill.
- README.md / embeddedcli.rs: correct the embedded-CLI documentation
to match what build.rs and embeddedcli.rs actually do — archives
come from the github/copilot-cli GitHub Releases, integrity is
SHA-256 against SHA256SUMS.txt, and the runtime cache path is
~/.cache/copilot-sdk-{version}/copilot.
- test/scenarios/sessions/streaming/verify.sh: drop a duplicate
'# Go: build' comment.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Regenerate Rust types for @github/copilot 1.0.39-0
Picks up the new model.call_failure session event (with its
ModelCallFailureData payload and ModelCallFailureSource enum) and
the new optional 'tip' field on session_info.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Scope codegen-check workflow changes to Rust only
Removes path triggers and the regenerate step for other languages'
protocol-version files. Those drift checks are a pre-existing gap on
main and out of scope for the Rust SDK port.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Point rust-publish-release workflow header to RELEASING.md
The 23-line setup checklist duplicated content already in
rust/RELEASING.md. One-line pointer is enough.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Update Rust scenario binaries for new define_tool signature
Two scenarios still used the old `Fn(P) -> Fut` shape and broke when
the SDK switched to `Fn(ToolInvocation, P) -> Fut`. They don't use
the invocation field, so just bind it as `_inv`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Rename Session::send_message -> send and align MessageOptions
Cross-SDK consistency: every other SDK (Node, Python, Go, .NET) uses
`send`/`Send`/`SendAsync` plus `MessageOptions` as the public
parameter type. Rust was the outlier with `send_message` and
`SendOptions`, and the asymmetry with the existing `send_and_wait`
method made it read awkwardly.
- Rename `Session::send_message` -> `Session::send` (and the private
helper `send_message_inner` -> `send_inner`).
- Rename the public `SendOptions` type -> `MessageOptions`.
- Delete the previous wire-level `MessageOptions` struct: it had no
internal callers (the wire payload is hand-rolled in send_inner) and
freeing the name was the cleanest path to parity.
Pre-1.0 type rename, no protocol or behavior change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Wrap subscribe() in EventSubscription / LifecycleSubscription newtypes
Previously Session::subscribe and Client::subscribe_lifecycle returned
raw tokio::sync::broadcast::Receiver<T> values. A survey of mature Rust
crates (tonic, lapin, rdkafka, redis-rs, tokio-tungstenite, iroh-gossip,
tokio-stream's BroadcastStream itself) found that none of them expose a
raw broadcast::Receiver in their public API; the dominant pattern is a
named newtype implementing futures::Stream, with overflow surfaced
explicitly in the item type.
Introduce a copilot::subscription module with:
- EventSubscription / LifecycleSubscription newtypes
- Inherent recv() returning Result<T, RecvError> for existing
while-let loop ergonomics
- Stream impl yielding Result<T, Lagged> so callers can use
tokio_stream::StreamExt or futures::StreamExt combinators
- Lagged / RecvError types owned by the SDK so consumers no longer
import tokio's broadcast error types
Net effect: the channel choice is now an internal implementation detail.
We can swap broadcast for async-broadcast / flume / a custom backpressure
policy, or convert lag into an Event::Lagged variant, without a breaking
change to the public surface.
Existing while-let loops in tests and examples continue to compile and
behave identically: close and lag both exit the loop, matching
tokio::sync::broadcast::Receiver.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply nightly rustfmt to subscription module
Local cargo +nightly fmt --check passed without `--config-path
.rustfmt.nightly.toml`, but CI runs with the explicit config and
flagged two diffs: import group flattening and test-mod import order.
Applied with the same flags CI uses.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix workspaces RPC method names (was singular `workspace.*`)
`Session::list_workspace_files`, `read_workspace_file`, and
`create_workspace_file` were calling the singular `session.workspace.*`
RPC methods. The CLI exposes these under the plural namespace
`session.workspaces.*` (see Node `session.rpc.workspaces`, Go
`session.RPC.Workspaces`, .NET `session.Rpc.Workspaces`, Python
`session.rpc.workspaces`, and the generated `SESSION_WORKSPACES_*`
constants).
Route the three calls through the typed constants in
`generated::api_types::rpc_methods` so the bug class is structurally
gone — a stale string literal can no longer drift from the schema.
Add three integration tests against the mock server in
`session_test.rs` asserting the wire method name and request shape for
each helper. This mirrors the
`session_rpc_methods_send_correct_method_names` table-driven test that
already covers the rest of the session RPCs.
Also fold the missing `create_workspace_file` into the workspace
helpers bullet in CHANGELOG.md.
The bug never shipped — the SDK is pre-publish — so no migration
notes. Heads-up filed to the github-app sister copy.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* rust: add typed RPC namespace, route helpers through it
Phase 4 § 4.6.A.5 — generate a fully-typed `client.rpc.*` /
`session.rpc.*` namespace from the Copilot CLI schema, mirroring the
Node/Python/Go/.NET SDKs. All hand-authored ergonomic helpers
(`list_workspace_files`, `read_plan`, `set_mode`, `list_models`,
`get_quota`, etc.) become one-line delegations over the namespace —
their public signatures are unchanged, but wire-method strings now
exist in exactly one place (`generated/rpc.rs`).
This makes the `session.workspace.*` → `session.workspaces.*` typo
bug class structurally impossible: helpers can't drift from the
schema because they no longer reference wire strings, and new RPCs
land in the namespace immediately as the schema regenerates.
- scripts/codegen/rust.ts: emit rust/src/generated/rpc.rs alongside
api_types.rs. Builds a namespace tree from `rpcMethod` paths,
resolves $ref/title/inline schemas for params + results, injects
sessionId for session methods, dispatches via rpc_methods constants.
- rust/src/generated/rpc.rs: new ~1370 LOC generated file.
- rust/src/lib.rs: Client::rpc() accessor; ping/list_models/get_quota
rewritten as delegations.
- rust/src/session.rs: Session::rpc() accessor; workspaces/plan/mode/
model/name/log/fleet/permissions helpers rewritten as delegations.
- rust/tests/session_test.rs: add 3 namespace-only tests
(session.rpc().agent().list, session.rpc().tasks().list,
client.rpc().models().list); update session.log mock response to
match typed LogResult shape (eventId required).
- rust/CHANGELOG.md, rust/README.md: document the new namespace.
cargo +nightly-2026-04-14 fmt --check, cargo clippy
--all-features --all-targets -- -D warnings, and cargo test
--all-features all green.
No breaking changes for SDK or github-app consumers — public helper
signatures are preserved by construction.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Rename crate to `github-copilot-sdk`
Per formal naming decision, the published crate name on crates.io will
be `github-copilot-sdk` and the import path will be
`use github_copilot_sdk::...`.
Changes:
- `Cargo.toml`: `name = "github-copilot-sdk"`, `[lib] name = "github_copilot_sdk"`,
`documentation = "https://docs.rs/github-copilot-sdk"`. Repository and
homepage URLs continue to point at `github/copilot-sdk` (the repo
name is unchanged).
- All `use copilot::` / `copilot::` references migrated to
`use github_copilot_sdk::` / `github_copilot_sdk::` across `src/`,
`tests/`, `examples/`, README, CHANGELOG, RELEASING.md, and codegen
scripts.
- Embedded-CLI cache directory renamed from `~/.cache/copilot-sdk-*` to
`~/.cache/github-copilot-sdk-*` for naming consistency. Pre-release
caches will be orphaned; acceptable given the crate is unpublished.
- `release-plz.toml` package name updated.
Verified: `cargo check`, `cargo clippy -D warnings`, `cargo test`,
nightly `cargo fmt --check` all green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add typed wrappers for filter/MCP/permission shapes (Bucket A.1, A.3, A.4)
Replaces the remaining `serde_json::Value` blobs on the public surface
with typed structs/enums, mirroring the wire shapes documented in
Node/Go/Python/.NET. Helper signatures already changed from `Value` to
typed equivalents in the same call-sites; this lands the type
definitions and constructor-site updates.
A.3 — typed wrappers
* `SessionListFilter { cwd, git_root, repository, branch }` for
`Client::list_sessions`. Was `Option<serde_json::Value>`.
* `McpServerConfig` tagged enum (Stdio/Http/Sse) with
`McpStdioServerConfig` / `McpHttpServerConfig` payloads.
`SessionConfig::mcp_servers`, `ResumeSessionConfig::mcp_servers`, and
`CustomAgentConfig::mcp_servers` are now
`Option<HashMap<String, McpServerConfig>>`. Accepts `type: "local"`
alias on deserialize for back-compat with the legacy CLI shape.
* `PermissionRequestData` gains `kind: Option<PermissionRequestKind>`
and `tool_call_id: Option<String>` fields. The eight CLI permission
categories (shell/write/read/url/mcp/custom-tool/memory/hook) are
enumerated; unknown kinds fall through to `Unknown`. Existing
`extra: Value` flatten is preserved so `data.extra["command"]` etc.
keep working.
A.4 — PermissionResult variants
* Adds `UserNotAvailable` (encodes as `{kind: "user-not-available"}`)
and `NoResult` (encodes as `{kind: "no-result"}`). Both paths —
notification (`handlePendingPermissionRequest`) and direct RPC
(`permission.request`) — emit the correct kind string.
* `pending_permission_result_kind` updated to handle `NoResult`
explicitly; `direct_permission_payload` updated to bypass
ApproveOnce/Reject conversion for these new variants.
A.1 — disable_resume
* `ResumeSessionConfig::disable_resume: Option<bool>`. Mirrors Node's
`ResumeSessionConfig.disableResume` and Go's `*bool DisableResume`.
Forces resume to fail if the session is missing on disk rather than
silently starting a new one.
Tests
* `list_sessions_serializes_typed_filter` — confirms the typed filter
serializes camelCase, omits None fields, and dispatches via
`session.list`.
* `mcp_server_config_roundtrips_through_tagged_enum` — confirms
`type: "stdio"` serialize, `type: "local"` alias on deserialize, and
HashMap roundtrip.
* `permission_request_data_extracts_typed_kind` — confirms typed kind
extraction including the kebab-case `custom-tool` rename and the
forward-compatible `Unknown` fallthrough.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Document infinite_sessions parity + Client::stop deferral (Bucket A.2/A.6)
Notes-only CHANGELOG entries closing out the remaining Bucket A items:
* A.2 (infinite_sessions) — already wired on both SessionConfig and
ResumeSessionConfig as `Option<InfiniteSessionConfig>` with
default-omit-on-the-wire semantics. Matches Node/Go (CLI-applied
defaults when the field is absent). No code change; document the
parity decision so it doesn't get re-flagged.
* A.6 (Client::stop error aggregation) — defer behind a Client-level
session registry. Real aggregation requires iterating Session handles
to disconnect-then-kill, but the Rust Client only tracks per-session
channel senders today (router::SessionSenders), not Session instances.
A cosmetic Result<(), Vec<Error>> change with no behavior shift would
be pure pass-through wrapping. Tracked for Bucket B alongside the
registry refactor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Aggregate Client::stop errors across active sessions (Bucket B / A.6)
Rewrite `Client::stop` to cooperatively shut down every still-registered
session before terminating the CLI child:
1. Snapshot active session IDs from the router (no lock held across
the destroy RPCs).
2. For each, send `session.destroy` and unregister.
3. Take + terminate the child.
4. Collect any errors from steps 2 and 3 into a new `StopErrors`
aggregate and return `Result<(), StopErrors>`.
`StopErrors` is a public newtype around `Vec<Error>` implementing
`std::error::Error`; `errors()` and `into_errors()` expose the
underlying errors. Mirrors Node's `Error[]` shutdown return shape and
closes Bucket A.6 (deferred from the prior batch since true aggregation
required iterating active sessions, not just changing the signature).
Implementation detail: rather than introducing a parallel
`Weak<Session>` registry on the Client, this leans on the router's
existing session-ID HashMap. The router already tracks every session
the client has registered, so a `session_ids()` snapshot helper is the
only new state. No new Arc<Session> ceremony, no Drop-impl back-pointer,
no public API change to Session.
This is a breaking change to `Client::stop`'s return type. README's
quickstart example switches from `client.stop().await?` to
`client.stop().await.ok();` (best-effort shutdown) — callers that
care about per-session destroy errors can match on `StopErrors` and
inspect `errors()`.
Tests:
- `client_stop_sends_session_destroy_for_each_active_session` — two
registered sessions, both destroys observed on the wire, returns Ok.
- `client_stop_aggregates_session_destroy_errors` — destroy returns
a JSON-RPC error; `StopErrors` carries it and Display includes the
underlying message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Bucket B.1 SessionConfig fields
Mirrors the six knobs the Node SDK exposes on `SessionConfig` /
`ResumeSessionConfig` that the Rust SDK was missing:
- session_id: Option<SessionId> on SessionConfig (custom session ID;
remains required on ResumeSessionConfig as the existing field).
- working_directory: Option<PathBuf> (per-session cwd override,
independent of ClientOptions::cwd).
- config_dir: Option<PathBuf> (override default config dir for this
session).
- model_capabilities: Option<ModelCapabilitiesOverride> (per-property
capability overrides, deep-merged at session create / resume time;
the type was already used by SetModelOptions).
- github_token: Option<String> (per-session GitHub token, distinct
from ClientOptions::github_token which authenticates the CLI process
itself). Redacted from Debug output.
- include_sub_agent_streaming_events: Option<bool>.
Wire format: snake_case round-trips to camelCase via
#[serde(rename_all = "camelCase")] except gitHubToken, which uses an
explicit #[serde(rename = "gitHubToken")] to match Node's mixed-case
spelling.
Adds two regression tests in tests/session_test.rs verifying serde
output for the wire field names + Debug-redaction of the token.
CHANGELOG: documents the six new fields under "Configuration parity".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Bucket B.2 ClientOptions fields (log_level + idle timeout)
Mirrors two of the five Node CopilotClientOptions knobs the Rust SDK
was missing:
- log_level: Option<LogLevel> with a new typed enum (None / Error /
Warning / Info / Debug / All), serialized lowercase to match the
CLI's --log-level argument values. Replaces the previously hardcoded
--log-level info in spawn_stdio / spawn_tcp; when unset the SDK still
passes "info" for parity with prior behavior.
- session_idle_timeout_seconds: Option<u64>. When Some(n) with n > 0,
the SDK passes --session-idle-timeout <n>. None or Some(0) leaves
sessions running indefinitely (CLI default). Implementation lives
in a new Client::session_idle_timeout_args helper, mirroring
auth_args.
Adds four lib unit tests covering the helper plus LogLevel serde
round-trip.
Two of the five Node knobs are intentionally not ported and are
documented in the CHANGELOG as N/A:
- isChildProcess: requires a parent-stdio transport variant the Rust
SDK does not yet support; tracked as a future addition rather than
a Bucket B item.
- autoStart: does not apply to the Rust SDK's API shape — Client::start
is a single explicit constructor rather than the deferred-init
pattern Node uses.
The remaining onListModels (BYOK callback) is tracked separately;
its callback type design is non-trivial and warrants its own commit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Bucket B.2 on_list_models BYOK callback override
Mirrors Node's `CopilotClientOptions.onListModels`: when set,
`Client::list_models` returns the handler's result without making
a `models.list` RPC. This is the BYOK escape hatch for environments
where the model catalog is provisioned separately from the CLI
(e.g. external inference servers selected via Transport::External).
API additions:
- `ListModelsHandler` async_trait with a single
`async fn list_models(&self) -> Result<Vec<Model>, Error>` method.
Mirrors the shape of `SessionHandler` / `SessionHooks` for
consistency.
- `ClientOptions::on_list_models: Option<Arc<dyn ListModelsHandler>>`.
- `ClientOptions` switches from `#[derive(Debug)]` to a manual Debug
impl that prints the handler as `<set>` / `None`. Same precedent
as `SessionConfig::handler` and the redacted `github_token` field.
Plumbing:
- `ClientInner` gains an `on_list_models` field carrying the handler.
- `Client::from_transport` takes the handler as a new parameter;
threaded through all 3 transport call sites in `Client::start`
(External / Tcp / Stdio). `Client::from_streams` (no
ClientOptions) passes None.
- `Client::list_models` consults the handler before falling back
to the RPC.
Tests:
- `client_options_debug_redacts_handler`: confirms manual Debug
prints `<set>` for the handler and continues redacting
`github_token`.
- `list_models_uses_on_list_models_handler_when_set`: end-to-end
override path with a counting handler.
Bucket B.2 is now complete: log_level, session_idle_timeout_seconds,
and on_list_models all ported. is_child_process and auto_start
remain documented as N/A (transport variant Rust doesn't have / API
shape mismatch).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add MessageOptions.request_headers (Phase 4 § 4.5)
Mirrors Node's `MessageOptions.requestHeaders` and Go's
`MessageOptions.RequestHeaders`: custom per-turn HTTP headers
forwarded to the CLI on the `session.send` request, used by the
CLI to inject headers into outbound model API calls for that turn.
API additions:
- `MessageOptions::request_headers: Option<HashMap<String, String>>`
(already `#[non_exhaustive]` with builder pattern, so the new field
is fully additive for existing callers).
- `MessageOptions::with_request_headers(headers)` chainable builder.
Wire format:
- `session.send` request payload gains a `requestHeaders` object
when the field is `Some(map)` and the map is non-empty.
- Omitted entirely when `None` or empty — same `omitempty` semantics
as Node's optional field (per Node types.ts:1521-1524).
Tests:
- `send_serializes_request_headers` — multi-header case verifies
field name (camelCase `requestHeaders`) and value pass-through.
- `send_omits_request_headers_when_unset_or_empty` — covers both
the unset (None) and empty-map cases, ensuring neither sends
`"requestHeaders": {}` on the wire.
Bucket scope: this is the smallest of Phase 4's four 1.0-blocking
gaps. 4.1 (slash commands) and 4.2 (SessionFsProvider, ADR-worthy)
remain. 4.3 / 4.4 are pending team review.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add slash command registration (Phase 4 § 4.1)
Introduces the consumer-facing surface for `/cmdname` slash commands,
matching Node's `SessionConfig.commands`, Python's, and Go's:
- `CommandHandler` async trait — single `on_command(ctx)` async method.
Mirrors the SDK's existing `SessionHandler` / `ToolHandler` pattern
(named trait, navigable in stack traces) over a boxed-closure shape.
- `CommandDefinition { name, description?, handler }` — `#[non_exhaustive]`
with `new` + `with_description` builders. Manual `Serialize` emits only
`{name, description?}` on the wire; handlers stay client-side. Manual
`Debug` prints the handler as `<set>` (same precedent as
`SessionConfig::handler`).
- `CommandContext { session_id, command, command_name, args }` —
`#[non_exhaustive]`, what the handler receives.
- `SessionConfig::commands` and `ResumeSessionConfig::commands` —
`Option<Vec<CommandDefinition>>` with `with_commands` builders. Field
is `skip_deserializing` since `CommandDefinition` carries an opaque
handler.
Wire-up:
- `Client::create_session` / `Client::resume_session` drain
`config.commands` into an `Arc<HashMap<String, Arc<dyn CommandHandler>>>`
via a new `build_command_handler_map` helper, threaded through
`spawn_event_loop` to `handle_notification`.
- New `SessionEventType::CommandExecute` arm dispatches incoming
`command.execute` notifications: looks up the handler by `commandName`,
invokes it on a spawned task, then acks via
`session.commands.handlePendingCommand` — no error on success,
`error: <handler message>` on `Err`, and
`error: "Unknown command: <name>"` when the name is unregistered
(matches Node's behavior verbatim).
Tests (4 new in `rust/tests/session_test.rs`, all green):
- `create_serializes_commands_strips_handler` — wire payload contains
only `{name, description?}`, no `handler` key.
- `command_execute_dispatches_to_registered_handler_and_acks_success`
— handler invoked with right `CommandContext`, ack has no error.
- `command_execute_unknown_command_acks_with_error` — unregistered
command name produces `Unknown command: <name>` ack.
- `command_execute_handler_error_propagates_to_ack` — handler `Err` is
surfaced verbatim in the ack's `error` field.
Schema-side types (`CommandExecuteData`,
`SessionRpcCommands::handle_pending_command`, `RPC_METHOD_*`) were
already in `rust/src/generated/` — no codegen changes needed.
Cross-repo impact (additive): github-app's `SessionConfig { ... }`
literal at `core.rs:710` will need `commands: None,` added at sync time,
same mechanical pattern as Bucket B.1 fields.
CHANGELOG entry added under "Configuration parity".
Gates:
- cargo +nightly-2026-04-14 fmt --check ✅
- cargo clippy --all-features --all-targets -- -D warnings ✅
- cargo test --all-features ✅ (84 lib + 5 + 3 + 68 + 17 = 177 tests)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add ADR 0001: SessionFsProvider trait and plumbing (Phase 4 § 4.2)
Drafts the architecture decision record for the last 1.0 parity gap —
SessionFsProvider — before any implementation lands. Per the global
"ADRs for major decisions" convention and tclem's explicit guidance
on §4.2.
Establishes `rust/docs/adr/` as the home for Rust SDK ADRs (this is
the first one) with a short index in `rust/docs/adr/README.md`.
ADR scope:
- **Methodology** — explicit verify-before-drafting audit, citing the
consistent wins from A.2 / A.6 / 4.5 / 4.1. Captures the cross-SDK
source map (Node + Python + Go + generated Rust types) so reviewers
can verify the proposed shape against actual upstream code.
- **Trait shape** — async_trait with 10 methods mirroring Node's
provider, returning `Result<T, FsError>`. Sync alternative and
trait-erased boxed-closure alternative both rejected with rationale.
- **Method signatures** — Rust-idiomatic `Result<T, FsError>` with the
SDK adapting to the schema's `{ ..., error: Option<SessionFsError> }`
payload. `FsError::NotFound` → `ENOENT`, `FsError::Other` → `UNKNOWN`.
`From<io::Error>` provided so `tokio::fs`-backed handlers can `?`.
- **Concurrency model** — concurrent dispatch (each `sessionFs.*`
request on its own spawned task), `Send + Sync` providers must be
re-entrant. Per-session sequential dispatch rejected with rationale
(CLI parallelism assumption).
- **Plumbing** — direct `Arc<dyn SessionFsProvider>` registration on
SessionConfig, NOT a factory closure like Node/Python/Go. Sidesteps
the "lambdas as fn args" rule, idiomatic Rust, callers can carry
session-id refs themselves. Trade-off documented in "Differences
from other SDKs" call-out planned for the README. Future
factory-closure form can land additively if needed.
- **Inbound dispatch** — new arms in `handle_request` for all 10
`sessionFs.*` methods, dispatched through a per-session
`Arc<HashMap<SessionId, Arc<dyn SessionFsProvider>>>` map mirroring
the §4.1 `command_handlers` shape.
- **Naming + module layout** — `rust/src/session_fs.rs` for public
surface, `rust/src/session_fs_dispatch.rs` (`pub(crate)`) for the
request adapters. `SessionFsConventions { Posix, Windows }` is
hand-authored to avoid the generated enum's catch-all `Unknown`
variant on the consumer-input side.
- **Forward compat** — `#[non_exhaustive]` on `SessionFsConfig`,
`FsError`, `FileInfo`, `DirEntry`, `DirEntryKind`. Future trait
methods land with default impls returning `Err(FsError::Other(...))`
so existing impls continue to compile.
- **Tauri-app non-impact** — audited; `src-tauri/` does not register
a virtual filesystem provider today. `ClientOptions { ... }` literal
in `cli.rs` will need `session_fs: None,` added at sync time, same
mechanical pattern as Bucket B.2.
- **Implementation order** — 11-step ordered plan, from trait
authoring through CHANGELOG entry, so the actual implementation
commit can follow ADR approval without sequencing surprises.
This commit ships the ADR only — no implementation code, no public
API changes, no test changes. The ADR is `Status: Proposed` until
tclem and the Rust SDK working group sign off.
ASCII-only diagrams per repo style. No external markdown rendering
dependencies.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* rust: implement SessionFsProvider (Phase 4 § 4.2)
Adds a virtualizable filesystem provider that lets host applications
sandbox sessions, project files into in-memory or remote storage, and
apply permission policies before bytes move. Last 1.0-blocking parity
gap before public release.
Public API additions (in `crate::session_fs`, re-exported from
`crate::types`):
- `SessionFsProvider` async trait with 10 methods (`read_file`,
`write_file`, `append_file`, `exists`, `stat`, `mkdir`, `readdir`,
`readdir_with_types`, `rm`, `rename`). All methods have default impls
returning `Err(FsError::Other("not supported"))` so providers only
override what they need and forward-compatible schema additions land
without breaking implementors.
- `SessionFsConfig` (initial_cwd, session_state_path, conventions),
`SessionFsConventions` (Posix/Windows), `FsError` (NotFound/Other),
`FileInfo`, `DirEntry`, `DirEntryKind`. All `#[non_exhaustive]`.
- `ClientOptions::session_fs: Option<SessionFsConfig>` — when set,
`Client::start` calls `sessionFs.setProvider` after protocol-version
verification.
- `SessionConfig::with_session_fs_provider` /
`ResumeSessionConfig::with_session_fs_provider` builders for
registering an `Arc<dyn SessionFsProvider>` per session.
- `From<std::io::Error>` on `FsError` (NotFound→NotFound, anything
else→Other) so handlers backed by `std::fs` / `tokio::fs` can use `?`.
Wire dispatch:
- `pub(crate) crate::session_fs_dispatch` module bridges the trait to
the schema. `FsError::NotFound` maps to wire `ENOENT`; all other
errors map to `UNKNOWN` with the message preserved for diagnostics.
- The session event loop forwards inbound `sessionFs.*` requests to
the dispatch module; non-fs methods continue to the existing
`unknown method` error path.
- New `SessionError::SessionFsProviderRequired` and
`SessionError::InvalidSessionFsConfig` variants surface
configuration errors at `Client::start` / `create_session` time.
Divergence from Node/Python/Go:
- This SDK accepts `Arc<dyn SessionFsProvider>` directly, rather than a
factory closure that builds a provider per session. There is no
`Session` value to thread into a factory at config time, and the SDK
already prefers traits over boxed closures for handler-shaped APIs
(`SessionHandler`, `SessionHooks`, `ToolHandler`). See
`rust/docs/adr/0001-session-fs-provider.md` for the rejected-factory
rationale and a forward-compat escape hatch
(`with_session_fs_provider_factory`) that can be added additively
post-1.0 if a real factory use case emerges. ADR status flipped from
Proposed to Accepted in this commit.
Tests:
- 7 mock-server tests in `rust/tests/session_test.rs` covering
read_file dispatch, NotFound→ENOENT mapping, Other→UNKNOWN mapping,
write_file with mode, readdir_with_types, rm with force, and
validation rejecting empty `initial_cwd`.
- Inline unit tests in `session_fs.rs` for the io::Error→FsError
conversion.
Docs / examples:
- `rust/examples/session_fs.rs` — in-memory provider example.
- `rust/CHANGELOG.md` — entry under "Configuration parity".
- `rust/README.md` — new "Differences From Other SDKs" section
linking to the ADR.
Verified:
- `cargo +nightly-2026-04-14 fmt --check`
- `cargo clippy --all-features --all-targets -- -D warnings`
- `cargo test --all-features` (190 tests pass; 3 ignored require CLI)
Cross-repo impact (additive only):
- `src-tauri/src/session/cli.rs:337` `ClientOptions { ... }` literal —
adds `session_fs: None`.
- `src-tauri/src/session/core.rs:710` `SessionConfig { ... }` literal —
adds `session_fs_provider: None`.
- `src-tauri/src/session/core.rs:749` `ResumeSessionConfig { ... }`
literal — adds `session_fs_provider: None`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add W3C Trace Context propagation (Phase 4 § 4.3)
Implements the cross-SDK 1.0-blocking `traceparent` / `tracestate` plumbing
across the three RPC injection points and the inbound tool-invocation read
path. Hybrid shape combines Node's callback ergonomics with Go's per-turn
override:
- New `TraceContext` struct (`#[non_exhaustive]`) and `TraceContextProvider`
async trait in `crate::trace_context`, re-exported from `crate::types`.
- `ClientOptions::on_get_trace_context: Option<Arc<dyn TraceContextProvider>>`
supplies an ambient provider invoked on `session.create`, `session.resume`,
and `session.send`. Manual `Debug` impl prints `<set>` / `None` matching
the `on_list_models` precedent.
- `MessageOptions` gains `traceparent` / `tracestate: Option<String>` plus
three builders — `with_trace_context`, `with_traceparent`,
`with_tracestate`. Per-turn values override the callback (provider is not
invoked when MessageOptions already carries trace headers).
- `ToolInvocation` flipped to `#[non_exhaustive]` and exposes inbound
`traceparent` / `tracestate` populated from `external_tool.requested`
events, plus a `trace_context()` helper. Wire fields are omitted when
unset, matching the schema's `omitempty` semantics.
- New test-only constructor `Client::from_streams_with_trace_provider`
(gated on `cfg(test)` / `feature = "test-support"`) so integration tests
can exercise the callback path.
Tests: 7 new mock-server scenarios in `tests/session_test.rs` covering
provider invocation on create/resume/send, MessageOptions-overrides-callback,
MessageOptions-without-callback, ToolInvocation read path, and wire-omits-
when-unset. Full suite: 88 lib + 83 integration green; clippy and nightly
fmt clean.
No formal ADR — `ListModelsHandler` and `MessageOptions` precedents already
cover both shapes. CHANGELOG entry under "Configuration parity" documents
the cross-SDK rationale.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Implement Default on ToolInvocation for test ergonomics
Adds `#[derive(Default)]` to `ToolInvocation` (and to `SessionId`, which
the derive depends on). Pure additive: production code never constructs
`ToolInvocation` literals (it's a CLI-emitted read-only type), so the
"meaningless empty defaults" cost is genuinely zero in practice.
The win is downstream test scaffolding. With `..Default::default()`
available, test sites that build `ToolInvocation` literals to drive
handler tests can collapse boilerplate and absorb future
`#[non_exhaustive]` field additions automatically — no need to
re-touch every test scaffold every time the schema grows. Aligns with
the rust-coding-skill's "use `..Default::default()` in tests to reduce
boilerplate when adding fields" guidance, and mirrors `MessageOptions`
which already derives `Default`.
`SessionId` is a transparent newtype around `String`, so its `Default`
is `SessionId(String::new())` — already the natural identity for an
unset session ID.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add TelemetryConfig env-var passthrough on ClientOptions (Phase 4 § 4.4)
Closes the last 1.0-blocking gap in Phase 4 of the Rust SDK release prep.
Adds opt-in OpenTelemetry configuration mirroring Node/Python/Go.
## Surface
- `TelemetryConfig` (`#[non_exhaustive]`, `Debug + Clone + Default`) with
five `Option`-typed fields: `otlp_endpoint`, `file_path`,
`exporter_type`, `source_name`, `capture_content`. No `Serialize` /
`Deserialize` derives — this type is not wire-serialized; it's pure
spawn-time env-var injection.
- `OtelExporterType` enum (`#[non_exhaustive]`, `Serialize`/`Deserialize`
with `rename_all = "kebab-case"`): `OtlpHttp`, `File`. Typed rather than
`Option<String>` to match the `LogLevel` precedent (B.2) for finite,
enumerated CLI knobs.
- `ClientOptions::telemetry: Option<TelemetryConfig>`, defaulting to
`None`. Field added to the manual `Debug` impl.
## Wire behavior
When `ClientOptions::telemetry` is `Some(...)`, `Client::build_command`
sets `COPILOT_OTEL_ENABLED=true` plus, for each populated field:
- `otlp_endpoint` -> `OTEL_EXPORTER_OTLP_ENDPOINT`
- `file_path` -> `COPILOT_OTEL_FILE_EXPORTER_PATH`
- `exporter_type` -> `COPILOT_OTEL_EXPORTER_TYPE` (`"otlp-http"` / `"file"`)
- `source_name` -> `COPILOT_OTEL_SOURCE_NAME`
- `capture_content` -> `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`
(`"true"` / `"false"`)
Env-var names are byte-for-byte identical to the Node/Python/Go SDKs.
`capture_content` serializes as lowercase `"true"`/`"false"` matching
Node's `String(t.captureContent)` and Python's `str(...).lower()`.
## Env-var precedence
`build_command` env order: auth token -> telemetry -> `options.env`
(user override) -> `options.env_remove`. User-supplied env always wins
over both auth and telemetry, by ordering. Tests cover this.
## Design notes
- Pure env-var passthrough: zero new dependencies. The CLI itself owns
the OpenTelemetry exporter; the SDK is just a config conduit. All
three reference SDKs (Node, Python, Go) work this way — verified
before drafting (verify-before-drafting tally now at 8 wins).
- No ADR. Plan-doc explicitly endorsed skipping the ADR since this
isn't an architectural decision: the shape is fully determined by
cross-SDK precedent. ADR directory is reserved for non-obvious
choices; precedent verification belongs in CHANGELOG + commit body.
- `#[non_exhaustive]` on both `TelemetryConfig` and `OtelExporterType`
to absorb future CLI-side telemetry knobs (sampling, additional
exporters) without breaking changes.
- `PathBuf` (not `String`) for `file_path` for type safety;
`Command::env` accepts `AsRef<OsStr>` so no conversion needed.
## Tests
Five new tests in `lib.rs`:
- `build_command_sets_otel_env_when_telemetry_enabled` — full
TelemetryConfig set; asserts all six expected env vars propagate with
correct values.
- `build_command_omits_otel_env_when_telemetry_none` — default
`ClientOptions`; asserts none of the six env vars are present.
- `build_command_omits_unset_telemetry_fields` — only `otlp_endpoint`
set; asserts that single field plus the implicit enabled flag are
set, and the four others are absent.
- `build_command_lets_user_env_override_telemetry` — telemetry sets
`OTEL_EXPORTER_OTLP_ENDPOINT="http://from-config:..."`, user env
sets it to `"http://from-user-env:..."`; asserts user env wins.
- `telemetry_config_capture_content_serializes_as_lowercase_bool` —
asserts both `Some(true)` and `Some(false)` serialize as the bare
lowercase boolean strings.
All tests pass; `cargo fmt --check`, `cargo clippy -- -D warnings`,
`cargo test --all-features` all green.
## Cross-repo impact
Existing `ClientOptions { ... }` literal in github-app `cli.rs:337`
gains one new field: `telemetry: None`. No other consumer-side
changes. The Tauri app's existing telemetry pipeline is for the host
app and doesn't interact with the SDK config — this is purely about
forwarding telemetry env vars to the spawned CLI child.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Document Rust-only API surface (Phase 4 § 4.7)
Doc-only follow-up to the Phase 4 parity work. Two pieces of content,
no API changes.
## Changes
1. **`Client::get_quota` rustdoc** — flags the method as Rust-only as
of 0.1.0. The underlying `account.getQuota` JSON-RPC endpoint is
exposed only by the Rust SDK in this release; Node, Python, Go, and
.NET don't surface it. Brief, factual, no backport-issue links.
2. **"Rust-only API" subsection in `rust/README.md`** — restructures
the existing "Differences From Other SDKs" section into two
subsections: "Shape divergence" (the existing `SessionFsProvider`
factory-vs-direct discussion) and a new "Rust-only API" listing the
surface that exists only on the Rust SDK as of 0.1.0:
- `Client::get_quota`
- First-class `Session` convenience methods (`set_mode`/`get_mode`,
`set_name`/`get_name`, `read_plan`/`update_plan`/`delete_plan`,
`start_fleet`, `list_workspace_files`/`read_workspace_file`/
`create_workspace_file`)
- Typed newtypes (`SessionId`, `RequestId`)
- Permission policy builders (`permission::approve_all`,
`permission::deny_all`, `permission::approve_if`)
- `Client::from_streams` (arbitrary `AsyncRead`/`AsyncWrite`)
- `enum Transport { Stdio, Tcp, External }`
- Split `prefix_args` / `extra_args`
Tone is "Rust gets to be Rust" — not apologetic, no backport
promises. Cross-SDK parity for these is explicitly framed as a
post-release conversation, not a release blocker.
## Bonus: drive-by intra-doc link fix
Fixed a pre-existing broken intra-doc link in `session_fs.rs` (`[`tokio::fs`]`
-> explicit URL link). Caught by `cargo doc --no-deps --all-features`,
which now builds clean. Doing it here rather than as a separate commit
since the testing-pass that comes next is going to require clean docs
output anyway, and the fix is one line.
`cargo fmt --check`, `cargo clippy -- -D warnings`, `cargo test --doc`,
and `cargo doc --no-deps --all-features` all green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Broaden skills discovery wording in copilot-instructions.md
Skills under .github/skills/ are auto-discovered by Copilot tooling
broadly (CLI, Copilot Coding Agent, etc.), not just Copilot CLI.
Per stephentoub's review on PR #1164.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix ConnectionState::Errored wire form to match Go ("error" not "errored")
Caught by stephentoub on PR #1164. ConnectionState's serde
`rename_all = "lowercase"` was producing "errored" for the Errored
variant, but Go's source-of-truth uses "error". Add explicit
`#[serde(rename = "error")]` on the variant so the wire form matches
Go's ConnectionState string. Variant name stays `Errored` to avoid
shadowing the std `Error` trait and the crate's own `Error` type.
Adds two unit tests locking in the wire format for all four variants.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Rename ConnectionState::Errored to ConnectionState::Error
Per follow-up on stephentoub's review of PR #1164: prefer renaming the
variant to drop the `#[serde(rename = "error")]` attribute. The variant
is unused outside types.rs (the Client transitions Disconnected /
Connecting / Connected today; Error is reserved for future use), so
renaming has no consumer impact and produces a cleaner enum surface.
`ConnectionState::Error` does not collide with anything in scope:
`types.rs` does not import `crate::Error` (it uses fully-qualified
`crate::Error` at use sites) and `std::error::Error` is unimported. The
variant lives inside a typed enum, so no shadowing concern.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address PR #1164 cross-SDK consistency review
Three review-feedback fixes folded into a single commit:
- `session.ui()` sub-API: move `elicitation`, `confirm`, `select`,
`input` onto a new `SessionUi<'a>` view returned by `Session::ui()`.
Mirrors .NET `session.UI`, Python `session.ui`, Go `session.UI()`.
Wire-method names unchanged. Per stephentoub on
https://github.com/github/copilot-sdk/pull/1164#discussion_r3161986035
and the bot duplicate at #discussion_r3163241529.
- Typed `Client::get_status` and `Client::get_auth_status` returns:
introduce `GetStatusResponse { version, protocol_version }` and
`GetAuthStatusResponse { is_authenticated, auth_type, host, login,
status_message }`, both `#[non_exhaustive]`. Matches Node / Go /
Python typed shapes. Per the bot at
https://github.com/github/copilot-sdk/pull/1164#discussion_r3162629019.
- `Session::set_model` now returns `Result<(), Error>` instead of
`Result<Option<String>, Error>`. Mirrors Node / Python / Go / .NET
void-on-success semantics. Per the bot at
https://github.com/github/copilot-sdk/pull/1164#discussion_r3162816817.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Type MessageOptions::mode as DeliveryMode enum
Promote MessageOptions::mode from Option<String> to Option<DeliveryMode>,
where DeliveryMode is a #[non_exhaustive] enum with Enqueue (default) and
Immediate variants serializing to "enqueue" / "immediate". Mirrors Node
types.ts:1519 (mode?: "enqueue" | "immediate") and Go types.go:858
(MessageOptions.Mode is the message delivery mode (default: "enqueue")).
The prior rustdoc on MessageOptions::mode incorrectly described the field
as a permission mode. The field has always controlled message delivery
relative to in-flight session work; the doc lied. Strings other than
"enqueue" / "immediate" were silently no-ops at the CLI, so the typed enum
turns wrong-string-at-call-site from a runtime nothing-happens into a
compile error.
Precedent for typed enums on enumerated CLI knobs: B.2 LogLevel
(c4132c2), 4.4 OtelExporterType (aefb108), SessionFsConventions, and
DirEntryKind. The asymmetric request_headers (4.5) stayed
HashMap<String, String> because that value space is open; mode is finite
and CLI-controlled.
with_mode signature shrinks from impl Into<String> to DeliveryMode and
the wire injection in session.rs uses serde_json::to_value(m). Added a
roundtrip test in types.rs covering both variants.
CHANGELOG entry under Configuration parity.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Default permission-flow flags to Some(true)
SessionConfig::default() and ResumeSessionConfig::new() now set the four
permission-flow flags to Some(true):
- request_user_input
- request_permission
- request_exit_plan_mode
- request_elicitation
Mirrors Node's client.ts:749-751 which always sets requestPermission: true
and derives requestUserInput/requestElicitation from handler presence. In
Rust, SessionHandler is trait-based so a handler is always installed
(DenyAllHandler is the default) — opt-in defaults of None meant a Rust
caller could install a SessionHandler and forget to flip the flags,
silently breaking permission flow vs. the equivalent Node code.
The default DenyAllHandler refuses all permission requests so the wire
surface is safe out-of-the-box. Callers that want the wire surface fully
disabled set the flags explicitly to Some(false).
SessionConfig drops its derived Default in favor of a manual impl that
enumerates every field. ResumeSessionConfig::new() switches the four
field assignments from None to Some(true). Field rustdoc updated on each
of the four fields to call out the default. Two unit tests added in
types.rs covering both constructors.
CHANGELOG entry under Configuration parity.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Mark remaining public config types non_exhaustive
Adds #[non_exhaustive] to the 10 remaining public configuration types
that didn't already carry the attribute:
- SessionConfig
- ResumeSessionConfig
- ClientOptions
- ProviderConfig
- McpServerConfig
- Tool
- CustomAgentConfig
- InfiniteSessionConfig
- SystemMessageConfig
- ConnectionState
HookEvent, HookOutput, MessageOptions, TelemetryConfig, SessionFsConfig,
FsError, FileInfo, DirEntry, ToolInvocation, Error, Transport, and the
new DeliveryMode were already marked. Closing the asymmetry now means
adding fields to any of these post-1.0 is non-breaking on consumers that
construct via Default::default() plus field assignment or the with_*
builders.
Tradeoff: external crates can no longer use struct-literal syntax for
these types -- not even with ..Default::default(), which only works
inside the defining crate. Tests, examples, and the tool_parameters
doctest are migrated to the let-mut + field-assignment pattern. Callers
porting from 0.1.0-* will see the same compile error and apply the same
mechanical transform.
CHANGELOG entry under Configuration parity.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix InputOptions doc-link to SessionUi::input
The UI-grouping commit f4aa8d9 moved input/select/confirm/elicitation off
Session into SessionUi, but the rustdoc on InputOptions still referenced
the old crate::session::Session::input path. Cargo doc with
-D rustdoc::broken_intra_doc_links rejected the link.
Repoints the link to crate::session::SessionUi::input.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Drop cross-SDK comparisons from Rust source comments
Per-symbol "Mirrors Node's Foo / Go's Bar / Python's Baz" rustdoc is
unscalable and drifts as the other SDKs evolve. The Rust SDK seeks parity
with Node/Python/Go/.NET; that fact is now stated once at the top of
rust/README.md, and intentional divergences live in the README's
"Differences From Other SDKs" section.
- Strip "Mirrors X" / "Unlike Y" / cross-SDK file:line citations from
rustdoc across lib.rs, session.rs, types.rs, session_fs.rs,
trace_context.rs. Replace with API-shape descriptions where the dropped
text carried real information (e.g. wire-string variants).
- README: add a one-line parity statement up top that points to the
existing Differences section.
- Update .github/skills/rust-coding-skill/SKILL.md to forbid cross-SDK
references in code comments and rustdoc going forward, with explicit
guidance that intra-SDK self-references ("Mirrors `from_streams`") are
still fine.
cargo doc -D warnings clean. cargo clippy clean. cargo fmt clean.
18 doctests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Move SessionFs ADR out of public crate
ADR 0001 (SessionFsProvider trait and plumbing) captured the design
rationale for choosing direct Arc<dyn SessionFsProvider> registration
over the factory-closure pattern that Node/Python/Go use. That
rationale is internal release-engineering history, not consumer-facing
API documentation; the README's "Differences From Other SDKs" section
already explains what consumers need to know about the divergence.
The full ADR is being relocated to the private SDK release-plan
documentation in github/github-app PR #3166 (docs/copilot/2026-04-14-sdk-release/).
- Delete rust/docs/ entirely (only contained ADR 0001 + index README).
- Strip ADR links from rustdoc, README, CHANGELOG, and the
session_fs example. The README's existing inline rationale (factory
pattern doesn't cleanly express in Rust at the session-config call
site, no `Session` value to thread in, SDK already prefers traits
over closures for handler-shaped APIs) is sufficient on its own.
cargo doc -D warnings clean. cargo clippy clean. cargo fmt clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix SessionUi::elicitation wire field name
The hand-authored `SessionUi::elicitation` convenience layer was
sending the JSON Schema payload as `"schema"` on the wire, but the
`session.ui.elicitation` request shape expects `"requestedSchema"`.
This is the field name in:
- this crate's own generated UIElicitationRequest type
(rust/src/generated/api_types.rs:1721, serde renames to
`requestedSchema` via #[serde(rename_all = "camelCase")])
- the generated typed RPC wrapper SessionRpcUi::elicitation
(rust/src/generated/rpc.rs:1245-1257), which is correct
- and the same wire field used by every other SDK we ship
So every elicitation call from the SessionUi convenience layer was
effectively dead — the CLI saw a missing required `requestedSchema`
field. The `confirm` / `select` / `input` helpers all delegate to
`elicitation`, so they were dead too.
The mock-server test for elicitation round-tripped through the same
misnamed field on both ends, so the bug slipped past unit tests
(`assert_eq!(request["params"]["schema"], schema)` matched the buggy
implementation). The fix is a one-line rename in session.rs plus a
test update that now asserts on `requestedSchema` and explicitly
rejects a stray `schema` key, so we can't regress the same way.
207 tests pass. doc / clippy / fmt clean.
Caught by the gap-analysis re-run before 0.1.0 cut. Wire-shape
divergence between hand-authored and generated layers — same class
of bug as the workspace RPC fix earlier in this stack.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add typed on_auto_mode_switch handler for rate-limit recovery
Wires the CLI's `autoModeSwitch.request` JSON-RPC callback (added by
copilot-agent-runtime PR #7024 — github/copilot-agent-runtime#7024) into
the SDK's typed handler surface. When an eligible rate limit is hit,
the runtime asks the SDK whether to switch the session to auto model;
this commit gives consumers a typed entry point for that prompt
matching the shape of the existing exit-plan-mode / user-input /
elicitation handshakes.
Background: the github-app's vendored copilot-sdk crate has carried a
private version of this handler since Apr 27 (github-app commit
02ec73588). The plumbing was never upstreamed because no other SDK had
a typed handler for it, and the wire-protocol gap on the runtime side
hadn't yet closed. With #7024 merged, the wire path is well-defined
(`autoModeSwitch.request` → `{ response: "yes" | "yes_always" | "no" }`)
and the typed handler can ship.
Cross-SDK divergence: typed handler exists only in the Rust SDK as of
0.1.0. Node, Python, Go, and .NET observe the request as a raw
JSON-RPC callback today; parity ports for those SDKs are post-release
follow-up work and are noted in the README's "Rust-only API" section
and in the changelog.
Wire shape and types:
- `handler::AutoModeSwitchResponse` — typed enum (`Yes`, `YesAlways`,
`No`) with `#[serde(rename_all = "snake_case")]`. Wire values are
byte-identical to the runtime's schema. `#[non_exhaustive]` so future
variants are additive. Ships as a typed enum rather than the
`{ response: String }` shape used in the github-app vendored copy —
consistent with the recently-landed `DeliveryMode` enum and the
general convention that finite, enumerated wire values get typed at
the API surface (LogLevel, OtelExporterType, ConnectionState,
PermissionRequestKind, etc.).
- `handler::HandlerEvent::AutoModeSwitch { session_id, error_code,
retry_after_seconds }` — new event variant. `error_code` is the
rate-limit kind (e.g. `user_weekly_rate_limited`). `retry_after_seconds`
is the RFC 9110 `Retry-After` `delta-seconds` value when the runtime
knows it; consumers can surface a humanized reset time alongside the
prompt.
- `handler::HandlerResponse::AutoModeSwitch(AutoModeSwitchResponse)` —
new response variant.
- `SessionHandler::on_auto_mode_switch` — new trait method with safe
default (`No`). Default `on_event` dispatches to it.
- `types::SessionConfig::request_auto_mode_switch: Option<bool>` and
`types::ResumeSessionConfig::request_auto_mode_switch: Option<bool>`
— new opt-in flags, both default to `Some(true)` via
`SessionConfig::default()` and `ResumeSessionConfig::new()` to match
the convention already established by the other four `request_*`
flags. Without the flag the runtime doesn't dispatch the callback.
- `session.rs` `handle_request` — new arm for `autoModeSwitch.request`
alongside the existing `permission.request` / `userInput.request` /
`exitPlanMode.request` cases. Extracts `errorCode` /
`retryAfterSeconds` from params, dispatches via
`handler.on_event(HandlerEvent::AutoModeSwitch { ... })`, serializes
the typed response back as `{ "response": "yes" | "yes_always" |
"no" }`. Falls through to `No` if the handler returns an unexpected
response variant.
Tests:
- `auto_mode_switch_dispatches_to_handler_and_serializes_response` —
asserts the inbound `autoModeSwitch.request` reaches the typed
handler with the expected `error_code` / `retry_after_seconds`, and
that returning `AutoModeSwitchResponse::YesAlways` serializes as
`{ "response": "yes_always" }` on the wire.
- `auto_mode_switch_default_handler_replies_no` — asserts the trait's
default impl replies `{ "response": "no" }` when no override is
provided.
- Updated `session_config_default_enables_permission_flow_flags` and
`resume_session_config_new_enables_permission_flow_flags` to assert
the new flag is `Some(true)` by default.
Documentation:
- README: new bullet under "Rust-only API" section.
- CHANGELOG: new entry under `### Added > Handlers + helpers`
documenting the wire path, the typed enum, and the cross-SDK
divergence with a note that Node / Python / Go / .NET parity is
post-release work.
- `request_auto_mode_switch` field rustdoc cross-references
`SessionHandler::on_auto_mode_switch`.
Validation:
- 209 tests pass (was 207 — 2 new auto-mode-switch tests).
- `cargo doc -D warnings` clean.
- `cargo +nightly-2026-04-14 fmt --check` clean.
- `cargo clippy --all-features --all-targets -- -D warnings` clean.
Migration note for the github-app sync session: the typed
`AutoModeSwitchResponse` enum replaces github-app's vendored
`AutoModeSwitchResponse { response: String }`. github-app's WebSocket
relay continues to receive the user's choice as a `String`; map to the
typed enum at the boundary
(`match s.as_str() { "yes" => Yes, "yes_always" => YesAlways, _ => No }`).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix Client::list_sessions wire shape — wrap filter under params.filter
The hand-authored `Client::list_sessions` was serializing the optional
`SessionListFilter` directly onto the JSON-RPC `params` object,
flattening fields like `repository` / `branch` / `cwd` / `gitRoot` to
the top level. The `session.list` request shape that the runtime accepts
puts the filter under `params.filter` — and that's what every other SDK
sends:
- Node `nodejs/src/client.ts:1178-1180`: `sendRequest("session.list", { filter })`
- Go `go/types.go`: `listSessionsRequest { Filter *SessionListFilter }`
- Python `python/copilot/client.py:1907-1911`: `payload["filter"] = ...`
- .NET `dotnet/src/Client.cs`: `record ListSessionsRequest(SessionListFilter? Filter)`
Because the runtime silently ignores unknown top-level keys on
`session.list`, calling `list_sessions(Some(filter))` was functionally
equivalent to `list_sessions(None)` in 0.0.x — every filter field was
discarded by the runtime, returning an unfiltered session list. No
runtime error, no log, just silently broken. Functionally dead on the
wire, same class as the elicitation `requestedSchema` fix in `c58e2f2`.
The mock-server test `list_sessions_serializes_typed_filter` asserted on
the flat shape it observed (`request["params"]["repository"]`) rather
than the schema's wrapped shape, so the bug round-tripped through both
ends — the implementation produced the wrong shape, the test verified
the wrong shape. Same root cause as the elicitation test gap.
Fix:
- `Client::list_sessions` now wraps the filter: `Some(f) ->
serde_json::json!({ "filter": f })`, `None -> serde_json::json!({})`.
`None` omits the filter key entirely (matches Go's `omitempty`
behavior; Node's `{ filter: undefined }` also omits via JSON-stringify).
- Mock-server test now asserts on the wrapped path
(`params.filter.repository`, `params.filter.branch`) AND explicitly
asserts the flattened fallback is gone (`params.get("repository")`
must return `None`). Same regression-prevention pattern as the
elicitation fix at `session_test.rs:1248-1251`.
- CHANGELOG entry under `### Fixed` documenting the wire-shape fix and
the test gap that masked it.
Validation:
- 209 tests pass (no count change — same test, stricter assertions).
- `cargo doc -D warnings` clean.
- `cargo +nightly-2026-04-14 fmt --check` clean.
- `cargo clippy --all-features --all-targets -- -D warnings` clean.
Caught by the gap-analysis structural-correctness pass walking every
hand-authored `client.call("...")` site against the schema and the four
other SDKs. This is the second wire-shape bug found by that pass; the
first was the `SessionUi::elicitation` `schema` -> `requestedSchema`
fix in `c58e2f2`. The audit confirms `session.list` is the only other
new bug — three Rust-unique surfaces (`session.respondToUserInput`,
`session.sendTelemetry`, top-level `sendTelemetry` /
`server.sendTelemetry`) are uncheckable cross-SDK and queued as
post-0.1.0 runtime-acceptance verification.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Bump @github/copilot pin to ^1.0.39 + regen Rust types
Per the auto-mode-switch CLI hunt session's verification, the
`@github/copilot@1.0.39-0` pin we were on does NOT contain
copilot-agent-runtime PR #7024 (auto-mode-switch wire support).
The runtime PR merged after `1.0.39-0` was cut as a release-candidate
and shipped in `1.0.39` final, with `1.0.40-0` after.
This commit:
- Bumps `nodejs/package.json` from `^1.0.39-0` to `^1.0.39` (final).
`npm install` resolved to `1.0.39`. The package-lock.json is
regenerated accordingly.
- Regenerates Rust t…
Adds a Rust SDK alongside the existing Node, Python, Go, and .NET SDKs in this repo. Same JSON-RPC client model, same protocol, same session lifecycle — just in Rust.
Important
Technical preview. This is published as
github-copilot-sdk = "0.1"(pre-1.0) and the public API is subject to breaking changes as we iterate. Pin to an exact version, expect churn, and please file issues for friction or missing parity.See
rust/README.mdfor the full overview, examples, and the build/test commands. Generated types follow the same schema-driven flow used by the other SDKs (scripts/codegen/rust.ts).CI for the new crate runs in
.github/workflows/rust-sdk-tests.yml.