A Rust SDK for programmatic access to the GitHub Copilot CLI.
Note: This SDK is in technical preview and may change in breaking ways.
cargo add copilot-sdk
cargo add tokio --features fullOr add to your Cargo.toml:
[dependencies]
copilot-sdk = "0.1"
tokio = { version = "1", features = ["full"] }use copilot_sdk::{CopilotClient, ClientOptions, SessionConfig, SessionEvent, MessageOptions, SessionEventType};
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create client (returns Result)
let client = CopilotClient::new(Some(ClientOptions {
log_level: Some("error".to_string()),
..Default::default()
}))?;
// Start the client
client.start().await?;
// Create a session
let session = client.create_session(Some(SessionConfig {
model: Some("gpt-5".to_string()),
..Default::default()
})).await?;
// Set up event handler (receives Arc<SessionEvent>)
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let tx_clone = tx.clone();
session.on(Arc::new(move |event: Arc<SessionEvent>| {
if event.event_type == SessionEventType::AssistantMessage {
if let Some(content) = &event.data.content {
println!("{}", content);
}
}
if event.event_type == SessionEventType::SessionIdle {
let _ = tx_clone.try_send(());
}
}));
// Send a message
session.send(MessageOptions {
prompt: "What is 2+2?".to_string(),
..Default::default()
}).await?;
// Wait for completion
rx.recv().await;
// Clean up
session.destroy().await?;
client.stop().await;
Ok(())
}The main client for interacting with the Copilot CLI server.
CopilotClient::new(options: Option<ClientOptions>) -> Result<Self, CopilotError>Creates a new client. Returns Result to handle invalid configuration errors.
ClientOptions:
| Field | Type | Default | Description |
|---|---|---|---|
cli_path |
Option<String> |
"copilot" |
Path to CLI executable (or COPILOT_CLI_PATH env var) |
cli_url |
Option<String> |
None |
URL of existing CLI server (e.g., "localhost:8080", "http://127.0.0.1:9000", or "8080"). When provided, the client will not spawn a CLI process. |
cwd |
Option<String> |
None |
Working directory for CLI process |
port |
Option<u16> |
0 |
Server port for TCP mode (0 = random) |
use_stdio |
Option<bool> |
true |
Use stdio transport instead of TCP |
log_level |
Option<String> |
"info" |
Log level for CLI server |
auto_start |
Option<bool> |
true |
Auto-start server on first use |
auto_restart |
Option<bool> |
true |
Auto-restart on crash |
env |
Option<HashMap<String, String>> |
None |
Environment variables for CLI process |
Start the CLI server and establish connection.
Stop the CLI server and close all sessions. Returns a list of any errors encountered during cleanup.
Forcefully stop without graceful cleanup. Use when stop() takes too long.
Create a new conversation session.
SessionConfig:
| Field | Type | Description |
|---|---|---|
session_id |
Option<String> |
Custom session ID |
model |
Option<String> |
Model to use ("gpt-5", "claude-sonnet-4.5", etc.) |
tools |
Vec<Tool> |
Custom tools exposed to the CLI |
streaming |
Option<bool> |
Enable streaming responses |
system_message |
Option<SystemMessageConfig> |
System message customization |
provider |
Option<ProviderConfig> |
Custom model provider |
mcp_servers |
Option<Vec<McpServerConfig>> |
MCP server configurations |
available_tools |
Option<Vec<String>> |
Allowlist of available tools |
excluded_tools |
Option<Vec<String>> |
Tools to exclude |
resume_session(session_id: &str, config: Option<ResumeSessionConfig>) -> Result<Arc<CopilotSession>>
Resume an existing session.
Get current connection state (Disconnected, Connecting, Connected, Error).
Ping the server to verify connectivity.
List all available sessions.
Delete a session and its data from disk.
Represents a single conversation session.
Send a message to the session. Returns immediately after the message is queued; use event handlers or send_and_wait() to wait for completion.
MessageOptions:
| Field | Type | Description |
|---|---|---|
prompt |
String |
The message/prompt to send |
attachments |
Option<Vec<Attachment>> |
File attachments |
mode |
Option<String> |
Delivery mode ("enqueue" or "immediate") |
Returns the message ID.
Send a message and wait until the session becomes idle. Returns the final assistant message event, or None if none was received.
Subscribe to session events. Returns an unsubscribe function.
Important: The handler receives Arc<SessionEvent> (not SessionEvent) to avoid expensive clones when dispatching to multiple handlers.
let unsubscribe = session.on(Arc::new(|event: Arc<SessionEvent>| {
println!("Event: {:?}", event.event_type);
}));
// Later...
unsubscribe();Abort the currently processing message.
Get all events/messages from this session's history.
Destroy the session and free resources.
Expose your own functionality to Copilot by attaching tools to a session.
Use define_tool for type-safe tools with automatic JSON schema generation:
use copilot_sdk::{define_tool, SessionConfig, ToolInvocation};
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Deserialize, JsonSchema)]
struct LookupIssueParams {
/// Issue identifier
id: String,
}
let lookup_issue = define_tool::<LookupIssueParams, _, _, _>(
"lookup_issue",
"Fetch issue details from our tracker",
|params, _inv| async move {
let issue = fetch_issue(¶ms.id).await?;
Ok(issue.summary)
},
);
let session = client.create_session(Some(SessionConfig {
model: Some("gpt-5".to_string()),
tools: vec![lookup_issue],
..Default::default()
})).await?;For more control over the JSON schema:
use copilot_sdk::{ToolBuilder, ToolResult};
use serde_json::json;
let lookup_issue = ToolBuilder::new("lookup_issue")
.description("Fetch issue details from our tracker")
.parameters(json!({
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Issue identifier"
}
},
"required": ["id"]
}))
.handler(|inv| async move {
let id = inv.arguments.get("id")
.and_then(|v| v.as_str())
.unwrap_or("");
let issue = fetch_issue(id).await?;
Ok(ToolResult::success(issue.summary))
});
let session = client.create_session(Some(SessionConfig {
model: Some("gpt-5".to_string()),
tools: vec![lookup_issue],
..Default::default()
})).await?;When the model selects a tool, the SDK automatically runs your handler and responds to the CLI's tool.call with the result.
Sessions emit various events during processing:
| Event Type | Description |
|---|---|
UserMessage |
User message added |
AssistantMessage |
Complete assistant response |
AssistantMessageDelta |
Streaming response chunk |
AssistantReasoning |
Complete reasoning content |
AssistantReasoningDelta |
Streaming reasoning chunk |
ToolExecutionStart |
Tool execution started |
ToolExecutionComplete |
Tool execution completed |
SessionIdle |
Session finished processing |
SessionError |
Error occurred |
SessionStart |
Session started |
See SessionEventType for the full list.
Enable streaming to receive assistant response chunks as they're generated:
use copilot_sdk::{CopilotClient, SessionConfig, SessionEvent, MessageOptions, SessionEventType};
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = CopilotClient::new(None)?;
client.start().await?;
let session = client.create_session(Some(SessionConfig {
model: Some("gpt-5".to_string()),
streaming: Some(true),
..Default::default()
})).await?;
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let tx_clone = tx.clone();
session.on(Arc::new(move |event: Arc<SessionEvent>| {
match event.event_type {
SessionEventType::AssistantMessageDelta => {
// Streaming message chunk - print incrementally
if let Some(delta) = &event.data.delta_content {
print!("{}", delta);
}
}
SessionEventType::AssistantReasoningDelta => {
// Streaming reasoning chunk (if model supports reasoning)
if let Some(delta) = &event.data.delta_content {
print!("{}", delta);
}
}
SessionEventType::AssistantMessage => {
// Final message - complete content
println!("\n--- Final message ---");
if let Some(content) = &event.data.content {
println!("{}", content);
}
}
SessionEventType::AssistantReasoning => {
// Final reasoning content (if model supports reasoning)
println!("--- Reasoning ---");
if let Some(content) = &event.data.content {
println!("{}", content);
}
}
SessionEventType::SessionIdle => {
let _ = tx_clone.try_send(());
}
_ => {}
}
}));
session.send(MessageOptions {
prompt: "Tell me a short story".to_string(),
..Default::default()
}).await?;
rx.recv().await;
session.destroy().await?;
client.stop().await;
Ok(())
}When streaming: Some(true):
AssistantMessageDeltaevents containdelta_contentwith incremental textAssistantReasoningDeltaevents contain reasoning chunks (model-dependent)- Accumulate
delta_contentvalues to build the response progressively - Final
AssistantMessageandAssistantReasoningevents contain complete content
Note: Final events are always sent regardless of streaming setting.
Communicates with CLI via stdin/stdout pipes. Recommended for most use cases.
let client = CopilotClient::new(None)?; // Uses stdio by defaultCommunicates with CLI via TCP socket. Useful for distributed scenarios.
let client = CopilotClient::new(Some(ClientOptions {
use_stdio: Some(false),
port: Some(3000),
..Default::default()
}))?;Connect to an already-running CLI server:
let client = CopilotClient::new(Some(ClientOptions {
cli_url: Some("localhost:8080".to_string()),
..Default::default()
}))?;The SDK uses CopilotError for all error types:
use copilot_sdk::{CopilotClient, CopilotError};
match CopilotClient::new(None) {
Ok(client) => {
// Use client...
}
Err(CopilotError::InvalidConfig(msg)) => {
eprintln!("Configuration error: {}", msg);
}
Err(e) => {
eprintln!("Error: {}", e);
}
}Common error types:
| Error | Description |
|---|---|
InvalidConfig |
Invalid client configuration |
NotConnected |
Client not connected |
Connection |
Connection error |
Process |
CLI process error |
Timeout |
Operation timed out |
JsonRpc |
JSON-RPC error from server |
Session |
Session-related error |
COPILOT_CLI_PATH- Path to the Copilot CLI executable
- Rust 1.75+ (2021 edition)
- Tokio async runtime
- GitHub Copilot CLI installed and accessible
MIT