Adding copilot-sdk stuff

This commit is contained in:
Aaron Powell
2026-01-22 15:51:40 +11:00
parent e397a4748c
commit 66b0bfb9cc
9 changed files with 2739 additions and 0 deletions

View File

@@ -0,0 +1,550 @@
---
applyTo: '**.cs, **.csproj'
description: 'This file provides guidance on building C# applications using GitHub Copilot SDK.'
name: 'GitHub Copilot SDK C# Instructions'
---
## Core Principles
- The SDK is in technical preview and may have breaking changes
- Requires .NET 10.0 or later
- Requires GitHub Copilot CLI installed and in PATH
- Uses async/await patterns throughout
- Implements IAsyncDisposable for resource cleanup
## Installation
Always install via NuGet:
```bash
dotnet add package GitHub.Copilot.SDK
```
## Client Initialization
### Basic Client Setup
```csharp
await using var client = new CopilotClient();
await client.StartAsync();
```
### Client Configuration Options
When creating a CopilotClient, use `CopilotClientOptions`:
- `CliPath` - Path to CLI executable (default: "copilot" from PATH)
- `CliArgs` - Extra arguments prepended before SDK-managed flags
- `CliUrl` - URL of existing CLI server (e.g., "localhost:8080"). When provided, client won't spawn a process
- `Port` - Server port (default: 0 for random)
- `UseStdio` - Use stdio transport instead of TCP (default: true)
- `LogLevel` - Log level (default: "info")
- `AutoStart` - Auto-start server (default: true)
- `AutoRestart` - Auto-restart on crash (default: true)
- `Cwd` - Working directory for the CLI process
- `Environment` - Environment variables for the CLI process
- `Logger` - ILogger instance for SDK logging
### Manual Server Control
For explicit control:
```csharp
var client = new CopilotClient(new CopilotClientOptions { AutoStart = false });
await client.StartAsync();
// Use client...
await client.StopAsync();
```
Use `ForceStopAsync()` when `StopAsync()` takes too long.
## Session Management
### Creating Sessions
Use `SessionConfig` for configuration:
```csharp
await using var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
Streaming = true,
Tools = [...],
SystemMessage = new SystemMessageConfig { ... },
AvailableTools = ["tool1", "tool2"],
ExcludedTools = ["tool3"],
Provider = new ProviderConfig { ... }
});
```
### Session Config Options
- `SessionId` - Custom session ID
- `Model` - Model name ("gpt-5", "claude-sonnet-4.5", etc.)
- `Tools` - Custom tools exposed to the CLI
- `SystemMessage` - System message customization
- `AvailableTools` - Allowlist of tool names
- `ExcludedTools` - Blocklist of tool names
- `Provider` - Custom API provider configuration (BYOK)
- `Streaming` - Enable streaming response chunks (default: false)
### Resuming Sessions
```csharp
var session = await client.ResumeSessionAsync(sessionId, new ResumeSessionConfig { ... });
```
### Session Operations
- `session.SessionId` - Get session identifier
- `session.SendAsync(new MessageOptions { Prompt = "...", Attachments = [...] })` - Send message
- `session.AbortAsync()` - Abort current processing
- `session.GetMessagesAsync()` - Get all events/messages
- `await session.DisposeAsync()` - Clean up resources
## Event Handling
### Event Subscription Pattern
ALWAYS use TaskCompletionSource for waiting on session events:
```csharp
var done = new TaskCompletionSource();
session.On(evt =>
{
if (evt is AssistantMessageEvent msg)
{
Console.WriteLine(msg.Data.Content);
}
else if (evt is SessionIdleEvent)
{
done.SetResult();
}
});
await session.SendAsync(new MessageOptions { Prompt = "..." });
await done.Task;
```
### Unsubscribing from Events
The `On()` method returns an IDisposable:
```csharp
var subscription = session.On(evt => { /* handler */ });
// Later...
subscription.Dispose();
```
### Event Types
Use pattern matching or switch expressions for event handling:
```csharp
session.On(evt =>
{
switch (evt)
{
case UserMessageEvent userMsg:
// Handle user message
break;
case AssistantMessageEvent assistantMsg:
Console.WriteLine(assistantMsg.Data.Content);
break;
case ToolExecutionStartEvent toolStart:
// Tool execution started
break;
case ToolExecutionCompleteEvent toolComplete:
// Tool execution completed
break;
case SessionStartEvent start:
// Session started
break;
case SessionIdleEvent idle:
// Session is idle (processing complete)
break;
case SessionErrorEvent error:
Console.WriteLine($"Error: {error.Data.Message}");
break;
}
});
```
## Streaming Responses
### Enabling Streaming
Set `Streaming = true` in SessionConfig:
```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
Streaming = true
});
```
### Handling Streaming Events
Handle both delta events (incremental) and final events:
```csharp
var done = new TaskCompletionSource();
session.On(evt =>
{
switch (evt)
{
case AssistantMessageDeltaEvent delta:
// Incremental text chunk
Console.Write(delta.Data.DeltaContent);
break;
case AssistantReasoningDeltaEvent reasoningDelta:
// Incremental reasoning chunk (model-dependent)
Console.Write(reasoningDelta.Data.DeltaContent);
break;
case AssistantMessageEvent msg:
// Final complete message
Console.WriteLine("\n--- Final ---");
Console.WriteLine(msg.Data.Content);
break;
case AssistantReasoningEvent reasoning:
// Final reasoning content
Console.WriteLine("--- Reasoning ---");
Console.WriteLine(reasoning.Data.Content);
break;
case SessionIdleEvent:
done.SetResult();
break;
}
});
await session.SendAsync(new MessageOptions { Prompt = "Tell me a story" });
await done.Task;
```
Note: Final events (`AssistantMessageEvent`, `AssistantReasoningEvent`) are ALWAYS sent regardless of streaming setting.
## Custom Tools
### Defining Tools with AIFunctionFactory
Use `Microsoft.Extensions.AI.AIFunctionFactory.Create` for type-safe tools:
```csharp
using Microsoft.Extensions.AI;
using System.ComponentModel;
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
Tools = [
AIFunctionFactory.Create(
async ([Description("Issue ID")] string id) => {
var issue = await FetchIssueAsync(id);
return issue;
},
"lookup_issue",
"Fetch issue details from tracker"),
]
});
```
### Tool Return Types
- Return any JSON-serializable value (automatically wrapped)
- Or return `ToolResultAIContent` wrapping `ToolResultObject` for full control over metadata
### Tool Execution Flow
When Copilot invokes a tool, the client automatically:
1. Runs your handler function
2. Serializes the return value
3. Responds to the CLI
## System Message Customization
### Append Mode (Default - Preserves Guardrails)
```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
SystemMessage = new SystemMessageConfig
{
Mode = SystemMessageMode.Append,
Content = @"
<workflow_rules>
- Always check for security vulnerabilities
- Suggest performance improvements when applicable
</workflow_rules>
"
}
});
```
### Replace Mode (Full Control - Removes Guardrails)
```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
SystemMessage = new SystemMessageConfig
{
Mode = SystemMessageMode.Replace,
Content = "You are a helpful assistant."
}
});
```
## File Attachments
Attach files to messages using `UserMessageDataAttachmentsItem`:
```csharp
await session.SendAsync(new MessageOptions
{
Prompt = "Analyze this file",
Attachments = new List<UserMessageDataAttachmentsItem>
{
new UserMessageDataAttachmentsItem
{
Type = UserMessageDataAttachmentsItemType.File,
Path = "/path/to/file.cs",
DisplayName = "My File"
}
}
});
```
## Message Delivery Modes
Use the `Mode` property in `MessageOptions`:
- `"enqueue"` - Queue message for processing
- `"immediate"` - Process message immediately
```csharp
await session.SendAsync(new MessageOptions
{
Prompt = "...",
Mode = "enqueue"
});
```
## Multiple Sessions
Sessions are independent and can run concurrently:
```csharp
var session1 = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" });
var session2 = await client.CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-4.5" });
await session1.SendAsync(new MessageOptions { Prompt = "Hello from session 1" });
await session2.SendAsync(new MessageOptions { Prompt = "Hello from session 2" });
```
## Bring Your Own Key (BYOK)
Use custom API providers via `ProviderConfig`:
```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
Provider = new ProviderConfig
{
Type = "openai",
BaseUrl = "https://api.openai.com/v1",
ApiKey = "your-api-key"
}
});
```
## Session Lifecycle Management
### Listing Sessions
```csharp
var sessions = await client.ListSessionsAsync();
foreach (var metadata in sessions)
{
Console.WriteLine($"Session: {metadata.SessionId}");
}
```
### Deleting Sessions
```csharp
await client.DeleteSessionAsync(sessionId);
```
### Checking Connection State
```csharp
var state = client.State;
```
## Error Handling
### Standard Exception Handling
```csharp
try
{
var session = await client.CreateSessionAsync();
await session.SendAsync(new MessageOptions { Prompt = "Hello" });
}
catch (StreamJsonRpc.RemoteInvocationException ex)
{
Console.Error.WriteLine($"JSON-RPC Error: {ex.Message}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
}
```
### Session Error Events
Monitor `SessionErrorEvent` for runtime errors:
```csharp
session.On(evt =>
{
if (evt is SessionErrorEvent error)
{
Console.Error.WriteLine($"Session Error: {error.Data.Message}");
}
});
```
## Connectivity Testing
Use PingAsync to verify server connectivity:
```csharp
var response = await client.PingAsync("test message");
```
## Resource Cleanup
### Automatic Cleanup with Using
ALWAYS use `await using` for automatic disposal:
```csharp
await using var client = new CopilotClient();
await using var session = await client.CreateSessionAsync();
// Resources automatically cleaned up
```
### Manual Cleanup
If not using `await using`:
```csharp
var client = new CopilotClient();
try
{
await client.StartAsync();
// Use client...
}
finally
{
await client.StopAsync();
}
```
## Best Practices
1. **Always use `await using`** for CopilotClient and CopilotSession
2. **Use TaskCompletionSource** to wait for SessionIdleEvent
3. **Handle SessionErrorEvent** for robust error handling
4. **Use pattern matching** (switch expressions) for event handling
5. **Enable streaming** for better UX in interactive scenarios
6. **Use AIFunctionFactory** for type-safe tool definitions
7. **Dispose event subscriptions** when no longer needed
8. **Use SystemMessageMode.Append** to preserve safety guardrails
9. **Provide descriptive tool names and descriptions** for better model understanding
10. **Handle both delta and final events** when streaming is enabled
## Common Patterns
### Simple Query-Response
```csharp
await using var client = new CopilotClient();
await client.StartAsync();
await using var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5"
});
var done = new TaskCompletionSource();
session.On(evt =>
{
if (evt is AssistantMessageEvent msg)
{
Console.WriteLine(msg.Data.Content);
}
else if (evt is SessionIdleEvent)
{
done.SetResult();
}
});
await session.SendAsync(new MessageOptions { Prompt = "What is 2+2?" });
await done.Task;
```
### Multi-Turn Conversation
```csharp
await using var session = await client.CreateSessionAsync();
async Task SendAndWait(string prompt)
{
var done = new TaskCompletionSource();
var subscription = session.On(evt =>
{
if (evt is AssistantMessageEvent msg)
{
Console.WriteLine(msg.Data.Content);
}
else if (evt is SessionIdleEvent)
{
done.SetResult();
}
});
await session.SendAsync(new MessageOptions { Prompt = prompt });
await done.Task;
subscription.Dispose();
}
await SendAndWait("What is the capital of France?");
await SendAndWait("What is its population?");
```
### Tool with Complex Return Type
```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
Tools = [
AIFunctionFactory.Create(
([Description("User ID")] string userId) => {
return new {
Id = userId,
Name = "John Doe",
Email = "john@example.com",
Role = "Developer"
};
},
"get_user",
"Retrieve user information")
]
});
```

