Skip to content

Add Rust version of the SDK - #112

Closed
danielsan wants to merge 2 commits into
github:mainfrom
danielsan:main
Closed

Add Rust version of the SDK#112
danielsan wants to merge 2 commits into
github:mainfrom
danielsan:main

Conversation

@danielsan

Copy link
Copy Markdown

Coded with the help of copilot-cli using Claude Sonnet 4.5

danielsan and others added 2 commits January 22, 2026 21:16
- Add error handling module for SDK operations.
- Create generated module for session events.
- Define session event types for communication.
- Implement JSON-RPC client for bidirectional communication.
- Add session management for interactive conversations.
- Introduce tool system for defining and handling custom tools.
- Define core types for the Copilot SDK, including client options and system messages.
- Add tests for JSON-RPC functionality, tool system, and core types.
@danielsan
danielsan requested a review from a team as a code owner January 23, 2026 02:18
Copilot AI review requested due to automatic review settings January 23, 2026 02:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds a Rust implementation of the GitHub Copilot SDK, following the same patterns as the existing Python, Go, Node.js, and .NET SDKs. The implementation was created with help from Copilot CLI using Claude Sonnet 4.5.

Changes:

  • New Rust SDK implementation with async/await support using Tokio
  • Core types and error handling
  • JSON-RPC client for communication with Copilot CLI
  • Session and client management
  • Tool and permission handler systems
  • Test suite and examples
  • Integration with the existing justfile build system

Reviewed changes

Copilot reviewed 20 out of 22 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
rust/Cargo.toml Package manifest with dependencies (tokio, serde, async-trait, etc.)
rust/src/lib.rs Main library entry point exposing public API
rust/src/types.rs Core type definitions for SDK
rust/src/error.rs Error types using thiserror
rust/src/tools.rs Tool system with handler traits and result types
rust/src/session.rs Session management and message handling
rust/src/client.rs Client implementation for CLI communication
rust/src/jsonrpc.rs JSON-RPC 2.0 protocol implementation
rust/src/generated/ Generated session event types
rust/tests/ Unit tests for types, tools, and JSON-RPC
rust/examples/ Example programs demonstrating SDK usage
rust/README.md Comprehensive documentation for Rust SDK
justfile Updated to include Rust formatting, linting, and testing
README.md Updated main README with Rust SDK entry

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread rust/tests/tools_tests.rs
Comment on lines +40 to +47
#[test]
fn test_tool_result_error() {
let result = ToolResult::error("Something went wrong");

assert_eq!(result.error, Some("Something went wrong".to_string()));
assert_eq!(result.success, Some(false));
assert!(result.content.is_none());
}

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

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

This test verifies the wrong ToolResult structure. The test should verify textResultForLlm and resultType: "failure" instead of checking for a boolean success field and the absence of content. Update this test after fixing the ToolResult structure.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nah yall dont close shit now im just know learning how to do everything I got all the files on another phone everyone that helped the bitch Ashley Patterson yall can go to jail with the bitch just hang tight let's see how smart yall really are

Comment thread rust/tests/tools_tests.rs
Comment on lines +49 to +60
#[test]
fn test_tool_result_binary() {
let data = vec![1, 2, 3, 4];
let result = ToolResult::binary(data, "application/octet-stream");

assert!(result.data.is_some());
assert_eq!(
result.mime_type,
Some("application/octet-stream".to_string())
);
assert_eq!(result.success, Some(true));
}

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

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

This test verifies the wrong ToolResult structure for binary data. After fixing the ToolResult structure to use binaryResultsForLlm array (matching other SDKs), this test needs to be updated to verify the correct structure where binary results are in an array with fields like data, mimeType, type, and optional description.

Copilot uses AI. Check for mistakes.
Comment thread rust/src/client.rs
Comment on lines +324 to +336
// Handle synchronously for now
let result = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
if let Some(session) = sessions.lock().await.get(&session_id) {
session.handle_permission_request(request).await
} else {
Ok(crate::types::PermissionRequestResult {
kind: "allow".to_string(),
rules: None,
})
}
})
});

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

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

Using tokio::task::block_in_place with block_on for permission request handling can cause performance issues and potential deadlocks in the Tokio runtime. This pattern blocks a worker thread while waiting for an async operation, which defeats the purpose of async/await. Consider refactoring the RequestHandler trait to support async handlers, or restructure the permission handling to be truly async without blocking.

Suggested change
// Handle synchronously for now
let result = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
if let Some(session) = sessions.lock().await.get(&session_id) {
session.handle_permission_request(request).await
} else {
Ok(crate::types::PermissionRequestResult {
kind: "allow".to_string(),
rules: None,
})
}
})
});
// Handle permission request asynchronously
let result = if let Some(session) = sessions.lock().await.get(&session_id) {
session.handle_permission_request(request).await
} else {
Ok(crate::types::PermissionRequestResult {
kind: "allow".to_string(),
rules: None,
})
};

