| Update SDK snapshot for Copilot CLI 1.0.102-1 | 4 天前 |
| Update SDK snapshot for Copilot CLI 1.0.99-1 | 10 天前 |
| Update SDK snapshot for Copilot CLI 1.0.100-0 | 8 天前 |
| Update SDK snapshot for Copilot CLI 1.0.92-0 | 23 天前 |
| Update SDK snapshot for Copilot CLI 1.0.104-0 | 1 天前 |
| Update SDK snapshot for Copilot CLI 1.0.103-0 | 3 天前 |
| Add in-process FFI transport for Rust SDK (#1915) | 3 个月前 |
| Add Rust SDK (technical preview) (#1164) * 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 types via `cd scripts/codegen && npm run generate:rust` to pick up the new schema fields. Surfaced four additive fields: - `SessionErrorData.eligible_for_auto_switch: Option<bool>` — set on `errorType: "rate_limit"` to signal the runtime will follow with an `auto_mode_switch.requested` event. UI clients can suppress duplicate rendering of the rate-limit error when they show their own auto-mode-switch prompt. - `SessionErrorData.error_code: Option<String>` — fine-grained upstream provider error code (`"user_weekly_rate_limited"`, `"integration_rate_limited"`, etc.). - `SessionModelChangeData.cause: Option<String>` — `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch recovery path. UI clients can render contextual copy. - `AutoModeSwitchRequestedData.retry_after_seconds: Option<f64>` — seconds until the rate limit resets, when known. All four are additive on existing struct types (`#[non_exhaustive]` was already in place via the codegen `#[serde(deny_unknown_fields)]`- free pattern, so additions are non-breaking on the deserialize side). - Reformats `rust/src/generated/rpc.rs` use-import (codegen output; fmt picked up the consolidated nested-imports form). - CHANGELOG entry under `### Added > Handlers + helpers` documenting the four new fields and clarifying that the request-callback `retry_after_seconds` parameter on `on_auto_mode_switch` keeps its `Option<u64>` HTTP `Retry-After` `delta-seconds` semantics distinct from the notification-event field's regenerated `Option<f64>` shape. The handler.rs / session.rs auto-mode-switch path from `a7c8215` is unchanged — that hand-authored layer doesn't depend on these specific generated types (the request-callback path is parsed manually in `session.rs:handle_request`). The bump confirms the wire path is real and our 0.1.0 ships against a runtime that actually implements the callback. Validation: - 209 tests pass (no count change — additive schema fields, no test impact). - `cargo doc -D warnings` clean. - `cargo +nightly-2026-04-14 fmt --check` clean. - `cargo clippy --all-features --all-targets -- -D warnings` clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CI: rename scenarios crate ref + nightly-fmt regen + non_exhaustive migration Three CI failures on PR #1164, all caught by green-light gating: 1. **`Verify generated files are up-to-date` (codegen-check workflow).** The Rust codegen (`scripts/codegen/rust.ts`) runs stable `rustfmt` on its output, which leaves `use super::api_types::rpc_methods;` and `use super::api_types::*;` as separate lines. Our committed canonical form is the consolidated `use super::api_types::{rpc_methods, *};` produced by nightly rustfmt with `imports_granularity = "Module"` (`rust/.rustfmt.nightly.toml`). The codegen-check workflow only installed stable, so its regen step produced the unconsolidated form and the diff check failed. Fix: install nightly rustfmt in the codegen-check workflow and run `cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml` on the Rust crate after `npm run generate`. Mirrors the same step the rust-sdk-tests workflow already runs for fmt --check, so the two CI surfaces converge on the same canonical form. 2. **`Rust scenarios` (scenario-builds workflow) — package name.** 18 scenarios under `test/scenarios/**/rust/` referenced the SDK as `copilot-sdk = { path = ... }` and used `use copilot::` in `main.rs`. The crate was renamed to `github-copilot-sdk` in `b136d3f` (with import path `github_copilot_sdk::`), but the scenarios were not updated alongside that rename. CI was failing with `error: no matching package named 'copilot-sdk' found`. Fix: rename in 18 `Cargo.toml` files (`copilot-sdk =` → `github-copilot-sdk =`) and 18 `main.rs` files (`use copilot::` → `use github_copilot_sdk::`, all `copilot::Foo` → `github_copilot_sdk::Foo`). 3. **`Rust scenarios` — `#[non_exhaustive]` cross-crate construction.** After fixing the package name, scenarios still failed with E0639: `cannot create non-exhaustive struct using struct expression`. The `cd3436f` non_exhaustive sweep blocks struct-literal syntax for external crates, including the `..Default::default()` functional- update form. Same migration we already did for `tests/` and `examples/` in `cd3436f`; the scenarios were a forgotten third external surface. Fix: migrate all 18 scenarios from struct-literal form to `let mut x = T::default(); x.field = value;` pattern. Affects `ClientOptions`, `SessionConfig`, `ResumeSessionConfig`, `SystemMessageConfig`, `Tool`, `CustomAgentConfig`, `ProviderConfig`, `InfiniteSessionConfig`, `ToolInvocation`. Inline nested struct literals (e.g. `SystemMessageConfig` inside `SessionConfig`) extracted to their own let-bindings. Builder chains (`.with_handler(...).with_hooks(...)`) preserved by terminating the let-mut-assign sequence with a re-binding before the chain. Drive-by fix to `tools/mcp-servers`: the original scenario assigned a `serde_json::json!({...})` value to `SessionConfig::mcp_servers`, which is typed `Option<HashMap<String, McpServerConfig>>`. The literal-only construction was a latent type error that must have only worked because the scenario was not previously built against the current SDK. Rewrote to construct a real `HashMap<String, McpServerConfig>` using `McpServerConfig::Stdio(McpStdioServerConfig { ... })`. 4. **Drive-by: `package: copilot-sdk` → `github-copilot-sdk` in `rust-sdk-tests.yml`'s `cargo-semver-checks-action`.** The action would have looked up the wrong package name on crates.io post-publish (currently silent because `continue-on-error: true` for the baseline-establishing pre-1.0 cycle, but worth fixing now so the post-publish flip-to-blocking just works). Validation: - 209 SDK tests still pass, doc -D warnings clean, fmt --check (nightly) clean, clippy --all-features --all-targets -- -D warnings clean. - All 18 scenarios build cleanly via the same loop CI runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use "GitHub Copilot CLI" consistently in user-facing docs Audit + replace bare "Copilot CLI" with "GitHub Copilot CLI" across the public-facing surface of the Rust SDK. Per branding consistency: the canonical name is "GitHub Copilot CLI"; "Copilot CLI" alone reads as informal shorthand. 22 occurrences across 10 files: - rust/CHANGELOG.md (2): release-notes prose. - rust/build.rs (2): cargo:warning + curl-required panic message. - rust/README.md (2): tagline + RPC namespace section. - rust/examples/chat.rs (1): example doc-comment. - rust/src/session.rs (1): `Session` rustdoc. - rust/src/types.rs (4): module rustdoc + skill_directories field rustdocs on SessionConfig and ResumeSessionConfig. - rust/src/resolve.rs (1): copilot_binary error hint. - rust/src/lib.rs (6): module-level rustdoc, ClientOptions field rustdocs, ConnectionState enum rustdoc. - rust/src/trace_context.rs (2): module rustdoc + TraceContext type rustdoc. - rust/src/session_fs.rs (1): SessionFsConventions rustdoc. The README's `# GitHub Copilot CLI SDK for Rust` title was already correct; the tagline beneath it ("A Rust SDK for programmatic access to the GitHub Copilot CLI.") was already correct as well — only the secondary mentions in the body needed updating. Test scenario READMEs under `test/scenarios/` are NOT updated: they contain ASCII diagrams with width-constrained boxes that would mis- align with the longer "GitHub Copilot CLI" string, and they're cross-language docs shared across all SDKs (Node, Python, Go, .NET, Rust) — out of scope for this Rust SDK release prep. Validation: cargo doc -D warnings clean, cargo fmt --check clean, cargo clippy clean, 209 tests pass (no semantic changes, only doc strings). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review: add prompts/attachments scenario + README parity sections Closes the two `github-actions` bot findings on PR #1164 that were the only remaining unresolved threads after the human-review cluster. **`prompts/attachments` Rust scenario** (PR comment 3164480775) The bot flagged that `prompts/attachments` was missing from the Rust coverage table — implemented in TS / Python / Go / .NET, full attachment support already in the Rust SDK (`Attachment::File` / `Blob` / `Selection`, `MessageOptions::with_attachments`). Accidental omission from the rest of the Rust port, not a deliberate deferral. - `test/scenarios/prompts/attachments/rust/{Cargo.toml, src/main.rs}` added, mirroring the existing four-language implementations (`claude-haiku-4.5`, replace-mode system prompt, single-file attachment from `sample-data.txt`, prompt asks "what languages are listed in the attached file?", verify.sh greps for TypeScript|Python|Go in the response). - `test/scenarios/RUST_COVERAGE.md`: new ✅ row. - `test/scenarios/prompts/attachments/README.md`: new Rust syntax row in the File and Blob attachment tables. - `test/scenarios/prompts/attachments/verify.sh`: Rust build + run steps wired into both phases. `cargo build` clean locally. **README parity sections** (PR comment 3164686206) The bot flagged that the Rust README has good coverage of the core API but is missing dedicated sections that exist in the Go / Python / .NET READMEs for several features the Rust SDK fully implements. None of these are implementation gaps — discoverability gaps for readers coming from other SDKs. Six new brief sections added to the API Reference between "Capabilities & Elicitation" and "Progress Reporting", each with a runnable code example: - **Slash Commands** — `CommandHandler` + `CommandDefinition::new` with the `name` / `description` wire-only contract called out. - **User Input Requests** — `SessionHandler::on_user_input` override example with the `None`-falls-back-to-CLI semantics. - **Streaming** — `streaming: true` + delta-event match loop reading the `delta` field from `event.data` (verified against the actual `SessionEvent` shape). - **Infinite Sessions** — `InfiniteSessionConfig::workspace_path`, compaction event names, resume-via-`Client::resume_session`, default state location at `~/.copilot/session-state/{sessionId}`. - **Custom Providers (BYOK)** — `ProviderConfig.provider_type` / `base_url` / `bearer_token` / `headers`, with the `openai`/`azure`/`anthropic` provider types and the `wire_api: "completions" | "responses"` flag. - **Telemetry** — `TelemetryConfig` + `OtelExporterType::OtlpHttp`/ `File` with the env-var injection note and the no-OpenTelemetry-dependency guarantee. All API shapes verified against the actual source. Caught one shape-mismatch while drafting (initial draft had `provider.kind: Some(...)` with `base_url: Option<String>`; actual API is `provider.provider_type: Option<String>` + `base_url: String`) — corrected in the final draft. cargo doc -D warnings clean, fmt --check clean, clippy clean, 209 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add get_model and send_telemetry to Rust-only API list Bot review on PR #1164 (comment 3164786116) caught two more accidental omissions in the README's 'Rust-only API' section. Both methods are real and exposed; just missing from this inventory: - Session::get_model — wraps session.model.getCurrent. Folded into the existing 'First-class Session convenience methods' bullet. - Client::send_telemetry / Session::send_telemetry — wrap sendTelemetry / session.sendTelemetry. Added a new dedicated bullet because no other SDK exposes these RPC endpoints in their public API at all (not even via typed namespaces), so it's a higher-divergence item worth its own line. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix update-copilot-dependency: format Rust generated output The CLI-version-bump workflow regenerates per-language types via \`scripts/codegen/npm run generate\` and then formats the output. The formatting step covered TypeScript (prettier) and .NET (dotnet format) but not Rust, so future CLI bumps would land unformatted generated code that codegen-check.yml then catches as a diff. Mirrors the fix already applied to codegen-check.yml in 2766a79: install nightly rustfmt alongside stable, then run \`cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml\` on the rust crate after generation. Same toolchain pin, same config — keeps the two CLI-codegen surfaces converging on the same canonical output. Caught by SDK + CLI version coordination research session. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Client::get_status and Client::get_auth_status wire method names Two more wire-shape bugs in the same class as `c58e2f2` (elicitation `requestedSchema`) and `64541af` (list_sessions filter wrapping): hand-authored RPC wrappers calling JSON-RPC methods that don't exist on the CLI runtime, masked by mock-server tests asserting the same wrong names the implementation sent. The CLI runtime (verified via `@github/copilot@1.0.39` app.js) registers two top-level RPCs: - `status.get` (returns version + protocol_version) - `auth.getStatus` (returns is_authenticated + auth_type) Every other public SDK calls those exact names: - Node `nodejs/src/client.ts:980, 992` - Go `go/client.go:1239, 1257` - Python `python/copilot/client.py:1806, 1827` - .NET `dotnet/src/Client.cs:718, 732` The Rust hand-authored layer was sending `getStatus` and `getAuthStatus` — names that don't exist on the wire. Result: both methods would have returned a method-not-found error (or a misleading no-such-method log on the CLI side) instead of the expected payload. The schema doesn't currently typed-define these top-level RPCs (only `session.auth.getStatus` is in the schema, a different namespaced endpoint), so the codegen didn't catch the hand-authored layer's drift. The mock-server test `client_rpc_methods_send_correct_method_names` asserted on the same wrong names the implementation used, so the bugs round-tripped through both ends. Same root cause as the elicitation and list_sessions tests. Fix: - `lib.rs:1301`: `"getStatus"` → `"status.get"` - `lib.rs:1308`: `"getAuthStatus"` → `"auth.getStatus"` - `tests/session_test.rs:488-509`: assertions migrated to the canonical names AND new explicit-absence assertions (`assert_ne!(request["method"], "getStatus")` / `"getAuthStatus"`) so the same regression class can't be reintroduced silently. Doc-comment on the test names the source- of-truth references (CLI runtime registration, four-SDK call sites). - `CHANGELOG.md`: new `### Fixed` entry documenting the wire-name 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 github-actions bot on PR #1164 (comments 3164826435 and 3164826441). Verify-before-drafting tally now 14 / 15 for these two saves. This brings the wire-shape audit save count to 4 (elicitation, list_sessions, get_status, get_auth_status) — all four were hand-authored RPC wrappers asserting on the implementation's wrong shape rather than the schema/runtime's expected shape. The structural fix (regression-prevention asserts in tests) is now in place for all four; the same bug class is no longer silently reproducible. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refactor JsonRpcClient writer to actor pattern (cancel-safety) Closes RFD-400 review findings #1 (writer mutex desync hazard) and #4 (send_request pending-entry leak on cancellation). See cancel-safety review session db4b1ac8-... for the full report. The previous design held a `tokio::sync::Mutex<Box<dyn AsyncWrite>>` across `write_all` + `flush` for Content-Length-framed JSON-RPC messages. Caller cancellation mid-frame (e.g. wrapping `Session::send` or `Client::call` in `tokio::time::timeout` or `select!`) could leave a partial frame on the wire, permanently desyncing the transport for the rest of the connection. The reader on the CLI side would block forever waiting for the missing bytes, and the next outbound message would be interpreted as the tail of the previous body. Fix: writer-actor task fed by an `mpsc::UnboundedSender<WriteCommand>`. Public `write` and `send_request` pre-serialize the body, enqueue the frame + an ack `oneshot`, and await the ack. Caller cancellation drops the ack receiver but the actor still completes the in-flight write — the frame either lands atomically or surfaces an io::Error to the (possibly already-dropped) ack receiver. A partial frame can never appear on the wire. Design notes: - Pre-serialize before enqueue: actor receives `Vec<u8>` (header + body, already concatenated). Serde errors fail synchronously at enqueue time before touching the actor — cleanest cancel-safety story. - Unbounded sender: RFD 400 explicitly permits this for cancel-safety, and the producer-consumer ratio is naturally bounded by request/ response semantics on the wire. - Writer actor lives until the JsonRpcClient drops. Channel closes on Drop -> actor sees recv None -> flushes and exits. `pending_requests` cleanup (#4): added a small `PendingGuard` RAII helper in `send_request` that arms after the map insert and disarms before the success return. Caller cancellation drops the guard, which removes the pending-entry synchronously via parking_lot::RwLock (the map mutex is converted in this commit too — it was never held across .await anyway, so the conversion is a pure cleanup). Mutex conversions in this commit: - `JsonRpcClient::pending_requests`: `tokio::sync::RwLock` -> `parking_lot::RwLock`. Lock is never held across .await; conversion is also what enables the synchronous PendingGuard drop. - The previous `Arc<Mutex<Box<dyn AsyncWrite>>>` is gone entirely. The actor task owns the writer directly with no shared synchronization. Tests: two new regression tests in tests/jsonrpc_test.rs - `write_actor_completes_on_caller_cancel`: tiny duplex (8 bytes) + gated reader so the actor's write_all blocks waiting for room. Race the caller's future against a sleep; sleep wins, future is dropped while suspended on ack_rx.await. Then enqueue a second write and release the reader. Both frames must land on the wire intact, in order, with no partial frame from the cancelled write. - `send_request_cancellation_does_not_leak_pending`: cancel an in-flight request, then send a (late) response for the cancelled id to verify the read loop logs and discards it without colliding, then send a fresh request and verify it works normally. Existing 209 tests continue to pass without modification — the wire semantics are unchanged. Total now 211. Validation: cargo test, cargo doc -D warnings, cargo +nightly fmt --check, cargo clippy --all-features --all-targets -- -D warnings all clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add WaiterGuard RAII for Session::send_and_wait (cancel-safety) Closes RFD-400 review finding #2 (idle_waiter slot leak on external cancellation). See cancel-safety review session db4b1ac8-... for the full report. `Session::send_and_wait` installs an `IdleWaiter` slot under `self.idle_waiter`, then awaits the response. Internal failure paths (send_inner error, oneshot closure, internal timeout) clean up the slot. External cancellation — caller wraps `send_and_wait` in `tokio::time::timeout`, races it in `select!`, or aborts the JoinHandle — does not. The cleanup at the event loop's `else` branch only fires when the channel itself closes (i.e., the entire session is going away), not when an individual call is cancelled. Effect: any external cancellation between "install waiter" and "drain response" leaves `idle_waiter = Some(...)` forever. All subsequent `send` and `send_and_wait` calls return `SendWhileWaiting`, permanently bricking the session. Fix: `WaiterGuard` RAII helper that takes the slot on Drop. Construct right after install, let RAII handle every exit path. The explicit `self.idle_waiter.lock().take()` calls inside the function disappear, and the slot is cleared on every cancellation path automatically. Mutex conversion (folded in from finding #5): - `Session::idle_waiter`: `tokio::sync::Mutex<Option<IdleWaiter>>` -> `parking_lot::Mutex<Option<IdleWaiter>>`. Lock is never held across `.await` in any code path; the conversion is what makes WaiterGuard's synchronous Drop work without needing an async-spawn fallback. `event_loop` mutex stays `tokio::sync::Mutex` for now — commit C converts it as part of cooperative shutdown (so the conversion lands alongside the matching change to Drop for Session). Tests: two new regression tests in tests/session_test.rs - `send_and_wait_outer_cancellation_clears_waiter`: outer `tokio::time::timeout(50ms, ...)` around `send_and_wait` with a 60-second inner timeout. Outer fires, dropping the future. Verify the next `send` succeeds (no SendWhileWaiting). - `send_and_wait_drop_clears_waiter`: explicit `JoinHandle::abort` of an in-flight send_and_wait. Verify the next `send` succeeds. Existing 211 tests continue to pass; total now 213. Validation: cargo test --all-features, cargo doc -D warnings, cargo +nightly fmt --check, cargo clippy -- -D warnings all clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Cooperative event-loop shutdown via Notify (cancel-safety) Closes RFD-400 review finding #3 (JoinHandle::abort lands at any await point in the event loop body) and the remaining bit of #5 (Session::event_loop tokio::sync::Mutex -> parking_lot::Mutex). See cancel-safety review session db4b1ac8-... for the full report. The previous design called `JoinHandle::abort()` in two places to stop the event-loop task: explicitly in `Session::stop_event_loop` and best-effort in `Drop for Session`. RFD 400's strongest single recommendation about task aborts: > With the abort(), the future returned by do_long_running_operation() > can be cancelled at *any await point*. It is unlikely that arbitrary > async Rust code is resilient to cancellations anywhere during > control flow. ... task aborts should be avoided entirely. The event loop body has many await points: - The outer `select!` itself - `handle_notification`'s nested awaits (acquire idle_waiter lock, send on broadcast, dispatch RPC callbacks for permission/tool/ elicitation requests) - `handle_request`'s inline awaits (deserialize, dispatch handler, build response, write back to client) If `abort()` landed mid-handler — say partway through writing the permission response — the CLI would keep that requestId in flight forever. Same hazard for the idle_waiter oneshot: an abort between "observed session.idle" and "send on waiter.tx" leaves the caller's `send_and_wait` await blocked until the entire transport tears down. Fix: cooperative cancellation via `Arc<tokio::sync::Notify>`. The event-loop body adds a fourth select arm: tokio::select! { _ = shutdown.notified() => break, Some(notification) = notifications.recv() => { ... } Some(request) = requests.recv() => { ... } else => break, } `Notify::notified()` is cancel-safe per RFD 400's framework: the unselected branch losing a poll is fine — it's reconstructed on the next iteration. `notify_one()` (NOT `notify_waiters`) is used because it buffers a single signal: if the loop is currently inside `handle_request(...).await` (no `.notified()` future registered), the signal is held until the handler returns and the loop re-enters select on the next iteration. `notify_waiters()` would silently lose the signal in this case. `stop_event_loop` calls `shutdown.notify_one()` then awaits the JoinHandle. The loop drains its current iteration's handler before observing the buffered signal and exiting cleanly — no mid-protocol state, no orphaned RPC ids on the CLI side. `Drop for Session` calls `notify_one()` and unregisters from the router. The handle is left in `event_loop` for tokio to reap when the loop's spawned task exits naturally; we intentionally don't await it because Drop is sync. Spawned child tasks inside `handle_notification` (permission/tool/elicitation callbacks) intentionally outlive the parent loop — RFD 400's "spawn background tasks to perform cancel-unsafe operations" pattern, documented in the loop's comment. Mutex conversion (folded in from finding #5): - `Session::event_loop`: `tokio::sync::Mutex<Option<JoinHandle<()>>>` -> `parking_lot::Mutex<Option<JoinHandle<()>>>`. Lock is never held across `.await` and the conversion eliminates the `try_lock()` Drop footgun (silent no-op on contention with stop_event_loop). This is the last tokio::sync::Mutex on Session — the import is no longer needed. Tests: two new regression tests in tests/session_test.rs - `stop_event_loop_completes_in_flight_handler`: install a `SlowHandler` whose userInput.request callback sleeps 150ms. Send the request from the server side, then 20ms in (handler is mid- flight) call `stop_event_loop`. Verify the response with `answer: "completed"` lands on the wire BEFORE stop_event_loop returns — proves the handler ran to completion, not aborted mid-await. - `drop_session_does_not_abort_handler`: same shape but via `drop(session)` instead of `stop_event_loop`. Tracks handler completion via an AtomicBool that the handler sets only after its sleep completes; verifies both that the response lands on the wire AND that the handler actually completed despite the Session being dropped. Existing 213 tests continue to pass; total now 215. Validation: cargo test --all-features, cargo doc -D warnings, cargo +nightly fmt --check, cargo clippy -- -D warnings all clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Document cancel-safety for public async APIs (RFD-400) Closes RFD-400 review finding #6 (per-method cancel-safety documentation). Doc-only follow-up to commits A/B/C; no behavioral change. RFD 400 § "Marking APIs as cancel-unsafe" calls for explicit documentation; with the underlying behavior now correct after A/B/C, documenting the contract makes it inspectable from rustdoc rather than requiring callers to read the implementation. Added `# Cancel safety` rustdoc sections to: - `Session::send` — cancel-safe (writer-actor); message lands on the wire even if the await is cancelled. - `Session::send_and_wait` — cancel-safe (WaiterGuard RAII); outer-cancellation distinct from internal-timeout, both clear the slot. - `Session::abort` — cancel-safe (single RPC via writer-actor). - `Client::call` — cancel-safe at the wire level; documents the caveat that the CLI may still process the request even if the caller doesn't see the response (idempotent vs non-idempotent methods). - `Client::stop` — cancel-unsafe-but-recoverable; the body sequentially destroys sessions (each individually cancel-safe) before killing the child. The existing `tokio::time::timeout` example with `force_stop` fallback is the documented recovery path. - `Client::force_stop` — synchronous, cannot be cancelled. Designed as the recovery path for `stop`'s timeout case. - `Subscription::recv` — cancel-safe by virtue of `tokio::sync::broadcast` + `BroadcastStream`. Validation: cargo doc -D warnings clean, cargo test (215 pass), cargo +nightly fmt --check clean, cargo clippy clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add scheduled trigger to update-copilot-dependency.yml Closes "Mode 1 hands-off loop" gap A from the SDK release plan-doc: update-copilot-dependency.yml is workflow_dispatch only today, so when a new @github/copilot publishes nobody automatically opens the regen PR. Today's bot-driven bump (commit ecdc5ec, "Update @github/copilot to 1.0.39 (#1167)") was triggered manually after the plan-doc session noticed the gap. Goal: the SDK auto-detects new CLI versions, opens a draft PR with all per-language regen + formatting + lockfile updates, and a human just reviews + merges. github-app's bundled-binary tracker picks the new SDK up via crates.io dependabot in the symmetric direction. Changes: 1. **`schedule` trigger.** Daily cron at 10:00 UTC, deliberately symmetric with github-app's check-bundled-binary-versions.yml so the SDK and consumer halves of the loop tick in lockstep on release days. 2. **`version` input is now optional.** Manual override still works for emergency back-patches or cherry-picking specific prereleases; an empty input on workflow_dispatch flows through the same auto-detect path the schedule uses. 3. **Two-job split: `detect` -> `update`.** The detect job is cheap (one `npm view @github/copilot version` + a jq read of nodejs/package.json + a strip of the `^`/`~` prefix). It sets two outputs: - `version`: the resolved target (input override, or latest from npm) - `should_continue`: 'true' if the resolved version differs from the current pin, 'false' otherwise The update job has `needs: detect` and `if: needs.detect.outputs.should_continue == 'true'`, so it skips entirely when there's nothing to do. This keeps the no-op daily cost effectively free. 4. **Validation moves into the resolve step.** Both manual and auto-detected versions go through the same semver regex check. The previous standalone "Validate version input" step is retired; the detect job's resolve step now owns format validation, version comparison, and skip logic. 5. **All `${{ inputs.version }}` references become `${{ needs.detect.outputs.version }}`.** Same value, just routed through the detect job's outputs so manual override and auto-detect both feed the update job uniformly. Doesn't touch SDK source; cargo test / cargo publish --dry-run are no-op confirmation post-merge. Companion gap on the github-app side (auto-open sync PR after each new SDK release) lives on Sync session PR #4140; this commit unblocks the SDK half. Plan-doc capture: tracked under "Steady-state CLI bump automation (Mode 1 hands-off loop)" — gap A closed; gap B (github-app sync-PR auto-open) remains. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Correct get_quota doc: endpoint exists cross-SDK, only wrapper is Rust-only Bot review caught an inaccuracy in the rustdoc and README: the prior wording claimed the underlying account.getQuota JSON-RPC endpoint was exposed only by the Rust SDK. In fact only the top-level Client::get_quota convenience wrapper is Rust-unique; the endpoint itself is generated and exposed cross-SDK via each SDK's typed rpc() namespace. Verified cross-SDK exposure: - Node: nodejs/src/generated/rpc.ts:2203 (account.getQuota in rpc() namespace) - Python: python/copilot/generated/rpc.py:5647 (rpc().account.get_quota) - Go: go/rpc/generated_rpc.go:2252 (RPC.Account.GetQuota) - .NET: dotnet/src/Generated/Rpc.cs:3140 (Rpc().Account().GetQuotaAsync) - Rust: rust/src/generated/rpc.rs:90 (rpc().account().get_quota) Updated rustdoc on Client::get_quota in rust/src/lib.rs and the "Rust-only API" bullet in rust/README.md to correctly state that the wrapper is Rust-only while the underlying endpoint is available across all SDKs via their typed rpc() namespaces. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add ClientOptions::new() and Tool::new() builder methods Mirrors the existing MessageOptions::new() / with_* builder pattern on the two other public #[non_exhaustive] config types that consumers construct most often. Closes the cross-crate ergonomics gap that #[non_exhaustive] introduces: external callers can't use struct- literal syntax (with or without ..Default::default()) on these types, so every field assignment becomes its own mut-let statement. The builder shape collapses that into a single chained expression. Caught by github-app PR #4140 review on src-tauri/src/session/cli.rs: the mut-let-and-assign pattern read as awkward to reviewers even after trimming to the four fields that diverge from default. Same ergonomic friction that prompted the make_tool() helper consumers were writing for Tool. Builder ships zero-cost convenience without giving up #[non_exhaustive]'s SemVer protection on growable config struct. ClientOptions builder (rust/src/lib.rs): - new() -> Self (documented entry point) - with_program(impl Into<CliProgram>) - with_prefix_args<I: IntoIterator<...>>(args) - with_cwd(impl Into<PathBuf>) - with_env<I: IntoIterator<(K, V)>> - with_env_remove<I: IntoIterator<S>> - with_extra_args<I: IntoIterator<S>> - with_transport(Transport) - with_github_token(impl Into<String>) - with_use_logged_in_user(bool) - with_log_level(LogLevel) - with_session_idle_timeout_seconds(u64) - with_list_models_handler<H: ListModelsHandler + 'static> (Arc-wrapped internally) - with_session_fs(SessionFsConfig) - with_trace_context_provider<P: TraceContextProvider + 'static> (Arc-wrapped internally) - with_telemetry(TelemetryConfig) Tool builder (rust/src/types.rs): - new(impl Into<String>) -> Self (name + defaults) - with_namespaced_name(impl Into<String>) - with_description(impl Into<String>) - with_instructions(impl Into<String>) - with_parameters(serde_json::Value) - with_overrides_built_in_tool(bool) - with_skip_permission(bool) Pure additive: no existing ClientOptions::default() / mut-let patterns break, no public field visibility changes. Both builders compose by wrapping ::default() and mutating-and-returning self, matching MessageOptions's idiom. Tests: client_options_builder_composes verifies all 10 chained builder methods produce the expected internal state. tool_builder_composes does the same for Tool's seven methods, plus tool_with_parameters_handles_non_object_value covers the HashMap-from-Value extraction edge case (json!(null) -> empty map rather than panic). CHANGELOG: new "Builder ergonomics" subsection under Documentation, explaining the rationale and listing both builders. Validation: 220 tests pass (was 215 + 5 new builder tests across unit + doctest), cargo doc -D warnings clean, cargo fmt --check clean, cargo clippy --all-features --all-targets -- -D warnings clean. Companion ask from github-app PR #4140 review (Sync session) follows up: github-app's `cli.rs` and ~25 Tool::new() call sites will switch to the builder form once this lands and the next sync round-trip pulls it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add per-field builder methods to SessionConfig and ResumeSessionConfig Mirrors the just-shipped ClientOptions/Tool builder pattern (commit 2d725a5) on the two structs that consumers reach for most when wiring up a session. Both types are #[non_exhaustive], so external callers have been writing the same mut-let-and-assign block at every construction site: let mut config = ResumeSessionConfig::new(session_id); config.client_name = Some("...".into()); config.streaming = Some(true); config.tools = Some(tools); // ...10+ more lines The new chainable form collapses that to: ResumeSessionConfig::new(session_id) .with_client_name("...") .with_streaming(true) .with_tools(tools) // ... Both types have ~30 public fields each. Coverage: SessionConfig: with_session_id, with_model, with_client_name, with_reasoning_effort, with_streaming, with_tools, with_available_tools, with_excluded_tools, with_mcp_servers, with_disabled_mcp_servers, with_env_value_mode, with_enable_config_discovery, with_request_user_input, with_request_permission, with_request_exit_plan_mode, with_request_auto_mode_switch, with_request_elicitation, with_skill_directories, with_disabled_skills, with_agent, with_custom_agents, with_config_dir, with_working_directory, with_github_token, with_include_sub_agent_streaming_events, with_system_message, with_skip_compaction_history. ResumeSessionConfig: same shape minus model/reasoning_effort/ available_tools/disabled_skills/disabled_mcp_servers (which the resume RPC doesn't accept), plus with_disable_resume. Existing closure-installing chains (with_handler, with_hooks, with_transform, with_commands, with_session_fs_provider, approve_all_permissions, deny_all_permissions, approve_permissions_if) and the manual mut-let pattern continue to work unchanged. The hooks bool is intentionally not exposed via a new with_* setter; with_hooks(handler) auto-enables the flag, and power users wanting to override can still set the .hooks field directly. Adds two integration-style tests covering composition across all field categories (scalars, bool flags, Vec setters, struct setters, HashMap setters) for both types. CHANGELOG entry extends the existing "Builder ergonomics" subsection. This is the third commit in the pre-1.0 builder-pattern series (MessageOptions in the original branch, ClientOptions/Tool in 2d725a5, SessionConfig/ResumeSessionConfig here). Pure additive, zero migration cost. Driven by github-app PR #4140 reviewer feedback on src-tauri/src/session/cli.rs and core.rs construction sites. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Scrub github-app references from public-repo files The copilot-sdk repo is public; github-app is private. Public-repo content must not name or depend on github-app, even in comments or test fixtures. - .github/workflows/update-copilot-dependency.yml: drop the "symmetric with github-app's scheduled" framing on the daily cron; rewrite the comment to describe what the schedule does in SDK-local terms only. - rust/CHANGELOG.md: rephrase the Tool builder rationale to talk about "real-world consumer code" rather than github-app's tool catalog specifically. - rust/src/lib.rs: change the `source_name` test fixture from "github-app" to "my-app" — it was just an example string for OTEL source-name plumbing, but the explicit consumer naming is unnecessary and confusing in a public repo. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Round out builder coverage and document the Option<T> escape hatch After landing per-field builders on the four highest-traffic consumer-facing config structs (MessageOptions, ClientOptions, Tool, SessionConfig/ResumeSessionConfig), this commit closes the long tail. Rust ecosystem research (tokio, reqwest, tonic, axum, AWS SDK) confirms two design points: 1. The dominant pattern for #[non_exhaustive] config builders is the single `with_<field>(impl Into<T>) -> Self` setter that always Some-wraps. AWS SDK's dual `field` + `set_field(Option<T>)` shape is justified by codegen, not by ergonomics — and the `impl Into<Option<T>>` magic that some older crates use has fallen out of fashion (it conflicts with `impl Into<T>` chaining and produces poor type-inference errors). 2. Direct field assignment on a `pub` field is idiomatic Rust for forwarding `Option<T>` values from upstream code. The http::request::Parts / hyper::Body::Builder pattern. We already support it; we just hadn't documented it. This commit therefore: - Adds full builders to the four bare consumer-facing structs: CustomAgentConfig (name + prompt required, six with_* setters), InfiniteSessionConfig (three with_* setters), ProviderConfig (base_url required, six with_* setters), SystemMessageConfig (three with_* setters). - Expands TraceContext with a symmetric new() + with_traceparent pair alongside the existing from_traceparent shorthand. The shorthand is now expressed in terms of the chain (`new().with_traceparent(x)`) for consistency. - Documents the direct-field-assignment escape hatch on SessionConfig and ResumeSessionConfig, with a doc example showing how to mix the fluent chain (for compile-time-known values) with direct mutation (for Option<T> pass-throughs). Per-field `with_<x>_opt(Option<T>)` setters were considered and deliberately rejected: doubling the method count for ~30-field configs would hurt rustdoc discoverability and conflict with the single-setter precedent we just shipped. The mut-let escape hatch covers the same use case without polluting the API surface. Adds 5 unit tests (one per new builder + a TraceContext suite) and extends the existing CHANGELOG "Builder ergonomics" subsection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert "Add scheduled trigger to update-copilot-dependency.yml" This reverts commit a88c3ebc7cf25d5b7194a37b51e52bf6a62bc8fc. The scheduled trigger was added to close a hands-off-loop gap that was framed in terms of a downstream consumer's needs. On reflection, the SDK should not be the only language in this repo with scheduled auto-detect; that's a cross-language platform decision, not a single- SDK one. Reverting brings update-copilot-dependency.yml back to manual-only (workflow_dispatch with a required version input) — the same shape as every other language's CLI update workflow in this repo. When the SDK collectively decides to add scheduled CLI tracking, that should land as a unified change across all languages, not as a Rust-only special case driven by a downstream consumer's automation desires. Manual dispatch remains the supported path until then. Conflict resolution: the e93ce8e ("Scrub github-app references") commit landed after a88c3eb and rewrote the cron block's comment. The revert drops the entire schedule: block (including the rewritten comment), restoring the workflow to its pre-a88c3eb manual-only shape exactly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add TelemetryConfig builder methods Final missing piece in the consumer-facing #[non_exhaustive] config struct audit. `TelemetryConfig` had public optional fields (otlp_endpoint, file_path, exporter_type, source_name, capture_content) and an is_empty() helper, but no `new()` or `with_*` setters — leaving callers to write `TelemetryConfig { otlp_endpoint: Some(...), ..Default::default() }` which doesn't compile from outside the crate due to #[non_exhaustive]. Adds `TelemetryConfig::new()` (delegates to Default) and `with_*` setters for all five fields, matching the existing pattern. The is_empty() helper stays unchanged. After this commit, every consumer-facing #[non_exhaustive] struct in the SDK has a fluent builder: ClientOptions, TelemetryConfig, Tool, CommandDefinition, CustomAgentConfig, InfiniteSessionConfig, ProviderConfig, SessionConfig, ResumeSessionConfig, SystemMessageConfig, MessageOptions, SessionFsConfig, TraceContext. One unit test (telemetry_config_builder_composes) covering all five setters + is_empty behavior on a fresh new(). Extends the existing CHANGELOG "Builder ergonomics" round-out bullet. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Switch build.rs from curl to ureq with bounded retries The bundled-CLI build path (gated by the embedded-cli feature and the COPILOT_CLI_VERSION env var) previously shelled out to `curl` to fetch SHA256SUMS.txt and the platform tarball from the copilot-cli releases. Two problems with that: 1. Implicit system dependency. `expect("curl is required ...")` panics when curl isn't on PATH — true on minimal Windows MSVC environments, some Docker images, and the CI matrices crates.io publishers tend to validate against. crates.io build environments themselves don't guarantee curl. 2. No retry on transient errors. Network blips during CI bundling surfaced as build failures rather than self-healing waits. Switch to ureq 2 with default-features=false + ["tls"] (rustls + webpki-roots, pure-Rust TLS, no OpenSSL or native-tls). Adds about 1.7MB and ~12 transitive deps to the build graph but only when the embedded-cli feature is on; the default cargo build is unaffected. Retry policy: - Up to 3 retries (4 total attempts) with exponential backoff: 1s, 2s, 4s. - 5xx responses, connect timeouts, read timeouts, and other transport-layer errors are treated as transient and retried. - 4xx responses fail fast (the URL is wrong; retrying won't help). Connect/read timeouts (30s/120s respectively) prevent indefinite hangs on stalled connections. The cache-hit path still bypasses the network entirely; only fresh downloads pay the HTTP cost. End-to-end smoke test against https://github.com/github/copilot-cli/releases/download/v1.0.39 : SHA256SUMS download succeeded, tarball download succeeded (134MB extracted, 44MB compressed), SHA-256 verified, embedded build succeeded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: validate embedded-cli bundle build on macOS / Linux / Windows The default `cargo test` job in rust-sdk-tests.yml runs on all three supported platforms but does not set `COPILOT_CLI_VERSION` — which means `build.rs` short-circuits immediately and the embedded-cli bundle path (download + SHA-256 verify + extract + zstd compress + embed) is never exercised in CI. After the recent switch from shelling out to `curl` over to `ureq` with retry logic in `build.rs`, that gap matters: the new HTTP client + cross-platform TLS code path needs cross-platform CI coverage before it ships. Adds a parallel `bundle` job to the same workflow: - 3-OS matrix: ubuntu-latest, macos-latest, windows-latest. - Reads the pinned CLI version from `nodejs/package.json` so the bundle test always tracks the same version the rest of the SDK ships against — no separate version source to drift. - Caches the downloaded archive in `./rust/.bundled-cli-cache` keyed on OS + CLI version, so steady-state CI doesn't refetch ~130 MB on every run. Cache miss (e.g. first run after a CLI bump) exercises the full download path end-to-end, which is the regression surface this job is meant to catch. - Runs `cargo build --features embedded-cli` to drive `build.rs` through the full pipeline. Doesn't run `cargo test` — the test job already covers runtime behavior; this job is specifically validating the bundle build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix duplicate user_input dispatch (github-app#4249) The CLI sends two messages for every user-input prompt: 1. A `user_input.requested` session-event notification, intended for observers (UI streaming, telemetry, logging). 2. A `userInput.request` JSON-RPC method call, which is the actual ask-and-wait prompt that drives the handler. The Rust SDK's session event loop was wiring up `HandlerEvent::UserInput` dispatch on BOTH paths. Result: the consumer's `on_event` ran twice for every prompt, which surfaced as duplicate `ask_user` / `exit_plan` widgets in github-app's session UI (#4249). Other SDKs (Node, Python, Go, .NET) only register the JSON-RPC handler; the notification is purely observational. Aligning with that convention. Changes: 1. `session.rs`: drop the `SessionEventType::UserInputRequested` auto-dispatch arm. The CLI's follow-up `userInput.request` JSON-RPC call still drives the handler. Replaced with a short comment block explaining why the notification is intentionally a no-op (and citing #4249 so the next person tempted to add it back understands the trade). 2. `tests/session_test.rs`: replaced the `user_input_requested_event_dispatches_to_handler_and_responds` test (which asserted the broken behavior) with a regression test (`user_input_requested_notification_does_not_double_dispatch`) that: - sends a `user_input.requested` notification and asserts no wire response, no handler invocation; - then sends a `userInput.request` JSON-RPC call and asserts the handler fires exactly once. This locks the fix in place and makes a future re-introduction surface immediately as a test failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Drop Skills section from .github/copilot-instructions.md stephentoub and tclem agreed the Skills section is unnecessary prompt-pollution. Skills are auto-discovered by Copilot CLI without needing to be enumerated in the instructions, and the one-line description of each skill duplicates what the SKILL.md frontmatter already says. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Render experimental RPC methods as a styled rustdoc admonition Codegen previously emitted a single bare `/// Stability: experimental.` line on each experimental wire method. stephentoub flagged this as weak signal -- there's no way for a consumer to tell from the rustdoc that they're using something that may change. Switch the rustdoc emit to a `<div class="warning">...</div>` block, which renders on docs.rs as a yellow-bordered callout (the standard rustdoc admonition convention). Matches the visibility Python's generated docs already get via Sphinx's `.. warning::` directive. This is documentation-quality only; no compile-time opt-in. Stable Rust has no first-class `#[experimental]` attribute (`#[unstable]` is rustc-internal, std-only). If we ever want compile-time friction the ecosystem patterns are a Cargo feature gate (e.g. `tokio_unstable`- style `features = ["experimental"]`) or an `unstable` module path; both are larger codegen changes affecting every experimental RPC across all SDK surfaces. Punted for 0.1.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(skill): clarify that handler thread-safety is compiler-enforced The "anti-pattern" framing in examples.md was misleading -- it implied the RefCell example might compile but produce bugs at runtime. In reality, SessionHandler's `Send + Sync + 'static` trait bound rejects any handler whose state contains a non-Sync type (RefCell, Cell, Rc), so the example fails at the impl site, not at use. Reframe as "Won't compile", show the actual trait-bound error inline, and add a one-paragraph note explaining the compiler-level enforcement so a reader doesn't have to mentally trace `spawn -> Send + Sync -> RefCell !Sync`. Addresses stephentoub's review feedback that the prior framing read as "we've buried some unsafe usage." Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refactor session shutdown from Notify to CancellationToken Replaces the per-session `Arc<Notify>` shutdown signal with `tokio_util::sync::CancellationToken`, the canonical primitive for SDK-internal task coordination in async Rust. Same cooperative-shutdown semantics, materially better fit for the use case: - `tonic` uses `CancellationToken` for the equivalent task coordination case (request cancel propagated into spawned server handlers). - Parent/child token tree gives us a clean way to expose a public `Session::cancellation_token()` accessor that returns a child token — consumers can `select!` on `child.cancelled()` to bind their own work to the session lifetime, but cancelling the child does NOT shut down the session itself (child cancel is isolated). - `Drop` impl just calls `cancel()`, no buffering quirks (Notify's `notify_one` was already documented as buffer-on-no-listeners; token cancellation is sticky, which is the simpler mental model). Plumbing: - `tokio-util = { version = "0.7", default-features = false }` added as a runtime dep. No new transitive deps the SDK didn't already pull through tokio. - `Session.shutdown` field type changes from `Arc<Notify>` to `CancellationToken` (which is itself internally `Arc`-backed and `Clone`). - `stop_event_loop()` calls `cancel()` instead of `notify_one()`. - `Drop` impl calls `cancel()` instead of `notify_one()`. - Event-loop `select!` arm becomes `_ = shutdown.cancelled() => break`. - New public `Session::cancellation_token()` returning a `child_token()` — power-user API for binding external tasks to session lifetime. Tests: - `cancellation_token_fires_on_session_drop`: dropping the session fires the child token within 2s. - `cancellation_token_child_cancel_does_not_kill_session`: cancelling a child does not propagate up to the parent. Closes the cancellation idiom gap stephentoub flagged on the SKILL doc. The framing "drop the future or call abort()" was too absolute — drop- to-cancel is the right primitive for caller-owned futures, but for SDK-internal task coordination CancellationToken is the canonical answer (citations: tokio-util docs, tonic cancellation example, withoutboats "Asynchronous clean-up", `tokio-graceful-shutdown` crate). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(skill): rewrite "Idioms that don't port" with full research backing stephentoub flagged that the "Idioms that don't port from Go or Node" section was lopsided (no C# / Python mention) and that the cancellation guidance was too absolute (it dismissed `ctx.Done()` analogs without mentioning tokio_util's CancellationToken). Both true. Replaces the section with a thorough rewrite covering all four idioms with citations and decision matrices: 1. Event subscription: keep "channels not callbacks" baseline; add the full primitive matrix (mpsc / oneshot / broadcast / watch); explain that the public API should expose `impl Stream<Item = T>` (the canonical IObservable<T> analog), citing tonic / reqwest / sqlx as precedent. 2. Cancellation: rewrite. "Drop = cancel" is the primitive for caller-owned futures. `tokio_util::sync::CancellationToken` is the canonical answer for SDK-internal task coordination — citations to tonic's cancellation example, tokio-util docs, withoutboats blog post, and Cybernetist's task cancellation patterns survey. Notes the SDK's own `Session.shutdown: CancellationToken` field and the `Session::cancellation_token()` accessor. 3. Optional fields: confirm Option<T> + Default baseline; add non_exhaustive + builder pairing (AWS SDK convention); note when typestate / Result-from-build is the right answer. 4. serde JSON: confirm rename_all + per-field rename baseline; add skip_serializing_if = "Option::is_none" for output omission and #[serde(default)] for input tolerance (LSP/JSON-RPC convention). Section title is also de-language-coupled: "Idioms that don't port from other languages" instead of "...from Go or Node". Lists Node / Python / C# / Go equivalents inline where pattern divergence is notable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add agentId envelope field on session events for sub-agent attribution The wire-protocol schema added top-level `agentId?: string` to every session event envelope in commit f8cf846 ("Derive session event envelopes from schema") for sub-agent attribution. Every other SDK carries it; Rust silently drops it at the deserialization boundary. Concretely: the schema describes `agentId` as "Sub-agent instance identifier. Absent for events from the root/main agent and session- level events." Without the field, Rust consumers can't distinguish events emitted by a sub-agent from events emitted by the root agent. Adds: - `pub agent_id: Option<String>` on `types::SessionEvent` (hand- authored consumer-facing). - `pub agent_id: Option<String>` on `generated::session_events::TypedSessionEvent` via codegen update in `scripts/codegen/rust.ts`. - `#[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]` via the existing container `rename_all = "camelCase"` (covered) plus skip-on-None for clean wire output. Round-trip tests cover both struct shapes — sub-agent event with the field set, root-agent event without — to lock the parity in place against future schema changes or codegen regressions. Caught by a fresh parity audit against Node/Python/Go/.NET as part of the post-main-merge gap review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove parity-audit scratch files Accidentally committed by the parity audit run; not intended as repo artifacts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(skill): tighten SKILL.md and codify trait-with-defaults extension pattern Two changes prompted by review: 1. SKILL.md was too prose-heavy for an agent to scan. The "Idioms that don't port from other languages" section had grown to ~110 lines of essay-shaped advice with key rules buried in narrative. Replaced with three tight rule sections (Concurrency primitives, Optional fields and serde, plus a one-paragraph cross-language porting note that points back to the rule sections). Net: 345 -> 255 lines, no rule lost. 2. The trait-vs-callback-fields rule was implicit in the SessionHandler description but not explicit. Codified in "Traits and conversions" as a primary directive: prefer one trait with one default-impl method per event over per-event Box<dyn Fn> fields. Cited the three precedent traits in async Rust: - tower_lsp::LanguageServer (63 methods, default impls; LSP wire protocol) - rmcp::ServerHandler (26 methods, all default; MCP wire protocol) - notify::EventHandler (single on_event(enum) for uniform-shape events) Confirmed via research that no major async Rust crate ships per-event Box<dyn Fn> callback fields as its primary API; the pattern fights Send + Sync + 'static, fragments consumer state across closures, and skips exhaustiveness. The SDK's SessionHandler already uses the recommended shape (per-method with defaults plus a default on_event dispatcher); the doc just hadn't named the pattern. Section ordering tightened: Traits/conversions now leads into Extension points, then Concurrency primitives (channels matrix + cancellation), then Optional fields/serde. Reads top-to-bottom for an agent picking up a fresh task. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add continue_pending_work field to ResumeSessionConfig 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> * Add copilot_home, instruction_directories, tcp_connection_token surface 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> * Replace uuid dep with getrandom helper + reviewer-requested test coverage 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(rust): apply stripBooleanLiterals to schemas 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> * Use typed connect RPC instead of hand-coded JSON for handshake 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> * Remove send_telemetry from public surface (Rust-only quirk) 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> * Remove Rust-only Session convenience methods for cross-SDK parity 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> --------- 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> | 5 个月前 |
| Add Rust SDK (technical preview) (#1164) * 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 types via `cd scripts/codegen && npm run generate:rust` to pick up the new schema fields. Surfaced four additive fields: - `SessionErrorData.eligible_for_auto_switch: Option<bool>` — set on `errorType: "rate_limit"` to signal the runtime will follow with an `auto_mode_switch.requested` event. UI clients can suppress duplicate rendering of the rate-limit error when they show their own auto-mode-switch prompt. - `SessionErrorData.error_code: Option<String>` — fine-grained upstream provider error code (`"user_weekly_rate_limited"`, `"integration_rate_limited"`, etc.). - `SessionModelChangeData.cause: Option<String>` — `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch recovery path. UI clients can render contextual copy. - `AutoModeSwitchRequestedData.retry_after_seconds: Option<f64>` — seconds until the rate limit resets, when known. All four are additive on existing struct types (`#[non_exhaustive]` was already in place via the codegen `#[serde(deny_unknown_fields)]`- free pattern, so additions are non-breaking on the deserialize side). - Reformats `rust/src/generated/rpc.rs` use-import (codegen output; fmt picked up the consolidated nested-imports form). - CHANGELOG entry under `### Added > Handlers + helpers` documenting the four new fields and clarifying that the request-callback `retry_after_seconds` parameter on `on_auto_mode_switch` keeps its `Option<u64>` HTTP `Retry-After` `delta-seconds` semantics distinct from the notification-event field's regenerated `Option<f64>` shape. The handler.rs / session.rs auto-mode-switch path from `a7c8215` is unchanged — that hand-authored layer doesn't depend on these specific generated types (the request-callback path is parsed manually in `session.rs:handle_request`). The bump confirms the wire path is real and our 0.1.0 ships against a runtime that actually implements the callback. Validation: - 209 tests pass (no count change — additive schema fields, no test impact). - `cargo doc -D warnings` clean. - `cargo +nightly-2026-04-14 fmt --check` clean. - `cargo clippy --all-features --all-targets -- -D warnings` clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CI: rename scenarios crate ref + nightly-fmt regen + non_exhaustive migration Three CI failures on PR #1164, all caught by green-light gating: 1. **`Verify generated files are up-to-date` (codegen-check workflow).** The Rust codegen (`scripts/codegen/rust.ts`) runs stable `rustfmt` on its output, which leaves `use super::api_types::rpc_methods;` and `use super::api_types::*;` as separate lines. Our committed canonical form is the consolidated `use super::api_types::{rpc_methods, *};` produced by nightly rustfmt with `imports_granularity = "Module"` (`rust/.rustfmt.nightly.toml`). The codegen-check workflow only installed stable, so its regen step produced the unconsolidated form and the diff check failed. Fix: install nightly rustfmt in the codegen-check workflow and run `cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml` on the Rust crate after `npm run generate`. Mirrors the same step the rust-sdk-tests workflow already runs for fmt --check, so the two CI surfaces converge on the same canonical form. 2. **`Rust scenarios` (scenario-builds workflow) — package name.** 18 scenarios under `test/scenarios/**/rust/` referenced the SDK as `copilot-sdk = { path = ... }` and used `use copilot::` in `main.rs`. The crate was renamed to `github-copilot-sdk` in `b136d3f` (with import path `github_copilot_sdk::`), but the scenarios were not updated alongside that rename. CI was failing with `error: no matching package named 'copilot-sdk' found`. Fix: rename in 18 `Cargo.toml` files (`copilot-sdk =` → `github-copilot-sdk =`) and 18 `main.rs` files (`use copilot::` → `use github_copilot_sdk::`, all `copilot::Foo` → `github_copilot_sdk::Foo`). 3. **`Rust scenarios` — `#[non_exhaustive]` cross-crate construction.** After fixing the package name, scenarios still failed with E0639: `cannot create non-exhaustive struct using struct expression`. The `cd3436f` non_exhaustive sweep blocks struct-literal syntax for external crates, including the `..Default::default()` functional- update form. Same migration we already did for `tests/` and `examples/` in `cd3436f`; the scenarios were a forgotten third external surface. Fix: migrate all 18 scenarios from struct-literal form to `let mut x = T::default(); x.field = value;` pattern. Affects `ClientOptions`, `SessionConfig`, `ResumeSessionConfig`, `SystemMessageConfig`, `Tool`, `CustomAgentConfig`, `ProviderConfig`, `InfiniteSessionConfig`, `ToolInvocation`. Inline nested struct literals (e.g. `SystemMessageConfig` inside `SessionConfig`) extracted to their own let-bindings. Builder chains (`.with_handler(...).with_hooks(...)`) preserved by terminating the let-mut-assign sequence with a re-binding before the chain. Drive-by fix to `tools/mcp-servers`: the original scenario assigned a `serde_json::json!({...})` value to `SessionConfig::mcp_servers`, which is typed `Option<HashMap<String, McpServerConfig>>`. The literal-only construction was a latent type error that must have only worked because the scenario was not previously built against the current SDK. Rewrote to construct a real `HashMap<String, McpServerConfig>` using `McpServerConfig::Stdio(McpStdioServerConfig { ... })`. 4. **Drive-by: `package: copilot-sdk` → `github-copilot-sdk` in `rust-sdk-tests.yml`'s `cargo-semver-checks-action`.** The action would have looked up the wrong package name on crates.io post-publish (currently silent because `continue-on-error: true` for the baseline-establishing pre-1.0 cycle, but worth fixing now so the post-publish flip-to-blocking just works). Validation: - 209 SDK tests still pass, doc -D warnings clean, fmt --check (nightly) clean, clippy --all-features --all-targets -- -D warnings clean. - All 18 scenarios build cleanly via the same loop CI runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use "GitHub Copilot CLI" consistently in user-facing docs Audit + replace bare "Copilot CLI" with "GitHub Copilot CLI" across the public-facing surface of the Rust SDK. Per branding consistency: the canonical name is "GitHub Copilot CLI"; "Copilot CLI" alone reads as informal shorthand. 22 occurrences across 10 files: - rust/CHANGELOG.md (2): release-notes prose. - rust/build.rs (2): cargo:warning + curl-required panic message. - rust/README.md (2): tagline + RPC namespace section. - rust/examples/chat.rs (1): example doc-comment. - rust/src/session.rs (1): `Session` rustdoc. - rust/src/types.rs (4): module rustdoc + skill_directories field rustdocs on SessionConfig and ResumeSessionConfig. - rust/src/resolve.rs (1): copilot_binary error hint. - rust/src/lib.rs (6): module-level rustdoc, ClientOptions field rustdocs, ConnectionState enum rustdoc. - rust/src/trace_context.rs (2): module rustdoc + TraceContext type rustdoc. - rust/src/session_fs.rs (1): SessionFsConventions rustdoc. The README's `# GitHub Copilot CLI SDK for Rust` title was already correct; the tagline beneath it ("A Rust SDK for programmatic access to the GitHub Copilot CLI.") was already correct as well — only the secondary mentions in the body needed updating. Test scenario READMEs under `test/scenarios/` are NOT updated: they contain ASCII diagrams with width-constrained boxes that would mis- align with the longer "GitHub Copilot CLI" string, and they're cross-language docs shared across all SDKs (Node, Python, Go, .NET, Rust) — out of scope for this Rust SDK release prep. Validation: cargo doc -D warnings clean, cargo fmt --check clean, cargo clippy clean, 209 tests pass (no semantic changes, only doc strings). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review: add prompts/attachments scenario + README parity sections Closes the two `github-actions` bot findings on PR #1164 that were the only remaining unresolved threads after the human-review cluster. **`prompts/attachments` Rust scenario** (PR comment 3164480775) The bot flagged that `prompts/attachments` was missing from the Rust coverage table — implemented in TS / Python / Go / .NET, full attachment support already in the Rust SDK (`Attachment::File` / `Blob` / `Selection`, `MessageOptions::with_attachments`). Accidental omission from the rest of the Rust port, not a deliberate deferral. - `test/scenarios/prompts/attachments/rust/{Cargo.toml, src/main.rs}` added, mirroring the existing four-language implementations (`claude-haiku-4.5`, replace-mode system prompt, single-file attachment from `sample-data.txt`, prompt asks "what languages are listed in the attached file?", verify.sh greps for TypeScript|Python|Go in the response). - `test/scenarios/RUST_COVERAGE.md`: new ✅ row. - `test/scenarios/prompts/attachments/README.md`: new Rust syntax row in the File and Blob attachment tables. - `test/scenarios/prompts/attachments/verify.sh`: Rust build + run steps wired into both phases. `cargo build` clean locally. **README parity sections** (PR comment 3164686206) The bot flagged that the Rust README has good coverage of the core API but is missing dedicated sections that exist in the Go / Python / .NET READMEs for several features the Rust SDK fully implements. None of these are implementation gaps — discoverability gaps for readers coming from other SDKs. Six new brief sections added to the API Reference between "Capabilities & Elicitation" and "Progress Reporting", each with a runnable code example: - **Slash Commands** — `CommandHandler` + `CommandDefinition::new` with the `name` / `description` wire-only contract called out. - **User Input Requests** — `SessionHandler::on_user_input` override example with the `None`-falls-back-to-CLI semantics. - **Streaming** — `streaming: true` + delta-event match loop reading the `delta` field from `event.data` (verified against the actual `SessionEvent` shape). - **Infinite Sessions** — `InfiniteSessionConfig::workspace_path`, compaction event names, resume-via-`Client::resume_session`, default state location at `~/.copilot/session-state/{sessionId}`. - **Custom Providers (BYOK)** — `ProviderConfig.provider_type` / `base_url` / `bearer_token` / `headers`, with the `openai`/`azure`/`anthropic` provider types and the `wire_api: "completions" | "responses"` flag. - **Telemetry** — `TelemetryConfig` + `OtelExporterType::OtlpHttp`/ `File` with the env-var injection note and the no-OpenTelemetry-dependency guarantee. All API shapes verified against the actual source. Caught one shape-mismatch while drafting (initial draft had `provider.kind: Some(...)` with `base_url: Option<String>`; actual API is `provider.provider_type: Option<String>` + `base_url: String`) — corrected in the final draft. cargo doc -D warnings clean, fmt --check clean, clippy clean, 209 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add get_model and send_telemetry to Rust-only API list Bot review on PR #1164 (comment 3164786116) caught two more accidental omissions in the README's 'Rust-only API' section. Both methods are real and exposed; just missing from this inventory: - Session::get_model — wraps session.model.getCurrent. Folded into the existing 'First-class Session convenience methods' bullet. - Client::send_telemetry / Session::send_telemetry — wrap sendTelemetry / session.sendTelemetry. Added a new dedicated bullet because no other SDK exposes these RPC endpoints in their public API at all (not even via typed namespaces), so it's a higher-divergence item worth its own line. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix update-copilot-dependency: format Rust generated output The CLI-version-bump workflow regenerates per-language types via \`scripts/codegen/npm run generate\` and then formats the output. The formatting step covered TypeScript (prettier) and .NET (dotnet format) but not Rust, so future CLI bumps would land unformatted generated code that codegen-check.yml then catches as a diff. Mirrors the fix already applied to codegen-check.yml in 2766a79: install nightly rustfmt alongside stable, then run \`cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml\` on the rust crate after generation. Same toolchain pin, same config — keeps the two CLI-codegen surfaces converging on the same canonical output. Caught by SDK + CLI version coordination research session. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Client::get_status and Client::get_auth_status wire method names Two more wire-shape bugs in the same class as `c58e2f2` (elicitation `requestedSchema`) and `64541af` (list_sessions filter wrapping): hand-authored RPC wrappers calling JSON-RPC methods that don't exist on the CLI runtime, masked by mock-server tests asserting the same wrong names the implementation sent. The CLI runtime (verified via `@github/copilot@1.0.39` app.js) registers two top-level RPCs: - `status.get` (returns version + protocol_version) - `auth.getStatus` (returns is_authenticated + auth_type) Every other public SDK calls those exact names: - Node `nodejs/src/client.ts:980, 992` - Go `go/client.go:1239, 1257` - Python `python/copilot/client.py:1806, 1827` - .NET `dotnet/src/Client.cs:718, 732` The Rust hand-authored layer was sending `getStatus` and `getAuthStatus` — names that don't exist on the wire. Result: both methods would have returned a method-not-found error (or a misleading no-such-method log on the CLI side) instead of the expected payload. The schema doesn't currently typed-define these top-level RPCs (only `session.auth.getStatus` is in the schema, a different namespaced endpoint), so the codegen didn't catch the hand-authored layer's drift. The mock-server test `client_rpc_methods_send_correct_method_names` asserted on the same wrong names the implementation used, so the bugs round-tripped through both ends. Same root cause as the elicitation and list_sessions tests. Fix: - `lib.rs:1301`: `"getStatus"` → `"status.get"` - `lib.rs:1308`: `"getAuthStatus"` → `"auth.getStatus"` - `tests/session_test.rs:488-509`: assertions migrated to the canonical names AND new explicit-absence assertions (`assert_ne!(request["method"], "getStatus")` / `"getAuthStatus"`) so the same regression class can't be reintroduced silently. Doc-comment on the test names the source- of-truth references (CLI runtime registration, four-SDK call sites). - `CHANGELOG.md`: new `### Fixed` entry documenting the wire-name 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 github-actions bot on PR #1164 (comments 3164826435 and 3164826441). Verify-before-drafting tally now 14 / 15 for these two saves. This brings the wire-shape audit save count to 4 (elicitation, list_sessions, get_status, get_auth_status) — all four were hand-authored RPC wrappers asserting on the implementation's wrong shape rather than the schema/runtime's expected shape. The structural fix (regression-prevention asserts in tests) is now in place for all four; the same bug class is no longer silently reproducible. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refactor JsonRpcClient writer to actor pattern (cancel-safety) Closes RFD-400 review findings #1 (writer mutex desync hazard) and #4 (send_request pending-entry leak on cancellation). See cancel-safety review session db4b1ac8-... for the full report. The previous design held a `tokio::sync::Mutex<Box<dyn AsyncWrite>>` across `write_all` + `flush` for Content-Length-framed JSON-RPC messages. Caller cancellation mid-frame (e.g. wrapping `Session::send` or `Client::call` in `tokio::time::timeout` or `select!`) could leave a partial frame on the wire, permanently desyncing the transport for the rest of the connection. The reader on the CLI side would block forever waiting for the missing bytes, and the next outbound message would be interpreted as the tail of the previous body. Fix: writer-actor task fed by an `mpsc::UnboundedSender<WriteCommand>`. Public `write` and `send_request` pre-serialize the body, enqueue the frame + an ack `oneshot`, and await the ack. Caller cancellation drops the ack receiver but the actor still completes the in-flight write — the frame either lands atomically or surfaces an io::Error to the (possibly already-dropped) ack receiver. A partial frame can never appear on the wire. Design notes: - Pre-serialize before enqueue: actor receives `Vec<u8>` (header + body, already concatenated). Serde errors fail synchronously at enqueue time before touching the actor — cleanest cancel-safety story. - Unbounded sender: RFD 400 explicitly permits this for cancel-safety, and the producer-consumer ratio is naturally bounded by request/ response semantics on the wire. - Writer actor lives until the JsonRpcClient drops. Channel closes on Drop -> actor sees recv None -> flushes and exits. `pending_requests` cleanup (#4): added a small `PendingGuard` RAII helper in `send_request` that arms after the map insert and disarms before the success return. Caller cancellation drops the guard, which removes the pending-entry synchronously via parking_lot::RwLock (the map mutex is converted in this commit too — it was never held across .await anyway, so the conversion is a pure cleanup). Mutex conversions in this commit: - `JsonRpcClient::pending_requests`: `tokio::sync::RwLock` -> `parking_lot::RwLock`. Lock is never held across .await; conversion is also what enables the synchronous PendingGuard drop. - The previous `Arc<Mutex<Box<dyn AsyncWrite>>>` is gone entirely. The actor task owns the writer directly with no shared synchronization. Tests: two new regression tests in tests/jsonrpc_test.rs - `write_actor_completes_on_caller_cancel`: tiny duplex (8 bytes) + gated reader so the actor's write_all blocks waiting for room. Race the caller's future against a sleep; sleep wins, future is dropped while suspended on ack_rx.await. Then enqueue a second write and release the reader. Both frames must land on the wire intact, in order, with no partial frame from the cancelled write. - `send_request_cancellation_does_not_leak_pending`: cancel an in-flight request, then send a (late) response for the cancelled id to verify the read loop logs and discards it without colliding, then send a fresh request and verify it works normally. Existing 209 tests continue to pass without modification — the wire semantics are unchanged. Total now 211. Validation: cargo test, cargo doc -D warnings, cargo +nightly fmt --check, cargo clippy --all-features --all-targets -- -D warnings all clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add WaiterGuard RAII for Session::send_and_wait (cancel-safety) Closes RFD-400 review finding #2 (idle_waiter slot leak on external cancellation). See cancel-safety review session db4b1ac8-... for the full report. `Session::send_and_wait` installs an `IdleWaiter` slot under `self.idle_waiter`, then awaits the response. Internal failure paths (send_inner error, oneshot closure, internal timeout) clean up the slot. External cancellation — caller wraps `send_and_wait` in `tokio::time::timeout`, races it in `select!`, or aborts the JoinHandle — does not. The cleanup at the event loop's `else` branch only fires when the channel itself closes (i.e., the entire session is going away), not when an individual call is cancelled. Effect: any external cancellation between "install waiter" and "drain response" leaves `idle_waiter = Some(...)` forever. All subsequent `send` and `send_and_wait` calls return `SendWhileWaiting`, permanently bricking the session. Fix: `WaiterGuard` RAII helper that takes the slot on Drop. Construct right after install, let RAII handle every exit path. The explicit `self.idle_waiter.lock().take()` calls inside the function disappear, and the slot is cleared on every cancellation path automatically. Mutex conversion (folded in from finding #5): - `Session::idle_waiter`: `tokio::sync::Mutex<Option<IdleWaiter>>` -> `parking_lot::Mutex<Option<IdleWaiter>>`. Lock is never held across `.await` in any code path; the conversion is what makes WaiterGuard's synchronous Drop work without needing an async-spawn fallback. `event_loop` mutex stays `tokio::sync::Mutex` for now — commit C converts it as part of cooperative shutdown (so the conversion lands alongside the matching change to Drop for Session). Tests: two new regression tests in tests/session_test.rs - `send_and_wait_outer_cancellation_clears_waiter`: outer `tokio::time::timeout(50ms, ...)` around `send_and_wait` with a 60-second inner timeout. Outer fires, dropping the future. Verify the next `send` succeeds (no SendWhileWaiting). - `send_and_wait_drop_clears_waiter`: explicit `JoinHandle::abort` of an in-flight send_and_wait. Verify the next `send` succeeds. Existing 211 tests continue to pass; total now 213. Validation: cargo test --all-features, cargo doc -D warnings, cargo +nightly fmt --check, cargo clippy -- -D warnings all clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Cooperative event-loop shutdown via Notify (cancel-safety) Closes RFD-400 review finding #3 (JoinHandle::abort lands at any await point in the event loop body) and the remaining bit of #5 (Session::event_loop tokio::sync::Mutex -> parking_lot::Mutex). See cancel-safety review session db4b1ac8-... for the full report. The previous design called `JoinHandle::abort()` in two places to stop the event-loop task: explicitly in `Session::stop_event_loop` and best-effort in `Drop for Session`. RFD 400's strongest single recommendation about task aborts: > With the abort(), the future returned by do_long_running_operation() > can be cancelled at *any await point*. It is unlikely that arbitrary > async Rust code is resilient to cancellations anywhere during > control flow. ... task aborts should be avoided entirely. The event loop body has many await points: - The outer `select!` itself - `handle_notification`'s nested awaits (acquire idle_waiter lock, send on broadcast, dispatch RPC callbacks for permission/tool/ elicitation requests) - `handle_request`'s inline awaits (deserialize, dispatch handler, build response, write back to client) If `abort()` landed mid-handler — say partway through writing the permission response — the CLI would keep that requestId in flight forever. Same hazard for the idle_waiter oneshot: an abort between "observed session.idle" and "send on waiter.tx" leaves the caller's `send_and_wait` await blocked until the entire transport tears down. Fix: cooperative cancellation via `Arc<tokio::sync::Notify>`. The event-loop body adds a fourth select arm: tokio::select! { _ = shutdown.notified() => break, Some(notification) = notifications.recv() => { ... } Some(request) = requests.recv() => { ... } else => break, } `Notify::notified()` is cancel-safe per RFD 400's framework: the unselected branch losing a poll is fine — it's reconstructed on the next iteration. `notify_one()` (NOT `notify_waiters`) is used because it buffers a single signal: if the loop is currently inside `handle_request(...).await` (no `.notified()` future registered), the signal is held until the handler returns and the loop re-enters select on the next iteration. `notify_waiters()` would silently lose the signal in this case. `stop_event_loop` calls `shutdown.notify_one()` then awaits the JoinHandle. The loop drains its current iteration's handler before observing the buffered signal and exiting cleanly — no mid-protocol state, no orphaned RPC ids on the CLI side. `Drop for Session` calls `notify_one()` and unregisters from the router. The handle is left in `event_loop` for tokio to reap when the loop's spawned task exits naturally; we intentionally don't await it because Drop is sync. Spawned child tasks inside `handle_notification` (permission/tool/elicitation callbacks) intentionally outlive the parent loop — RFD 400's "spawn background tasks to perform cancel-unsafe operations" pattern, documented in the loop's comment. Mutex conversion (folded in from finding #5): - `Session::event_loop`: `tokio::sync::Mutex<Option<JoinHandle<()>>>` -> `parking_lot::Mutex<Option<JoinHandle<()>>>`. Lock is never held across `.await` and the conversion eliminates the `try_lock()` Drop footgun (silent no-op on contention with stop_event_loop). This is the last tokio::sync::Mutex on Session — the import is no longer needed. Tests: two new regression tests in tests/session_test.rs - `stop_event_loop_completes_in_flight_handler`: install a `SlowHandler` whose userInput.request callback sleeps 150ms. Send the request from the server side, then 20ms in (handler is mid- flight) call `stop_event_loop`. Verify the response with `answer: "completed"` lands on the wire BEFORE stop_event_loop returns — proves the handler ran to completion, not aborted mid-await. - `drop_session_does_not_abort_handler`: same shape but via `drop(session)` instead of `stop_event_loop`. Tracks handler completion via an AtomicBool that the handler sets only after its sleep completes; verifies both that the response lands on the wire AND that the handler actually completed despite the Session being dropped. Existing 213 tests continue to pass; total now 215. Validation: cargo test --all-features, cargo doc -D warnings, cargo +nightly fmt --check, cargo clippy -- -D warnings all clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Document cancel-safety for public async APIs (RFD-400) Closes RFD-400 review finding #6 (per-method cancel-safety documentation). Doc-only follow-up to commits A/B/C; no behavioral change. RFD 400 § "Marking APIs as cancel-unsafe" calls for explicit documentation; with the underlying behavior now correct after A/B/C, documenting the contract makes it inspectable from rustdoc rather than requiring callers to read the implementation. Added `# Cancel safety` rustdoc sections to: - `Session::send` — cancel-safe (writer-actor); message lands on the wire even if the await is cancelled. - `Session::send_and_wait` — cancel-safe (WaiterGuard RAII); outer-cancellation distinct from internal-timeout, both clear the slot. - `Session::abort` — cancel-safe (single RPC via writer-actor). - `Client::call` — cancel-safe at the wire level; documents the caveat that the CLI may still process the request even if the caller doesn't see the response (idempotent vs non-idempotent methods). - `Client::stop` — cancel-unsafe-but-recoverable; the body sequentially destroys sessions (each individually cancel-safe) before killing the child. The existing `tokio::time::timeout` example with `force_stop` fallback is the documented recovery path. - `Client::force_stop` — synchronous, cannot be cancelled. Designed as the recovery path for `stop`'s timeout case. - `Subscription::recv` — cancel-safe by virtue of `tokio::sync::broadcast` + `BroadcastStream`. Validation: cargo doc -D warnings clean, cargo test (215 pass), cargo +nightly fmt --check clean, cargo clippy clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add scheduled trigger to update-copilot-dependency.yml Closes "Mode 1 hands-off loop" gap A from the SDK release plan-doc: update-copilot-dependency.yml is workflow_dispatch only today, so when a new @github/copilot publishes nobody automatically opens the regen PR. Today's bot-driven bump (commit ecdc5ec, "Update @github/copilot to 1.0.39 (#1167)") was triggered manually after the plan-doc session noticed the gap. Goal: the SDK auto-detects new CLI versions, opens a draft PR with all per-language regen + formatting + lockfile updates, and a human just reviews + merges. github-app's bundled-binary tracker picks the new SDK up via crates.io dependabot in the symmetric direction. Changes: 1. **`schedule` trigger.** Daily cron at 10:00 UTC, deliberately symmetric with github-app's check-bundled-binary-versions.yml so the SDK and consumer halves of the loop tick in lockstep on release days. 2. **`version` input is now optional.** Manual override still works for emergency back-patches or cherry-picking specific prereleases; an empty input on workflow_dispatch flows through the same auto-detect path the schedule uses. 3. **Two-job split: `detect` -> `update`.** The detect job is cheap (one `npm view @github/copilot version` + a jq read of nodejs/package.json + a strip of the `^`/`~` prefix). It sets two outputs: - `version`: the resolved target (input override, or latest from npm) - `should_continue`: 'true' if the resolved version differs from the current pin, 'false' otherwise The update job has `needs: detect` and `if: needs.detect.outputs.should_continue == 'true'`, so it skips entirely when there's nothing to do. This keeps the no-op daily cost effectively free. 4. **Validation moves into the resolve step.** Both manual and auto-detected versions go through the same semver regex check. The previous standalone "Validate version input" step is retired; the detect job's resolve step now owns format validation, version comparison, and skip logic. 5. **All `${{ inputs.version }}` references become `${{ needs.detect.outputs.version }}`.** Same value, just routed through the detect job's outputs so manual override and auto-detect both feed the update job uniformly. Doesn't touch SDK source; cargo test / cargo publish --dry-run are no-op confirmation post-merge. Companion gap on the github-app side (auto-open sync PR after each new SDK release) lives on Sync session PR #4140; this commit unblocks the SDK half. Plan-doc capture: tracked under "Steady-state CLI bump automation (Mode 1 hands-off loop)" — gap A closed; gap B (github-app sync-PR auto-open) remains. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Correct get_quota doc: endpoint exists cross-SDK, only wrapper is Rust-only Bot review caught an inaccuracy in the rustdoc and README: the prior wording claimed the underlying account.getQuota JSON-RPC endpoint was exposed only by the Rust SDK. In fact only the top-level Client::get_quota convenience wrapper is Rust-unique; the endpoint itself is generated and exposed cross-SDK via each SDK's typed rpc() namespace. Verified cross-SDK exposure: - Node: nodejs/src/generated/rpc.ts:2203 (account.getQuota in rpc() namespace) - Python: python/copilot/generated/rpc.py:5647 (rpc().account.get_quota) - Go: go/rpc/generated_rpc.go:2252 (RPC.Account.GetQuota) - .NET: dotnet/src/Generated/Rpc.cs:3140 (Rpc().Account().GetQuotaAsync) - Rust: rust/src/generated/rpc.rs:90 (rpc().account().get_quota) Updated rustdoc on Client::get_quota in rust/src/lib.rs and the "Rust-only API" bullet in rust/README.md to correctly state that the wrapper is Rust-only while the underlying endpoint is available across all SDKs via their typed rpc() namespaces. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add ClientOptions::new() and Tool::new() builder methods Mirrors the existing MessageOptions::new() / with_* builder pattern on the two other public #[non_exhaustive] config types that consumers construct most often. Closes the cross-crate ergonomics gap that #[non_exhaustive] introduces: external callers can't use struct- literal syntax (with or without ..Default::default()) on these types, so every field assignment becomes its own mut-let statement. The builder shape collapses that into a single chained expression. Caught by github-app PR #4140 review on src-tauri/src/session/cli.rs: the mut-let-and-assign pattern read as awkward to reviewers even after trimming to the four fields that diverge from default. Same ergonomic friction that prompted the make_tool() helper consumers were writing for Tool. Builder ships zero-cost convenience without giving up #[non_exhaustive]'s SemVer protection on growable config struct. ClientOptions builder (rust/src/lib.rs): - new() -> Self (documented entry point) - with_program(impl Into<CliProgram>) - with_prefix_args<I: IntoIterator<...>>(args) - with_cwd(impl Into<PathBuf>) - with_env<I: IntoIterator<(K, V)>> - with_env_remove<I: IntoIterator<S>> - with_extra_args<I: IntoIterator<S>> - with_transport(Transport) - with_github_token(impl Into<String>) - with_use_logged_in_user(bool) - with_log_level(LogLevel) - with_session_idle_timeout_seconds(u64) - with_list_models_handler<H: ListModelsHandler + 'static> (Arc-wrapped internally) - with_session_fs(SessionFsConfig) - with_trace_context_provider<P: TraceContextProvider + 'static> (Arc-wrapped internally) - with_telemetry(TelemetryConfig) Tool builder (rust/src/types.rs): - new(impl Into<String>) -> Self (name + defaults) - with_namespaced_name(impl Into<String>) - with_description(impl Into<String>) - with_instructions(impl Into<String>) - with_parameters(serde_json::Value) - with_overrides_built_in_tool(bool) - with_skip_permission(bool) Pure additive: no existing ClientOptions::default() / mut-let patterns break, no public field visibility changes. Both builders compose by wrapping ::default() and mutating-and-returning self, matching MessageOptions's idiom. Tests: client_options_builder_composes verifies all 10 chained builder methods produce the expected internal state. tool_builder_composes does the same for Tool's seven methods, plus tool_with_parameters_handles_non_object_value covers the HashMap-from-Value extraction edge case (json!(null) -> empty map rather than panic). CHANGELOG: new "Builder ergonomics" subsection under Documentation, explaining the rationale and listing both builders. Validation: 220 tests pass (was 215 + 5 new builder tests across unit + doctest), cargo doc -D warnings clean, cargo fmt --check clean, cargo clippy --all-features --all-targets -- -D warnings clean. Companion ask from github-app PR #4140 review (Sync session) follows up: github-app's `cli.rs` and ~25 Tool::new() call sites will switch to the builder form once this lands and the next sync round-trip pulls it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add per-field builder methods to SessionConfig and ResumeSessionConfig Mirrors the just-shipped ClientOptions/Tool builder pattern (commit 2d725a5) on the two structs that consumers reach for most when wiring up a session. Both types are #[non_exhaustive], so external callers have been writing the same mut-let-and-assign block at every construction site: let mut config = ResumeSessionConfig::new(session_id); config.client_name = Some("...".into()); config.streaming = Some(true); config.tools = Some(tools); // ...10+ more lines The new chainable form collapses that to: ResumeSessionConfig::new(session_id) .with_client_name("...") .with_streaming(true) .with_tools(tools) // ... Both types have ~30 public fields each. Coverage: SessionConfig: with_session_id, with_model, with_client_name, with_reasoning_effort, with_streaming, with_tools, with_available_tools, with_excluded_tools, with_mcp_servers, with_disabled_mcp_servers, with_env_value_mode, with_enable_config_discovery, with_request_user_input, with_request_permission, with_request_exit_plan_mode, with_request_auto_mode_switch, with_request_elicitation, with_skill_directories, with_disabled_skills, with_agent, with_custom_agents, with_config_dir, with_working_directory, with_github_token, with_include_sub_agent_streaming_events, with_system_message, with_skip_compaction_history. ResumeSessionConfig: same shape minus model/reasoning_effort/ available_tools/disabled_skills/disabled_mcp_servers (which the resume RPC doesn't accept), plus with_disable_resume. Existing closure-installing chains (with_handler, with_hooks, with_transform, with_commands, with_session_fs_provider, approve_all_permissions, deny_all_permissions, approve_permissions_if) and the manual mut-let pattern continue to work unchanged. The hooks bool is intentionally not exposed via a new with_* setter; with_hooks(handler) auto-enables the flag, and power users wanting to override can still set the .hooks field directly. Adds two integration-style tests covering composition across all field categories (scalars, bool flags, Vec setters, struct setters, HashMap setters) for both types. CHANGELOG entry extends the existing "Builder ergonomics" subsection. This is the third commit in the pre-1.0 builder-pattern series (MessageOptions in the original branch, ClientOptions/Tool in 2d725a5, SessionConfig/ResumeSessionConfig here). Pure additive, zero migration cost. Driven by github-app PR #4140 reviewer feedback on src-tauri/src/session/cli.rs and core.rs construction sites. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Scrub github-app references from public-repo files The copilot-sdk repo is public; github-app is private. Public-repo content must not name or depend on github-app, even in comments or test fixtures. - .github/workflows/update-copilot-dependency.yml: drop the "symmetric with github-app's scheduled" framing on the daily cron; rewrite the comment to describe what the schedule does in SDK-local terms only. - rust/CHANGELOG.md: rephrase the Tool builder rationale to talk about "real-world consumer code" rather than github-app's tool catalog specifically. - rust/src/lib.rs: change the `source_name` test fixture from "github-app" to "my-app" — it was just an example string for OTEL source-name plumbing, but the explicit consumer naming is unnecessary and confusing in a public repo. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Round out builder coverage and document the Option<T> escape hatch After landing per-field builders on the four highest-traffic consumer-facing config structs (MessageOptions, ClientOptions, Tool, SessionConfig/ResumeSessionConfig), this commit closes the long tail. Rust ecosystem research (tokio, reqwest, tonic, axum, AWS SDK) confirms two design points: 1. The dominant pattern for #[non_exhaustive] config builders is the single `with_<field>(impl Into<T>) -> Self` setter that always Some-wraps. AWS SDK's dual `field` + `set_field(Option<T>)` shape is justified by codegen, not by ergonomics — and the `impl Into<Option<T>>` magic that some older crates use has fallen out of fashion (it conflicts with `impl Into<T>` chaining and produces poor type-inference errors). 2. Direct field assignment on a `pub` field is idiomatic Rust for forwarding `Option<T>` values from upstream code. The http::request::Parts / hyper::Body::Builder pattern. We already support it; we just hadn't documented it. This commit therefore: - Adds full builders to the four bare consumer-facing structs: CustomAgentConfig (name + prompt required, six with_* setters), InfiniteSessionConfig (three with_* setters), ProviderConfig (base_url required, six with_* setters), SystemMessageConfig (three with_* setters). - Expands TraceContext with a symmetric new() + with_traceparent pair alongside the existing from_traceparent shorthand. The shorthand is now expressed in terms of the chain (`new().with_traceparent(x)`) for consistency. - Documents the direct-field-assignment escape hatch on SessionConfig and ResumeSessionConfig, with a doc example showing how to mix the fluent chain (for compile-time-known values) with direct mutation (for Option<T> pass-throughs). Per-field `with_<x>_opt(Option<T>)` setters were considered and deliberately rejected: doubling the method count for ~30-field configs would hurt rustdoc discoverability and conflict with the single-setter precedent we just shipped. The mut-let escape hatch covers the same use case without polluting the API surface. Adds 5 unit tests (one per new builder + a TraceContext suite) and extends the existing CHANGELOG "Builder ergonomics" subsection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert "Add scheduled trigger to update-copilot-dependency.yml" This reverts commit a88c3ebc7cf25d5b7194a37b51e52bf6a62bc8fc. The scheduled trigger was added to close a hands-off-loop gap that was framed in terms of a downstream consumer's needs. On reflection, the SDK should not be the only language in this repo with scheduled auto-detect; that's a cross-language platform decision, not a single- SDK one. Reverting brings update-copilot-dependency.yml back to manual-only (workflow_dispatch with a required version input) — the same shape as every other language's CLI update workflow in this repo. When the SDK collectively decides to add scheduled CLI tracking, that should land as a unified change across all languages, not as a Rust-only special case driven by a downstream consumer's automation desires. Manual dispatch remains the supported path until then. Conflict resolution: the e93ce8e ("Scrub github-app references") commit landed after a88c3eb and rewrote the cron block's comment. The revert drops the entire schedule: block (including the rewritten comment), restoring the workflow to its pre-a88c3eb manual-only shape exactly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add TelemetryConfig builder methods Final missing piece in the consumer-facing #[non_exhaustive] config struct audit. `TelemetryConfig` had public optional fields (otlp_endpoint, file_path, exporter_type, source_name, capture_content) and an is_empty() helper, but no `new()` or `with_*` setters — leaving callers to write `TelemetryConfig { otlp_endpoint: Some(...), ..Default::default() }` which doesn't compile from outside the crate due to #[non_exhaustive]. Adds `TelemetryConfig::new()` (delegates to Default) and `with_*` setters for all five fields, matching the existing pattern. The is_empty() helper stays unchanged. After this commit, every consumer-facing #[non_exhaustive] struct in the SDK has a fluent builder: ClientOptions, TelemetryConfig, Tool, CommandDefinition, CustomAgentConfig, InfiniteSessionConfig, ProviderConfig, SessionConfig, ResumeSessionConfig, SystemMessageConfig, MessageOptions, SessionFsConfig, TraceContext. One unit test (telemetry_config_builder_composes) covering all five setters + is_empty behavior on a fresh new(). Extends the existing CHANGELOG "Builder ergonomics" round-out bullet. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Switch build.rs from curl to ureq with bounded retries The bundled-CLI build path (gated by the embedded-cli feature and the COPILOT_CLI_VERSION env var) previously shelled out to `curl` to fetch SHA256SUMS.txt and the platform tarball from the copilot-cli releases. Two problems with that: 1. Implicit system dependency. `expect("curl is required ...")` panics when curl isn't on PATH — true on minimal Windows MSVC environments, some Docker images, and the CI matrices crates.io publishers tend to validate against. crates.io build environments themselves don't guarantee curl. 2. No retry on transient errors. Network blips during CI bundling surfaced as build failures rather than self-healing waits. Switch to ureq 2 with default-features=false + ["tls"] (rustls + webpki-roots, pure-Rust TLS, no OpenSSL or native-tls). Adds about 1.7MB and ~12 transitive deps to the build graph but only when the embedded-cli feature is on; the default cargo build is unaffected. Retry policy: - Up to 3 retries (4 total attempts) with exponential backoff: 1s, 2s, 4s. - 5xx responses, connect timeouts, read timeouts, and other transport-layer errors are treated as transient and retried. - 4xx responses fail fast (the URL is wrong; retrying won't help). Connect/read timeouts (30s/120s respectively) prevent indefinite hangs on stalled connections. The cache-hit path still bypasses the network entirely; only fresh downloads pay the HTTP cost. End-to-end smoke test against https://github.com/github/copilot-cli/releases/download/v1.0.39 : SHA256SUMS download succeeded, tarball download succeeded (134MB extracted, 44MB compressed), SHA-256 verified, embedded build succeeded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: validate embedded-cli bundle build on macOS / Linux / Windows The default `cargo test` job in rust-sdk-tests.yml runs on all three supported platforms but does not set `COPILOT_CLI_VERSION` — which means `build.rs` short-circuits immediately and the embedded-cli bundle path (download + SHA-256 verify + extract + zstd compress + embed) is never exercised in CI. After the recent switch from shelling out to `curl` over to `ureq` with retry logic in `build.rs`, that gap matters: the new HTTP client + cross-platform TLS code path needs cross-platform CI coverage before it ships. Adds a parallel `bundle` job to the same workflow: - 3-OS matrix: ubuntu-latest, macos-latest, windows-latest. - Reads the pinned CLI version from `nodejs/package.json` so the bundle test always tracks the same version the rest of the SDK ships against — no separate version source to drift. - Caches the downloaded archive in `./rust/.bundled-cli-cache` keyed on OS + CLI version, so steady-state CI doesn't refetch ~130 MB on every run. Cache miss (e.g. first run after a CLI bump) exercises the full download path end-to-end, which is the regression surface this job is meant to catch. - Runs `cargo build --features embedded-cli` to drive `build.rs` through the full pipeline. Doesn't run `cargo test` — the test job already covers runtime behavior; this job is specifically validating the bundle build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix duplicate user_input dispatch (github-app#4249) The CLI sends two messages for every user-input prompt: 1. A `user_input.requested` session-event notification, intended for observers (UI streaming, telemetry, logging). 2. A `userInput.request` JSON-RPC method call, which is the actual ask-and-wait prompt that drives the handler. The Rust SDK's session event loop was wiring up `HandlerEvent::UserInput` dispatch on BOTH paths. Result: the consumer's `on_event` ran twice for every prompt, which surfaced as duplicate `ask_user` / `exit_plan` widgets in github-app's session UI (#4249). Other SDKs (Node, Python, Go, .NET) only register the JSON-RPC handler; the notification is purely observational. Aligning with that convention. Changes: 1. `session.rs`: drop the `SessionEventType::UserInputRequested` auto-dispatch arm. The CLI's follow-up `userInput.request` JSON-RPC call still drives the handler. Replaced with a short comment block explaining why the notification is intentionally a no-op (and citing #4249 so the next person tempted to add it back understands the trade). 2. `tests/session_test.rs`: replaced the `user_input_requested_event_dispatches_to_handler_and_responds` test (which asserted the broken behavior) with a regression test (`user_input_requested_notification_does_not_double_dispatch`) that: - sends a `user_input.requested` notification and asserts no wire response, no handler invocation; - then sends a `userInput.request` JSON-RPC call and asserts the handler fires exactly once. This locks the fix in place and makes a future re-introduction surface immediately as a test failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Drop Skills section from .github/copilot-instructions.md stephentoub and tclem agreed the Skills section is unnecessary prompt-pollution. Skills are auto-discovered by Copilot CLI without needing to be enumerated in the instructions, and the one-line description of each skill duplicates what the SKILL.md frontmatter already says. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Render experimental RPC methods as a styled rustdoc admonition Codegen previously emitted a single bare `/// Stability: experimental.` line on each experimental wire method. stephentoub flagged this as weak signal -- there's no way for a consumer to tell from the rustdoc that they're using something that may change. Switch the rustdoc emit to a `<div class="warning">...</div>` block, which renders on docs.rs as a yellow-bordered callout (the standard rustdoc admonition convention). Matches the visibility Python's generated docs already get via Sphinx's `.. warning::` directive. This is documentation-quality only; no compile-time opt-in. Stable Rust has no first-class `#[experimental]` attribute (`#[unstable]` is rustc-internal, std-only). If we ever want compile-time friction the ecosystem patterns are a Cargo feature gate (e.g. `tokio_unstable`- style `features = ["experimental"]`) or an `unstable` module path; both are larger codegen changes affecting every experimental RPC across all SDK surfaces. Punted for 0.1.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(skill): clarify that handler thread-safety is compiler-enforced The "anti-pattern" framing in examples.md was misleading -- it implied the RefCell example might compile but produce bugs at runtime. In reality, SessionHandler's `Send + Sync + 'static` trait bound rejects any handler whose state contains a non-Sync type (RefCell, Cell, Rc), so the example fails at the impl site, not at use. Reframe as "Won't compile", show the actual trait-bound error inline, and add a one-paragraph note explaining the compiler-level enforcement so a reader doesn't have to mentally trace `spawn -> Send + Sync -> RefCell !Sync`. Addresses stephentoub's review feedback that the prior framing read as "we've buried some unsafe usage." Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refactor session shutdown from Notify to CancellationToken Replaces the per-session `Arc<Notify>` shutdown signal with `tokio_util::sync::CancellationToken`, the canonical primitive for SDK-internal task coordination in async Rust. Same cooperative-shutdown semantics, materially better fit for the use case: - `tonic` uses `CancellationToken` for the equivalent task coordination case (request cancel propagated into spawned server handlers). - Parent/child token tree gives us a clean way to expose a public `Session::cancellation_token()` accessor that returns a child token — consumers can `select!` on `child.cancelled()` to bind their own work to the session lifetime, but cancelling the child does NOT shut down the session itself (child cancel is isolated). - `Drop` impl just calls `cancel()`, no buffering quirks (Notify's `notify_one` was already documented as buffer-on-no-listeners; token cancellation is sticky, which is the simpler mental model). Plumbing: - `tokio-util = { version = "0.7", default-features = false }` added as a runtime dep. No new transitive deps the SDK didn't already pull through tokio. - `Session.shutdown` field type changes from `Arc<Notify>` to `CancellationToken` (which is itself internally `Arc`-backed and `Clone`). - `stop_event_loop()` calls `cancel()` instead of `notify_one()`. - `Drop` impl calls `cancel()` instead of `notify_one()`. - Event-loop `select!` arm becomes `_ = shutdown.cancelled() => break`. - New public `Session::cancellation_token()` returning a `child_token()` — power-user API for binding external tasks to session lifetime. Tests: - `cancellation_token_fires_on_session_drop`: dropping the session fires the child token within 2s. - `cancellation_token_child_cancel_does_not_kill_session`: cancelling a child does not propagate up to the parent. Closes the cancellation idiom gap stephentoub flagged on the SKILL doc. The framing "drop the future or call abort()" was too absolute — drop- to-cancel is the right primitive for caller-owned futures, but for SDK-internal task coordination CancellationToken is the canonical answer (citations: tokio-util docs, tonic cancellation example, withoutboats "Asynchronous clean-up", `tokio-graceful-shutdown` crate). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(skill): rewrite "Idioms that don't port" with full research backing stephentoub flagged that the "Idioms that don't port from Go or Node" section was lopsided (no C# / Python mention) and that the cancellation guidance was too absolute (it dismissed `ctx.Done()` analogs without mentioning tokio_util's CancellationToken). Both true. Replaces the section with a thorough rewrite covering all four idioms with citations and decision matrices: 1. Event subscription: keep "channels not callbacks" baseline; add the full primitive matrix (mpsc / oneshot / broadcast / watch); explain that the public API should expose `impl Stream<Item = T>` (the canonical IObservable<T> analog), citing tonic / reqwest / sqlx as precedent. 2. Cancellation: rewrite. "Drop = cancel" is the primitive for caller-owned futures. `tokio_util::sync::CancellationToken` is the canonical answer for SDK-internal task coordination — citations to tonic's cancellation example, tokio-util docs, withoutboats blog post, and Cybernetist's task cancellation patterns survey. Notes the SDK's own `Session.shutdown: CancellationToken` field and the `Session::cancellation_token()` accessor. 3. Optional fields: confirm Option<T> + Default baseline; add non_exhaustive + builder pairing (AWS SDK convention); note when typestate / Result-from-build is the right answer. 4. serde JSON: confirm rename_all + per-field rename baseline; add skip_serializing_if = "Option::is_none" for output omission and #[serde(default)] for input tolerance (LSP/JSON-RPC convention). Section title is also de-language-coupled: "Idioms that don't port from other languages" instead of "...from Go or Node". Lists Node / Python / C# / Go equivalents inline where pattern divergence is notable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add agentId envelope field on session events for sub-agent attribution The wire-protocol schema added top-level `agentId?: string` to every session event envelope in commit f8cf846 ("Derive session event envelopes from schema") for sub-agent attribution. Every other SDK carries it; Rust silently drops it at the deserialization boundary. Concretely: the schema describes `agentId` as "Sub-agent instance identifier. Absent for events from the root/main agent and session- level events." Without the field, Rust consumers can't distinguish events emitted by a sub-agent from events emitted by the root agent. Adds: - `pub agent_id: Option<String>` on `types::SessionEvent` (hand- authored consumer-facing). - `pub agent_id: Option<String>` on `generated::session_events::TypedSessionEvent` via codegen update in `scripts/codegen/rust.ts`. - `#[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]` via the existing container `rename_all = "camelCase"` (covered) plus skip-on-None for clean wire output. Round-trip tests cover both struct shapes — sub-agent event with the field set, root-agent event without — to lock the parity in place against future schema changes or codegen regressions. Caught by a fresh parity audit against Node/Python/Go/.NET as part of the post-main-merge gap review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove parity-audit scratch files Accidentally committed by the parity audit run; not intended as repo artifacts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(skill): tighten SKILL.md and codify trait-with-defaults extension pattern Two changes prompted by review: 1. SKILL.md was too prose-heavy for an agent to scan. The "Idioms that don't port from other languages" section had grown to ~110 lines of essay-shaped advice with key rules buried in narrative. Replaced with three tight rule sections (Concurrency primitives, Optional fields and serde, plus a one-paragraph cross-language porting note that points back to the rule sections). Net: 345 -> 255 lines, no rule lost. 2. The trait-vs-callback-fields rule was implicit in the SessionHandler description but not explicit. Codified in "Traits and conversions" as a primary directive: prefer one trait with one default-impl method per event over per-event Box<dyn Fn> fields. Cited the three precedent traits in async Rust: - tower_lsp::LanguageServer (63 methods, default impls; LSP wire protocol) - rmcp::ServerHandler (26 methods, all default; MCP wire protocol) - notify::EventHandler (single on_event(enum) for uniform-shape events) Confirmed via research that no major async Rust crate ships per-event Box<dyn Fn> callback fields as its primary API; the pattern fights Send + Sync + 'static, fragments consumer state across closures, and skips exhaustiveness. The SDK's SessionHandler already uses the recommended shape (per-method with defaults plus a default on_event dispatcher); the doc just hadn't named the pattern. Section ordering tightened: Traits/conversions now leads into Extension points, then Concurrency primitives (channels matrix + cancellation), then Optional fields/serde. Reads top-to-bottom for an agent picking up a fresh task. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add continue_pending_work field to ResumeSessionConfig 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> * Add copilot_home, instruction_directories, tcp_connection_token surface 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> * Replace uuid dep with getrandom helper + reviewer-requested test coverage 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(rust): apply stripBooleanLiterals to schemas 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> * Use typed connect RPC instead of hand-coded JSON for handshake 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> * Remove send_telemetry from public surface (Rust-only quirk) 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> * Remove Rust-only Session convenience methods for cross-SDK parity 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> --------- 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> | 5 个月前 |
| Update SDK snapshot for Copilot CLI 1.0.102-0 | 4 天前 |
| Update SDK snapshot for Copilot CLI 1.0.90-4 | 25 天前 |
| Update SDK snapshot for Copilot CLI 1.0.102-1 | 4 天前 |
| Update SDK snapshot for Copilot CLI 1.0.100-0 | 8 天前 |
| Add Rust SDK (technical preview) (#1164) * 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 types via `cd scripts/codegen && npm run generate:rust` to pick up the new schema fields. Surfaced four additive fields: - `SessionErrorData.eligible_for_auto_switch: Option<bool>` — set on `errorType: "rate_limit"` to signal the runtime will follow with an `auto_mode_switch.requested` event. UI clients can suppress duplicate rendering of the rate-limit error when they show their own auto-mode-switch prompt. - `SessionErrorData.error_code: Option<String>` — fine-grained upstream provider error code (`"user_weekly_rate_limited"`, `"integration_rate_limited"`, etc.). - `SessionModelChangeData.cause: Option<String>` — `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch recovery path. UI clients can render contextual copy. - `AutoModeSwitchRequestedData.retry_after_seconds: Option<f64>` — seconds until the rate limit resets, when known. All four are additive on existing struct types (`#[non_exhaustive]` was already in place via the codegen `#[serde(deny_unknown_fields)]`- free pattern, so additions are non-breaking on the deserialize side). - Reformats `rust/src/generated/rpc.rs` use-import (codegen output; fmt picked up the consolidated nested-imports form). - CHANGELOG entry under `### Added > Handlers + helpers` documenting the four new fields and clarifying that the request-callback `retry_after_seconds` parameter on `on_auto_mode_switch` keeps its `Option<u64>` HTTP `Retry-After` `delta-seconds` semantics distinct from the notification-event field's regenerated `Option<f64>` shape. The handler.rs / session.rs auto-mode-switch path from `a7c8215` is unchanged — that hand-authored layer doesn't depend on these specific generated types (the request-callback path is parsed manually in `session.rs:handle_request`). The bump confirms the wire path is real and our 0.1.0 ships against a runtime that actually implements the callback. Validation: - 209 tests pass (no count change — additive schema fields, no test impact). - `cargo doc -D warnings` clean. - `cargo +nightly-2026-04-14 fmt --check` clean. - `cargo clippy --all-features --all-targets -- -D warnings` clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CI: rename scenarios crate ref + nightly-fmt regen + non_exhaustive migration Three CI failures on PR #1164, all caught by green-light gating: 1. **`Verify generated files are up-to-date` (codegen-check workflow).** The Rust codegen (`scripts/codegen/rust.ts`) runs stable `rustfmt` on its output, which leaves `use super::api_types::rpc_methods;` and `use super::api_types::*;` as separate lines. Our committed canonical form is the consolidated `use super::api_types::{rpc_methods, *};` produced by nightly rustfmt with `imports_granularity = "Module"` (`rust/.rustfmt.nightly.toml`). The codegen-check workflow only installed stable, so its regen step produced the unconsolidated form and the diff check failed. Fix: install nightly rustfmt in the codegen-check workflow and run `cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml` on the Rust crate after `npm run generate`. Mirrors the same step the rust-sdk-tests workflow already runs for fmt --check, so the two CI surfaces converge on the same canonical form. 2. **`Rust scenarios` (scenario-builds workflow) — package name.** 18 scenarios under `test/scenarios/**/rust/` referenced the SDK as `copilot-sdk = { path = ... }` and used `use copilot::` in `main.rs`. The crate was renamed to `github-copilot-sdk` in `b136d3f` (with import path `github_copilot_sdk::`), but the scenarios were not updated alongside that rename. CI was failing with `error: no matching package named 'copilot-sdk' found`. Fix: rename in 18 `Cargo.toml` files (`copilot-sdk =` → `github-copilot-sdk =`) and 18 `main.rs` files (`use copilot::` → `use github_copilot_sdk::`, all `copilot::Foo` → `github_copilot_sdk::Foo`). 3. **`Rust scenarios` — `#[non_exhaustive]` cross-crate construction.** After fixing the package name, scenarios still failed with E0639: `cannot create non-exhaustive struct using struct expression`. The `cd3436f` non_exhaustive sweep blocks struct-literal syntax for external crates, including the `..Default::default()` functional- update form. Same migration we already did for `tests/` and `examples/` in `cd3436f`; the scenarios were a forgotten third external surface. Fix: migrate all 18 scenarios from struct-literal form to `let mut x = T::default(); x.field = value;` pattern. Affects `ClientOptions`, `SessionConfig`, `ResumeSessionConfig`, `SystemMessageConfig`, `Tool`, `CustomAgentConfig`, `ProviderConfig`, `InfiniteSessionConfig`, `ToolInvocation`. Inline nested struct literals (e.g. `SystemMessageConfig` inside `SessionConfig`) extracted to their own let-bindings. Builder chains (`.with_handler(...).with_hooks(...)`) preserved by terminating the let-mut-assign sequence with a re-binding before the chain. Drive-by fix to `tools/mcp-servers`: the original scenario assigned a `serde_json::json!({...})` value to `SessionConfig::mcp_servers`, which is typed `Option<HashMap<String, McpServerConfig>>`. The literal-only construction was a latent type error that must have only worked because the scenario was not previously built against the current SDK. Rewrote to construct a real `HashMap<String, McpServerConfig>` using `McpServerConfig::Stdio(McpStdioServerConfig { ... })`. 4. **Drive-by: `package: copilot-sdk` → `github-copilot-sdk` in `rust-sdk-tests.yml`'s `cargo-semver-checks-action`.** The action would have looked up the wrong package name on crates.io post-publish (currently silent because `continue-on-error: true` for the baseline-establishing pre-1.0 cycle, but worth fixing now so the post-publish flip-to-blocking just works). Validation: - 209 SDK tests still pass, doc -D warnings clean, fmt --check (nightly) clean, clippy --all-features --all-targets -- -D warnings clean. - All 18 scenarios build cleanly via the same loop CI runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use "GitHub Copilot CLI" consistently in user-facing docs Audit + replace bare "Copilot CLI" with "GitHub Copilot CLI" across the public-facing surface of the Rust SDK. Per branding consistency: the canonical name is "GitHub Copilot CLI"; "Copilot CLI" alone reads as informal shorthand. 22 occurrences across 10 files: - rust/CHANGELOG.md (2): release-notes prose. - rust/build.rs (2): cargo:warning + curl-required panic message. - rust/README.md (2): tagline + RPC namespace section. - rust/examples/chat.rs (1): example doc-comment. - rust/src/session.rs (1): `Session` rustdoc. - rust/src/types.rs (4): module rustdoc + skill_directories field rustdocs on SessionConfig and ResumeSessionConfig. - rust/src/resolve.rs (1): copilot_binary error hint. - rust/src/lib.rs (6): module-level rustdoc, ClientOptions field rustdocs, ConnectionState enum rustdoc. - rust/src/trace_context.rs (2): module rustdoc + TraceContext type rustdoc. - rust/src/session_fs.rs (1): SessionFsConventions rustdoc. The README's `# GitHub Copilot CLI SDK for Rust` title was already correct; the tagline beneath it ("A Rust SDK for programmatic access to the GitHub Copilot CLI.") was already correct as well — only the secondary mentions in the body needed updating. Test scenario READMEs under `test/scenarios/` are NOT updated: they contain ASCII diagrams with width-constrained boxes that would mis- align with the longer "GitHub Copilot CLI" string, and they're cross-language docs shared across all SDKs (Node, Python, Go, .NET, Rust) — out of scope for this Rust SDK release prep. Validation: cargo doc -D warnings clean, cargo fmt --check clean, cargo clippy clean, 209 tests pass (no semantic changes, only doc strings). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review: add prompts/attachments scenario + README parity sections Closes the two `github-actions` bot findings on PR #1164 that were the only remaining unresolved threads after the human-review cluster. **`prompts/attachments` Rust scenario** (PR comment 3164480775) The bot flagged that `prompts/attachments` was missing from the Rust coverage table — implemented in TS / Python / Go / .NET, full attachment support already in the Rust SDK (`Attachment::File` / `Blob` / `Selection`, `MessageOptions::with_attachments`). Accidental omission from the rest of the Rust port, not a deliberate deferral. - `test/scenarios/prompts/attachments/rust/{Cargo.toml, src/main.rs}` added, mirroring the existing four-language implementations (`claude-haiku-4.5`, replace-mode system prompt, single-file attachment from `sample-data.txt`, prompt asks "what languages are listed in the attached file?", verify.sh greps for TypeScript|Python|Go in the response). - `test/scenarios/RUST_COVERAGE.md`: new ✅ row. - `test/scenarios/prompts/attachments/README.md`: new Rust syntax row in the File and Blob attachment tables. - `test/scenarios/prompts/attachments/verify.sh`: Rust build + run steps wired into both phases. `cargo build` clean locally. **README parity sections** (PR comment 3164686206) The bot flagged that the Rust README has good coverage of the core API but is missing dedicated sections that exist in the Go / Python / .NET READMEs for several features the Rust SDK fully implements. None of these are implementation gaps — discoverability gaps for readers coming from other SDKs. Six new brief sections added to the API Reference between "Capabilities & Elicitation" and "Progress Reporting", each with a runnable code example: - **Slash Commands** — `CommandHandler` + `CommandDefinition::new` with the `name` / `description` wire-only contract called out. - **User Input Requests** — `SessionHandler::on_user_input` override example with the `None`-falls-back-to-CLI semantics. - **Streaming** — `streaming: true` + delta-event match loop reading the `delta` field from `event.data` (verified against the actual `SessionEvent` shape). - **Infinite Sessions** — `InfiniteSessionConfig::workspace_path`, compaction event names, resume-via-`Client::resume_session`, default state location at `~/.copilot/session-state/{sessionId}`. - **Custom Providers (BYOK)** — `ProviderConfig.provider_type` / `base_url` / `bearer_token` / `headers`, with the `openai`/`azure`/`anthropic` provider types and the `wire_api: "completions" | "responses"` flag. - **Telemetry** — `TelemetryConfig` + `OtelExporterType::OtlpHttp`/ `File` with the env-var injection note and the no-OpenTelemetry-dependency guarantee. All API shapes verified against the actual source. Caught one shape-mismatch while drafting (initial draft had `provider.kind: Some(...)` with `base_url: Option<String>`; actual API is `provider.provider_type: Option<String>` + `base_url: String`) — corrected in the final draft. cargo doc -D warnings clean, fmt --check clean, clippy clean, 209 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add get_model and send_telemetry to Rust-only API list Bot review on PR #1164 (comment 3164786116) caught two more accidental omissions in the README's 'Rust-only API' section. Both methods are real and exposed; just missing from this inventory: - Session::get_model — wraps session.model.getCurrent. Folded into the existing 'First-class Session convenience methods' bullet. - Client::send_telemetry / Session::send_telemetry — wrap sendTelemetry / session.sendTelemetry. Added a new dedicated bullet because no other SDK exposes these RPC endpoints in their public API at all (not even via typed namespaces), so it's a higher-divergence item worth its own line. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix update-copilot-dependency: format Rust generated output The CLI-version-bump workflow regenerates per-language types via \`scripts/codegen/npm run generate\` and then formats the output. The formatting step covered TypeScript (prettier) and .NET (dotnet format) but not Rust, so future CLI bumps would land unformatted generated code that codegen-check.yml then catches as a diff. Mirrors the fix already applied to codegen-check.yml in 2766a79: install nightly rustfmt alongside stable, then run \`cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml\` on the rust crate after generation. Same toolchain pin, same config — keeps the two CLI-codegen surfaces converging on the same canonical output. Caught by SDK + CLI version coordination research session. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Client::get_status and Client::get_auth_status wire method names Two more wire-shape bugs in the same class as `c58e2f2` (elicitation `requestedSchema`) and `64541af` (list_sessions filter wrapping): hand-authored RPC wrappers calling JSON-RPC methods that don't exist on the CLI runtime, masked by mock-server tests asserting the same wrong names the implementation sent. The CLI runtime (verified via `@github/copilot@1.0.39` app.js) registers two top-level RPCs: - `status.get` (returns version + protocol_version) - `auth.getStatus` (returns is_authenticated + auth_type) Every other public SDK calls those exact names: - Node `nodejs/src/client.ts:980, 992` - Go `go/client.go:1239, 1257` - Python `python/copilot/client.py:1806, 1827` - .NET `dotnet/src/Client.cs:718, 732` The Rust hand-authored layer was sending `getStatus` and `getAuthStatus` — names that don't exist on the wire. Result: both methods would have returned a method-not-found error (or a misleading no-such-method log on the CLI side) instead of the expected payload. The schema doesn't currently typed-define these top-level RPCs (only `session.auth.getStatus` is in the schema, a different namespaced endpoint), so the codegen didn't catch the hand-authored layer's drift. The mock-server test `client_rpc_methods_send_correct_method_names` asserted on the same wrong names the implementation used, so the bugs round-tripped through both ends. Same root cause as the elicitation and list_sessions tests. Fix: - `lib.rs:1301`: `"getStatus"` → `"status.get"` - `lib.rs:1308`: `"getAuthStatus"` → `"auth.getStatus"` - `tests/session_test.rs:488-509`: assertions migrated to the canonical names AND new explicit-absence assertions (`assert_ne!(request["method"], "getStatus")` / `"getAuthStatus"`) so the same regression class can't be reintroduced silently. Doc-comment on the test names the source- of-truth references (CLI runtime registration, four-SDK call sites). - `CHANGELOG.md`: new `### Fixed` entry documenting the wire-name 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 github-actions bot on PR #1164 (comments 3164826435 and 3164826441). Verify-before-drafting tally now 14 / 15 for these two saves. This brings the wire-shape audit save count to 4 (elicitation, list_sessions, get_status, get_auth_status) — all four were hand-authored RPC wrappers asserting on the implementation's wrong shape rather than the schema/runtime's expected shape. The structural fix (regression-prevention asserts in tests) is now in place for all four; the same bug class is no longer silently reproducible. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refactor JsonRpcClient writer to actor pattern (cancel-safety) Closes RFD-400 review findings #1 (writer mutex desync hazard) and #4 (send_request pending-entry leak on cancellation). See cancel-safety review session db4b1ac8-... for the full report. The previous design held a `tokio::sync::Mutex<Box<dyn AsyncWrite>>` across `write_all` + `flush` for Content-Length-framed JSON-RPC messages. Caller cancellation mid-frame (e.g. wrapping `Session::send` or `Client::call` in `tokio::time::timeout` or `select!`) could leave a partial frame on the wire, permanently desyncing the transport for the rest of the connection. The reader on the CLI side would block forever waiting for the missing bytes, and the next outbound message would be interpreted as the tail of the previous body. Fix: writer-actor task fed by an `mpsc::UnboundedSender<WriteCommand>`. Public `write` and `send_request` pre-serialize the body, enqueue the frame + an ack `oneshot`, and await the ack. Caller cancellation drops the ack receiver but the actor still completes the in-flight write — the frame either lands atomically or surfaces an io::Error to the (possibly already-dropped) ack receiver. A partial frame can never appear on the wire. Design notes: - Pre-serialize before enqueue: actor receives `Vec<u8>` (header + body, already concatenated). Serde errors fail synchronously at enqueue time before touching the actor — cleanest cancel-safety story. - Unbounded sender: RFD 400 explicitly permits this for cancel-safety, and the producer-consumer ratio is naturally bounded by request/ response semantics on the wire. - Writer actor lives until the JsonRpcClient drops. Channel closes on Drop -> actor sees recv None -> flushes and exits. `pending_requests` cleanup (#4): added a small `PendingGuard` RAII helper in `send_request` that arms after the map insert and disarms before the success return. Caller cancellation drops the guard, which removes the pending-entry synchronously via parking_lot::RwLock (the map mutex is converted in this commit too — it was never held across .await anyway, so the conversion is a pure cleanup). Mutex conversions in this commit: - `JsonRpcClient::pending_requests`: `tokio::sync::RwLock` -> `parking_lot::RwLock`. Lock is never held across .await; conversion is also what enables the synchronous PendingGuard drop. - The previous `Arc<Mutex<Box<dyn AsyncWrite>>>` is gone entirely. The actor task owns the writer directly with no shared synchronization. Tests: two new regression tests in tests/jsonrpc_test.rs - `write_actor_completes_on_caller_cancel`: tiny duplex (8 bytes) + gated reader so the actor's write_all blocks waiting for room. Race the caller's future against a sleep; sleep wins, future is dropped while suspended on ack_rx.await. Then enqueue a second write and release the reader. Both frames must land on the wire intact, in order, with no partial frame from the cancelled write. - `send_request_cancellation_does_not_leak_pending`: cancel an in-flight request, then send a (late) response for the cancelled id to verify the read loop logs and discards it without colliding, then send a fresh request and verify it works normally. Existing 209 tests continue to pass without modification — the wire semantics are unchanged. Total now 211. Validation: cargo test, cargo doc -D warnings, cargo +nightly fmt --check, cargo clippy --all-features --all-targets -- -D warnings all clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add WaiterGuard RAII for Session::send_and_wait (cancel-safety) Closes RFD-400 review finding #2 (idle_waiter slot leak on external cancellation). See cancel-safety review session db4b1ac8-... for the full report. `Session::send_and_wait` installs an `IdleWaiter` slot under `self.idle_waiter`, then awaits the response. Internal failure paths (send_inner error, oneshot closure, internal timeout) clean up the slot. External cancellation — caller wraps `send_and_wait` in `tokio::time::timeout`, races it in `select!`, or aborts the JoinHandle — does not. The cleanup at the event loop's `else` branch only fires when the channel itself closes (i.e., the entire session is going away), not when an individual call is cancelled. Effect: any external cancellation between "install waiter" and "drain response" leaves `idle_waiter = Some(...)` forever. All subsequent `send` and `send_and_wait` calls return `SendWhileWaiting`, permanently bricking the session. Fix: `WaiterGuard` RAII helper that takes the slot on Drop. Construct right after install, let RAII handle every exit path. The explicit `self.idle_waiter.lock().take()` calls inside the function disappear, and the slot is cleared on every cancellation path automatically. Mutex conversion (folded in from finding #5): - `Session::idle_waiter`: `tokio::sync::Mutex<Option<IdleWaiter>>` -> `parking_lot::Mutex<Option<IdleWaiter>>`. Lock is never held across `.await` in any code path; the conversion is what makes WaiterGuard's synchronous Drop work without needing an async-spawn fallback. `event_loop` mutex stays `tokio::sync::Mutex` for now — commit C converts it as part of cooperative shutdown (so the conversion lands alongside the matching change to Drop for Session). Tests: two new regression tests in tests/session_test.rs - `send_and_wait_outer_cancellation_clears_waiter`: outer `tokio::time::timeout(50ms, ...)` around `send_and_wait` with a 60-second inner timeout. Outer fires, dropping the future. Verify the next `send` succeeds (no SendWhileWaiting). - `send_and_wait_drop_clears_waiter`: explicit `JoinHandle::abort` of an in-flight send_and_wait. Verify the next `send` succeeds. Existing 211 tests continue to pass; total now 213. Validation: cargo test --all-features, cargo doc -D warnings, cargo +nightly fmt --check, cargo clippy -- -D warnings all clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Cooperative event-loop shutdown via Notify (cancel-safety) Closes RFD-400 review finding #3 (JoinHandle::abort lands at any await point in the event loop body) and the remaining bit of #5 (Session::event_loop tokio::sync::Mutex -> parking_lot::Mutex). See cancel-safety review session db4b1ac8-... for the full report. The previous design called `JoinHandle::abort()` in two places to stop the event-loop task: explicitly in `Session::stop_event_loop` and best-effort in `Drop for Session`. RFD 400's strongest single recommendation about task aborts: > With the abort(), the future returned by do_long_running_operation() > can be cancelled at *any await point*. It is unlikely that arbitrary > async Rust code is resilient to cancellations anywhere during > control flow. ... task aborts should be avoided entirely. The event loop body has many await points: - The outer `select!` itself - `handle_notification`'s nested awaits (acquire idle_waiter lock, send on broadcast, dispatch RPC callbacks for permission/tool/ elicitation requests) - `handle_request`'s inline awaits (deserialize, dispatch handler, build response, write back to client) If `abort()` landed mid-handler — say partway through writing the permission response — the CLI would keep that requestId in flight forever. Same hazard for the idle_waiter oneshot: an abort between "observed session.idle" and "send on waiter.tx" leaves the caller's `send_and_wait` await blocked until the entire transport tears down. Fix: cooperative cancellation via `Arc<tokio::sync::Notify>`. The event-loop body adds a fourth select arm: tokio::select! { _ = shutdown.notified() => break, Some(notification) = notifications.recv() => { ... } Some(request) = requests.recv() => { ... } else => break, } `Notify::notified()` is cancel-safe per RFD 400's framework: the unselected branch losing a poll is fine — it's reconstructed on the next iteration. `notify_one()` (NOT `notify_waiters`) is used because it buffers a single signal: if the loop is currently inside `handle_request(...).await` (no `.notified()` future registered), the signal is held until the handler returns and the loop re-enters select on the next iteration. `notify_waiters()` would silently lose the signal in this case. `stop_event_loop` calls `shutdown.notify_one()` then awaits the JoinHandle. The loop drains its current iteration's handler before observing the buffered signal and exiting cleanly — no mid-protocol state, no orphaned RPC ids on the CLI side. `Drop for Session` calls `notify_one()` and unregisters from the router. The handle is left in `event_loop` for tokio to reap when the loop's spawned task exits naturally; we intentionally don't await it because Drop is sync. Spawned child tasks inside `handle_notification` (permission/tool/elicitation callbacks) intentionally outlive the parent loop — RFD 400's "spawn background tasks to perform cancel-unsafe operations" pattern, documented in the loop's comment. Mutex conversion (folded in from finding #5): - `Session::event_loop`: `tokio::sync::Mutex<Option<JoinHandle<()>>>` -> `parking_lot::Mutex<Option<JoinHandle<()>>>`. Lock is never held across `.await` and the conversion eliminates the `try_lock()` Drop footgun (silent no-op on contention with stop_event_loop). This is the last tokio::sync::Mutex on Session — the import is no longer needed. Tests: two new regression tests in tests/session_test.rs - `stop_event_loop_completes_in_flight_handler`: install a `SlowHandler` whose userInput.request callback sleeps 150ms. Send the request from the server side, then 20ms in (handler is mid- flight) call `stop_event_loop`. Verify the response with `answer: "completed"` lands on the wire BEFORE stop_event_loop returns — proves the handler ran to completion, not aborted mid-await. - `drop_session_does_not_abort_handler`: same shape but via `drop(session)` instead of `stop_event_loop`. Tracks handler completion via an AtomicBool that the handler sets only after its sleep completes; verifies both that the response lands on the wire AND that the handler actually completed despite the Session being dropped. Existing 213 tests continue to pass; total now 215. Validation: cargo test --all-features, cargo doc -D warnings, cargo +nightly fmt --check, cargo clippy -- -D warnings all clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Document cancel-safety for public async APIs (RFD-400) Closes RFD-400 review finding #6 (per-method cancel-safety documentation). Doc-only follow-up to commits A/B/C; no behavioral change. RFD 400 § "Marking APIs as cancel-unsafe" calls for explicit documentation; with the underlying behavior now correct after A/B/C, documenting the contract makes it inspectable from rustdoc rather than requiring callers to read the implementation. Added `# Cancel safety` rustdoc sections to: - `Session::send` — cancel-safe (writer-actor); message lands on the wire even if the await is cancelled. - `Session::send_and_wait` — cancel-safe (WaiterGuard RAII); outer-cancellation distinct from internal-timeout, both clear the slot. - `Session::abort` — cancel-safe (single RPC via writer-actor). - `Client::call` — cancel-safe at the wire level; documents the caveat that the CLI may still process the request even if the caller doesn't see the response (idempotent vs non-idempotent methods). - `Client::stop` — cancel-unsafe-but-recoverable; the body sequentially destroys sessions (each individually cancel-safe) before killing the child. The existing `tokio::time::timeout` example with `force_stop` fallback is the documented recovery path. - `Client::force_stop` — synchronous, cannot be cancelled. Designed as the recovery path for `stop`'s timeout case. - `Subscription::recv` — cancel-safe by virtue of `tokio::sync::broadcast` + `BroadcastStream`. Validation: cargo doc -D warnings clean, cargo test (215 pass), cargo +nightly fmt --check clean, cargo clippy clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add scheduled trigger to update-copilot-dependency.yml Closes "Mode 1 hands-off loop" gap A from the SDK release plan-doc: update-copilot-dependency.yml is workflow_dispatch only today, so when a new @github/copilot publishes nobody automatically opens the regen PR. Today's bot-driven bump (commit ecdc5ec, "Update @github/copilot to 1.0.39 (#1167)") was triggered manually after the plan-doc session noticed the gap. Goal: the SDK auto-detects new CLI versions, opens a draft PR with all per-language regen + formatting + lockfile updates, and a human just reviews + merges. github-app's bundled-binary tracker picks the new SDK up via crates.io dependabot in the symmetric direction. Changes: 1. **`schedule` trigger.** Daily cron at 10:00 UTC, deliberately symmetric with github-app's check-bundled-binary-versions.yml so the SDK and consumer halves of the loop tick in lockstep on release days. 2. **`version` input is now optional.** Manual override still works for emergency back-patches or cherry-picking specific prereleases; an empty input on workflow_dispatch flows through the same auto-detect path the schedule uses. 3. **Two-job split: `detect` -> `update`.** The detect job is cheap (one `npm view @github/copilot version` + a jq read of nodejs/package.json + a strip of the `^`/`~` prefix). It sets two outputs: - `version`: the resolved target (input override, or latest from npm) - `should_continue`: 'true' if the resolved version differs from the current pin, 'false' otherwise The update job has `needs: detect` and `if: needs.detect.outputs.should_continue == 'true'`, so it skips entirely when there's nothing to do. This keeps the no-op daily cost effectively free. 4. **Validation moves into the resolve step.** Both manual and auto-detected versions go through the same semver regex check. The previous standalone "Validate version input" step is retired; the detect job's resolve step now owns format validation, version comparison, and skip logic. 5. **All `${{ inputs.version }}` references become `${{ needs.detect.outputs.version }}`.** Same value, just routed through the detect job's outputs so manual override and auto-detect both feed the update job uniformly. Doesn't touch SDK source; cargo test / cargo publish --dry-run are no-op confirmation post-merge. Companion gap on the github-app side (auto-open sync PR after each new SDK release) lives on Sync session PR #4140; this commit unblocks the SDK half. Plan-doc capture: tracked under "Steady-state CLI bump automation (Mode 1 hands-off loop)" — gap A closed; gap B (github-app sync-PR auto-open) remains. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Correct get_quota doc: endpoint exists cross-SDK, only wrapper is Rust-only Bot review caught an inaccuracy in the rustdoc and README: the prior wording claimed the underlying account.getQuota JSON-RPC endpoint was exposed only by the Rust SDK. In fact only the top-level Client::get_quota convenience wrapper is Rust-unique; the endpoint itself is generated and exposed cross-SDK via each SDK's typed rpc() namespace. Verified cross-SDK exposure: - Node: nodejs/src/generated/rpc.ts:2203 (account.getQuota in rpc() namespace) - Python: python/copilot/generated/rpc.py:5647 (rpc().account.get_quota) - Go: go/rpc/generated_rpc.go:2252 (RPC.Account.GetQuota) - .NET: dotnet/src/Generated/Rpc.cs:3140 (Rpc().Account().GetQuotaAsync) - Rust: rust/src/generated/rpc.rs:90 (rpc().account().get_quota) Updated rustdoc on Client::get_quota in rust/src/lib.rs and the "Rust-only API" bullet in rust/README.md to correctly state that the wrapper is Rust-only while the underlying endpoint is available across all SDKs via their typed rpc() namespaces. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add ClientOptions::new() and Tool::new() builder methods Mirrors the existing MessageOptions::new() / with_* builder pattern on the two other public #[non_exhaustive] config types that consumers construct most often. Closes the cross-crate ergonomics gap that #[non_exhaustive] introduces: external callers can't use struct- literal syntax (with or without ..Default::default()) on these types, so every field assignment becomes its own mut-let statement. The builder shape collapses that into a single chained expression. Caught by github-app PR #4140 review on src-tauri/src/session/cli.rs: the mut-let-and-assign pattern read as awkward to reviewers even after trimming to the four fields that diverge from default. Same ergonomic friction that prompted the make_tool() helper consumers were writing for Tool. Builder ships zero-cost convenience without giving up #[non_exhaustive]'s SemVer protection on growable config struct. ClientOptions builder (rust/src/lib.rs): - new() -> Self (documented entry point) - with_program(impl Into<CliProgram>) - with_prefix_args<I: IntoIterator<...>>(args) - with_cwd(impl Into<PathBuf>) - with_env<I: IntoIterator<(K, V)>> - with_env_remove<I: IntoIterator<S>> - with_extra_args<I: IntoIterator<S>> - with_transport(Transport) - with_github_token(impl Into<String>) - with_use_logged_in_user(bool) - with_log_level(LogLevel) - with_session_idle_timeout_seconds(u64) - with_list_models_handler<H: ListModelsHandler + 'static> (Arc-wrapped internally) - with_session_fs(SessionFsConfig) - with_trace_context_provider<P: TraceContextProvider + 'static> (Arc-wrapped internally) - with_telemetry(TelemetryConfig) Tool builder (rust/src/types.rs): - new(impl Into<String>) -> Self (name + defaults) - with_namespaced_name(impl Into<String>) - with_description(impl Into<String>) - with_instructions(impl Into<String>) - with_parameters(serde_json::Value) - with_overrides_built_in_tool(bool) - with_skip_permission(bool) Pure additive: no existing ClientOptions::default() / mut-let patterns break, no public field visibility changes. Both builders compose by wrapping ::default() and mutating-and-returning self, matching MessageOptions's idiom. Tests: client_options_builder_composes verifies all 10 chained builder methods produce the expected internal state. tool_builder_composes does the same for Tool's seven methods, plus tool_with_parameters_handles_non_object_value covers the HashMap-from-Value extraction edge case (json!(null) -> empty map rather than panic). CHANGELOG: new "Builder ergonomics" subsection under Documentation, explaining the rationale and listing both builders. Validation: 220 tests pass (was 215 + 5 new builder tests across unit + doctest), cargo doc -D warnings clean, cargo fmt --check clean, cargo clippy --all-features --all-targets -- -D warnings clean. Companion ask from github-app PR #4140 review (Sync session) follows up: github-app's `cli.rs` and ~25 Tool::new() call sites will switch to the builder form once this lands and the next sync round-trip pulls it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add per-field builder methods to SessionConfig and ResumeSessionConfig Mirrors the just-shipped ClientOptions/Tool builder pattern (commit 2d725a5) on the two structs that consumers reach for most when wiring up a session. Both types are #[non_exhaustive], so external callers have been writing the same mut-let-and-assign block at every construction site: let mut config = ResumeSessionConfig::new(session_id); config.client_name = Some("...".into()); config.streaming = Some(true); config.tools = Some(tools); // ...10+ more lines The new chainable form collapses that to: ResumeSessionConfig::new(session_id) .with_client_name("...") .with_streaming(true) .with_tools(tools) // ... Both types have ~30 public fields each. Coverage: SessionConfig: with_session_id, with_model, with_client_name, with_reasoning_effort, with_streaming, with_tools, with_available_tools, with_excluded_tools, with_mcp_servers, with_disabled_mcp_servers, with_env_value_mode, with_enable_config_discovery, with_request_user_input, with_request_permission, with_request_exit_plan_mode, with_request_auto_mode_switch, with_request_elicitation, with_skill_directories, with_disabled_skills, with_agent, with_custom_agents, with_config_dir, with_working_directory, with_github_token, with_include_sub_agent_streaming_events, with_system_message, with_skip_compaction_history. ResumeSessionConfig: same shape minus model/reasoning_effort/ available_tools/disabled_skills/disabled_mcp_servers (which the resume RPC doesn't accept), plus with_disable_resume. Existing closure-installing chains (with_handler, with_hooks, with_transform, with_commands, with_session_fs_provider, approve_all_permissions, deny_all_permissions, approve_permissions_if) and the manual mut-let pattern continue to work unchanged. The hooks bool is intentionally not exposed via a new with_* setter; with_hooks(handler) auto-enables the flag, and power users wanting to override can still set the .hooks field directly. Adds two integration-style tests covering composition across all field categories (scalars, bool flags, Vec setters, struct setters, HashMap setters) for both types. CHANGELOG entry extends the existing "Builder ergonomics" subsection. This is the third commit in the pre-1.0 builder-pattern series (MessageOptions in the original branch, ClientOptions/Tool in 2d725a5, SessionConfig/ResumeSessionConfig here). Pure additive, zero migration cost. Driven by github-app PR #4140 reviewer feedback on src-tauri/src/session/cli.rs and core.rs construction sites. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Scrub github-app references from public-repo files The copilot-sdk repo is public; github-app is private. Public-repo content must not name or depend on github-app, even in comments or test fixtures. - .github/workflows/update-copilot-dependency.yml: drop the "symmetric with github-app's scheduled" framing on the daily cron; rewrite the comment to describe what the schedule does in SDK-local terms only. - rust/CHANGELOG.md: rephrase the Tool builder rationale to talk about "real-world consumer code" rather than github-app's tool catalog specifically. - rust/src/lib.rs: change the `source_name` test fixture from "github-app" to "my-app" — it was just an example string for OTEL source-name plumbing, but the explicit consumer naming is unnecessary and confusing in a public repo. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Round out builder coverage and document the Option<T> escape hatch After landing per-field builders on the four highest-traffic consumer-facing config structs (MessageOptions, ClientOptions, Tool, SessionConfig/ResumeSessionConfig), this commit closes the long tail. Rust ecosystem research (tokio, reqwest, tonic, axum, AWS SDK) confirms two design points: 1. The dominant pattern for #[non_exhaustive] config builders is the single `with_<field>(impl Into<T>) -> Self` setter that always Some-wraps. AWS SDK's dual `field` + `set_field(Option<T>)` shape is justified by codegen, not by ergonomics — and the `impl Into<Option<T>>` magic that some older crates use has fallen out of fashion (it conflicts with `impl Into<T>` chaining and produces poor type-inference errors). 2. Direct field assignment on a `pub` field is idiomatic Rust for forwarding `Option<T>` values from upstream code. The http::request::Parts / hyper::Body::Builder pattern. We already support it; we just hadn't documented it. This commit therefore: - Adds full builders to the four bare consumer-facing structs: CustomAgentConfig (name + prompt required, six with_* setters), InfiniteSessionConfig (three with_* setters), ProviderConfig (base_url required, six with_* setters), SystemMessageConfig (three with_* setters). - Expands TraceContext with a symmetric new() + with_traceparent pair alongside the existing from_traceparent shorthand. The shorthand is now expressed in terms of the chain (`new().with_traceparent(x)`) for consistency. - Documents the direct-field-assignment escape hatch on SessionConfig and ResumeSessionConfig, with a doc example showing how to mix the fluent chain (for compile-time-known values) with direct mutation (for Option<T> pass-throughs). Per-field `with_<x>_opt(Option<T>)` setters were considered and deliberately rejected: doubling the method count for ~30-field configs would hurt rustdoc discoverability and conflict with the single-setter precedent we just shipped. The mut-let escape hatch covers the same use case without polluting the API surface. Adds 5 unit tests (one per new builder + a TraceContext suite) and extends the existing CHANGELOG "Builder ergonomics" subsection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert "Add scheduled trigger to update-copilot-dependency.yml" This reverts commit a88c3ebc7cf25d5b7194a37b51e52bf6a62bc8fc. The scheduled trigger was added to close a hands-off-loop gap that was framed in terms of a downstream consumer's needs. On reflection, the SDK should not be the only language in this repo with scheduled auto-detect; that's a cross-language platform decision, not a single- SDK one. Reverting brings update-copilot-dependency.yml back to manual-only (workflow_dispatch with a required version input) — the same shape as every other language's CLI update workflow in this repo. When the SDK collectively decides to add scheduled CLI tracking, that should land as a unified change across all languages, not as a Rust-only special case driven by a downstream consumer's automation desires. Manual dispatch remains the supported path until then. Conflict resolution: the e93ce8e ("Scrub github-app references") commit landed after a88c3eb and rewrote the cron block's comment. The revert drops the entire schedule: block (including the rewritten comment), restoring the workflow to its pre-a88c3eb manual-only shape exactly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add TelemetryConfig builder methods Final missing piece in the consumer-facing #[non_exhaustive] config struct audit. `TelemetryConfig` had public optional fields (otlp_endpoint, file_path, exporter_type, source_name, capture_content) and an is_empty() helper, but no `new()` or `with_*` setters — leaving callers to write `TelemetryConfig { otlp_endpoint: Some(...), ..Default::default() }` which doesn't compile from outside the crate due to #[non_exhaustive]. Adds `TelemetryConfig::new()` (delegates to Default) and `with_*` setters for all five fields, matching the existing pattern. The is_empty() helper stays unchanged. After this commit, every consumer-facing #[non_exhaustive] struct in the SDK has a fluent builder: ClientOptions, TelemetryConfig, Tool, CommandDefinition, CustomAgentConfig, InfiniteSessionConfig, ProviderConfig, SessionConfig, ResumeSessionConfig, SystemMessageConfig, MessageOptions, SessionFsConfig, TraceContext. One unit test (telemetry_config_builder_composes) covering all five setters + is_empty behavior on a fresh new(). Extends the existing CHANGELOG "Builder ergonomics" round-out bullet. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Switch build.rs from curl to ureq with bounded retries The bundled-CLI build path (gated by the embedded-cli feature and the COPILOT_CLI_VERSION env var) previously shelled out to `curl` to fetch SHA256SUMS.txt and the platform tarball from the copilot-cli releases. Two problems with that: 1. Implicit system dependency. `expect("curl is required ...")` panics when curl isn't on PATH — true on minimal Windows MSVC environments, some Docker images, and the CI matrices crates.io publishers tend to validate against. crates.io build environments themselves don't guarantee curl. 2. No retry on transient errors. Network blips during CI bundling surfaced as build failures rather than self-healing waits. Switch to ureq 2 with default-features=false + ["tls"] (rustls + webpki-roots, pure-Rust TLS, no OpenSSL or native-tls). Adds about 1.7MB and ~12 transitive deps to the build graph but only when the embedded-cli feature is on; the default cargo build is unaffected. Retry policy: - Up to 3 retries (4 total attempts) with exponential backoff: 1s, 2s, 4s. - 5xx responses, connect timeouts, read timeouts, and other transport-layer errors are treated as transient and retried. - 4xx responses fail fast (the URL is wrong; retrying won't help). Connect/read timeouts (30s/120s respectively) prevent indefinite hangs on stalled connections. The cache-hit path still bypasses the network entirely; only fresh downloads pay the HTTP cost. End-to-end smoke test against https://github.com/github/copilot-cli/releases/download/v1.0.39 : SHA256SUMS download succeeded, tarball download succeeded (134MB extracted, 44MB compressed), SHA-256 verified, embedded build succeeded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: validate embedded-cli bundle build on macOS / Linux / Windows The default `cargo test` job in rust-sdk-tests.yml runs on all three supported platforms but does not set `COPILOT_CLI_VERSION` — which means `build.rs` short-circuits immediately and the embedded-cli bundle path (download + SHA-256 verify + extract + zstd compress + embed) is never exercised in CI. After the recent switch from shelling out to `curl` over to `ureq` with retry logic in `build.rs`, that gap matters: the new HTTP client + cross-platform TLS code path needs cross-platform CI coverage before it ships. Adds a parallel `bundle` job to the same workflow: - 3-OS matrix: ubuntu-latest, macos-latest, windows-latest. - Reads the pinned CLI version from `nodejs/package.json` so the bundle test always tracks the same version the rest of the SDK ships against — no separate version source to drift. - Caches the downloaded archive in `./rust/.bundled-cli-cache` keyed on OS + CLI version, so steady-state CI doesn't refetch ~130 MB on every run. Cache miss (e.g. first run after a CLI bump) exercises the full download path end-to-end, which is the regression surface this job is meant to catch. - Runs `cargo build --features embedded-cli` to drive `build.rs` through the full pipeline. Doesn't run `cargo test` — the test job already covers runtime behavior; this job is specifically validating the bundle build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix duplicate user_input dispatch (github-app#4249) The CLI sends two messages for every user-input prompt: 1. A `user_input.requested` session-event notification, intended for observers (UI streaming, telemetry, logging). 2. A `userInput.request` JSON-RPC method call, which is the actual ask-and-wait prompt that drives the handler. The Rust SDK's session event loop was wiring up `HandlerEvent::UserInput` dispatch on BOTH paths. Result: the consumer's `on_event` ran twice for every prompt, which surfaced as duplicate `ask_user` / `exit_plan` widgets in github-app's session UI (#4249). Other SDKs (Node, Python, Go, .NET) only register the JSON-RPC handler; the notification is purely observational. Aligning with that convention. Changes: 1. `session.rs`: drop the `SessionEventType::UserInputRequested` auto-dispatch arm. The CLI's follow-up `userInput.request` JSON-RPC call still drives the handler. Replaced with a short comment block explaining why the notification is intentionally a no-op (and citing #4249 so the next person tempted to add it back understands the trade). 2. `tests/session_test.rs`: replaced the `user_input_requested_event_dispatches_to_handler_and_responds` test (which asserted the broken behavior) with a regression test (`user_input_requested_notification_does_not_double_dispatch`) that: - sends a `user_input.requested` notification and asserts no wire response, no handler invocation; - then sends a `userInput.request` JSON-RPC call and asserts the handler fires exactly once. This locks the fix in place and makes a future re-introduction surface immediately as a test failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Drop Skills section from .github/copilot-instructions.md stephentoub and tclem agreed the Skills section is unnecessary prompt-pollution. Skills are auto-discovered by Copilot CLI without needing to be enumerated in the instructions, and the one-line description of each skill duplicates what the SKILL.md frontmatter already says. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Render experimental RPC methods as a styled rustdoc admonition Codegen previously emitted a single bare `/// Stability: experimental.` line on each experimental wire method. stephentoub flagged this as weak signal -- there's no way for a consumer to tell from the rustdoc that they're using something that may change. Switch the rustdoc emit to a `<div class="warning">...</div>` block, which renders on docs.rs as a yellow-bordered callout (the standard rustdoc admonition convention). Matches the visibility Python's generated docs already get via Sphinx's `.. warning::` directive. This is documentation-quality only; no compile-time opt-in. Stable Rust has no first-class `#[experimental]` attribute (`#[unstable]` is rustc-internal, std-only). If we ever want compile-time friction the ecosystem patterns are a Cargo feature gate (e.g. `tokio_unstable`- style `features = ["experimental"]`) or an `unstable` module path; both are larger codegen changes affecting every experimental RPC across all SDK surfaces. Punted for 0.1.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(skill): clarify that handler thread-safety is compiler-enforced The "anti-pattern" framing in examples.md was misleading -- it implied the RefCell example might compile but produce bugs at runtime. In reality, SessionHandler's `Send + Sync + 'static` trait bound rejects any handler whose state contains a non-Sync type (RefCell, Cell, Rc), so the example fails at the impl site, not at use. Reframe as "Won't compile", show the actual trait-bound error inline, and add a one-paragraph note explaining the compiler-level enforcement so a reader doesn't have to mentally trace `spawn -> Send + Sync -> RefCell !Sync`. Addresses stephentoub's review feedback that the prior framing read as "we've buried some unsafe usage." Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refactor session shutdown from Notify to CancellationToken Replaces the per-session `Arc<Notify>` shutdown signal with `tokio_util::sync::CancellationToken`, the canonical primitive for SDK-internal task coordination in async Rust. Same cooperative-shutdown semantics, materially better fit for the use case: - `tonic` uses `CancellationToken` for the equivalent task coordination case (request cancel propagated into spawned server handlers). - Parent/child token tree gives us a clean way to expose a public `Session::cancellation_token()` accessor that returns a child token — consumers can `select!` on `child.cancelled()` to bind their own work to the session lifetime, but cancelling the child does NOT shut down the session itself (child cancel is isolated). - `Drop` impl just calls `cancel()`, no buffering quirks (Notify's `notify_one` was already documented as buffer-on-no-listeners; token cancellation is sticky, which is the simpler mental model). Plumbing: - `tokio-util = { version = "0.7", default-features = false }` added as a runtime dep. No new transitive deps the SDK didn't already pull through tokio. - `Session.shutdown` field type changes from `Arc<Notify>` to `CancellationToken` (which is itself internally `Arc`-backed and `Clone`). - `stop_event_loop()` calls `cancel()` instead of `notify_one()`. - `Drop` impl calls `cancel()` instead of `notify_one()`. - Event-loop `select!` arm becomes `_ = shutdown.cancelled() => break`. - New public `Session::cancellation_token()` returning a `child_token()` — power-user API for binding external tasks to session lifetime. Tests: - `cancellation_token_fires_on_session_drop`: dropping the session fires the child token within 2s. - `cancellation_token_child_cancel_does_not_kill_session`: cancelling a child does not propagate up to the parent. Closes the cancellation idiom gap stephentoub flagged on the SKILL doc. The framing "drop the future or call abort()" was too absolute — drop- to-cancel is the right primitive for caller-owned futures, but for SDK-internal task coordination CancellationToken is the canonical answer (citations: tokio-util docs, tonic cancellation example, withoutboats "Asynchronous clean-up", `tokio-graceful-shutdown` crate). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(skill): rewrite "Idioms that don't port" with full research backing stephentoub flagged that the "Idioms that don't port from Go or Node" section was lopsided (no C# / Python mention) and that the cancellation guidance was too absolute (it dismissed `ctx.Done()` analogs without mentioning tokio_util's CancellationToken). Both true. Replaces the section with a thorough rewrite covering all four idioms with citations and decision matrices: 1. Event subscription: keep "channels not callbacks" baseline; add the full primitive matrix (mpsc / oneshot / broadcast / watch); explain that the public API should expose `impl Stream<Item = T>` (the canonical IObservable<T> analog), citing tonic / reqwest / sqlx as precedent. 2. Cancellation: rewrite. "Drop = cancel" is the primitive for caller-owned futures. `tokio_util::sync::CancellationToken` is the canonical answer for SDK-internal task coordination — citations to tonic's cancellation example, tokio-util docs, withoutboats blog post, and Cybernetist's task cancellation patterns survey. Notes the SDK's own `Session.shutdown: CancellationToken` field and the `Session::cancellation_token()` accessor. 3. Optional fields: confirm Option<T> + Default baseline; add non_exhaustive + builder pairing (AWS SDK convention); note when typestate / Result-from-build is the right answer. 4. serde JSON: confirm rename_all + per-field rename baseline; add skip_serializing_if = "Option::is_none" for output omission and #[serde(default)] for input tolerance (LSP/JSON-RPC convention). Section title is also de-language-coupled: "Idioms that don't port from other languages" instead of "...from Go or Node". Lists Node / Python / C# / Go equivalents inline where pattern divergence is notable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add agentId envelope field on session events for sub-agent attribution The wire-protocol schema added top-level `agentId?: string` to every session event envelope in commit f8cf846 ("Derive session event envelopes from schema") for sub-agent attribution. Every other SDK carries it; Rust silently drops it at the deserialization boundary. Concretely: the schema describes `agentId` as "Sub-agent instance identifier. Absent for events from the root/main agent and session- level events." Without the field, Rust consumers can't distinguish events emitted by a sub-agent from events emitted by the root agent. Adds: - `pub agent_id: Option<String>` on `types::SessionEvent` (hand- authored consumer-facing). - `pub agent_id: Option<String>` on `generated::session_events::TypedSessionEvent` via codegen update in `scripts/codegen/rust.ts`. - `#[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]` via the existing container `rename_all = "camelCase"` (covered) plus skip-on-None for clean wire output. Round-trip tests cover both struct shapes — sub-agent event with the field set, root-agent event without — to lock the parity in place against future schema changes or codegen regressions. Caught by a fresh parity audit against Node/Python/Go/.NET as part of the post-main-merge gap review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove parity-audit scratch files Accidentally committed by the parity audit run; not intended as repo artifacts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(skill): tighten SKILL.md and codify trait-with-defaults extension pattern Two changes prompted by review: 1. SKILL.md was too prose-heavy for an agent to scan. The "Idioms that don't port from other languages" section had grown to ~110 lines of essay-shaped advice with key rules buried in narrative. Replaced with three tight rule sections (Concurrency primitives, Optional fields and serde, plus a one-paragraph cross-language porting note that points back to the rule sections). Net: 345 -> 255 lines, no rule lost. 2. The trait-vs-callback-fields rule was implicit in the SessionHandler description but not explicit. Codified in "Traits and conversions" as a primary directive: prefer one trait with one default-impl method per event over per-event Box<dyn Fn> fields. Cited the three precedent traits in async Rust: - tower_lsp::LanguageServer (63 methods, default impls; LSP wire protocol) - rmcp::ServerHandler (26 methods, all default; MCP wire protocol) - notify::EventHandler (single on_event(enum) for uniform-shape events) Confirmed via research that no major async Rust crate ships per-event Box<dyn Fn> callback fields as its primary API; the pattern fights Send + Sync + 'static, fragments consumer state across closures, and skips exhaustiveness. The SDK's SessionHandler already uses the recommended shape (per-method with defaults plus a default on_event dispatcher); the doc just hadn't named the pattern. Section ordering tightened: Traits/conversions now leads into Extension points, then Concurrency primitives (channels matrix + cancellation), then Optional fields/serde. Reads top-to-bottom for an agent picking up a fresh task. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add continue_pending_work field to ResumeSessionConfig 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> * Add copilot_home, instruction_directories, tcp_connection_token surface 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> * Replace uuid dep with getrandom helper + reviewer-requested test coverage 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(rust): apply stripBooleanLiterals to schemas 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> * Use typed connect RPC instead of hand-coded JSON for handshake 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> * Remove send_telemetry from public surface (Rust-only quirk) 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> * Remove Rust-only Session convenience methods for cross-SDK parity 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> --------- 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> | 5 个月前 |
| Update SDK snapshot for Copilot CLI 1.0.103-0 | 3 天前 |
| Update SDK snapshot for Copilot CLI 1.0.92-0 | 23 天前 |
| Launch managed SDK servers through the Rust runtime wrapper (#2395) * Integrate out-of-process Rust runtime wrapper Stage and launch copilot-runtime beside runtime.node across all SDKs, with an opt-in local runtime-worktree override until published packages include the wrapper. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Validate runtime wrapper across SDK harnesses Route out-of-process E2E harnesses through the local runtime override while preserving the residual CLI for in-process execution. Keep Java bundled residual injection intact and surface Python fast-exit diagnostics reliably. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Use residual CLI for Python FFI test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Honor per-client runtime environment in .NET Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Handle Rust session requests during creation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Skip Go telemetry callback test in-process Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Add legacy CLI launch escape hatch Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Remove residual Node runtime compatibility Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Remove COPILOT_RUNTIME_PATH override Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * fix(rust): materialize runtime launch contract Ensure managed wrapper launches materialize and hand off the compatible host, and expose the same launch descriptor for health checks and intermediate launchers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Remove residual runtime host contract Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2455380f-4747-4900-89ca-93b30399de03 * fix(rust): materialize sibling CLI host Extract the bundled CLI beside copilot-runtime and runtime.node so managed and intermediate launches retain compatibility through the wrapper's sibling fallback. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * fix(rust): create runtime install directory Ensure a clean bundled runtime cache can publish runtime.node, the wrapper, and sibling CLI before any artifact path exists. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Complete managed runtime bundle materialization Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Remove managed SEA staging for hostless runtime Keep the root CLI artifact bundled for direct and in-process use, while managed out-of-process caches materialize only copilot-runtime and runtime.node. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Stage auxiliary runtime assets from npm packages Preserve unknown package assets by default while filtering known CLI-only content in each SDK's existing staging path. Keep wrapper companions adjacent, migrate caches safely, and retain executable metadata for external runtime tools. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Exclude runtime package documentation from staging Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Finalize runtime wrapper integration after rebase Preserve the explicit .NET single-binary override, refresh stale Rust runtime bundles safely, and document the Python npm registry override. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Use executable cache for Node runtime wrapper Stage the managed wrapper under the platform user cache instead of the temporary directory so noexec temp mounts do not prevent startup. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Temporarily skip extension-host E2E coverage Document PR #2395 and the Rust-only out-of-process transition on Node extension-authored factory coverage, the real-host extension environment test, and .NET extension lifecycle tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Address runtime wrapper review feedback Match the successful grep completion to the grep tool invocation and document Java's bundled runtime-wrapper default. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Stop resolving SEA for in-process hosting Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Keep default runtime bundles SEA-free Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Fix cross-platform runtime integration tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Fix runtime CI harness portability Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Relax closed-stream startup assertion Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Fix failed-start runtime test handling Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Format Python runtime test setup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Clean up failed Node runtime startup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Fix Java token provider sample Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Fix runtime integration CI checks Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Make Java relative path test drive-safe Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Use generated logging for permission failures Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Suppress Node pipe writes after runtime exit Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Preserve Node startup transport errors Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Add Rust extension launch provider Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Clarify extension launch environment Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: efc29e35-611b-46d8-9b8b-e1912d2eccf4 * test(rust): cover runtime extension lifecycle Add a real-wrapper regression that exercises typed provider dispatch, extension registration, tool invocation, disable/re-enable, environment ownership, and process teardown. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * fix(java): honor runtime path environment override Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * test(node): skip host-dependent ask user metadata Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(node): preserve startup stderr after pipe errors Wait for the child close event before selecting startup diagnostics so early EPIPE failures do not hide the runtime's actionable stderr output. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * refactor(node): separate exit and diagnostic waits Keep startup RPCs fail-fast on process exit while using child close only to collect fully drained stderr after an early pipe error. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test(node): preserve ask user metadata coverage Run the Node-hosted metadata assertion through the harness's explicit legacy CLI path instead of disabling it for managed hostless connections. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * refactor(node): simplify startup failure handling Keep startup teardown fail-fast and verify startup failures without waiting separately for diagnostic stream closure. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * style(node): format startup failure test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * test(python): set plan mode before handler test Await the explicit mode transition before sending the replayed prompt so exit_plan_mode is deterministically available to the model. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * test: preserve live in-process runtime state Keep isolated runtime home files until the shared in-process client shuts down so later tests never operate on POSIX-unlinked state. Apply the same lifecycle rule to the Node and Python E2E harnesses. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * test: stabilize transient runtime checks Observe Rust processing state concurrently with message dispatch so fast in-process turns cannot complete before polling starts. Exercise rewind through an edit of a pre-existing file across SDKs, avoiding the runtime's unreliable new-file capture path while preserving file restoration coverage on every platform. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * Stabilize Rust processing state E2E Hold a replayed custom tool invocation open while asserting the transient processing state, then release it and verify completion. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * Stabilize Windows .NET E2E coverage Use explicit .NET Framework handling for nondeterministic stdio startup diagnostics and complete a warm-up turn before exercising rewind file tracking across SDKs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * test: stabilize rewind and Rust resume E2E Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * fix: stabilize runtime disconnect races Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Stephen Toub <stoub@microsoft.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 Copilot-Session: 2455380f-4747-4900-89ca-93b30399de03 Copilot-Session: efc29e35-611b-46d8-9b8b-e1912d2eccf4 Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 | 1 个月前 |
| Update SDK snapshot for Copilot CLI 1.0.91-0 | 24 天前 |
| Add Rust SDK (technical preview) (#1164) * 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 types via `cd scripts/codegen && npm run generate:rust` to pick up the new schema fields. Surfaced four additive fields: - `SessionErrorData.eligible_for_auto_switch: Option<bool>` — set on `errorType: "rate_limit"` to signal the runtime will follow with an `auto_mode_switch.requested` event. UI clients can suppress duplicate rendering of the rate-limit error when they show their own auto-mode-switch prompt. - `SessionErrorData.error_code: Option<String>` — fine-grained upstream provider error code (`"user_weekly_rate_limited"`, `"integration_rate_limited"`, etc.). - `SessionModelChangeData.cause: Option<String>` — `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch recovery path. UI clients can render contextual copy. - `AutoModeSwitchRequestedData.retry_after_seconds: Option<f64>` — seconds until the rate limit resets, when known. All four are additive on existing struct types (`#[non_exhaustive]` was already in place via the codegen `#[serde(deny_unknown_fields)]`- free pattern, so additions are non-breaking on the deserialize side). - Reformats `rust/src/generated/rpc.rs` use-import (codegen output; fmt picked up the consolidated nested-imports form). - CHANGELOG entry under `### Added > Handlers + helpers` documenting the four new fields and clarifying that the request-callback `retry_after_seconds` parameter on `on_auto_mode_switch` keeps its `Option<u64>` HTTP `Retry-After` `delta-seconds` semantics distinct from the notification-event field's regenerated `Option<f64>` shape. The handler.rs / session.rs auto-mode-switch path from `a7c8215` is unchanged — that hand-authored layer doesn't depend on these specific generated types (the request-callback path is parsed manually in `session.rs:handle_request`). The bump confirms the wire path is real and our 0.1.0 ships against a runtime that actually implements the callback. Validation: - 209 tests pass (no count change — additive schema fields, no test impact). - `cargo doc -D warnings` clean. - `cargo +nightly-2026-04-14 fmt --check` clean. - `cargo clippy --all-features --all-targets -- -D warnings` clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CI: rename scenarios crate ref + nightly-fmt regen + non_exhaustive migration Three CI failures on PR #1164, all caught by green-light gating: 1. **`Verify generated files are up-to-date` (codegen-check workflow).** The Rust codegen (`scripts/codegen/rust.ts`) runs stable `rustfmt` on its output, which leaves `use super::api_types::rpc_methods;` and `use super::api_types::*;` as separate lines. Our committed canonical form is the consolidated `use super::api_types::{rpc_methods, *};` produced by nightly rustfmt with `imports_granularity = "Module"` (`rust/.rustfmt.nightly.toml`). The codegen-check workflow only installed stable, so its regen step produced the unconsolidated form and the diff check failed. Fix: install nightly rustfmt in the codegen-check workflow and run `cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml` on the Rust crate after `npm run generate`. Mirrors the same step the rust-sdk-tests workflow already runs for fmt --check, so the two CI surfaces converge on the same canonical form. 2. **`Rust scenarios` (scenario-builds workflow) — package name.** 18 scenarios under `test/scenarios/**/rust/` referenced the SDK as `copilot-sdk = { path = ... }` and used `use copilot::` in `main.rs`. The crate was renamed to `github-copilot-sdk` in `b136d3f` (with import path `github_copilot_sdk::`), but the scenarios were not updated alongside that rename. CI was failing with `error: no matching package named 'copilot-sdk' found`. Fix: rename in 18 `Cargo.toml` files (`copilot-sdk =` → `github-copilot-sdk =`) and 18 `main.rs` files (`use copilot::` → `use github_copilot_sdk::`, all `copilot::Foo` → `github_copilot_sdk::Foo`). 3. **`Rust scenarios` — `#[non_exhaustive]` cross-crate construction.** After fixing the package name, scenarios still failed with E0639: `cannot create non-exhaustive struct using struct expression`. The `cd3436f` non_exhaustive sweep blocks struct-literal syntax for external crates, including the `..Default::default()` functional- update form. Same migration we already did for `tests/` and `examples/` in `cd3436f`; the scenarios were a forgotten third external surface. Fix: migrate all 18 scenarios from struct-literal form to `let mut x = T::default(); x.field = value;` pattern. Affects `ClientOptions`, `SessionConfig`, `ResumeSessionConfig`, `SystemMessageConfig`, `Tool`, `CustomAgentConfig`, `ProviderConfig`, `InfiniteSessionConfig`, `ToolInvocation`. Inline nested struct literals (e.g. `SystemMessageConfig` inside `SessionConfig`) extracted to their own let-bindings. Builder chains (`.with_handler(...).with_hooks(...)`) preserved by terminating the let-mut-assign sequence with a re-binding before the chain. Drive-by fix to `tools/mcp-servers`: the original scenario assigned a `serde_json::json!({...})` value to `SessionConfig::mcp_servers`, which is typed `Option<HashMap<String, McpServerConfig>>`. The literal-only construction was a latent type error that must have only worked because the scenario was not previously built against the current SDK. Rewrote to construct a real `HashMap<String, McpServerConfig>` using `McpServerConfig::Stdio(McpStdioServerConfig { ... })`. 4. **Drive-by: `package: copilot-sdk` → `github-copilot-sdk` in `rust-sdk-tests.yml`'s `cargo-semver-checks-action`.** The action would have looked up the wrong package name on crates.io post-publish (currently silent because `continue-on-error: true` for the baseline-establishing pre-1.0 cycle, but worth fixing now so the post-publish flip-to-blocking just works). Validation: - 209 SDK tests still pass, doc -D warnings clean, fmt --check (nightly) clean, clippy --all-features --all-targets -- -D warnings clean. - All 18 scenarios build cleanly via the same loop CI runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use "GitHub Copilot CLI" consistently in user-facing docs Audit + replace bare "Copilot CLI" with "GitHub Copilot CLI" across the public-facing surface of the Rust SDK. Per branding consistency: the canonical name is "GitHub Copilot CLI"; "Copilot CLI" alone reads as informal shorthand. 22 occurrences across 10 files: - rust/CHANGELOG.md (2): release-notes prose. - rust/build.rs (2): cargo:warning + curl-required panic message. - rust/README.md (2): tagline + RPC namespace section. - rust/examples/chat.rs (1): example doc-comment. - rust/src/session.rs (1): `Session` rustdoc. - rust/src/types.rs (4): module rustdoc + skill_directories field rustdocs on SessionConfig and ResumeSessionConfig. - rust/src/resolve.rs (1): copilot_binary error hint. - rust/src/lib.rs (6): module-level rustdoc, ClientOptions field rustdocs, ConnectionState enum rustdoc. - rust/src/trace_context.rs (2): module rustdoc + TraceContext type rustdoc. - rust/src/session_fs.rs (1): SessionFsConventions rustdoc. The README's `# GitHub Copilot CLI SDK for Rust` title was already correct; the tagline beneath it ("A Rust SDK for programmatic access to the GitHub Copilot CLI.") was already correct as well — only the secondary mentions in the body needed updating. Test scenario READMEs under `test/scenarios/` are NOT updated: they contain ASCII diagrams with width-constrained boxes that would mis- align with the longer "GitHub Copilot CLI" string, and they're cross-language docs shared across all SDKs (Node, Python, Go, .NET, Rust) — out of scope for this Rust SDK release prep. Validation: cargo doc -D warnings clean, cargo fmt --check clean, cargo clippy clean, 209 tests pass (no semantic changes, only doc strings). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review: add prompts/attachments scenario + README parity sections Closes the two `github-actions` bot findings on PR #1164 that were the only remaining unresolved threads after the human-review cluster. **`prompts/attachments` Rust scenario** (PR comment 3164480775) The bot flagged that `prompts/attachments` was missing from the Rust coverage table — implemented in TS / Python / Go / .NET, full attachment support already in the Rust SDK (`Attachment::File` / `Blob` / `Selection`, `MessageOptions::with_attachments`). Accidental omission from the rest of the Rust port, not a deliberate deferral. - `test/scenarios/prompts/attachments/rust/{Cargo.toml, src/main.rs}` added, mirroring the existing four-language implementations (`claude-haiku-4.5`, replace-mode system prompt, single-file attachment from `sample-data.txt`, prompt asks "what languages are listed in the attached file?", verify.sh greps for TypeScript|Python|Go in the response). - `test/scenarios/RUST_COVERAGE.md`: new ✅ row. - `test/scenarios/prompts/attachments/README.md`: new Rust syntax row in the File and Blob attachment tables. - `test/scenarios/prompts/attachments/verify.sh`: Rust build + run steps wired into both phases. `cargo build` clean locally. **README parity sections** (PR comment 3164686206) The bot flagged that the Rust README has good coverage of the core API but is missing dedicated sections that exist in the Go / Python / .NET READMEs for several features the Rust SDK fully implements. None of these are implementation gaps — discoverability gaps for readers coming from other SDKs. Six new brief sections added to the API Reference between "Capabilities & Elicitation" and "Progress Reporting", each with a runnable code example: - **Slash Commands** — `CommandHandler` + `CommandDefinition::new` with the `name` / `description` wire-only contract called out. - **User Input Requests** — `SessionHandler::on_user_input` override example with the `None`-falls-back-to-CLI semantics. - **Streaming** — `streaming: true` + delta-event match loop reading the `delta` field from `event.data` (verified against the actual `SessionEvent` shape). - **Infinite Sessions** — `InfiniteSessionConfig::workspace_path`, compaction event names, resume-via-`Client::resume_session`, default state location at `~/.copilot/session-state/{sessionId}`. - **Custom Providers (BYOK)** — `ProviderConfig.provider_type` / `base_url` / `bearer_token` / `headers`, with the `openai`/`azure`/`anthropic` provider types and the `wire_api: "completions" | "responses"` flag. - **Telemetry** — `TelemetryConfig` + `OtelExporterType::OtlpHttp`/ `File` with the env-var injection note and the no-OpenTelemetry-dependency guarantee. All API shapes verified against the actual source. Caught one shape-mismatch while drafting (initial draft had `provider.kind: Some(...)` with `base_url: Option<String>`; actual API is `provider.provider_type: Option<String>` + `base_url: String`) — corrected in the final draft. cargo doc -D warnings clean, fmt --check clean, clippy clean, 209 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add get_model and send_telemetry to Rust-only API list Bot review on PR #1164 (comment 3164786116) caught two more accidental omissions in the README's 'Rust-only API' section. Both methods are real and exposed; just missing from this inventory: - Session::get_model — wraps session.model.getCurrent. Folded into the existing 'First-class Session convenience methods' bullet. - Client::send_telemetry / Session::send_telemetry — wrap sendTelemetry / session.sendTelemetry. Added a new dedicated bullet because no other SDK exposes these RPC endpoints in their public API at all (not even via typed namespaces), so it's a higher-divergence item worth its own line. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix update-copilot-dependency: format Rust generated output The CLI-version-bump workflow regenerates per-language types via \`scripts/codegen/npm run generate\` and then formats the output. The formatting step covered TypeScript (prettier) and .NET (dotnet format) but not Rust, so future CLI bumps would land unformatted generated code that codegen-check.yml then catches as a diff. Mirrors the fix already applied to codegen-check.yml in 2766a79: install nightly rustfmt alongside stable, then run \`cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml\` on the rust crate after generation. Same toolchain pin, same config — keeps the two CLI-codegen surfaces converging on the same canonical output. Caught by SDK + CLI version coordination research session. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Client::get_status and Client::get_auth_status wire method names Two more wire-shape bugs in the same class as `c58e2f2` (elicitation `requestedSchema`) and `64541af` (list_sessions filter wrapping): hand-authored RPC wrappers calling JSON-RPC methods that don't exist on the CLI runtime, masked by mock-server tests asserting the same wrong names the implementation sent. The CLI runtime (verified via `@github/copilot@1.0.39` app.js) registers two top-level RPCs: - `status.get` (returns version + protocol_version) - `auth.getStatus` (returns is_authenticated + auth_type) Every other public SDK calls those exact names: - Node `nodejs/src/client.ts:980, 992` - Go `go/client.go:1239, 1257` - Python `python/copilot/client.py:1806, 1827` - .NET `dotnet/src/Client.cs:718, 732` The Rust hand-authored layer was sending `getStatus` and `getAuthStatus` — names that don't exist on the wire. Result: both methods would have returned a method-not-found error (or a misleading no-such-method log on the CLI side) instead of the expected payload. The schema doesn't currently typed-define these top-level RPCs (only `session.auth.getStatus` is in the schema, a different namespaced endpoint), so the codegen didn't catch the hand-authored layer's drift. The mock-server test `client_rpc_methods_send_correct_method_names` asserted on the same wrong names the implementation used, so the bugs round-tripped through both ends. Same root cause as the elicitation and list_sessions tests. Fix: - `lib.rs:1301`: `"getStatus"` → `"status.get"` - `lib.rs:1308`: `"getAuthStatus"` → `"auth.getStatus"` - `tests/session_test.rs:488-509`: assertions migrated to the canonical names AND new explicit-absence assertions (`assert_ne!(request["method"], "getStatus")` / `"getAuthStatus"`) so the same regression class can't be reintroduced silently. Doc-comment on the test names the source- of-truth references (CLI runtime registration, four-SDK call sites). - `CHANGELOG.md`: new `### Fixed` entry documenting the wire-name 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 github-actions bot on PR #1164 (comments 3164826435 and 3164826441). Verify-before-drafting tally now 14 / 15 for these two saves. This brings the wire-shape audit save count to 4 (elicitation, list_sessions, get_status, get_auth_status) — all four were hand-authored RPC wrappers asserting on the implementation's wrong shape rather than the schema/runtime's expected shape. The structural fix (regression-prevention asserts in tests) is now in place for all four; the same bug class is no longer silently reproducible. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refactor JsonRpcClient writer to actor pattern (cancel-safety) Closes RFD-400 review findings #1 (writer mutex desync hazard) and #4 (send_request pending-entry leak on cancellation). See cancel-safety review session db4b1ac8-... for the full report. The previous design held a `tokio::sync::Mutex<Box<dyn AsyncWrite>>` across `write_all` + `flush` for Content-Length-framed JSON-RPC messages. Caller cancellation mid-frame (e.g. wrapping `Session::send` or `Client::call` in `tokio::time::timeout` or `select!`) could leave a partial frame on the wire, permanently desyncing the transport for the rest of the connection. The reader on the CLI side would block forever waiting for the missing bytes, and the next outbound message would be interpreted as the tail of the previous body. Fix: writer-actor task fed by an `mpsc::UnboundedSender<WriteCommand>`. Public `write` and `send_request` pre-serialize the body, enqueue the frame + an ack `oneshot`, and await the ack. Caller cancellation drops the ack receiver but the actor still completes the in-flight write — the frame either lands atomically or surfaces an io::Error to the (possibly already-dropped) ack receiver. A partial frame can never appear on the wire. Design notes: - Pre-serialize before enqueue: actor receives `Vec<u8>` (header + body, already concatenated). Serde errors fail synchronously at enqueue time before touching the actor — cleanest cancel-safety story. - Unbounded sender: RFD 400 explicitly permits this for cancel-safety, and the producer-consumer ratio is naturally bounded by request/ response semantics on the wire. - Writer actor lives until the JsonRpcClient drops. Channel closes on Drop -> actor sees recv None -> flushes and exits. `pending_requests` cleanup (#4): added a small `PendingGuard` RAII helper in `send_request` that arms after the map insert and disarms before the success return. Caller cancellation drops the guard, which removes the pending-entry synchronously via parking_lot::RwLock (the map mutex is converted in this commit too — it was never held across .await anyway, so the conversion is a pure cleanup). Mutex conversions in this commit: - `JsonRpcClient::pending_requests`: `tokio::sync::RwLock` -> `parking_lot::RwLock`. Lock is never held across .await; conversion is also what enables the synchronous PendingGuard drop. - The previous `Arc<Mutex<Box<dyn AsyncWrite>>>` is gone entirely. The actor task owns the writer directly with no shared synchronization. Tests: two new regression tests in tests/jsonrpc_test.rs - `write_actor_completes_on_caller_cancel`: tiny duplex (8 bytes) + gated reader so the actor's write_all blocks waiting for room. Race the caller's future against a sleep; sleep wins, future is dropped while suspended on ack_rx.await. Then enqueue a second write and release the reader. Both frames must land on the wire intact, in order, with no partial frame from the cancelled write. - `send_request_cancellation_does_not_leak_pending`: cancel an in-flight request, then send a (late) response for the cancelled id to verify the read loop logs and discards it without colliding, then send a fresh request and verify it works normally. Existing 209 tests continue to pass without modification — the wire semantics are unchanged. Total now 211. Validation: cargo test, cargo doc -D warnings, cargo +nightly fmt --check, cargo clippy --all-features --all-targets -- -D warnings all clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add WaiterGuard RAII for Session::send_and_wait (cancel-safety) Closes RFD-400 review finding #2 (idle_waiter slot leak on external cancellation). See cancel-safety review session db4b1ac8-... for the full report. `Session::send_and_wait` installs an `IdleWaiter` slot under `self.idle_waiter`, then awaits the response. Internal failure paths (send_inner error, oneshot closure, internal timeout) clean up the slot. External cancellation — caller wraps `send_and_wait` in `tokio::time::timeout`, races it in `select!`, or aborts the JoinHandle — does not. The cleanup at the event loop's `else` branch only fires when the channel itself closes (i.e., the entire session is going away), not when an individual call is cancelled. Effect: any external cancellation between "install waiter" and "drain response" leaves `idle_waiter = Some(...)` forever. All subsequent `send` and `send_and_wait` calls return `SendWhileWaiting`, permanently bricking the session. Fix: `WaiterGuard` RAII helper that takes the slot on Drop. Construct right after install, let RAII handle every exit path. The explicit `self.idle_waiter.lock().take()` calls inside the function disappear, and the slot is cleared on every cancellation path automatically. Mutex conversion (folded in from finding #5): - `Session::idle_waiter`: `tokio::sync::Mutex<Option<IdleWaiter>>` -> `parking_lot::Mutex<Option<IdleWaiter>>`. Lock is never held across `.await` in any code path; the conversion is what makes WaiterGuard's synchronous Drop work without needing an async-spawn fallback. `event_loop` mutex stays `tokio::sync::Mutex` for now — commit C converts it as part of cooperative shutdown (so the conversion lands alongside the matching change to Drop for Session). Tests: two new regression tests in tests/session_test.rs - `send_and_wait_outer_cancellation_clears_waiter`: outer `tokio::time::timeout(50ms, ...)` around `send_and_wait` with a 60-second inner timeout. Outer fires, dropping the future. Verify the next `send` succeeds (no SendWhileWaiting). - `send_and_wait_drop_clears_waiter`: explicit `JoinHandle::abort` of an in-flight send_and_wait. Verify the next `send` succeeds. Existing 211 tests continue to pass; total now 213. Validation: cargo test --all-features, cargo doc -D warnings, cargo +nightly fmt --check, cargo clippy -- -D warnings all clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Cooperative event-loop shutdown via Notify (cancel-safety) Closes RFD-400 review finding #3 (JoinHandle::abort lands at any await point in the event loop body) and the remaining bit of #5 (Session::event_loop tokio::sync::Mutex -> parking_lot::Mutex). See cancel-safety review session db4b1ac8-... for the full report. The previous design called `JoinHandle::abort()` in two places to stop the event-loop task: explicitly in `Session::stop_event_loop` and best-effort in `Drop for Session`. RFD 400's strongest single recommendation about task aborts: > With the abort(), the future returned by do_long_running_operation() > can be cancelled at *any await point*. It is unlikely that arbitrary > async Rust code is resilient to cancellations anywhere during > control flow. ... task aborts should be avoided entirely. The event loop body has many await points: - The outer `select!` itself - `handle_notification`'s nested awaits (acquire idle_waiter lock, send on broadcast, dispatch RPC callbacks for permission/tool/ elicitation requests) - `handle_request`'s inline awaits (deserialize, dispatch handler, build response, write back to client) If `abort()` landed mid-handler — say partway through writing the permission response — the CLI would keep that requestId in flight forever. Same hazard for the idle_waiter oneshot: an abort between "observed session.idle" and "send on waiter.tx" leaves the caller's `send_and_wait` await blocked until the entire transport tears down. Fix: cooperative cancellation via `Arc<tokio::sync::Notify>`. The event-loop body adds a fourth select arm: tokio::select! { _ = shutdown.notified() => break, Some(notification) = notifications.recv() => { ... } Some(request) = requests.recv() => { ... } else => break, } `Notify::notified()` is cancel-safe per RFD 400's framework: the unselected branch losing a poll is fine — it's reconstructed on the next iteration. `notify_one()` (NOT `notify_waiters`) is used because it buffers a single signal: if the loop is currently inside `handle_request(...).await` (no `.notified()` future registered), the signal is held until the handler returns and the loop re-enters select on the next iteration. `notify_waiters()` would silently lose the signal in this case. `stop_event_loop` calls `shutdown.notify_one()` then awaits the JoinHandle. The loop drains its current iteration's handler before observing the buffered signal and exiting cleanly — no mid-protocol state, no orphaned RPC ids on the CLI side. `Drop for Session` calls `notify_one()` and unregisters from the router. The handle is left in `event_loop` for tokio to reap when the loop's spawned task exits naturally; we intentionally don't await it because Drop is sync. Spawned child tasks inside `handle_notification` (permission/tool/elicitation callbacks) intentionally outlive the parent loop — RFD 400's "spawn background tasks to perform cancel-unsafe operations" pattern, documented in the loop's comment. Mutex conversion (folded in from finding #5): - `Session::event_loop`: `tokio::sync::Mutex<Option<JoinHandle<()>>>` -> `parking_lot::Mutex<Option<JoinHandle<()>>>`. Lock is never held across `.await` and the conversion eliminates the `try_lock()` Drop footgun (silent no-op on contention with stop_event_loop). This is the last tokio::sync::Mutex on Session — the import is no longer needed. Tests: two new regression tests in tests/session_test.rs - `stop_event_loop_completes_in_flight_handler`: install a `SlowHandler` whose userInput.request callback sleeps 150ms. Send the request from the server side, then 20ms in (handler is mid- flight) call `stop_event_loop`. Verify the response with `answer: "completed"` lands on the wire BEFORE stop_event_loop returns — proves the handler ran to completion, not aborted mid-await. - `drop_session_does_not_abort_handler`: same shape but via `drop(session)` instead of `stop_event_loop`. Tracks handler completion via an AtomicBool that the handler sets only after its sleep completes; verifies both that the response lands on the wire AND that the handler actually completed despite the Session being dropped. Existing 213 tests continue to pass; total now 215. Validation: cargo test --all-features, cargo doc -D warnings, cargo +nightly fmt --check, cargo clippy -- -D warnings all clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Document cancel-safety for public async APIs (RFD-400) Closes RFD-400 review finding #6 (per-method cancel-safety documentation). Doc-only follow-up to commits A/B/C; no behavioral change. RFD 400 § "Marking APIs as cancel-unsafe" calls for explicit documentation; with the underlying behavior now correct after A/B/C, documenting the contract makes it inspectable from rustdoc rather than requiring callers to read the implementation. Added `# Cancel safety` rustdoc sections to: - `Session::send` — cancel-safe (writer-actor); message lands on the wire even if the await is cancelled. - `Session::send_and_wait` — cancel-safe (WaiterGuard RAII); outer-cancellation distinct from internal-timeout, both clear the slot. - `Session::abort` — cancel-safe (single RPC via writer-actor). - `Client::call` — cancel-safe at the wire level; documents the caveat that the CLI may still process the request even if the caller doesn't see the response (idempotent vs non-idempotent methods). - `Client::stop` — cancel-unsafe-but-recoverable; the body sequentially destroys sessions (each individually cancel-safe) before killing the child. The existing `tokio::time::timeout` example with `force_stop` fallback is the documented recovery path. - `Client::force_stop` — synchronous, cannot be cancelled. Designed as the recovery path for `stop`'s timeout case. - `Subscription::recv` — cancel-safe by virtue of `tokio::sync::broadcast` + `BroadcastStream`. Validation: cargo doc -D warnings clean, cargo test (215 pass), cargo +nightly fmt --check clean, cargo clippy clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add scheduled trigger to update-copilot-dependency.yml Closes "Mode 1 hands-off loop" gap A from the SDK release plan-doc: update-copilot-dependency.yml is workflow_dispatch only today, so when a new @github/copilot publishes nobody automatically opens the regen PR. Today's bot-driven bump (commit ecdc5ec, "Update @github/copilot to 1.0.39 (#1167)") was triggered manually after the plan-doc session noticed the gap. Goal: the SDK auto-detects new CLI versions, opens a draft PR with all per-language regen + formatting + lockfile updates, and a human just reviews + merges. github-app's bundled-binary tracker picks the new SDK up via crates.io dependabot in the symmetric direction. Changes: 1. **`schedule` trigger.** Daily cron at 10:00 UTC, deliberately symmetric with github-app's check-bundled-binary-versions.yml so the SDK and consumer halves of the loop tick in lockstep on release days. 2. **`version` input is now optional.** Manual override still works for emergency back-patches or cherry-picking specific prereleases; an empty input on workflow_dispatch flows through the same auto-detect path the schedule uses. 3. **Two-job split: `detect` -> `update`.** The detect job is cheap (one `npm view @github/copilot version` + a jq read of nodejs/package.json + a strip of the `^`/`~` prefix). It sets two outputs: - `version`: the resolved target (input override, or latest from npm) - `should_continue`: 'true' if the resolved version differs from the current pin, 'false' otherwise The update job has `needs: detect` and `if: needs.detect.outputs.should_continue == 'true'`, so it skips entirely when there's nothing to do. This keeps the no-op daily cost effectively free. 4. **Validation moves into the resolve step.** Both manual and auto-detected versions go through the same semver regex check. The previous standalone "Validate version input" step is retired; the detect job's resolve step now owns format validation, version comparison, and skip logic. 5. **All `${{ inputs.version }}` references become `${{ needs.detect.outputs.version }}`.** Same value, just routed through the detect job's outputs so manual override and auto-detect both feed the update job uniformly. Doesn't touch SDK source; cargo test / cargo publish --dry-run are no-op confirmation post-merge. Companion gap on the github-app side (auto-open sync PR after each new SDK release) lives on Sync session PR #4140; this commit unblocks the SDK half. Plan-doc capture: tracked under "Steady-state CLI bump automation (Mode 1 hands-off loop)" — gap A closed; gap B (github-app sync-PR auto-open) remains. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Correct get_quota doc: endpoint exists cross-SDK, only wrapper is Rust-only Bot review caught an inaccuracy in the rustdoc and README: the prior wording claimed the underlying account.getQuota JSON-RPC endpoint was exposed only by the Rust SDK. In fact only the top-level Client::get_quota convenience wrapper is Rust-unique; the endpoint itself is generated and exposed cross-SDK via each SDK's typed rpc() namespace. Verified cross-SDK exposure: - Node: nodejs/src/generated/rpc.ts:2203 (account.getQuota in rpc() namespace) - Python: python/copilot/generated/rpc.py:5647 (rpc().account.get_quota) - Go: go/rpc/generated_rpc.go:2252 (RPC.Account.GetQuota) - .NET: dotnet/src/Generated/Rpc.cs:3140 (Rpc().Account().GetQuotaAsync) - Rust: rust/src/generated/rpc.rs:90 (rpc().account().get_quota) Updated rustdoc on Client::get_quota in rust/src/lib.rs and the "Rust-only API" bullet in rust/README.md to correctly state that the wrapper is Rust-only while the underlying endpoint is available across all SDKs via their typed rpc() namespaces. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add ClientOptions::new() and Tool::new() builder methods Mirrors the existing MessageOptions::new() / with_* builder pattern on the two other public #[non_exhaustive] config types that consumers construct most often. Closes the cross-crate ergonomics gap that #[non_exhaustive] introduces: external callers can't use struct- literal syntax (with or without ..Default::default()) on these types, so every field assignment becomes its own mut-let statement. The builder shape collapses that into a single chained expression. Caught by github-app PR #4140 review on src-tauri/src/session/cli.rs: the mut-let-and-assign pattern read as awkward to reviewers even after trimming to the four fields that diverge from default. Same ergonomic friction that prompted the make_tool() helper consumers were writing for Tool. Builder ships zero-cost convenience without giving up #[non_exhaustive]'s SemVer protection on growable config struct. ClientOptions builder (rust/src/lib.rs): - new() -> Self (documented entry point) - with_program(impl Into<CliProgram>) - with_prefix_args<I: IntoIterator<...>>(args) - with_cwd(impl Into<PathBuf>) - with_env<I: IntoIterator<(K, V)>> - with_env_remove<I: IntoIterator<S>> - with_extra_args<I: IntoIterator<S>> - with_transport(Transport) - with_github_token(impl Into<String>) - with_use_logged_in_user(bool) - with_log_level(LogLevel) - with_session_idle_timeout_seconds(u64) - with_list_models_handler<H: ListModelsHandler + 'static> (Arc-wrapped internally) - with_session_fs(SessionFsConfig) - with_trace_context_provider<P: TraceContextProvider + 'static> (Arc-wrapped internally) - with_telemetry(TelemetryConfig) Tool builder (rust/src/types.rs): - new(impl Into<String>) -> Self (name + defaults) - with_namespaced_name(impl Into<String>) - with_description(impl Into<String>) - with_instructions(impl Into<String>) - with_parameters(serde_json::Value) - with_overrides_built_in_tool(bool) - with_skip_permission(bool) Pure additive: no existing ClientOptions::default() / mut-let patterns break, no public field visibility changes. Both builders compose by wrapping ::default() and mutating-and-returning self, matching MessageOptions's idiom. Tests: client_options_builder_composes verifies all 10 chained builder methods produce the expected internal state. tool_builder_composes does the same for Tool's seven methods, plus tool_with_parameters_handles_non_object_value covers the HashMap-from-Value extraction edge case (json!(null) -> empty map rather than panic). CHANGELOG: new "Builder ergonomics" subsection under Documentation, explaining the rationale and listing both builders. Validation: 220 tests pass (was 215 + 5 new builder tests across unit + doctest), cargo doc -D warnings clean, cargo fmt --check clean, cargo clippy --all-features --all-targets -- -D warnings clean. Companion ask from github-app PR #4140 review (Sync session) follows up: github-app's `cli.rs` and ~25 Tool::new() call sites will switch to the builder form once this lands and the next sync round-trip pulls it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add per-field builder methods to SessionConfig and ResumeSessionConfig Mirrors the just-shipped ClientOptions/Tool builder pattern (commit 2d725a5) on the two structs that consumers reach for most when wiring up a session. Both types are #[non_exhaustive], so external callers have been writing the same mut-let-and-assign block at every construction site: let mut config = ResumeSessionConfig::new(session_id); config.client_name = Some("...".into()); config.streaming = Some(true); config.tools = Some(tools); // ...10+ more lines The new chainable form collapses that to: ResumeSessionConfig::new(session_id) .with_client_name("...") .with_streaming(true) .with_tools(tools) // ... Both types have ~30 public fields each. Coverage: SessionConfig: with_session_id, with_model, with_client_name, with_reasoning_effort, with_streaming, with_tools, with_available_tools, with_excluded_tools, with_mcp_servers, with_disabled_mcp_servers, with_env_value_mode, with_enable_config_discovery, with_request_user_input, with_request_permission, with_request_exit_plan_mode, with_request_auto_mode_switch, with_request_elicitation, with_skill_directories, with_disabled_skills, with_agent, with_custom_agents, with_config_dir, with_working_directory, with_github_token, with_include_sub_agent_streaming_events, with_system_message, with_skip_compaction_history. ResumeSessionConfig: same shape minus model/reasoning_effort/ available_tools/disabled_skills/disabled_mcp_servers (which the resume RPC doesn't accept), plus with_disable_resume. Existing closure-installing chains (with_handler, with_hooks, with_transform, with_commands, with_session_fs_provider, approve_all_permissions, deny_all_permissions, approve_permissions_if) and the manual mut-let pattern continue to work unchanged. The hooks bool is intentionally not exposed via a new with_* setter; with_hooks(handler) auto-enables the flag, and power users wanting to override can still set the .hooks field directly. Adds two integration-style tests covering composition across all field categories (scalars, bool flags, Vec setters, struct setters, HashMap setters) for both types. CHANGELOG entry extends the existing "Builder ergonomics" subsection. This is the third commit in the pre-1.0 builder-pattern series (MessageOptions in the original branch, ClientOptions/Tool in 2d725a5, SessionConfig/ResumeSessionConfig here). Pure additive, zero migration cost. Driven by github-app PR #4140 reviewer feedback on src-tauri/src/session/cli.rs and core.rs construction sites. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Scrub github-app references from public-repo files The copilot-sdk repo is public; github-app is private. Public-repo content must not name or depend on github-app, even in comments or test fixtures. - .github/workflows/update-copilot-dependency.yml: drop the "symmetric with github-app's scheduled" framing on the daily cron; rewrite the comment to describe what the schedule does in SDK-local terms only. - rust/CHANGELOG.md: rephrase the Tool builder rationale to talk about "real-world consumer code" rather than github-app's tool catalog specifically. - rust/src/lib.rs: change the `source_name` test fixture from "github-app" to "my-app" — it was just an example string for OTEL source-name plumbing, but the explicit consumer naming is unnecessary and confusing in a public repo. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Round out builder coverage and document the Option<T> escape hatch After landing per-field builders on the four highest-traffic consumer-facing config structs (MessageOptions, ClientOptions, Tool, SessionConfig/ResumeSessionConfig), this commit closes the long tail. Rust ecosystem research (tokio, reqwest, tonic, axum, AWS SDK) confirms two design points: 1. The dominant pattern for #[non_exhaustive] config builders is the single `with_<field>(impl Into<T>) -> Self` setter that always Some-wraps. AWS SDK's dual `field` + `set_field(Option<T>)` shape is justified by codegen, not by ergonomics — and the `impl Into<Option<T>>` magic that some older crates use has fallen out of fashion (it conflicts with `impl Into<T>` chaining and produces poor type-inference errors). 2. Direct field assignment on a `pub` field is idiomatic Rust for forwarding `Option<T>` values from upstream code. The http::request::Parts / hyper::Body::Builder pattern. We already support it; we just hadn't documented it. This commit therefore: - Adds full builders to the four bare consumer-facing structs: CustomAgentConfig (name + prompt required, six with_* setters), InfiniteSessionConfig (three with_* setters), ProviderConfig (base_url required, six with_* setters), SystemMessageConfig (three with_* setters). - Expands TraceContext with a symmetric new() + with_traceparent pair alongside the existing from_traceparent shorthand. The shorthand is now expressed in terms of the chain (`new().with_traceparent(x)`) for consistency. - Documents the direct-field-assignment escape hatch on SessionConfig and ResumeSessionConfig, with a doc example showing how to mix the fluent chain (for compile-time-known values) with direct mutation (for Option<T> pass-throughs). Per-field `with_<x>_opt(Option<T>)` setters were considered and deliberately rejected: doubling the method count for ~30-field configs would hurt rustdoc discoverability and conflict with the single-setter precedent we just shipped. The mut-let escape hatch covers the same use case without polluting the API surface. Adds 5 unit tests (one per new builder + a TraceContext suite) and extends the existing CHANGELOG "Builder ergonomics" subsection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert "Add scheduled trigger to update-copilot-dependency.yml" This reverts commit a88c3ebc7cf25d5b7194a37b51e52bf6a62bc8fc. The scheduled trigger was added to close a hands-off-loop gap that was framed in terms of a downstream consumer's needs. On reflection, the SDK should not be the only language in this repo with scheduled auto-detect; that's a cross-language platform decision, not a single- SDK one. Reverting brings update-copilot-dependency.yml back to manual-only (workflow_dispatch with a required version input) — the same shape as every other language's CLI update workflow in this repo. When the SDK collectively decides to add scheduled CLI tracking, that should land as a unified change across all languages, not as a Rust-only special case driven by a downstream consumer's automation desires. Manual dispatch remains the supported path until then. Conflict resolution: the e93ce8e ("Scrub github-app references") commit landed after a88c3eb and rewrote the cron block's comment. The revert drops the entire schedule: block (including the rewritten comment), restoring the workflow to its pre-a88c3eb manual-only shape exactly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add TelemetryConfig builder methods Final missing piece in the consumer-facing #[non_exhaustive] config struct audit. `TelemetryConfig` had public optional fields (otlp_endpoint, file_path, exporter_type, source_name, capture_content) and an is_empty() helper, but no `new()` or `with_*` setters — leaving callers to write `TelemetryConfig { otlp_endpoint: Some(...), ..Default::default() }` which doesn't compile from outside the crate due to #[non_exhaustive]. Adds `TelemetryConfig::new()` (delegates to Default) and `with_*` setters for all five fields, matching the existing pattern. The is_empty() helper stays unchanged. After this commit, every consumer-facing #[non_exhaustive] struct in the SDK has a fluent builder: ClientOptions, TelemetryConfig, Tool, CommandDefinition, CustomAgentConfig, InfiniteSessionConfig, ProviderConfig, SessionConfig, ResumeSessionConfig, SystemMessageConfig, MessageOptions, SessionFsConfig, TraceContext. One unit test (telemetry_config_builder_composes) covering all five setters + is_empty behavior on a fresh new(). Extends the existing CHANGELOG "Builder ergonomics" round-out bullet. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Switch build.rs from curl to ureq with bounded retries The bundled-CLI build path (gated by the embedded-cli feature and the COPILOT_CLI_VERSION env var) previously shelled out to `curl` to fetch SHA256SUMS.txt and the platform tarball from the copilot-cli releases. Two problems with that: 1. Implicit system dependency. `expect("curl is required ...")` panics when curl isn't on PATH — true on minimal Windows MSVC environments, some Docker images, and the CI matrices crates.io publishers tend to validate against. crates.io build environments themselves don't guarantee curl. 2. No retry on transient errors. Network blips during CI bundling surfaced as build failures rather than self-healing waits. Switch to ureq 2 with default-features=false + ["tls"] (rustls + webpki-roots, pure-Rust TLS, no OpenSSL or native-tls). Adds about 1.7MB and ~12 transitive deps to the build graph but only when the embedded-cli feature is on; the default cargo build is unaffected. Retry policy: - Up to 3 retries (4 total attempts) with exponential backoff: 1s, 2s, 4s. - 5xx responses, connect timeouts, read timeouts, and other transport-layer errors are treated as transient and retried. - 4xx responses fail fast (the URL is wrong; retrying won't help). Connect/read timeouts (30s/120s respectively) prevent indefinite hangs on stalled connections. The cache-hit path still bypasses the network entirely; only fresh downloads pay the HTTP cost. End-to-end smoke test against https://github.com/github/copilot-cli/releases/download/v1.0.39 : SHA256SUMS download succeeded, tarball download succeeded (134MB extracted, 44MB compressed), SHA-256 verified, embedded build succeeded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: validate embedded-cli bundle build on macOS / Linux / Windows The default `cargo test` job in rust-sdk-tests.yml runs on all three supported platforms but does not set `COPILOT_CLI_VERSION` — which means `build.rs` short-circuits immediately and the embedded-cli bundle path (download + SHA-256 verify + extract + zstd compress + embed) is never exercised in CI. After the recent switch from shelling out to `curl` over to `ureq` with retry logic in `build.rs`, that gap matters: the new HTTP client + cross-platform TLS code path needs cross-platform CI coverage before it ships. Adds a parallel `bundle` job to the same workflow: - 3-OS matrix: ubuntu-latest, macos-latest, windows-latest. - Reads the pinned CLI version from `nodejs/package.json` so the bundle test always tracks the same version the rest of the SDK ships against — no separate version source to drift. - Caches the downloaded archive in `./rust/.bundled-cli-cache` keyed on OS + CLI version, so steady-state CI doesn't refetch ~130 MB on every run. Cache miss (e.g. first run after a CLI bump) exercises the full download path end-to-end, which is the regression surface this job is meant to catch. - Runs `cargo build --features embedded-cli` to drive `build.rs` through the full pipeline. Doesn't run `cargo test` — the test job already covers runtime behavior; this job is specifically validating the bundle build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix duplicate user_input dispatch (github-app#4249) The CLI sends two messages for every user-input prompt: 1. A `user_input.requested` session-event notification, intended for observers (UI streaming, telemetry, logging). 2. A `userInput.request` JSON-RPC method call, which is the actual ask-and-wait prompt that drives the handler. The Rust SDK's session event loop was wiring up `HandlerEvent::UserInput` dispatch on BOTH paths. Result: the consumer's `on_event` ran twice for every prompt, which surfaced as duplicate `ask_user` / `exit_plan` widgets in github-app's session UI (#4249). Other SDKs (Node, Python, Go, .NET) only register the JSON-RPC handler; the notification is purely observational. Aligning with that convention. Changes: 1. `session.rs`: drop the `SessionEventType::UserInputRequested` auto-dispatch arm. The CLI's follow-up `userInput.request` JSON-RPC call still drives the handler. Replaced with a short comment block explaining why the notification is intentionally a no-op (and citing #4249 so the next person tempted to add it back understands the trade). 2. `tests/session_test.rs`: replaced the `user_input_requested_event_dispatches_to_handler_and_responds` test (which asserted the broken behavior) with a regression test (`user_input_requested_notification_does_not_double_dispatch`) that: - sends a `user_input.requested` notification and asserts no wire response, no handler invocation; - then sends a `userInput.request` JSON-RPC call and asserts the handler fires exactly once. This locks the fix in place and makes a future re-introduction surface immediately as a test failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Drop Skills section from .github/copilot-instructions.md stephentoub and tclem agreed the Skills section is unnecessary prompt-pollution. Skills are auto-discovered by Copilot CLI without needing to be enumerated in the instructions, and the one-line description of each skill duplicates what the SKILL.md frontmatter already says. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Render experimental RPC methods as a styled rustdoc admonition Codegen previously emitted a single bare `/// Stability: experimental.` line on each experimental wire method. stephentoub flagged this as weak signal -- there's no way for a consumer to tell from the rustdoc that they're using something that may change. Switch the rustdoc emit to a `<div class="warning">...</div>` block, which renders on docs.rs as a yellow-bordered callout (the standard rustdoc admonition convention). Matches the visibility Python's generated docs already get via Sphinx's `.. warning::` directive. This is documentation-quality only; no compile-time opt-in. Stable Rust has no first-class `#[experimental]` attribute (`#[unstable]` is rustc-internal, std-only). If we ever want compile-time friction the ecosystem patterns are a Cargo feature gate (e.g. `tokio_unstable`- style `features = ["experimental"]`) or an `unstable` module path; both are larger codegen changes affecting every experimental RPC across all SDK surfaces. Punted for 0.1.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(skill): clarify that handler thread-safety is compiler-enforced The "anti-pattern" framing in examples.md was misleading -- it implied the RefCell example might compile but produce bugs at runtime. In reality, SessionHandler's `Send + Sync + 'static` trait bound rejects any handler whose state contains a non-Sync type (RefCell, Cell, Rc), so the example fails at the impl site, not at use. Reframe as "Won't compile", show the actual trait-bound error inline, and add a one-paragraph note explaining the compiler-level enforcement so a reader doesn't have to mentally trace `spawn -> Send + Sync -> RefCell !Sync`. Addresses stephentoub's review feedback that the prior framing read as "we've buried some unsafe usage." Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refactor session shutdown from Notify to CancellationToken Replaces the per-session `Arc<Notify>` shutdown signal with `tokio_util::sync::CancellationToken`, the canonical primitive for SDK-internal task coordination in async Rust. Same cooperative-shutdown semantics, materially better fit for the use case: - `tonic` uses `CancellationToken` for the equivalent task coordination case (request cancel propagated into spawned server handlers). - Parent/child token tree gives us a clean way to expose a public `Session::cancellation_token()` accessor that returns a child token — consumers can `select!` on `child.cancelled()` to bind their own work to the session lifetime, but cancelling the child does NOT shut down the session itself (child cancel is isolated). - `Drop` impl just calls `cancel()`, no buffering quirks (Notify's `notify_one` was already documented as buffer-on-no-listeners; token cancellation is sticky, which is the simpler mental model). Plumbing: - `tokio-util = { version = "0.7", default-features = false }` added as a runtime dep. No new transitive deps the SDK didn't already pull through tokio. - `Session.shutdown` field type changes from `Arc<Notify>` to `CancellationToken` (which is itself internally `Arc`-backed and `Clone`). - `stop_event_loop()` calls `cancel()` instead of `notify_one()`. - `Drop` impl calls `cancel()` instead of `notify_one()`. - Event-loop `select!` arm becomes `_ = shutdown.cancelled() => break`. - New public `Session::cancellation_token()` returning a `child_token()` — power-user API for binding external tasks to session lifetime. Tests: - `cancellation_token_fires_on_session_drop`: dropping the session fires the child token within 2s. - `cancellation_token_child_cancel_does_not_kill_session`: cancelling a child does not propagate up to the parent. Closes the cancellation idiom gap stephentoub flagged on the SKILL doc. The framing "drop the future or call abort()" was too absolute — drop- to-cancel is the right primitive for caller-owned futures, but for SDK-internal task coordination CancellationToken is the canonical answer (citations: tokio-util docs, tonic cancellation example, withoutboats "Asynchronous clean-up", `tokio-graceful-shutdown` crate). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(skill): rewrite "Idioms that don't port" with full research backing stephentoub flagged that the "Idioms that don't port from Go or Node" section was lopsided (no C# / Python mention) and that the cancellation guidance was too absolute (it dismissed `ctx.Done()` analogs without mentioning tokio_util's CancellationToken). Both true. Replaces the section with a thorough rewrite covering all four idioms with citations and decision matrices: 1. Event subscription: keep "channels not callbacks" baseline; add the full primitive matrix (mpsc / oneshot / broadcast / watch); explain that the public API should expose `impl Stream<Item = T>` (the canonical IObservable<T> analog), citing tonic / reqwest / sqlx as precedent. 2. Cancellation: rewrite. "Drop = cancel" is the primitive for caller-owned futures. `tokio_util::sync::CancellationToken` is the canonical answer for SDK-internal task coordination — citations to tonic's cancellation example, tokio-util docs, withoutboats blog post, and Cybernetist's task cancellation patterns survey. Notes the SDK's own `Session.shutdown: CancellationToken` field and the `Session::cancellation_token()` accessor. 3. Optional fields: confirm Option<T> + Default baseline; add non_exhaustive + builder pairing (AWS SDK convention); note when typestate / Result-from-build is the right answer. 4. serde JSON: confirm rename_all + per-field rename baseline; add skip_serializing_if = "Option::is_none" for output omission and #[serde(default)] for input tolerance (LSP/JSON-RPC convention). Section title is also de-language-coupled: "Idioms that don't port from other languages" instead of "...from Go or Node". Lists Node / Python / C# / Go equivalents inline where pattern divergence is notable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add agentId envelope field on session events for sub-agent attribution The wire-protocol schema added top-level `agentId?: string` to every session event envelope in commit f8cf846 ("Derive session event envelopes from schema") for sub-agent attribution. Every other SDK carries it; Rust silently drops it at the deserialization boundary. Concretely: the schema describes `agentId` as "Sub-agent instance identifier. Absent for events from the root/main agent and session- level events." Without the field, Rust consumers can't distinguish events emitted by a sub-agent from events emitted by the root agent. Adds: - `pub agent_id: Option<String>` on `types::SessionEvent` (hand- authored consumer-facing). - `pub agent_id: Option<String>` on `generated::session_events::TypedSessionEvent` via codegen update in `scripts/codegen/rust.ts`. - `#[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]` via the existing container `rename_all = "camelCase"` (covered) plus skip-on-None for clean wire output. Round-trip tests cover both struct shapes — sub-agent event with the field set, root-agent event without — to lock the parity in place against future schema changes or codegen regressions. Caught by a fresh parity audit against Node/Python/Go/.NET as part of the post-main-merge gap review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove parity-audit scratch files Accidentally committed by the parity audit run; not intended as repo artifacts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(skill): tighten SKILL.md and codify trait-with-defaults extension pattern Two changes prompted by review: 1. SKILL.md was too prose-heavy for an agent to scan. The "Idioms that don't port from other languages" section had grown to ~110 lines of essay-shaped advice with key rules buried in narrative. Replaced with three tight rule sections (Concurrency primitives, Optional fields and serde, plus a one-paragraph cross-language porting note that points back to the rule sections). Net: 345 -> 255 lines, no rule lost. 2. The trait-vs-callback-fields rule was implicit in the SessionHandler description but not explicit. Codified in "Traits and conversions" as a primary directive: prefer one trait with one default-impl method per event over per-event Box<dyn Fn> fields. Cited the three precedent traits in async Rust: - tower_lsp::LanguageServer (63 methods, default impls; LSP wire protocol) - rmcp::ServerHandler (26 methods, all default; MCP wire protocol) - notify::EventHandler (single on_event(enum) for uniform-shape events) Confirmed via research that no major async Rust crate ships per-event Box<dyn Fn> callback fields as its primary API; the pattern fights Send + Sync + 'static, fragments consumer state across closures, and skips exhaustiveness. The SDK's SessionHandler already uses the recommended shape (per-method with defaults plus a default on_event dispatcher); the doc just hadn't named the pattern. Section ordering tightened: Traits/conversions now leads into Extension points, then Concurrency primitives (channels matrix + cancellation), then Optional fields/serde. Reads top-to-bottom for an agent picking up a fresh task. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add continue_pending_work field to ResumeSessionConfig 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> * Add copilot_home, instruction_directories, tcp_connection_token surface 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> * Replace uuid dep with getrandom helper + reviewer-requested test coverage 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(rust): apply stripBooleanLiterals to schemas 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> * Use typed connect RPC instead of hand-coded JSON for handshake 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> * Remove send_telemetry from public surface (Rust-only quirk) 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> * Remove Rust-only Session convenience methods for cross-SDK parity 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> --------- 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> | 5 个月前 |
| Update SDK snapshot for Copilot CLI 1.0.100-0 | 8 天前 |