View File

@@ -0,0 +1,626 @@
---
applyTo: "**.go, go.mod"
description: "This file provides guidance on building Go applications using GitHub Copilot SDK."
name: "GitHub Copilot SDK Go Instructions"
---
## Core Principles
- The SDK is in technical preview and may have breaking changes
- Requires Go 1.21 or later
- Requires GitHub Copilot CLI installed and in PATH
- Uses goroutines and channels for concurrent operations
- No external dependencies beyond the standard library
## Installation
Always install via Go modules:
```bash
go get github.com/github/copilot-sdk/go
```
## Client Initialization
### Basic Client Setup
```go
import "github.com/github/copilot-sdk/go"
client := copilot.NewClient(nil)
if err := client.Start(); err != nil {
log.Fatal(err)
}
defer client.Stop()
```
### Client Configuration Options
When creating a CopilotClient, use `ClientOptions`:
- `CLIPath` - Path to CLI executable (default: "copilot" from PATH)
- `CLIUrl` - URL of existing CLI server (e.g., "localhost:8080"). When provided, client won't spawn a process
- `Port` - Server port (default: 0 for random)
- `UseStdio` - Use stdio transport instead of TCP (default: true)
- `LogLevel` - Log level (default: "info")
- `AutoStart` - Auto-start server (default: true, use pointer: `boolPtr(true)`)
- `AutoRestart` - Auto-restart on crash (default: true, use pointer: `boolPtr(true)`)
- `Cwd` - Working directory for the CLI process
- `Env` - Environment variables for the CLI process ([]string)
### Manual Server Control
For explicit control:
```go
autoStart := false
client := copilot.NewClient(&copilot.ClientOptions{AutoStart: &autoStart})
if err := client.Start(); err != nil {
log.Fatal(err)
}
// Use client...
client.Stop()
```
Use `ForceStop()` when `Stop()` takes too long.
## Session Management
### Creating Sessions
Use `SessionConfig` for configuration:
```go
session, err := client.CreateSession(&copilot.SessionConfig{
Model: "gpt-5",
Streaming: true,
Tools: []copilot.Tool{...},
SystemMessage: &copilot.SystemMessageConfig{ ... },
AvailableTools: []string{"tool1", "tool2"},
ExcludedTools: []string{"tool3"},
Provider: &copilot.ProviderConfig{ ... },
})
if err != nil {
log.Fatal(err)
}
```
### Session Config Options
- `SessionID` - Custom session ID
- `Model` - Model name ("gpt-5", "claude-sonnet-4.5", etc.)
- `Tools` - Custom tools exposed to the CLI ([]Tool)
- `SystemMessage` - System message customization (\*SystemMessageConfig)
- `AvailableTools` - Allowlist of tool names ([]string)
- `ExcludedTools` - Blocklist of tool names ([]string)
- `Provider` - Custom API provider configuration (BYOK) (\*ProviderConfig)
- `Streaming` - Enable streaming response chunks (bool)
- `MCPServers` - MCP server configurations
- `CustomAgents` - Custom agent configurations
- `ConfigDir` - Config directory override
- `SkillDirectories` - Skill directories ([]string)
- `DisabledSkills` - Disabled skills ([]string)
### Resuming Sessions
```go
session, err := client.ResumeSession("session-id")
// Or with options:
session, err := client.ResumeSessionWithOptions("session-id", &copilot.ResumeSessionConfig{ ... })
```
### Session Operations
- `session.SessionID` - Get session identifier (string)
- `session.Send(copilot.MessageOptions{Prompt: "...", Attachments: []copilot.Attachment{...}})` - Send message, returns (messageID string, error)
- `session.SendAndWait(options, timeout)` - Send and wait for idle, returns (\*SessionEvent, error)
- `session.Abort()` - Abort current processing, returns error
- `session.GetMessages()` - Get all events/messages, returns ([]SessionEvent, error)
- `session.Destroy()` - Clean up session, returns error
## Event Handling
### Event Subscription Pattern
ALWAYS use channels or done signals for waiting on session events:
```go
done := make(chan struct{})
unsubscribe := session.On(func(evt copilot.SessionEvent) {
switch evt.Type {
case copilot.AssistantMessage:
fmt.Println(*evt.Data.Content)
case copilot.SessionIdle:
close(done)
}
})
defer unsubscribe()
session.Send(copilot.MessageOptions{Prompt: "..."})
<-done
```
### Unsubscribing from Events
The `On()` method returns a function that unsubscribes:
```go
unsubscribe := session.On(func(evt copilot.SessionEvent) {
// handler
})
// Later...
unsubscribe()
```
### Event Types
Use type switches for event handling:
```go
session.On(func(evt copilot.SessionEvent) {
switch evt.Type {
case copilot.UserMessage:
// Handle user message
case copilot.AssistantMessage:
if evt.Data.Content != nil {
fmt.Println(*evt.Data.Content)
}
case copilot.ToolExecutionStart:
// Tool execution started
case copilot.ToolExecutionComplete:
// Tool execution completed
case copilot.SessionStart:
// Session started
case copilot.SessionIdle:
// Session is idle (processing complete)
case copilot.SessionError:
if evt.Data.Message != nil {
fmt.Println("Error:", *evt.Data.Message)
}
}
})
```
## Streaming Responses
### Enabling Streaming
Set `Streaming: true` in SessionConfig:
```go
session, err := client.CreateSession(&copilot.SessionConfig{
Model: "gpt-5",
Streaming: true,
})
```
### Handling Streaming Events
Handle both delta events (incremental) and final events:
```go
done := make(chan struct{})
session.On(func(evt copilot.SessionEvent) {
switch evt.Type {
case copilot.AssistantMessageDelta:
// Incremental text chunk
if evt.Data.DeltaContent != nil {
fmt.Print(*evt.Data.DeltaContent)
}
case copilot.AssistantReasoningDelta:
// Incremental reasoning chunk (model-dependent)
if evt.Data.DeltaContent != nil {
fmt.Print(*evt.Data.DeltaContent)
}
case copilot.AssistantMessage:
// Final complete message
fmt.Println("\n--- Final ---")
if evt.Data.Content != nil {
fmt.Println(*evt.Data.Content)
}
case copilot.AssistantReasoning:
// Final reasoning content
fmt.Println("--- Reasoning ---")
if evt.Data.Content != nil {
fmt.Println(*evt.Data.Content)
}
case copilot.SessionIdle:
close(done)
}
})
session.Send(copilot.MessageOptions{Prompt: "Tell me a story"})
<-done
```
Note: Final events (`AssistantMessage`, `AssistantReasoning`) are ALWAYS sent regardless of streaming setting.
## Custom Tools
### Defining Tools
```go
session, err := client.CreateSession(&copilot.SessionConfig{
Model: "gpt-5",
Tools: []copilot.Tool{
{
Name: "lookup_issue",
Description: "Fetch issue details from tracker",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"id": map[string]interface{}{
"type": "string",
"description": "Issue ID",
},
},
"required": []string{"id"},
},
Handler: func(inv copilot.ToolInvocation) (copilot.ToolResult, error) {
args := inv.Arguments.(map[string]interface{})
issueID := args["id"].(string)
issue, err := fetchIssue(issueID)
if err != nil {
return copilot.ToolResult{}, err
}
return copilot.ToolResult{
TextResultForLLM: fmt.Sprintf("Issue: %v", issue),
ResultType: "success",
ToolTelemetry: map[string]interface{}{},
}, nil
},
},
},
})
```
### Tool Return Types
- Return `ToolResult` struct with fields:
- `TextResultForLLM` (string) - Result text for the LLM
- `ResultType` (string) - "success" or "failure"
- `Error` (string, optional) - Internal error message (not shown to LLM)
- `ToolTelemetry` (map[string]interface{}) - Telemetry data
### Tool Execution Flow
When Copilot invokes a tool, the client automatically:
1. Runs your handler function
2. Returns the ToolResult
3. Responds to the CLI
## System Message Customization
### Append Mode (Default - Preserves Guardrails)
```go
session, err := client.CreateSession(&copilot.SessionConfig{
Model: "gpt-5",
SystemMessage: &copilot.SystemMessageConfig{
Mode: "append",
Content: `
<workflow_rules>
- Always check for security vulnerabilities
- Suggest performance improvements when applicable
</workflow_rules>
`,
},
})
```
### Replace Mode (Full Control - Removes Guardrails)
```go
session, err := client.CreateSession(&copilot.SessionConfig{
Model: "gpt-5",
SystemMessage: &copilot.SystemMessageConfig{
Mode: "replace",
Content: "You are a helpful assistant.",
},
})
```
## File Attachments
Attach files to messages using `Attachment`:
```go
messageID, err := session.Send(copilot.MessageOptions{
Prompt: "Analyze this file",
Attachments: []copilot.Attachment{
{
Type: "file",
Path: "/path/to/file.go",
DisplayName: "My File",
},
},
})
```
## Message Delivery Modes
Use the `Mode` field in `MessageOptions`:
- `"enqueue"` - Queue message for processing
- `"immediate"` - Process message immediately
```go
session.Send(copilot.MessageOptions{
Prompt: "...",
Mode: "enqueue",
})
```
## Multiple Sessions
Sessions are independent and can run concurrently:
```go
session1, _ := client.CreateSession(&copilot.SessionConfig{Model: "gpt-5"})
session2, _ := client.CreateSession(&copilot.SessionConfig{Model: "claude-sonnet-4.5"})
session1.Send(copilot.MessageOptions{Prompt: "Hello from session 1"})
session2.Send(copilot.MessageOptions{Prompt: "Hello from session 2"})
```
## Bring Your Own Key (BYOK)
Use custom API providers via `ProviderConfig`:
```go
session, err := client.CreateSession(&copilot.SessionConfig{
Provider: &copilot.ProviderConfig{
Type: "openai",
BaseURL: "https://api.openai.com/v1",
APIKey: "your-api-key",
},
})
```
## Session Lifecycle Management
### Checking Connection State
```go
state := client.GetState()
// Returns: "disconnected", "connecting", "connected", or "error"
```
## Error Handling
### Standard Exception Handling
```go
session, err := client.CreateSession(&copilot.SessionConfig{})
if err != nil {
log.Fatalf("Failed to create session: %v", err)
}
_, err = session.Send(copilot.MessageOptions{Prompt: "Hello"})
if err != nil {
log.Printf("Failed to send: %v", err)
}
```
### Session Error Events
Monitor `SessionError` type for runtime errors:
```go
session.On(func(evt copilot.SessionEvent) {
if evt.Type == copilot.SessionError {
if evt.Data.Message != nil {
fmt.Fprintf(os.Stderr, "Session Error: %s\n", *evt.Data.Message)
}
}
})
```
## Connectivity Testing
Use Ping to verify server connectivity:
```go
resp, err := client.Ping("test message")
if err != nil {
log.Printf("Server unreachable: %v", err)
} else {
log.Printf("Server responded at %d", resp.Timestamp)
}
```
## Resource Cleanup
### Cleanup with Defer
ALWAYS use `defer` for cleanup:
```go
client := copilot.NewClient(nil)
if err := client.Start(); err != nil {
log.Fatal(err)
}
defer client.Stop()
session, err := client.CreateSession(nil)
if err != nil {
log.Fatal(err)
}
defer session.Destroy()
```
### Manual Cleanup
If not using defer:
```go
client := copilot.NewClient(nil)
err := client.Start()
if err != nil {
log.Fatal(err)
}
session, err := client.CreateSession(nil)
if err != nil {
client.Stop()
log.Fatal(err)
}
// Use session...
session.Destroy()
errors := client.Stop()
for _, err := range errors {
log.Printf("Cleanup error: %v", err)
}
```
## Best Practices
1. **Always use `defer`** for cleanup of clients and sessions
2. **Use channels** to wait for SessionIdle event
3. **Handle SessionError** events for robust error handling
4. **Use type switches** for event handling
5. **Enable streaming** for better UX in interactive scenarios
6. **Provide descriptive tool names and descriptions** for better model understanding
7. **Call unsubscribe functions** when no longer needed
8. **Use SystemMessageConfig with Mode: "append"** to preserve safety guardrails
9. **Handle both delta and final events** when streaming is enabled
10. **Check nil pointers** in event data (Content, Message, etc. are pointers)
## Common Patterns
### Simple Query-Response
```go
client := copilot.NewClient(nil)
if err := client.Start(); err != nil {
log.Fatal(err)
}
defer client.Stop()
session, err := client.CreateSession(&copilot.SessionConfig{Model: "gpt-5"})
if err != nil {
log.Fatal(err)
}
defer session.Destroy()
done := make(chan struct{})
session.On(func(evt copilot.SessionEvent) {
if evt.Type == copilot.AssistantMessage && evt.Data.Content != nil {
fmt.Println(*evt.Data.Content)
} else if evt.Type == copilot.SessionIdle {
close(done)
}
})
session.Send(copilot.MessageOptions{Prompt: "What is 2+2?"})
<-done
```
### Multi-Turn Conversation
```go
session, _ := client.CreateSession(nil)
defer session.Destroy()
sendAndWait := func(prompt string) error {
done := make(chan struct{})
var eventErr error
unsubscribe := session.On(func(evt copilot.SessionEvent) {
switch evt.Type {
case copilot.AssistantMessage:
if evt.Data.Content != nil {
fmt.Println(*evt.Data.Content)
}
case copilot.SessionIdle:
close(done)
case copilot.SessionError:
if evt.Data.Message != nil {
eventErr = fmt.Errorf(*evt.Data.Message)
}
}
})
defer unsubscribe()
if _, err := session.Send(copilot.MessageOptions{Prompt: prompt}); err != nil {
return err
}
<-done
return eventErr
}
sendAndWait("What is the capital of France?")
sendAndWait("What is its population?")
```
### SendAndWait Helper
```go
// Use built-in SendAndWait for simpler synchronous interaction
response, err := session.SendAndWait(copilot.MessageOptions{
Prompt: "What is 2+2?",
}, 0) // 0 uses default 60s timeout
if err != nil {
log.Printf("Error: %v", err)
}
if response != nil && response.Data.Content != nil {
fmt.Println(*response.Data.Content)
}
```
### Tool with Struct Return Type
```go
type UserInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Role string `json:"role"`
}
session, _ := client.CreateSession(&copilot.SessionConfig{
Tools: []copilot.Tool{
{
Name: "get_user",
Description: "Retrieve user information",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"user_id": map[string]interface{}{
"type": "string",
"description": "User ID",
},
},
"required": []string{"user_id"},
},
Handler: func(inv copilot.ToolInvocation) (copilot.ToolResult, error) {
args := inv.Arguments.(map[string]interface{})
userID := args["user_id"].(string)
user := UserInfo{
ID: userID,
Name: "John Doe",
Email: "john@example.com",
Role: "Developer",
}
jsonBytes, _ := json.Marshal(user)
return copilot.ToolResult{
TextResultForLLM: string(jsonBytes),
ResultType: "success",
ToolTelemetry: map[string]interface{}{},
}, nil
},
},
},
})
```