Copilot uses AI. Check for mistakes.
Comment thread rust/Cargo.toml
Comment on lines +1 to +13
[package]
name = "github-copilot-sdk"
version = "0.1.0"
edition = "2021"
authors = ["GitHub <opensource+copilot-sdk@github.com>"]
description = "GitHub Copilot SDK for Rust - Embed Copilot's agentic workflows in your application"
license = "MIT"
repository = "https://github.com/github/copilot-sdk"
homepage = "https://github.com/github/copilot-sdk"
documentation = "https://docs.rs/github-copilot-sdk"
readme = "README.md"
keywords = ["github", "copilot", "ai", "agent", "sdk"]
categories = ["api-bindings", "development-tools"]

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

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

The Cargo.toml should specify a minimum supported Rust version (MSRV) using the rust-version field. The README claims "Rust: 1.70 or higher" but this is not enforced in the package manifest. Add rust-version = "1.70" to the [package] section to ensure compatibility.

Copilot uses AI. Check for mistakes.
Comment thread rust/tests/tools_tests.rs
Comment on lines +31 to +38
#[test]
fn test_tool_result_text() {
let result = ToolResult::text("Hello");

assert_eq!(result.content, Some("Hello".to_string()));
assert_eq!(result.success, Some(true));
assert!(result.error.is_none());
}

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

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

This test verifies the wrong ToolResult structure. Since the ToolResult structure needs to be fixed to match other SDKs (using textResultForLlm instead of content, and resultType instead of success boolean), this test will need to be updated accordingly to verify the correct field names.

Copilot uses AI. Check for mistakes.
Comment thread rust/tests/tools_tests.rs
Comment on lines +62 to +75
#[test]
fn test_tool_result_with_telemetry() {
let mut telemetry = std::collections::HashMap::new();
telemetry.insert("duration_ms".to_string(), serde_json::json!(42));

let result = ToolResult::text("Done").with_telemetry(telemetry.clone());

assert_eq!(result.content, Some("Done".to_string()));
assert!(result.telemetry.is_some());
assert_eq!(
result.telemetry.unwrap().get("duration_ms"),
Some(&serde_json::json!(42))
);
}

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

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

This test is verifying the telemetry field name, but the correct field name per other SDKs should be toolTelemetry. Update this test after renaming the field in the ToolResult structure.

Copilot uses AI. Check for mistakes.
Comment thread rust/src/session.rs
Comment on lines +41 to +49
/// Options for sending a message
#[derive(Debug, Clone, Default)]
pub struct MessageOptions {
/// The prompt to send
pub prompt: String,

/// Additional context or parameters
pub context: Option<HashMap<String, Value>>,
}

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

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

The MessageOptions structure is incomplete. It should include attachments (optional list of Attachment) and mode (optional string: "enqueue" or "immediate") fields to match other SDK implementations. The current context field is not present in other SDKs and appears to be incorrect. Please align this structure with the Python/Go/Node.js implementations which use prompt, attachments, and mode.

Copilot uses AI. Check for mistakes.
Comment thread rust/src/types.rs
Comment on lines +1 to +127
//! Core types for the Copilot SDK

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Connection state of the client
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ConnectionState {
Disconnected,
Connecting,
Connected,
Error,
}

/// Options for configuring the Copilot CLI client
#[derive(Debug, Clone)]
pub struct ClientOptions {
/// Path to the Copilot CLI executable (default: "copilot")
pub cli_path: String,
/// Working directory for the CLI process (default: current directory)
pub cwd: Option<String>,
/// Port for TCP transport (default: 0 = random port)
pub port: u16,
/// Use stdio transport instead of TCP (default: true)
pub use_stdio: bool,
/// URL of an existing Copilot CLI server to connect to over TCP
/// Format: "host:port", "http://host:port", or just "port"
/// Mutually exclusive with cli_path when use_stdio is true
pub cli_url: Option<String>,
/// Log level for the CLI server (default: "info")
pub log_level: String,
/// Automatically start the CLI server on first use (default: true)
pub auto_start: bool,
/// Automatically restart the CLI server if it crashes (default: true)
pub auto_restart: bool,
/// Environment variables for the CLI process
pub env: Option<HashMap<String, String>>,
}

impl Default for ClientOptions {
fn default() -> Self {
Self {
cli_path: "copilot".to_string(),
cwd: None,
port: 0,
use_stdio: true,
cli_url: None,
log_level: "info".to_string(),
auto_start: true,
auto_restart: true,
env: None,
}
}
}

/// System message configuration mode
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "lowercase")]
pub enum SystemMessage {
/// Append content to the default system message
Append {
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<String>,
},
/// Replace the entire system message (removes SDK guardrails)
Replace { content: String },
}

