Add Rust version of the SDK - #112
Conversation
- 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.
There was a problem hiding this comment.
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.
| #[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()); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
There was a problem hiding this comment.
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
| #[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)); | ||
| } |
There was a problem hiding this comment.
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.
| // 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, | ||
| }) | ||
| } | ||
| }) | ||
| }); |
There was a problem hiding this comment.
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.
| // 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, | |
| }) | |
| }; |
| [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"] |
There was a problem hiding this comment.
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.
| #[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()); | ||
| } |
There was a problem hiding this comment.
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.
| #[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)) | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| /// 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>>, | ||
| } |
There was a problem hiding this comment.
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.
| //! 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>>, | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| /// 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); |
There was a problem hiding this comment.
The ToolResult structure is inconsistent with other SDK implementations (Python, Go, Node.js). The other SDKs use:
textResultForLlm(required string field) - notcontentbinaryResultsForLlm(optional array) - notdataandmime_typeas separate fieldsresultType(string: "success", "failure", "rejected", "denied") - notsuccess(boolean)error(optional string) - this is present correctlysessionLog(optional string) - missingtoolTelemetry(optional map) - namedtelemetryinstead
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.
| /// 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); |
| // 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| // 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) | |
| } | |
| } |
|
Per https://github.com/github/copilot-sdk/blob/main/CONTRIBUTING.md#what-kinds-of-contributions-were-looking-for it states:
A Rust version exists are part of the community project - https://github.com/copilot-community-sdk/copilot-sdk-rust |
|
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! |
Coded with the help of copilot-cli using Claude Sonnet 4.5