View File

@@ -0,0 +1,717 @@
---
applyTo: "**.ts, **.js, package.json"
description: "This file provides guidance on building Node.js/TypeScript applications using GitHub Copilot SDK."
name: "GitHub Copilot SDK Node.js Instructions"
---
## Core Principles
- The SDK is in technical preview and may have breaking changes
- Requires Node.js 18.0 or later
- Requires GitHub Copilot CLI installed and in PATH
- Built with TypeScript for type safety
- Uses async/await patterns throughout
- Provides full TypeScript type definitions
## Installation
Always install via npm/pnpm/yarn:
```bash
npm install @github/copilot-sdk
# or
pnpm add @github/copilot-sdk
# or
yarn add @github/copilot-sdk
```
## Client Initialization
### Basic Client Setup
```typescript
import { CopilotClient } from "@github/copilot-sdk";
const client = new CopilotClient();
await client.start();
// Use client...
await client.stop();
```
### Client Configuration Options
When creating a CopilotClient, use `CopilotClientOptions`:
- `cliPath` - Path to CLI executable (default: "copilot" from PATH)
- `cliArgs` - Extra arguments prepended before SDK-managed flags (string[])
- `cliUrl` - URL of existing CLI server (e.g., "localhost:8080"). When provided, client won't spawn a process
- `port` - Server port (default: 0 for random)
- `useStdio` - Use stdio transport instead of TCP (default: true)
- `logLevel` - Log level (default: "debug")
- `autoStart` - Auto-start server (default: true)
- `autoRestart` - Auto-restart on crash (default: true)
- `cwd` - Working directory for the CLI process (default: process.cwd())
- `env` - Environment variables for the CLI process (default: process.env)
### Manual Server Control
For explicit control:
```typescript
const client = new CopilotClient({ autoStart: false });
await client.start();
// Use client...
await client.stop();
```
Use `forceStop()` when `stop()` takes too long.
## Session Management
### Creating Sessions
Use `SessionConfig` for configuration:
```typescript
const session = await client.createSession({
model: "gpt-5",
streaming: true,
tools: [...],
systemMessage: { ... },
availableTools: ["tool1", "tool2"],
excludedTools: ["tool3"],
provider: { ... }
});
```
### Session Config Options
- `sessionId` - Custom session ID (string)
- `model` - Model name ("gpt-5", "claude-sonnet-4.5", etc.)
- `tools` - Custom tools exposed to the CLI (Tool[])
- `systemMessage` - System message customization (SystemMessageConfig)
- `availableTools` - Allowlist of tool names (string[])
- `excludedTools` - Blocklist of tool names (string[])
- `provider` - Custom API provider configuration (BYOK) (ProviderConfig)
- `streaming` - Enable streaming response chunks (boolean)
- `mcpServers` - MCP server configurations (MCPServerConfig[])
- `customAgents` - Custom agent configurations (CustomAgentConfig[])
- `configDir` - Config directory override (string)
- `skillDirectories` - Skill directories (string[])
- `disabledSkills` - Disabled skills (string[])
- `onPermissionRequest` - Permission request handler (PermissionHandler)
### Resuming Sessions
```typescript
const session = await client.resumeSession("session-id", {
tools: [myNewTool],
});
```
### Session Operations
- `session.sessionId` - Get session identifier (string)
- `await session.send({ prompt: "...", attachments: [...] })` - Send message, returns Promise<string>
- `await session.sendAndWait({ prompt: "..." }, timeout)` - Send and wait for idle, returns Promise<AssistantMessageEvent | null>
- `await session.abort()` - Abort current processing
- `await session.getMessages()` - Get all events/messages, returns Promise<SessionEvent[]>
- `await session.destroy()` - Clean up session
## Event Handling
### Event Subscription Pattern
ALWAYS use async/await or Promises for waiting on session events:
```typescript
await new Promise<void>((resolve) => {
session.on((event) => {
if (event.type === "assistant.message") {
console.log(event.data.content);
} else if (event.type === "session.idle") {
resolve();
}
});
session.send({ prompt: "..." });
});
```
### Unsubscribing from Events
The `on()` method returns a function that unsubscribes:
```typescript
const unsubscribe = session.on((event) => {
// handler
});
// Later...
unsubscribe();
```
### Event Types
Use discriminated unions with type guards for event handling:
```typescript
session.on((event) => {
switch (event.type) {
case "user.message":
// Handle user message
break;
case "assistant.message":
console.log(event.data.content);
break;
case "tool.executionStart":
// Tool execution started
break;
case "tool.executionComplete":
// Tool execution completed
break;
case "session.start":
// Session started
break;
case "session.idle":
// Session is idle (processing complete)
break;
case "session.error":
console.error(`Error: ${event.data.message}`);
break;
}
});
```
## Streaming Responses
### Enabling Streaming
Set `streaming: true` in SessionConfig:
```typescript
const session = await client.createSession({
model: "gpt-5",
streaming: true,
});
```
### Handling Streaming Events
Handle both delta events (incremental) and final events:
```typescript
await new Promise<void>((resolve) => {
session.on((event) => {
switch (event.type) {
case "assistant.message.delta":
// Incremental text chunk
process.stdout.write(event.data.deltaContent);
break;
case "assistant.reasoning.delta":
// Incremental reasoning chunk (model-dependent)
process.stdout.write(event.data.deltaContent);
break;
case "assistant.message":
// Final complete message
console.log("\n--- Final ---");
console.log(event.data.content);
break;
case "assistant.reasoning":
// Final reasoning content
console.log("--- Reasoning ---");
console.log(event.data.content);
break;
case "session.idle":
resolve();
break;
}
});
session.send({ prompt: "Tell me a story" });
});
```
Note: Final events (`assistant.message`, `assistant.reasoning`) are ALWAYS sent regardless of streaming setting.
## Custom Tools
### Defining Tools with defineTool
Use `defineTool` for type-safe tool definitions:
```typescript
import { defineTool } from "@github/copilot-sdk";
const session = await client.createSession({
model: "gpt-5",
tools: [
defineTool({
name: "lookup_issue",
description: "Fetch issue details from tracker",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Issue ID" },
},
required: ["id"],
},
handler: async (args) => {
const issue = await fetchIssue(args.id);
return issue;
},
}),
],
});
```
### Using Zod for Parameters
The SDK supports Zod schemas for parameters:
```typescript
import { z } from "zod";
const session = await client.createSession({
tools: [
defineTool({
name: "get_weather",
description: "Get weather for a location",
parameters: z.object({
location: z.string().describe("City name"),
units: z.enum(["celsius", "fahrenheit"]).optional(),
}),
handler: async (args) => {
return { temperature: 72, units: args.units || "fahrenheit" };
},
}),
],
});
```
### Tool Return Types
- Return any JSON-serializable value (automatically wrapped)
- Or return `ToolResultObject` for full control over metadata:
```typescript
{
textResultForLlm: string; // Result shown to LLM
resultType: "success" | "failure";
error?: string; // Internal error (not shown to LLM)
toolTelemetry?: Record<string, unknown>;
}
```
### Tool Execution Flow
When Copilot invokes a tool, the client automatically:
1. Runs your handler function
2. Serializes the return value
3. Responds to the CLI
## System Message Customization
### Append Mode (Default - Preserves Guardrails)
```typescript
const session = await client.createSession({
model: "gpt-5",
systemMessage: {
mode: "append",
content: `
<workflow_rules>
- Always check for security vulnerabilities
- Suggest performance improvements when applicable
</workflow_rules>
`,
},
});
```
### Replace Mode (Full Control - Removes Guardrails)
```typescript
const session = await client.createSession({
model: "gpt-5",
systemMessage: {
mode: "replace",
content: "You are a helpful assistant.",
},
});
```
## File Attachments
Attach files to messages:
```typescript
await session.send({
prompt: "Analyze this file",
attachments: [
{
type: "file",
path: "/path/to/file.ts",
displayName: "My File",
},
],
});
```
## Message Delivery Modes
Use the `mode` property in message options:
- `"enqueue"` - Queue message for processing
- `"immediate"` - Process message immediately
```typescript
await session.send({
prompt: "...",
mode: "enqueue",
});
```
## Multiple Sessions
Sessions are independent and can run concurrently:
```typescript
const session1 = await client.createSession({ model: "gpt-5" });
const session2 = await client.createSession({ model: "claude-sonnet-4.5" });
await Promise.all([
session1.send({ prompt: "Hello from session 1" }),
session2.send({ prompt: "Hello from session 2" }),
]);
```
## Bring Your Own Key (BYOK)
Use custom API providers via `provider`:
```typescript
const session = await client.createSession({
provider: {
type: "openai",
baseUrl: "https://api.openai.com/v1",
apiKey: "your-api-key",
},
});
```
## Session Lifecycle Management
### Listing Sessions
```typescript
const sessions = await client.listSessions();
for (const metadata of sessions) {
console.log(`${metadata.sessionId}: ${metadata.summary}`);
}
```
### Deleting Sessions
```typescript
await client.deleteSession(sessionId);
```
### Getting Last Session ID
```typescript
const lastId = await client.getLastSessionId();
if (lastId) {
const session = await client.resumeSession(lastId);
}
```
### Checking Connection State
```typescript
const state = client.getState();
// Returns: "disconnected" | "connecting" | "connected" | "error"
```
## Error Handling
### Standard Exception Handling
```typescript
try {
const session = await client.createSession();
await session.send({ prompt: "Hello" });
} catch (error) {
console.error(`Error: ${error.message}`);
}
```
### Session Error Events
Monitor `session.error` event type for runtime errors:
```typescript
session.on((event) => {
if (event.type === "session.error") {
console.error(`Session Error: ${event.data.message}`);
}
});
```
## Connectivity Testing
Use ping to verify server connectivity:
```typescript
const response = await client.ping("health check");
console.log(`Server responded at ${new Date(response.timestamp)}`);
```
## Resource Cleanup
### Automatic Cleanup with Try-Finally
ALWAYS use try-finally or cleanup in a finally block:
```typescript
const client = new CopilotClient();
try {
await client.start();
const session = await client.createSession();
try {
// Use session...
} finally {
await session.destroy();
}
} finally {
await client.stop();
}
```
### Cleanup Function Pattern
```typescript
async function withClient<T>(
fn: (client: CopilotClient) => Promise<T>,
): Promise<T> {
const client = new CopilotClient();
try {
await client.start();
return await fn(client);
} finally {
await client.stop();
}
}
async function withSession<T>(
client: CopilotClient,
fn: (session: CopilotSession) => Promise<T>,
): Promise<T> {
const session = await client.createSession();
try {
return await fn(session);
} finally {
await session.destroy();
}
}
// Usage
await withClient(async (client) => {
await withSession(client, async (session) => {
await session.send({ prompt: "Hello!" });
});
});
```
## Best Practices
1. **Always use try-finally** for resource cleanup
2. **Use Promises** to wait for session.idle event
3. **Handle session.error** events for robust error handling
4. **Use type guards or switch statements** for event handling
5. **Enable streaming** for better UX in interactive scenarios
6. **Use defineTool** for type-safe tool definitions
7. **Use Zod schemas** for runtime parameter validation
8. **Dispose event subscriptions** when no longer needed
9. **Use systemMessage with mode: "append"** to preserve safety guardrails
10. **Handle both delta and final events** when streaming is enabled
11. **Leverage TypeScript types** for compile-time safety
## Common Patterns
### Simple Query-Response
```typescript
import { CopilotClient } from "@github/copilot-sdk";
const client = new CopilotClient();
try {
await client.start();
const session = await client.createSession({ model: "gpt-5" });
try {
await new Promise<void>((resolve) => {
session.on((event) => {
if (event.type === "assistant.message") {
console.log(event.data.content);
} else if (event.type === "session.idle") {
resolve();
}
});
session.send({ prompt: "What is 2+2?" });
});
} finally {
await session.destroy();
}
} finally {
await client.stop();
}
```
### Multi-Turn Conversation
```typescript
const session = await client.createSession();
async function sendAndWait(prompt: string): Promise<void> {
await new Promise<void>((resolve, reject) => {
const unsubscribe = session.on((event) => {
if (event.type === "assistant.message") {
console.log(event.data.content);
} else if (event.type === "session.idle") {
unsubscribe();
resolve();
} else if (event.type === "session.error") {
unsubscribe();
reject(new Error(event.data.message));
}
});
session.send({ prompt });
});
}
await sendAndWait("What is the capital of France?");
await sendAndWait("What is its population?");
```
### SendAndWait Helper
```typescript
// Use built-in sendAndWait for simpler synchronous interaction
const response = await session.sendAndWait({ prompt: "What is 2+2?" }, 60000);
if (response) {
console.log(response.data.content);
}
```
### Tool with Type-Safe Parameters
```typescript
import { z } from "zod";
import { defineTool } from "@github/copilot-sdk";
interface UserInfo {
id: string;
name: string;
email: string;
role: string;
}
const session = await client.createSession({
tools: [
defineTool({
name: "get_user",
description: "Retrieve user information",
parameters: z.object({
userId: z.string().describe("User ID"),
}),
handler: async (args): Promise<UserInfo> => {
return {
id: args.userId,
name: "John Doe",
email: "john@example.com",
role: "Developer",
};
},
}),
],
});
```
### Streaming with Progress
```typescript
let currentMessage = "";
const unsubscribe = session.on((event) => {
if (event.type === "assistant.message.delta") {
currentMessage += event.data.deltaContent;
process.stdout.write(event.data.deltaContent);
} else if (event.type === "assistant.message") {
console.log("\n\n=== Complete ===");
console.log(`Total length: ${event.data.content.length} chars`);
} else if (event.type === "session.idle") {
unsubscribe();
}
});
await session.send({ prompt: "Write a long story" });
```
### Error Recovery
```typescript
session.on((event) => {
if (event.type === "session.error") {
console.error("Session error:", event.data.message);
// Optionally retry or handle error
}
});
try {
await session.send({ prompt: "risky operation" });
} catch (error) {
// Handle send errors
console.error("Failed to send:", error);
}
```
## TypeScript-Specific Features
### Type Inference
```typescript
import type { SessionEvent, AssistantMessageEvent } from "@github/copilot-sdk";
session.on((event: SessionEvent) => {
if (event.type === "assistant.message") {
// TypeScript knows event is AssistantMessageEvent here
const content: string = event.data.content;
}
});
```
### Generic Helper
```typescript
async function waitForEvent<T extends SessionEvent["type"]>(
session: CopilotSession,
eventType: T,
): Promise<Extract<SessionEvent, { type: T }>> {
return new Promise((resolve) => {
const unsubscribe = session.on((event) => {
if (event.type === eventType) {
unsubscribe();
resolve(event as Extract<SessionEvent, { type: T }>);
}
});
});
}
// Usage
const message = await waitForEvent(session, "assistant.message");
console.log(message.data.content);
```