/// Permission request from the server
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionRequest {
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}

/// Result of a permission request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionRequestResult {
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub rules: Option<Vec<serde_json::Value>>,
}

/// Context for permission invocation
#[derive(Debug, Clone)]
pub struct PermissionInvocation {
pub session_id: String,
}

/// MCP (Model Context Protocol) server configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MCPServerConfig {
Local(MCPLocalServerConfig),
Remote(MCPRemoteServerConfig),
}

/// Local/stdio MCP server configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MCPLocalServerConfig {
pub tools: Vec<String>,
#[serde(rename = "type")]
pub server_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<u32>,
pub command: String,
pub args: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<HashMap<String, String>>,
}

/// Remote MCP server configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MCPRemoteServerConfig {
pub tools: Vec<String>,
#[serde(rename = "type")]
pub server_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<u32>,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<HashMap<String, String>>,
}

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

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

The Attachment type is missing from the types module. Other SDKs define an Attachment type with fields like type ("file" or "directory"), path, and optional displayName. This type is needed for the MessageOptions attachments field. Add this type definition to maintain API consistency with other SDKs.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

Comment thread rust/src/tools.rs
Comment on lines +56 to +123
/// Result of a tool execution
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
/// Result content (text or data)
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,

/// Binary data (base64 encoded)
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<String>,

/// MIME type for binary data
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,

/// Telemetry data
#[serde(skip_serializing_if = "Option::is_none")]
pub telemetry: Option<HashMap<String, Value>>,

/// Whether the tool execution was successful
#[serde(skip_serializing_if = "Option::is_none")]
pub success: Option<bool>,

/// Error message if execution failed
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}

impl ToolResult {
/// Create a text result
pub fn text(content: impl Into<String>) -> Self {
Self {
content: Some(content.into()),
data: None,
mime_type: None,
telemetry: None,
success: Some(true),
error: None,
}
}

/// Create a binary result
pub fn binary(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
Self {
content: None,
data: Some(base64::encode(&data)),
mime_type: Some(mime_type.into()),
telemetry: None,
success: Some(true),
error: None,
}
}

/// Create an error result
pub fn error(message: impl Into<String>) -> Self {
Self {
content: None,
data: None,
mime_type: None,
telemetry: None,
success: Some(false),
error: Some(message.into()),
}
}

/// Add telemetry data to the result
pub fn with_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
self.telemetry = Some(telemetry);

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

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

The ToolResult structure is inconsistent with other SDK implementations (Python, Go, Node.js). The other SDKs use:

