Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
5cdedb7
Add Rust SDK
tclem Apr 28, 2026
7a786c7
Polish public API for 0.1.0 release
tclem Apr 28, 2026
d3a309a
Route generated SessionId/RequestId fields through hand-authored newt…
tclem Apr 28, 2026
b6f65a4
Pass ToolInvocation to define_tool closures
tclem Apr 28, 2026
9e5683f
Make ping message argument optional
tclem Apr 28, 2026
457b63a
Build Rust docs with all features in CI
tclem Apr 29, 2026
8b60330
Merge remote-tracking branch 'origin/main' into tclem/rust-sdk-releas…
tclem Apr 29, 2026
14faf4a
Address PR #1164 review feedback
tclem Apr 29, 2026
9f07406
Regenerate Rust types for @github/copilot 1.0.39-0
tclem Apr 29, 2026
6827987
Scope codegen-check workflow changes to Rust only
tclem Apr 29, 2026
5605747
Point rust-publish-release workflow header to RELEASING.md
tclem Apr 29, 2026
6d450cc
Update Rust scenario binaries for new define_tool signature
tclem Apr 29, 2026
56dbf76
Rename Session::send_message -> send and align MessageOptions
tclem Apr 29, 2026
92b25f8
Wrap subscribe() in EventSubscription / LifecycleSubscription newtypes
tclem Apr 29, 2026
894593f
Apply nightly rustfmt to subscription module
tclem Apr 29, 2026
0a4a257
Fix workspaces RPC method names (was singular `workspace.*`)
tclem Apr 29, 2026
7156735
rust: add typed RPC namespace, route helpers through it
tclem Apr 29, 2026
b136d3f
Rename crate to `github-copilot-sdk`
tclem Apr 29, 2026
7068f87
Add typed wrappers for filter/MCP/permission shapes (Bucket A.1, A.3,…
tclem Apr 29, 2026
056ff6e
Document infinite_sessions parity + Client::stop deferral (Bucket A.2…
tclem Apr 29, 2026
33544f9
Aggregate Client::stop errors across active sessions (Bucket B / A.6)
tclem Apr 29, 2026
ead063b
Add Bucket B.1 SessionConfig fields
tclem Apr 29, 2026
c4132c2
Add Bucket B.2 ClientOptions fields (log_level + idle timeout)
tclem Apr 29, 2026
3b30d5a
Add Bucket B.2 on_list_models BYOK callback override
tclem Apr 29, 2026
8c00cc0
Add MessageOptions.request_headers (Phase 4 § 4.5)
tclem Apr 29, 2026
2a8f4e8
Add slash command registration (Phase 4 § 4.1)
tclem Apr 29, 2026
bfa519d
Add ADR 0001: SessionFsProvider trait and plumbing (Phase 4 § 4.2)
tclem Apr 29, 2026
95a2ece
rust: implement SessionFsProvider (Phase 4 § 4.2)
tclem Apr 29, 2026
7d45a83
Add W3C Trace Context propagation (Phase 4 § 4.3)
tclem Apr 29, 2026
0abe6b2
Implement Default on ToolInvocation for test ergonomics
tclem Apr 29, 2026
aefb108
Add TelemetryConfig env-var passthrough on ClientOptions (Phase 4 § 4.4)
tclem Apr 29, 2026
b502b82
Document Rust-only API surface (Phase 4 § 4.7)
tclem Apr 29, 2026
cd8d6bb
Broaden skills discovery wording in copilot-instructions.md
tclem Apr 29, 2026
3109b77
Fix ConnectionState::Errored wire form to match Go ("error" not "erro…
tclem Apr 29, 2026
9062771
Rename ConnectionState::Errored to ConnectionState::Error
tclem Apr 29, 2026
f4aa8d9
Address PR #1164 cross-SDK consistency review
tclem Apr 29, 2026
37ba14f
Type MessageOptions::mode as DeliveryMode enum
tclem Apr 29, 2026
f3a5987
Default permission-flow flags to Some(true)
tclem Apr 29, 2026
cd3436f
Mark remaining public config types non_exhaustive
tclem Apr 29, 2026
078f0f1
Fix InputOptions doc-link to SessionUi::input
tclem Apr 29, 2026
802dc3b
Drop cross-SDK comparisons from Rust source comments
tclem Apr 29, 2026
85483d5
Move SessionFs ADR out of public crate
tclem Apr 29, 2026
c58e2f2
Fix SessionUi::elicitation wire field name
tclem Apr 29, 2026
a7c8215
Add typed on_auto_mode_switch handler for rate-limit recovery
tclem Apr 29, 2026
64541af
Fix Client::list_sessions wire shape — wrap filter under params.filter
tclem Apr 29, 2026
8308c3f
Bump @github/copilot pin to ^1.0.39 + regen Rust types
tclem Apr 29, 2026
2766a79
Fix CI: rename scenarios crate ref + nightly-fmt regen + non_exhausti…
tclem Apr 29, 2026
32b8b18
Use "GitHub Copilot CLI" consistently in user-facing docs
tclem Apr 29, 2026
a1e61d9
Address PR review: add prompts/attachments scenario + README parity s…
tclem Apr 29, 2026
9c055aa
Add get_model and send_telemetry to Rust-only API list
tclem Apr 29, 2026
da486a0
Fix update-copilot-dependency: format Rust generated output
tclem Apr 29, 2026
0f6a2ed
Fix Client::get_status and Client::get_auth_status wire method names
tclem Apr 29, 2026
4a46f18
Refactor JsonRpcClient writer to actor pattern (cancel-safety)
tclem Apr 30, 2026
9a1d9f3
Add WaiterGuard RAII for Session::send_and_wait (cancel-safety)
tclem Apr 30, 2026
c118701
Cooperative event-loop shutdown via Notify (cancel-safety)
tclem Apr 30, 2026
d97877a
Document cancel-safety for public async APIs (RFD-400)
tclem Apr 30, 2026
19c865d
Merge remote-tracking branch 'origin/main' into tclem/rust-sdk-releas…
tclem Apr 30, 2026
a88c3eb
Add scheduled trigger to update-copilot-dependency.yml
tclem Apr 30, 2026
e04357c
Correct get_quota doc: endpoint exists cross-SDK, only wrapper is Rus…
tclem Apr 30, 2026
2d725a5
Add ClientOptions::new() and Tool::new() builder methods
tclem Apr 30, 2026
d72e205
Add per-field builder methods to SessionConfig and ResumeSessionConfig
tclem Apr 30, 2026
e93ce8e
Scrub github-app references from public-repo files
tclem Apr 30, 2026
a870a3d
Round out builder coverage and document the Option<T> escape hatch
tclem Apr 30, 2026
d20cd3e
Revert "Add scheduled trigger to update-copilot-dependency.yml"
tclem Apr 30, 2026
1742053
Add TelemetryConfig builder methods
tclem Apr 30, 2026
96a710c
Merge remote-tracking branch 'origin/main' into tclem/rust-sdk-releas…
tclem Apr 30, 2026
7a5f57e
Switch build.rs from curl to ureq with bounded retries
tclem Apr 30, 2026
393d158
ci: validate embedded-cli bundle build on macOS / Linux / Windows
tclem Apr 30, 2026
6dd07b9
Fix duplicate user_input dispatch (github-app#4249)
tclem Apr 30, 2026
17a7755
Drop Skills section from .github/copilot-instructions.md
tclem May 2, 2026
c8fe373
Render experimental RPC methods as a styled rustdoc admonition
tclem May 2, 2026
b907681
docs(skill): clarify that handler thread-safety is compiler-enforced
tclem May 2, 2026
7202dfd
Refactor session shutdown from Notify to CancellationToken
tclem May 2, 2026
e96aea6
docs(skill): rewrite "Idioms that don't port" with full research backing
tclem May 2, 2026
6ac706d
Merge remote-tracking branch 'origin/main' into tclem/rust-sdk-releas…
tclem May 2, 2026
a60a54b
Add agentId envelope field on session events for sub-agent attribution
tclem May 2, 2026
acc52d6
Remove parity-audit scratch files
tclem May 2, 2026
8a2d62b
docs(skill): tighten SKILL.md and codify trait-with-defaults extensio…
tclem May 2, 2026
59557da
Add continue_pending_work field to ResumeSessionConfig
tclem May 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,13 @@

## Where to add new code or tests 🧭

- SDK code: `nodejs/src`, `python/copilot`, `go`, `dotnet/src`
- Unit tests: `nodejs/test`, `python/*`, `go/*`, `dotnet/test`
- SDK code: `nodejs/src`, `python/copilot`, `go`, `dotnet/src`, `rust/src`
- Unit tests: `nodejs/test`, `python/*`, `go/*`, `dotnet/test`, `rust/tests`
- E2E tests: `*/e2e/` folders that use the shared replay proxy and `test/snapshots/`
- Generated types: update schema in `@github/copilot` then run `cd nodejs && npm run generate:session-types` and commit generated files in `src/generated` or language generated location.

## Skills 🛠️

Repo-scoped skills live under `.github/skills/<skill-name>/` and are auto-discovered by Copilot CLI. Load the relevant skill before editing the matching file types.
Comment thread
tclem marked this conversation as resolved.
Outdated

- **`rust-coding-skill`** (`.github/skills/rust-coding-skill/SKILL.md`) — load before editing any `*.rs` file in `rust/`. Covers error handling, async/concurrency, tracing, codegen workflow, and Rust SDK-specific trait patterns.
243 changes: 243 additions & 0 deletions .github/skills/rust-coding-skill/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
---
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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Curious the lack of C# mentioned

Copy link
Copy Markdown
Member Author

@tclem tclem May 2, 2026

Choose a reason for hiding this comment

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

This is probably keying off of my guidance and copilot giving up after looking at only a couple of examples. No python mentioned either.

Honestly, I'm not sure how helpful this is, but I do know that copilot's first pass at this was to write some very non-idiomatic rust. You have to prompt it to look at what the community projects do and what they avoid. This language can probably be tightened up...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch — and your hunch that the framing was lopsided is right. Let me come back with a thorough rewrite.

Doing a proper research pass across the section's four claims (event subscription, cancellation, optional fields, serde) and auditing the SDK's current code against them. The cancellation guidance in particular is too absolute — tokio_util::sync::CancellationToken is real and idiomatic for SDK-internal task coordination (it's what tonic does). Will follow up here once the doc rewrite + matching SDK refactor land.

  Generated via Copilot (Claude Opus 4.7) on behalf of @tclem

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done. Two commits landed addressing this and your original "lopsided framing" callout:

  • e96aea6 -- rewrites the SKILL "Idioms that don't port" section.
  • 7202dfd -- refactors Session.shutdown from Arc<Notify> to tokio_util::sync::CancellationToken and exposes Session::cancellation_token().

Section is now language-agnostic -- title is "Idioms that don't port from other languages", lists Node / Python / C# / Go equivalents inline where pattern divergence is notable.

Cancellation guidance rewritten. The previous framing ("drop the future or call abort() -- there is no ctx.Done() analogue") was wrong. Drop-to-cancel is the primitive, but tokio_util::sync::CancellationToken IS the Rust analog to ctx.Done() / .NET's CancellationToken, just optional and explicit per-API. The right framing is two cases:

  1. Caller-owned futures (send_message, send_and_wait, subscription streams): drop the future. select! / timeout / drop give the caller everything they need; adding a token parameter just duplicates that. This is what reqwest / sqlx / aws-sdk-* / tonic's client side do.
  2. SDK-internal task coordination (event loop, subprocess reader, anything tokio::spawned): use CancellationToken. This is what tonic does for client-disconnect propagation. tokio-graceful-shutdown builds an entire framework on it.

The SDK now follows pattern 2 internally: Session.shutdown is a CancellationToken (refactored from the previous Arc<Notify>), and there's a public Session::cancellation_token() returning a child token so power users can bind their own work to the session lifetime via select!. Cancelling the child does NOT shut down the session -- isolation by design.

Other three idioms (event subscription, optional fields, serde JSON) -- guidance was correct but incomplete; doc enrichment landed in e96aea6 with the full primitive matrix, #[non_exhaustive] + builder pairing, and skip_serializing_if/default notes. No code refactor needed for those -- already idiomatic.

Citations for the cancellation framing:

Validation green: 237 tests (was 233; +4 from new builder + cancellation_token tests), full clippy / fmt / doc -D warnings clean.

  Generated via Copilot (Claude Opus 4.7) on behalf of @tclem

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
Comment thread
tclem marked this conversation as resolved.
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 ────────`).
Comment thread
tclem marked this conversation as resolved.

## 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.
Loading
Loading