Patterns specific to the Rust SDK in this repo (rust/) that aren't obvious
from general Rust knowledge.
let raw = serde_json::json!({
"name": "get_weather",
"description": "...",
"parameters": { "type": "object", ... },
});
config.tools = Some(vec![serde_json::from_value(raw)?]);use copilot::tool::{Tool, ToolHandler, ToolHandlerRouter, ToolInvocation, ToolResult};
use copilot::Error;
struct GetWeatherTool;
#[async_trait::async_trait]
impl ToolHandler for GetWeatherTool {
fn tool(&self) -> Tool {
Tool {
name: "get_weather".to_string(),
description: "Get the current weather for a city.".to_string(),
// ..Default::default() — leaves namespaced_name, instructions,
// overrides_built_in_tool, skip_permission at their defaults.
..Default::default()
}
}
async fn call(&self, invocation: ToolInvocation) -> Result<ToolResult, Error> {
// ...
Ok(ToolResult::Text("...".into()))
}
}
use copilot::handler::ApproveAllHandler;
use std::sync::Arc;
let router = ToolHandlerRouter::new(
vec![Box::new(GetWeatherTool)],
Arc::new(ApproveAllHandler),
);The session event loop is spawned per session. Always attach a span so events emitted inside it correlate.
tokio::spawn(async move {
while let Some(event) = rx.recv().await {
info!("event {:?}", event); // No span — can't filter by session
}
});use tracing::Instrument;
let span = tracing::error_span!("session_event_loop", session_id = %id);
tokio::spawn(async move {
while let Some(event) = rx.recv().await {
info!(event_type = ?event.kind, "session event");
}
}.instrument(span));HandlerEvent::PermissionRequest and HandlerEvent::ExternalTool are dispatched
on spawned tasks (see rust/src/session.rs:973 and :1022). Implementations
must be safe for concurrent invocation.
The SessionHandler trait declares Send + Sync + 'static, so the compiler
enforces this — handlers with non-Sync state (e.g. RefCell, Cell,
Rc) won't compile. The examples below make the rejection mechanism explicit.
struct MyHandler {
last_request: std::cell::RefCell<Option<String>>, // RefCell: !Sync
}
#[async_trait]
impl SessionHandler for MyHandler {
// ^^^^^^^^^^^^^^ the trait `Sync` is not implemented for `RefCell<...>`
async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { /* ... */ }
}The error surfaces at the impl site, not at use site, because the trait's
Send + Sync bound makes RefCell ineligible for any field of any type that
implements SessionHandler.
struct MyHandler {
last_request: parking_lot::Mutex<Option<String>>, // Mutex<T>: Sync if T: Send
}Adding a field to a public, non-exhaustive struct is a breaking change because existing callers' struct literals stop compiling. Two patterns soften this:
#[derive(Default)]
pub struct Tool {
pub name: String,
pub description: String,
// new field
pub overrides_built_in_tool: bool,
}
// In docs and examples:
let t = Tool {
name: "x".into(),
description: "y".into(),
..Default::default()
};Use sparingly — only for types that are only meant to be received from the SDK, never built by users.
#[non_exhaustive]
pub struct CreateSessionResult {
pub session_id: SessionId,
// ...
}When a test doesn't exercise the permission flow, use the SDK's built-in
ApproveAllHandler instead of writing a custom one:
use copilot::handler::ApproveAllHandler;
use copilot::types::SessionConfig;
use std::sync::Arc;
let session = client
.create_session(SessionConfig::default().with_handler(Arc::new(ApproveAllHandler)))
.await?;# 1. Update schema (usually arrives with @github/copilot package update)
cd nodejs && npm install @github/copilot@latest && cd ..
# 2. Regenerate Rust types
cd scripts/codegen && npm run generate:rust
# 3. Verify
cd ../../rust && cargo check --all-featuresIf a generated type changes shape, hand-fix any user-facing wrappers in
rust/src/types.rs rather than monkey-patching the generated file.