View File

@@ -0,0 +1,806 @@
---
applyTo: "**.py, pyproject.toml, setup.py"
description: "This file provides guidance on building Python applications using GitHub Copilot SDK."
name: "GitHub Copilot SDK Python Instructions"
---
## Core Principles
- The SDK is in technical preview and may have breaking changes
- Requires Python 3.9 or later
- Requires GitHub Copilot CLI installed and in PATH
- Uses async/await patterns throughout (asyncio)
- Supports both async context managers and manual lifecycle management
- Type hints provided for better IDE support
## Installation
Always install via pip:
```bash
pip install copilot-sdk
# or with poetry
poetry add copilot-sdk
# or with uv
uv add copilot-sdk
```
## Client Initialization
### Basic Client Setup
```python
from copilot import CopilotClient
async with CopilotClient() as client:
# Use client...
pass
```
### Client Configuration Options
When creating a CopilotClient, use a dict with these keys:
- `cli_path` - Path to CLI executable (default: "copilot" from PATH or COPILOT_CLI_PATH env var)
- `cli_url` - URL of existing CLI server (e.g., "localhost:8080"). When provided, client won't spawn a process
- `port` - Server port (default: 0 for random)
- `use_stdio` - Use stdio transport instead of TCP (default: True)
- `log_level` - Log level (default: "info")
- `auto_start` - Auto-start server (default: True)
- `auto_restart` - Auto-restart on crash (default: True)
- `cwd` - Working directory for the CLI process (default: os.getcwd())
- `env` - Environment variables for the CLI process (dict)
### Manual Server Control
For explicit control:
```python
client = CopilotClient({"auto_start": False})
await client.start()
# Use client...
await client.stop()
```
Use `force_stop()` when `stop()` takes too long.
## Session Management
### Creating Sessions
Use a dict for SessionConfig:
```python
session = await client.create_session({
"model": "gpt-5",
"streaming": True,
"tools": [...],
"system_message": { ... },
"available_tools": ["tool1", "tool2"],
"excluded_tools": ["tool3"],
"provider": { ... }
})
```
### Session Config Options
- `session_id` - Custom session ID (str)
- `model` - Model name ("gpt-5", "claude-sonnet-4.5", etc.)
- `tools` - Custom tools exposed to the CLI (list[Tool])
- `system_message` - System message customization (dict)
- `available_tools` - Allowlist of tool names (list[str])
- `excluded_tools` - Blocklist of tool names (list[str])
- `provider` - Custom API provider configuration (BYOK) (ProviderConfig)
- `streaming` - Enable streaming response chunks (bool)
- `mcp_servers` - MCP server configurations (list)
- `custom_agents` - Custom agent configurations (list)
- `config_dir` - Config directory override (str)
- `skill_directories` - Skill directories (list[str])
- `disabled_skills` - Disabled skills (list[str])
- `on_permission_request` - Permission request handler (callable)
### Resuming Sessions
```python
session = await client.resume_session("session-id", {
"tools": [my_new_tool]
})
```
### Session Operations
- `session.session_id` - Get session identifier (str)
- `await session.send({"prompt": "...", "attachments": [...]})` - Send message, returns str (message ID)
- `await session.send_and_wait({"prompt": "..."}, timeout=60.0)` - Send and wait for idle, returns SessionEvent | None
- `await session.abort()` - Abort current processing
- `await session.get_messages()` - Get all events/messages, returns list[SessionEvent]
- `await session.destroy()` - Clean up session
## Event Handling
### Event Subscription Pattern
ALWAYS use asyncio events or futures for waiting on session events:
```python
import asyncio
done = asyncio.Event()
def handler(event):
if event.type == "assistant.message":
print(event.data.content)
elif event.type == "session.idle":
done.set()
session.on(handler)
await session.send({"prompt": "..."})
await done.wait()
```
### Unsubscribing from Events
The `on()` method returns a function that unsubscribes:
```python
unsubscribe = session.on(lambda event: print(event.type))
# Later...
unsubscribe()
```
### Event Types
Use attribute access for event type checking:
```python
def handler(event):
if event.type == "user.message":
# Handle user message
pass
elif event.type == "assistant.message":
print(event.data.content)
elif event.type == "tool.executionStart":
# Tool execution started
pass
elif event.type == "tool.executionComplete":
# Tool execution completed
pass
elif event.type == "session.start":
# Session started
pass
elif event.type == "session.idle":
# Session is idle (processing complete)
pass
elif event.type == "session.error":
print(f"Error: {event.data.message}")
session.on(handler)
```
## Streaming Responses
### Enabling Streaming
Set `streaming: True` in SessionConfig:
```python
session = await client.create_session({
"model": "gpt-5",
"streaming": True
})
```
### Handling Streaming Events
Handle both delta events (incremental) and final events:
```python
import asyncio
done = asyncio.Event()
def handler(event):
if event.type == "assistant.message.delta":
# Incremental text chunk
print(event.data.delta_content, end="", flush=True)
elif event.type == "assistant.reasoning.delta":
# Incremental reasoning chunk (model-dependent)
print(event.data.delta_content, end="", flush=True)
elif event.type == "assistant.message":
# Final complete message
print("\n--- Final ---")
print(event.data.content)
elif event.type == "assistant.reasoning":
# Final reasoning content
print("--- Reasoning ---")
print(event.data.content)
elif event.type == "session.idle":
done.set()
session.on(handler)
await session.send({"prompt": "Tell me a story"})
await done.wait()
```
Note: Final events (`assistant.message`, `assistant.reasoning`) are ALWAYS sent regardless of streaming setting.
## Custom Tools
### Defining Tools with define_tool
Use `define_tool` for tool definitions:
```python
from copilot import define_tool
async def fetch_issue(issue_id: str):
# Fetch issue from tracker
return {"id": issue_id, "status": "open"}
session = await client.create_session({
"model": "gpt-5",
"tools": [
define_tool(
name="lookup_issue",
description="Fetch issue details from tracker",
parameters={
"type": "object",
"properties": {
"id": {"type": "string", "description": "Issue ID"}
},
"required": ["id"]
},
handler=lambda args, inv: fetch_issue(args["id"])
)
]
})
```
### Using Pydantic for Parameters
The SDK works well with Pydantic models:
```python
from pydantic import BaseModel, Field
class WeatherArgs(BaseModel):
location: str = Field(description="City name")
units: str = Field(default="fahrenheit", description="Temperature units")
async def get_weather(args: WeatherArgs, inv):
return {"temperature": 72, "units": args.units}
session = await client.create_session({
"tools": [
define_tool(
name="get_weather",
description="Get weather for a location",
parameters=WeatherArgs.model_json_schema(),
handler=lambda args, inv: get_weather(WeatherArgs(**args), inv)
)
]
})
```
### Tool Return Types
- Return any JSON-serializable value (automatically wrapped)
- Or return a ToolResult dict for full control:
```python
{
"text_result_for_llm": str, # Result shown to LLM
"result_type": "success" | "failure",
"error": str, # Optional: Internal error (not shown to LLM)
"tool_telemetry": dict # Optional: Telemetry data
}
```
### Tool Handler Signature
Tool handlers receive two arguments:
- `args` (dict) - The tool arguments passed by the LLM
- `invocation` (ToolInvocation) - Metadata about the invocation
- `invocation.session_id` - Session ID
- `invocation.tool_call_id` - Tool call ID
- `invocation.tool_name` - Tool name
- `invocation.arguments` - Same as args parameter
### Tool Execution Flow
When Copilot invokes a tool, the client automatically:
1. Runs your handler function
2. Serializes the return value
3. Responds to the CLI
## System Message Customization
### Append Mode (Default - Preserves Guardrails)
```python
session = await client.create_session({
"model": "gpt-5",
"system_message": {
"mode": "append",
"content": """
<workflow_rules>
- Always check for security vulnerabilities
- Suggest performance improvements when applicable
</workflow_rules>
"""
}
})
```
### Replace Mode (Full Control - Removes Guardrails)
```python
session = await client.create_session({
"model": "gpt-5",
"system_message": {
"mode": "replace",
"content": "You are a helpful assistant."
}
})
```
## File Attachments
Attach files to messages:
```python
await session.send({
"prompt": "Analyze this file",
"attachments": [
{
"type": "file",
"path": "/path/to/file.py",
"display_name": "My File"
}
]
})
```
## Message Delivery Modes
Use the `mode` key in message options:
- `"enqueue"` - Queue message for processing
- `"immediate"` - Process message immediately
```python
await session.send({
"prompt": "...",
"mode": "enqueue"
})
```
## Multiple Sessions
Sessions are independent and can run concurrently:
```python
session1 = await client.create_session({"model": "gpt-5"})
session2 = await client.create_session({"model": "claude-sonnet-4.5"})
await asyncio.gather(
session1.send({"prompt": "Hello from session 1"}),
session2.send({"prompt": "Hello from session 2"})
)
```
## Bring Your Own Key (BYOK)
Use custom API providers via `provider`:
```python
session = await client.create_session({
"provider": {
"type": "openai",
"base_url": "https://api.openai.com/v1",
"api_key": "your-api-key"
}
})
```
## Session Lifecycle Management
### Listing Sessions
```python
sessions = await client.list_sessions()
for metadata in sessions:
print(f"{metadata.session_id}: {metadata.summary}")
```
### Deleting Sessions
```python
await client.delete_session(session_id)
```
### Getting Last Session ID
```python
last_id = await client.get_last_session_id()
if last_id:
session = await client.resume_session(last_id)
```
### Checking Connection State
```python
state = client.get_state()
# Returns: "disconnected" | "connecting" | "connected" | "error"
```
## Error Handling
### Standard Exception Handling
```python
try:
session = await client.create_session()
await session.send({"prompt": "Hello"})
except Exception as e:
print(f"Error: {e}")
```
### Session Error Events
Monitor `session.error` event type for runtime errors:
```python
def handler(event):
if event.type == "session.error":
print(f"Session Error: {event.data.message}")
session.on(handler)
```
## Connectivity Testing
Use ping to verify server connectivity:
```python
response = await client.ping("health check")
print(f"Server responded at {response['timestamp']}")
```
## Resource Cleanup
### Automatic Cleanup with Context Managers
ALWAYS use async context managers for automatic cleanup:
```python
async with CopilotClient() as client:
async with await client.create_session() as session:
# Use session...
await session.send({"prompt": "Hello"})
# Session automatically destroyed
# Client automatically stopped
```
### Manual Cleanup with Try-Finally
```python
client = CopilotClient()
try:
await client.start()
session = await client.create_session()
try:
# Use session...
pass
finally:
await session.destroy()
finally:
await client.stop()
```
## Best Practices
1. **Always use async context managers** (`async with`) for automatic cleanup
2. **Use asyncio.Event or asyncio.Future** to wait for session.idle event
3. **Handle session.error** events for robust error handling
4. **Use if/elif chains** for event type checking
5. **Enable streaming** for better UX in interactive scenarios
6. **Use define_tool** for tool definitions
7. **Use Pydantic models** for type-safe parameter validation
8. **Dispose event subscriptions** when no longer needed
9. **Use system_message with mode: "append"** to preserve safety guardrails
10. **Handle both delta and final events** when streaming is enabled
11. **Use type hints** for better IDE support and code clarity
## Common Patterns
### Simple Query-Response
```python
from copilot import CopilotClient
import asyncio
async def main():
async with CopilotClient() as client:
async with await client.create_session({"model": "gpt-5"}) as session:
done = asyncio.Event()
def handler(event):
if event.type == "assistant.message":
print(event.data.content)
elif event.type == "session.idle":
done.set()
session.on(handler)
await session.send({"prompt": "What is 2+2?"})
await done.wait()
asyncio.run(main())
```
### Multi-Turn Conversation
```python
async def send_and_wait(session, prompt: str):
done = asyncio.Event()
result = []
def handler(event):
if event.type == "assistant.message":
result.append(event.data.content)
print(event.data.content)
elif event.type == "session.idle":
done.set()
elif event.type == "session.error":
result.append(None)
done.set()
unsubscribe = session.on(handler)
await session.send({"prompt": prompt})
await done.wait()
unsubscribe()
return result[0] if result else None
async with await client.create_session() as session:
await send_and_wait(session, "What is the capital of France?")
await send_and_wait(session, "What is its population?")
```
### SendAndWait Helper
```python
# Use built-in send_and_wait for simpler synchronous interaction
async with await client.create_session() as session:
response = await session.send_and_wait(
{"prompt": "What is 2+2?"},
timeout=60.0
)
if response and response.type == "assistant.message":
print(response.data.content)
```
### Tool with Dataclass Return Type
```python
from dataclasses import dataclass, asdict
from copilot import define_tool
@dataclass
class UserInfo:
id: str
name: str
email: str
role: str
async def get_user(args, inv) -> dict:
user = UserInfo(
id=args["user_id"],
name="John Doe",
email="john@example.com",
role="Developer"
)
return asdict(user)
session = await client.create_session({
"tools": [
define_tool(
name="get_user",
description="Retrieve user information",
parameters={
"type": "object",
"properties": {
"user_id": {"type": "string", "description": "User ID"}
},
"required": ["user_id"]
},
handler=get_user
)
]
})
```
### Streaming with Progress
```python
import asyncio
current_message = []
done = asyncio.Event()
def handler(event):
if event.type == "assistant.message.delta":
current_message.append(event.data.delta_content)
print(event.data.delta_content, end="", flush=True)
elif event.type == "assistant.message":
print(f"\n\n=== Complete ===")
print(f"Total length: {len(event.data.content)} chars")
elif event.type == "session.idle":
done.set()
unsubscribe = session.on(handler)
await session.send({"prompt": "Write a long story"})
await done.wait()
unsubscribe()
```
### Error Recovery
```python
def handler(event):
if event.type == "session.error":
print(f"Session error: {event.data.message}")
# Optionally retry or handle error
session.on(handler)
try:
await session.send({"prompt": "risky operation"})
except Exception as e:
# Handle send errors
print(f"Failed to send: {e}")
```
### Using TypedDict for Type Safety
```python
from typing import TypedDict, List
class MessageOptions(TypedDict, total=False):
prompt: str
attachments: List[dict]
mode: str
class SessionConfig(TypedDict, total=False):
model: str
streaming: bool
tools: List
# Usage with type hints
options: MessageOptions = {
"prompt": "Hello",
"mode": "enqueue"
}
await session.send(options)
config: SessionConfig = {
"model": "gpt-5",
"streaming": True
}
session = await client.create_session(config)
```
### Async Generator for Streaming
```python
from typing import AsyncGenerator
async def stream_response(session, prompt: str) -> AsyncGenerator[str, None]:
"""Stream response chunks as an async generator."""
queue = asyncio.Queue()
done = asyncio.Event()
def handler(event):
if event.type == "assistant.message.delta":
queue.put_nowait(event.data.delta_content)
elif event.type == "session.idle":
done.set()
unsubscribe = session.on(handler)
await session.send({"prompt": prompt})
while not done.is_set():
try:
chunk = await asyncio.wait_for(queue.get(), timeout=0.1)
yield chunk
except asyncio.TimeoutError:
continue
# Drain remaining items
while not queue.empty():
yield queue.get_nowait()
unsubscribe()
# Usage
async for chunk in stream_response(session, "Tell me a story"):
print(chunk, end="", flush=True)
```
### Decorator Pattern for Tools
```python
from typing import Callable, Any
from copilot import define_tool
def copilot_tool(
name: str,
description: str,
parameters: dict
) -> Callable:
"""Decorator to convert a function into a Copilot tool."""
def decorator(func: Callable) -> Any:
return define_tool(
name=name,
description=description,
parameters=parameters,
handler=lambda args, inv: func(**args)
)
return decorator
@copilot_tool(
name="calculate",
description="Perform a calculation",
parameters={
"type": "object",
"properties": {
"expression": {"type": "string", "description": "Math expression"}
},
"required": ["expression"]
}
)
def calculate(expression: str) -> float:
return eval(expression)
session = await client.create_session({"tools": [calculate]})
```
## Python-Specific Features
### Async Context Manager Protocol
The SDK implements `__aenter__` and `__aexit__`:
```python
class CopilotClient:
async def __aenter__(self):
await self.start()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.stop()
return False
class CopilotSession:
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.destroy()
return False
```
### Dataclass Support
Event data is available as attributes:
```python
def handler(event):
# Access event attributes directly
print(event.type)
print(event.data.content) # For assistant.message
print(event.data.delta_content) # For assistant.message.delta
```