-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add Rust SDK (technical preview) #1164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tclem
wants to merge
69
commits into
main
Choose a base branch
from
tclem/rust-sdk-release-prep
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
69 commits
Select commit
Hold shift + click to select a range
5cdedb7
Add Rust SDK
tclem 7a786c7
Polish public API for 0.1.0 release
tclem d3a309a
Route generated SessionId/RequestId fields through hand-authored newt…
tclem b6f65a4
Pass ToolInvocation to define_tool closures
tclem 9e5683f
Make ping message argument optional
tclem 457b63a
Build Rust docs with all features in CI
tclem 8b60330
Merge remote-tracking branch 'origin/main' into tclem/rust-sdk-releas…
tclem 14faf4a
Address PR #1164 review feedback
tclem 9f07406
Regenerate Rust types for @github/copilot 1.0.39-0
tclem 6827987
Scope codegen-check workflow changes to Rust only
tclem 5605747
Point rust-publish-release workflow header to RELEASING.md
tclem 6d450cc
Update Rust scenario binaries for new define_tool signature
tclem 56dbf76
Rename Session::send_message -> send and align MessageOptions
tclem 92b25f8
Wrap subscribe() in EventSubscription / LifecycleSubscription newtypes
tclem 894593f
Apply nightly rustfmt to subscription module
tclem 0a4a257
Fix workspaces RPC method names (was singular `workspace.*`)
tclem 7156735
rust: add typed RPC namespace, route helpers through it
tclem b136d3f
Rename crate to `github-copilot-sdk`
tclem 7068f87
Add typed wrappers for filter/MCP/permission shapes (Bucket A.1, A.3,…
tclem 056ff6e
Document infinite_sessions parity + Client::stop deferral (Bucket A.2…
tclem 33544f9
Aggregate Client::stop errors across active sessions (Bucket B / A.6)
tclem ead063b
Add Bucket B.1 SessionConfig fields
tclem c4132c2
Add Bucket B.2 ClientOptions fields (log_level + idle timeout)
tclem 3b30d5a
Add Bucket B.2 on_list_models BYOK callback override
tclem 8c00cc0
Add MessageOptions.request_headers (Phase 4 § 4.5)
tclem 2a8f4e8
Add slash command registration (Phase 4 § 4.1)
tclem bfa519d
Add ADR 0001: SessionFsProvider trait and plumbing (Phase 4 § 4.2)
tclem 95a2ece
rust: implement SessionFsProvider (Phase 4 § 4.2)
tclem 7d45a83
Add W3C Trace Context propagation (Phase 4 § 4.3)
tclem 0abe6b2
Implement Default on ToolInvocation for test ergonomics
tclem aefb108
Add TelemetryConfig env-var passthrough on ClientOptions (Phase 4 § 4.4)
tclem b502b82
Document Rust-only API surface (Phase 4 § 4.7)
tclem cd8d6bb
Broaden skills discovery wording in copilot-instructions.md
tclem 3109b77
Fix ConnectionState::Errored wire form to match Go ("error" not "erro…
tclem 9062771
Rename ConnectionState::Errored to ConnectionState::Error
tclem f4aa8d9
Address PR #1164 cross-SDK consistency review
tclem 37ba14f
Type MessageOptions::mode as DeliveryMode enum
tclem f3a5987
Default permission-flow flags to Some(true)
tclem cd3436f
Mark remaining public config types non_exhaustive
tclem 078f0f1
Fix InputOptions doc-link to SessionUi::input
tclem 802dc3b
Drop cross-SDK comparisons from Rust source comments
tclem 85483d5
Move SessionFs ADR out of public crate
tclem c58e2f2
Fix SessionUi::elicitation wire field name
tclem a7c8215
Add typed on_auto_mode_switch handler for rate-limit recovery
tclem 64541af
Fix Client::list_sessions wire shape — wrap filter under params.filter
tclem 8308c3f
Bump @github/copilot pin to ^1.0.39 + regen Rust types
tclem 2766a79
Fix CI: rename scenarios crate ref + nightly-fmt regen + non_exhausti…
tclem 32b8b18
Use "GitHub Copilot CLI" consistently in user-facing docs
tclem a1e61d9
Address PR review: add prompts/attachments scenario + README parity s…
tclem 9c055aa
Add get_model and send_telemetry to Rust-only API list
tclem da486a0
Fix update-copilot-dependency: format Rust generated output
tclem 0f6a2ed
Fix Client::get_status and Client::get_auth_status wire method names
tclem 4a46f18
Refactor JsonRpcClient writer to actor pattern (cancel-safety)
tclem 9a1d9f3
Add WaiterGuard RAII for Session::send_and_wait (cancel-safety)
tclem c118701
Cooperative event-loop shutdown via Notify (cancel-safety)
tclem d97877a
Document cancel-safety for public async APIs (RFD-400)
tclem 19c865d
Merge remote-tracking branch 'origin/main' into tclem/rust-sdk-releas…
tclem a88c3eb
Add scheduled trigger to update-copilot-dependency.yml
tclem e04357c
Correct get_quota doc: endpoint exists cross-SDK, only wrapper is Rus…
tclem 2d725a5
Add ClientOptions::new() and Tool::new() builder methods
tclem d72e205
Add per-field builder methods to SessionConfig and ResumeSessionConfig
tclem e93ce8e
Scrub github-app references from public-repo files
tclem a870a3d
Round out builder coverage and document the Option<T> escape hatch
tclem d20cd3e
Revert "Add scheduled trigger to update-copilot-dependency.yml"
tclem 1742053
Add TelemetryConfig builder methods
tclem 96a710c
Merge remote-tracking branch 'origin/main' into tclem/rust-sdk-releas…
tclem 7a5f57e
Switch build.rs from curl to ureq with bounded retries
tclem 393d158
ci: validate embedded-cli bundle build on macOS / Linux / Windows
tclem 6dd07b9
Fix duplicate user_input dispatch (github-app#4249)
tclem File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,254 @@ | ||
| --- | ||
| name: rust-coding-skill | ||
| description: "Use this skill whenever editing `*.rs` files in the `rust/` SDK in order to write idiomatic, efficient, well-structured Rust code" | ||
| --- | ||
|
|
||
| # Rust Coding Skill | ||
|
|
||
| Opinionated Rust rules for the Copilot Rust SDK (`rust/`). Priority order: | ||
|
|
||
| 1. **Readable code** — every line should earn its place | ||
| 2. **Correct code** — especially in concurrent/async contexts | ||
| 3. **Performant code** — think about allocations, data structures, hot paths | ||
|
|
||
| ## Error handling | ||
|
|
||
| The SDK's public error type is `crate::Error` (`rust/src/error.rs`). Add new | ||
| variants there rather than introducing parallel error enums per module — every | ||
| public failure mode is part of the API contract and should be expressible in one | ||
| type. Internal modules can use `thiserror` enums when a richer local taxonomy | ||
| helps; convert at the boundary. | ||
|
|
||
| `anyhow` is reserved for binaries and example code. Library code never returns | ||
| `anyhow::Result` — callers can't pattern-match on `anyhow::Error`, so it would | ||
| prevent them from handling specific failures. | ||
|
|
||
| In production code, prefer `?`, `let-else`, and `if let`. Reach for `expect("…")` | ||
| when an invariant cannot fail and the message would help debug a future | ||
| regression. `unwrap()` belongs in tests only — Clippy enforces this in the SDK | ||
| via `#![cfg_attr(test, allow(clippy::unwrap_used))]` in `lib.rs`. | ||
|
|
||
| When you need to log on the way through, prefer | ||
| `.inspect_err(|e| warn!(error = ?e, "context"))?` over a `match` that logs and | ||
| re-wraps. It reads top-to-bottom and keeps the happy path uncluttered. | ||
|
|
||
| ## Async and concurrency | ||
|
|
||
| The default for request-scoped I/O is `async fn` plus `.await` — futures | ||
| inherit cancellation from their parent task and can borrow local references. | ||
| Reach for `tokio::spawn` only when you genuinely need background work (an event | ||
| loop, a long-lived watcher) and track the `JoinHandle` so you can cancel or join | ||
| it on shutdown. Fire-and-forget spawns silently swallow panics and outlive the | ||
| session; don't. | ||
|
|
||
| Blocking calls (filesystem, subprocess wait) belong in | ||
| `tokio::task::spawn_blocking`, *not* on the async runtime. The blocking pool is | ||
| bounded, so for genuinely long-lived workers (think: file watchers that run for | ||
| the lifetime of a session) prefer `std::thread::spawn` with a channel back into | ||
| async land. | ||
|
|
||
| Lock choice matters. `tokio::sync::Mutex` is correct when you must hold the | ||
| guard across `.await`; `parking_lot::Mutex` (or `RwLock`) is faster on hot | ||
| synchronous paths and is what `session.rs` uses for capability state. | ||
| `std::sync::Mutex` is rarely the right answer in this crate — its poisoning | ||
| semantics buy us nothing and it's slower than `parking_lot`. Never hold a | ||
| `std::sync::Mutex` guard across an `.await`; Clippy will catch this, but the | ||
| fix is to move the await out, not silence the lint. | ||
|
|
||
| For lazy statics use `std::sync::LazyLock`. The `once_cell` crate is no longer | ||
| needed. | ||
|
|
||
| ## Traits and conversions | ||
|
|
||
| Plain functions on a type beat traits for navigability — IDE "Go to definition" | ||
| on an inherent method jumps directly to the implementation, while a trait method | ||
| hops to the trait declaration first. Use that as the default. | ||
|
|
||
| There are four intentional exceptions where the SDK exposes a trait because it | ||
| *is* an extension point — code paths consumers must be able to plug behaviour | ||
| into: | ||
|
|
||
| - **`SessionHandler`** (`rust/src/handler.rs`) — single `on_event()` dispatches | ||
| CLI events. Notification-triggered events (`permission.requested`, | ||
| `external_tool.requested`, `elicitation.requested`) are dispatched on spawned | ||
| tasks, so implementations must be safe for concurrent invocation. Use | ||
| `ApproveAllHandler` in tests and examples. | ||
| - **`SessionHooks`** (`rust/src/hooks.rs`) — optional lifecycle callbacks. The | ||
| SDK auto-enables hooks (`config.hooks = Some(true)`) when an impl is supplied | ||
| to `create_session` / `resume_session`. | ||
| - **`SystemMessageTransform`** (`rust/src/system_message.rs`) — declare | ||
| `section_ids()` and return content from `transform_section()`. | ||
| - **`ToolHandler`** (`rust/src/tool.rs`) — client-side tool implementations. | ||
| Dispatch by name via `ToolHandlerRouter`. | ||
|
|
||
| Don't add new traits without a clear extension story. In particular, don't | ||
| implement `From`/`Into` for SDK-internal conversions: they can't take extra | ||
| parameters, can't return `Result`, and hide which conversion is happening at | ||
| call sites. Prefer named methods like `to_info(&self)` or | ||
| `MyType::from_record(record, ctx)`. | ||
|
|
||
| Trivial field re-shaping ("flatten this struct into that one") is best inlined | ||
| at the call site. A free-standing `map_x_to_y(x) -> Y` adds a hop without | ||
| adding clarity. | ||
|
|
||
| Closures should stay short — under ~10 lines is a good rule. Long anonymous | ||
| closures show up as opaque frames in stack traces. Extract them to named | ||
| functions when they grow. Visitor patterns are a closure-fest in disguise; | ||
| expose an `iter()` method instead and let the consumer drive the traversal. | ||
|
|
||
| ## Tracing — `#[tracing::instrument]` is banned | ||
|
|
||
| Banned via `clippy.toml`. Use manual spans with `error_span!`: | ||
|
|
||
| - **Almost always use `error_span!`**, not `info_span!`. Span level controls | ||
| the *minimum* filter at which the span appears. An `info_span` disappears when | ||
| the filter is `warn` or `error` — taking all child events with it, even | ||
| errors. `error_span!` ensures the span is always present. | ||
| - **Spawned tasks lose parent context.** Attach a span with `.instrument()` or | ||
| events inside won't correlate. | ||
| - **Never hold `span.enter()` guards across `.await`** — use `.instrument(span)` | ||
| instead (also enforced by Clippy). | ||
|
|
||
| ```rust | ||
| use tracing::Instrument; | ||
|
|
||
| async fn send_message(&self, session_id: &str, prompt: &str) -> Result<(), Error> { | ||
| let span = tracing::error_span!("send_message", session_id = %session_id); | ||
| async { /* body */ }.instrument(span).await | ||
| } | ||
|
|
||
| let span = tracing::error_span!("event_loop", session_id = %id); | ||
| tokio::spawn(async move { run_loop().await }.instrument(span)); | ||
| ``` | ||
|
|
||
| Log with structured fields: `info!(session_id = %id, "Session created")`. | ||
| Static messages stay greppable; dynamic data goes in named fields, not | ||
| interpolated into the message string. | ||
|
|
||
| ## Idioms that don't port from Go or Node | ||
|
|
||
| The most common pitfall when adapting code from the Node and Go SDKs is the | ||
| event subscription pattern. Those SDKs expose `client.on(handler)` callback | ||
| registration; the Rust SDK uses typed channels (`tokio::sync::broadcast` for | ||
| fan-out, `tokio::sync::mpsc` for single-consumer streams). Don't try to | ||
| recreate observer-style callbacks — drop the consumer onto a channel and let | ||
| each subscriber `.recv()` on its own task. See `Session::events_subscribe()` for | ||
| the canonical example. | ||
|
|
||
| Similarly, contexts and cancellation in Go/Node map to dropping a future or | ||
| calling `JoinHandle::abort()` — there is no `ctx.Done()` analogue to plumb | ||
| through every call site. Optional fields use `Option<T>`, not nullable | ||
| pointers; defaults come from `Default` impls, not constructors that accept | ||
| zero values. JSON tag attributes become `#[serde(rename_all = "camelCase")]` at | ||
| the type level plus `#[serde(rename = "…")]` on the occasional outlier. | ||
|
|
||
| ## Code organization | ||
|
|
||
| - **Public API:** every `pub` item in the crate is part of the SDK's contract. | ||
| Adding a field to a `pub struct` is a breaking change unless the struct is | ||
| `#[non_exhaustive]` or constructors hide field-by-field literals. Prefer | ||
| `Default + ..Default::default()` patterns and document new fields with | ||
| rustdoc. | ||
| - **Generated code lives in `rust/src/generated/`** and must not be | ||
| hand-edited. Regenerate with `cd scripts/codegen && npm run generate:rust`. | ||
| When a generated type lacks a field the schema doesn't yet describe (e.g. | ||
| `Tool::overrides_built_in_tool`), hand-author the user-facing type in | ||
| `rust/src/types.rs` and stop re-exporting the generated one. | ||
| - **`#[expect(dead_code)]`** instead of `#[allow(dead_code)]` on individual | ||
| fields — it forces a cleanup once the field gets used. | ||
| - **`..Default::default()`** — avoid in production code (be explicit about | ||
| which fields you're setting); prefer it in tests and doc examples to keep | ||
| the focus on the values that matter for the test. | ||
| - **Import grouping** — three blocks separated by blank lines: | ||
| (1) `std`/`core`/`alloc`, (2) external crates, (3) | ||
| `crate::`/`super::`/`self::`. Enforced by nightly `cargo fmt` via | ||
| `rust/.rustfmt.nightly.toml`. | ||
| - **`pub(crate)` vs `pub`** — most modules in `lib.rs` are private (`mod`), so | ||
| `pub` items inside them are already crate-private. Use `pub(crate)` only when | ||
| you want to be explicit that an item must not become part of the public API. | ||
|
|
||
| ## Testing | ||
|
|
||
| - **No mock testing.** Depend on real implementations, spin up lightweight | ||
| versions (e.g. `MockServer` in tests), or restructure code so the logic | ||
| under test takes its dependency's output as input. | ||
| - `assert_eq!(actual, expected)` — actual first, for readable diffs. | ||
| - Tests at end of file: `#[cfg(test)] mod tests`. Never place production code | ||
| after the test module. | ||
| - Keep tests concurrent-safe — unique temp dirs (`tempfile::tempdir()`), | ||
| unique data, no global state. | ||
| - `ApproveAllHandler` is the standard test handler for sessions that don't | ||
| exercise permission logic — see `rust/src/handler.rs:174`. | ||
|
|
||
| ## Cross-platform | ||
|
|
||
| The SDK ships on macOS, Windows, and Linux; CI exercises all three. Construct | ||
| paths with `Path::join` rather than string concatenation — `/` and `\` are not | ||
| interchangeable, and string equality breaks on Windows UNC paths. Log paths | ||
| with `path.display()`; serialize with `to_string_lossy()` only when you need a | ||
| `String`. | ||
|
|
||
| Process spawning needs care. The SDK applies `CREATE_NO_WINDOW` on Windows | ||
| when launching the CLI (see `Client::build_command`); preserve that if you | ||
| touch process spawning. Subprocess stdout often contains `\r` on Windows — strip | ||
| or split on `\r?\n` rather than assuming `\n`. | ||
|
|
||
| Tests must use `tempfile::tempdir()`, never hardcoded `/tmp/`, and any test | ||
| that asserts on a path string needs to normalize separators or use | ||
| `std::path::MAIN_SEPARATOR`. | ||
|
|
||
| ## Build speed | ||
|
|
||
| Specify Tokio features explicitly — never `features = ["full"]`. Iterate with | ||
| `cargo check`; reach for `cargo build` only when you need the binary. Audit | ||
| new dependency feature flags with `cargo tree` before committing. | ||
|
|
||
| ## Comments | ||
|
|
||
| Explain **why**, never **what**. No comments that restate code. No decorative | ||
| banners (`// ── Section ────────`). | ||
|
tclem marked this conversation as resolved.
|
||
|
|
||
| **Never compare to other SDKs in code comments or rustdoc.** Don't write | ||
| "Mirrors Node's `Foo`", "Like Go's `Bar`", "Unlike Python's `Baz`", or include | ||
| file/line citations into other SDKs (`nodejs/src/types.ts:1592`, `go/types.go:14`). | ||
| The Rust SDK seeks parity with the Node, Python, Go, and .NET SDKs, and that | ||
| fact is stated once at the top of `rust/README.md`. Intentional divergences | ||
| live in the README's "Differences From Other SDKs" section. Repeating the | ||
| relationship per-symbol is unscalable, drifts as the other SDKs evolve, and | ||
| adds noise to consumer-facing rustdoc — Rust users care about the Rust API, | ||
| not its lineage. Self-references within the Rust crate (e.g. "Mirrors | ||
| [`from_streams`] but adds…") are fine. | ||
|
|
||
| ## Toolchain | ||
|
|
||
| The SDK is pinned to `rust 1.94.0` via `rust/rust-toolchain.toml`. Formatting | ||
| uses nightly (`nightly-2026-04-14`) so unstable rustfmt options like grouped | ||
| imports work — see `rust/.rustfmt.nightly.toml`. CI runs: | ||
|
|
||
| ```bash | ||
| cd rust | ||
| cargo +nightly-2026-04-14 fmt --check | ||
| cargo clippy --all-features --all-targets -- -D warnings | ||
| cargo test --all-features | ||
| ``` | ||
|
|
||
| Match those exact commands locally before pushing. | ||
|
|
||
| ## Codegen | ||
|
|
||
| JSON-RPC and session-event types are generated from the Copilot CLI schema: | ||
|
|
||
| | Source | Output | | ||
| |---|---| | ||
| | `nodejs/node_modules/@github/copilot/schemas/api.schema.json` | `rust/src/generated/api_types.rs` | | ||
| | `nodejs/node_modules/@github/copilot/schemas/session-events.schema.json` | `rust/src/generated/session_events.rs` | | ||
|
|
||
| Regenerate with: | ||
|
|
||
| ```bash | ||
| cd scripts/codegen && npm run generate:rust | ||
| ``` | ||
|
|
||
| Never hand-edit files under `rust/src/generated/`. If a generated type needs a | ||
| field the schema lacks, hand-author the user-facing type in `rust/src/types.rs` | ||
| and stop re-exporting the generated one. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.