  • textResultForLlm (required string field) - not content
  • binaryResultsForLlm (optional array) - not data and mime_type as separate fields
  • resultType (string: "success", "failure", "rejected", "denied") - not success (boolean)
  • error (optional string) - this is present correctly
  • sessionLog (optional string) - missing
  • toolTelemetry (optional map) - named telemetry instead

The current structure will not be compatible with the Copilot CLI server expectations. Please align the ToolResult structure with the protocol specification used by other SDKs.

Suggested change
/// Result of a tool execution
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
/// Result content (text or data)
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
/// Binary data (base64 encoded)
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<String>,
/// MIME type for binary data
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
/// Telemetry data
#[serde(skip_serializing_if = "Option::is_none")]
pub telemetry: Option<HashMap<String, Value>>,
/// Whether the tool execution was successful
#[serde(skip_serializing_if = "Option::is_none")]
pub success: Option<bool>,
/// Error message if execution failed
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl ToolResult {
/// Create a text result
pub fn text(content: impl Into<String>) -> Self {
Self {
content: Some(content.into()),
data: None,
mime_type: None,
telemetry: None,
success: Some(true),
error: None,
}
}
/// Create a binary result
pub fn binary(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
Self {
content: None,
data: Some(base64::encode(&data)),
mime_type: Some(mime_type.into()),
telemetry: None,
success: Some(true),
error: None,
}
}
/// Create an error result
pub fn error(message: impl Into<String>) -> Self {
Self {
content: None,
data: None,
mime_type: None,
telemetry: None,
success: Some(false),
error: Some(message.into()),
}
}
/// Add telemetry data to the result
pub fn with_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
self.telemetry = Some(telemetry);
/// Binary result entry for LLM (base64-encoded data plus MIME type)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BinaryResultForLlm {
/// Binary data (base64 encoded)
#[serde(rename = "data")]
pub data: String,
/// MIME type for binary data
#[serde(rename = "mimeType")]
pub mime_type: String,
}
/// Result of a tool execution
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
/// Primary textual result for the LLM
#[serde(rename = "textResultForLlm")]
pub text_result_for_llm: String,
/// Optional binary results for the LLM
#[serde(
rename = "binaryResultsForLlm",
skip_serializing_if = "Option::is_none"
)]
pub binary_results_for_llm: Option<Vec<BinaryResultForLlm>>,
/// Result type: "success", "failure", "rejected", or "denied"
#[serde(rename = "resultType")]
pub result_type: String,
/// Error message if execution failed
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
/// Optional session log associated with the tool execution
#[serde(rename = "sessionLog", skip_serializing_if = "Option::is_none")]
pub session_log: Option<String>,
/// Telemetry data for the tool execution
#[serde(rename = "toolTelemetry", skip_serializing_if = "Option::is_none")]
pub tool_telemetry: Option<HashMap<String, Value>>,
}
impl ToolResult {
/// Create a text result
pub fn text(content: impl Into<String>) -> Self {
Self {
text_result_for_llm: content.into(),
binary_results_for_llm: None,
result_type: "success".to_string(),
error: None,
session_log: None,
tool_telemetry: None,
}
}
/// Create a binary result
pub fn binary(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
let binary_result = BinaryResultForLlm {
data: base64::encode(&data),
mime_type: mime_type.into(),
};
Self {
// Protocol requires this field; for pure binary results we leave it empty.
text_result_for_llm: String::new(),
binary_results_for_llm: Some(vec![binary_result]),
result_type: "success".to_string(),
error: None,
session_log: None,
tool_telemetry: None,
}
}
/// Create an error result
pub fn error(message: impl Into<String>) -> Self {
Self {
// No useful text result when there is an error.
text_result_for_llm: String::new(),
binary_results_for_llm: None,
result_type: "failure".to_string(),
error: Some(message.into()),
session_log: None,
tool_telemetry: None,
}
}
/// Add telemetry data to the result
pub fn with_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
self.tool_telemetry = Some(telemetry);

Copilot uses AI. Check for mistakes.
Comment thread rust/src/tools.rs
Comment on lines +188 to +221
// Helper module for base64 encoding (simple implementation)
mod base64 {
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

pub fn encode(data: &[u8]) -> String {
let mut result = String::new();
let mut i = 0;

while i < data.len() {
let b0 = data[i];
let b1 = if i + 1 < data.len() { data[i + 1] } else { 0 };
let b2 = if i + 2 < data.len() { data[i + 2] } else { 0 };

result.push(CHARSET[(b0 >> 2) as usize] as char);
result.push(CHARSET[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);

if i + 1 < data.len() {
result.push(CHARSET[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char);
} else {
result.push('=');
}

if i + 2 < data.len() {
result.push(CHARSET[(b2 & 0x3f) as usize] as char);
} else {
result.push('=');
}

i += 3;
}

result
}
}

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

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

Implementing a custom base64 encoder is unnecessary and risky. Rust has well-tested base64 crates available (e.g., base64 crate). Custom cryptographic or encoding implementations are prone to bugs and should be avoided when standard, audited libraries exist. Consider adding base64 = "0.21" to dependencies and using it instead of this custom implementation.

Suggested change
// Helper module for base64 encoding (simple implementation)
mod base64 {
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
pub fn encode(data: &[u8]) -> String {
let mut result = String::new();
let mut i = 0;
while i < data.len() {
let b0 = data[i];
let b1 = if i + 1 < data.len() { data[i + 1] } else { 0 };
let b2 = if i + 2 < data.len() { data[i + 2] } else { 0 };
result.push(CHARSET[(b0 >> 2) as usize] as char);
result.push(CHARSET[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
if i + 1 < data.len() {
result.push(CHARSET[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char);
} else {
result.push('=');
}
if i + 2 < data.len() {
result.push(CHARSET[(b2 & 0x3f) as usize] as char);
} else {
result.push('=');
}
i += 3;
}
result
}
}
// Helper module for base64 encoding using the standard `base64` crate
mod base64 {
use base64::{engine::general_purpose, Engine as _};
pub fn encode(data: &[u8]) -> String {
general_purpose::STANDARD.encode(data)
}
}

Copilot uses AI. Check for mistakes.
@aaronpowell

Copy link
Copy Markdown
Contributor

Per https://github.com/github/copilot-sdk/blob/main/CONTRIBUTING.md#what-kinds-of-contributions-were-looking-for it states:

Currently we are not looking to add SDKs for other languages.

A Rust version exists are part of the community project - https://github.com/copilot-community-sdk/copilot-sdk-rust

@SteveSandersonMS

Copy link
Copy Markdown
Contributor

Thanks for contributing, @danielsan!

As we've mentioned in the contribution guide, we're not currently accepting SDKs for other languages into this repo. Please see https://github.com/github/copilot-sdk/blob/main/CONTRIBUTING.md#what-kinds-of-contributions-were-looking-for for the reasoning.

We'd love to have contributions towards improving the existing SDKs though. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants