Skip to content

Latest commit

 

History

History
184 lines (140 loc) · 4.82 KB

File metadata and controls

184 lines (140 loc) · 4.82 KB

Rust Coding Skill — Examples

Patterns specific to the Rust SDK in this repo (rust/) that aren't obvious from general Rust knowledge.

Defining a tool

Anti-pattern — building the wire payload by hand

let raw = serde_json::json!({
    "name": "get_weather",
    "description": "...",
    "parameters": { "type": "object", ... },
});
config.tools = Some(vec![serde_json::from_value(raw)?]);

Preferred — implement ToolHandler, route via ToolHandlerRouter

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),
);

Spans for spawned event loops

The session event loop is spawned per session. Always attach a span so events emitted inside it correlate.

Anti-pattern — losing parent context

tokio::spawn(async move {
    while let Some(event) = rx.recv().await {
        info!("event {:?}", event);  // No span — can't filter by session
    }
});

Preferred — error_span! + .instrument()

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));

Concurrent permission handlers

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.

Won't compile — non-Sync state

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.

Preferred — parking_lot::Mutex or atomics

struct MyHandler {
    last_request: parking_lot::Mutex<Option<String>>,  // Mutex<T>: Sync if T: Send
}

Adding a field to a public struct

Adding a field to a public, non-exhaustive struct is a breaking change because existing callers' struct literals stop compiling. Two patterns soften this:

Pattern 1 — Default + ..Default::default() in docs

#[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()
};

Pattern 2 — #[non_exhaustive] for types callers shouldn't construct

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,
    // ...
}

Test handler for non-permission scenarios

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?;

Regenerating types after a schema bump

# 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-features

If a generated type changes shape, hand-fix any user-facing wrappers in rust/src/types.rs rather than monkey-patching the generated file.