# Copilot SDK SDK for programmatic control of GitHub Copilot CLI. ## Prerequisites To use the SDK, you'll need: - Any of the [.NET Standard 2.0-compatible .NET implementations](https://learn.microsoft.com/dotnet/standard/net-standard?tabs=net-standard-2-0#select-net-standard-version) ## Installation ```bash dotnet add package GitHub.Copilot.SDK ``` The package downloads the pinned Copilot CLI runtime for the build RID from the matching `github/copilot-cli` GitHub release and verifies the archive against that release's `SHA256SUMS.txt`. Set `CopilotCliReleaseBaseUrl` in MSBuild (or `COPILOT_CLI_DOWNLOAD_BASE_URL` in the environment) to use a release mirror. Set `CopilotCliBinaryPath` to copy a preinstalled binary instead, or set `CopilotSkipCliDownload=true` to omit runtime acquisition. ## Run the Samples Try the interactive chat sample (from the repo root): ```bash dotnet run --file dotnet/samples/Chat.cs ``` The manual permission/tool-result resume sample can be run the same way: ```bash dotnet run --file dotnet/samples/ManualToolResume.cs ``` ## Quick Start ```csharp using GitHub.Copilot; // Create and start client await using var client = new CopilotClient(); await client.StartAsync(); // ApproveAll is only valid when managed settings are disabled. await using var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", OnPermissionRequest = PermissionHandler.ApproveAll, }); // Wait for the response using the session.idle event var done = new TaskCompletionSource(); session.On(evt => { if (evt is AssistantMessageEvent msg) { Console.WriteLine(msg.Data.Content); } else if (evt is SessionIdleEvent) { done.SetResult(); } }); // Send a message and wait for completion await session.SendAsync(new MessageOptions { Prompt = "What is 2+2?" }); await done.Task; ``` When targeting MCP tools configured through `McpServers`, remember the runtime tool name is `-`. For `AvailableTools` and `ExcludedTools`, prefer the source-qualified form `mcp:-`. For `CustomAgents[].Tools` and `DefaultAgent.ExcludedTools`, use `-` directly. ## API Reference ### CopilotClient #### Constructor ```csharp new CopilotClient(CopilotClientOptions? options = null) ``` **Options:** - `Connection` - How to connect to the Copilot runtime. Defaults to `null` (equivalent to `RuntimeConnection.ForStdio()` with the bundled runtime). See "RuntimeConnection" below. - `LogLevel` - Runtime log level. Accepts well-known values `CopilotLogLevel.None`, `Error`, `Warning`, `Info`, `Debug`, `All`. Defaults to null (the runtime's own default). - `WorkingDirectory` - Working directory for the runtime process. When not set, the spawned runtime inherits the calling application's current working directory. - `BaseDirectory` - Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned runtime process. When not set, the runtime defaults to `~/.copilot`. Useful in restricted environments where only specific directories are writable. Ignored when connecting via `RuntimeConnection.ForUri(...)`. - `EnableRemoteSessions` - Enables remote-session features. - `Environment` - Environment variables to pass to the runtime process. - `Logger` - `ILogger` instance for SDK logging. - `GitHubToken` - GitHub token for authentication. When provided, takes priority over other auth methods. - `UseLoggedInUser` - Whether to use logged-in user for authentication (default: true, but false when `GitHubToken` is provided). Cannot be used with `RuntimeConnection.ForUri(...)`. - `Telemetry` - OpenTelemetry configuration for the runtime process. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. #### RuntimeConnection `CopilotClientOptions.Connection` describes how the SDK reaches a Copilot runtime. There are three flavors, all constructed via static factories: - `RuntimeConnection.ForStdio(path?, args?)` — spawns the runtime as a child process and communicates over stdio. This is the default when `Connection` is null. - `RuntimeConnection.ForTcp(port = 0, connectionToken?, path?, args?)` — spawns the runtime as a child process listening on a TCP port. `port = 0` auto-allocates; if a non-zero port is already in use, startup fails (no fallback). Use `CopilotClient.RuntimePort` after `StartAsync` to read the assigned port. `connectionToken` is required if other clients will connect via `RuntimeConnection.ForUri(...)`. - `RuntimeConnection.ForUri(url, connectionToken?)` — connects to an already-running runtime at `url` (e.g., `"localhost:8080"`). Does not spawn a process. Managed stdio and TCP connections use the bundled `copilot-runtime[.exe]` and adjacent `runtime.node` by default. An explicit connection path or `COPILOT_CLI_PATH` overrides the bundled runtime. Managed launch fails if the bundled wrapper pair is unavailable. #### Methods ##### `StartAsync(): Task` Start the CLI server and establish connection. ##### `StopAsync(): Task` Stop the server and close all sessions. Throws if errors are encountered during cleanup. ##### `ForceStopAsync(): Task` Force stop the CLI server without graceful cleanup. Use when `StopAsync()` takes too long. ##### `CreateSessionAsync(SessionConfig? config = null): Task` Create a new conversation session. **Config:** - `SessionId` - Custom session ID - `Model` - Model to use ("gpt-5", "claude-sonnet-4.5", etc.) - `ReasoningEffort` - Reasoning effort level for models that support it ("low", "medium", "high", "xhigh", "max"). Use `ListModelsAsync()` to check which models support this option. - `Tools` - Custom tool declarations exposed to the CLI. Declarations without an invocable `AIFunction` are left pending for manual resolution. - `SystemMessage` - System message customization - `AvailableTools` - List of tool names to allow - `ExcludedTools` - List of tool names to disable - `Provider` - Custom API provider configuration (BYOK) - `Streaming` - Enable streaming of response chunks (default: false) - `InfiniteSessions` - Configure automatic context compaction (see below) - `WorkingDirectory` - Working directory for the session. When not set, the runtime uses its own process working directory. - `EnableSessionStore` - Enables the cross-session store for search and retrieval across sessions. When unset in `CopilotClientMode.CopilotCli`, the runtime default applies (enabled). In `CopilotClientMode.Empty`, defaults to disabled. - `GitHubTokenProvider` - Acquires session-scoped GitHub tokens on demand. Return `GitHubTokenProviderResult.FromToken` with a positive `ExpiresIn` value (production GitHub tokens typically use `8 * 60 * 60` seconds), or `GitHubTokenProviderResult.Cancel()`. Cannot be combined with `GitHubToken`. - `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `PermissionHandler.ApproveAll` approves requests when managed settings are disabled and throws when `EnableManagedSettings` is true. Custom handlers can inspect `ManagedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. - `OnUserInputRequest` - Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section. - `AskUserVariant` - Selects the model-facing `ask_user` tool shape. Defaults to `AskUserVariant.Legacy`; use `AskUserVariant.Elicitation` with `OnElicitationRequest`. - `Hooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. ##### `ResumeSessionAsync(string sessionId, ResumeSessionConfig? config = null): Task` Resume an existing session. Returns the session with `WorkspacePath` populated if infinite sessions were enabled. **ResumeSessionConfig:** - `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section. - `GitHubTokenProvider` - Replaces the session-scoped token provider when resuming. Cannot be combined with `GitHubToken`. - `AskUserVariant` - Re-supplies the model-facing `ask_user` tool shape on cold resume. ```csharp await using var session = await client.CreateSessionAsync(new SessionConfig { GitHubTokenProvider = async args => { var token = await AcquireTokenAsync(args.Host); return GitHubTokenProviderResult.FromToken(new GitHubToken { AccessToken = token, ExpiresIn = 8 * 60 * 60 }); } }); ``` Initial acquisition runs during session creation or resume. Cancellation, provider errors, and invalid token responses reject that operation instead of falling back to ambient authentication. Idle sessions refresh only before their next credential-consuming operation; there is no background refresh timer. ##### `PingAsync(string? message = null): Task` Ping the server to check connectivity. ##### `State: ConnectionState` Get current connection state. ##### `ListSessionsAsync(): Task>` List all available sessions. ##### `DeleteSessionAsync(string sessionId): Task` Delete a session and its data from disk. ##### `GetForegroundSessionIdAsync(): Task` Get the ID of the session currently displayed in the TUI. Only available when connecting to a server running in TUI+server mode (`--ui-server`). ##### `SetForegroundSessionIdAsync(string sessionId): Task` Request the TUI to switch to displaying the specified session. Only available in TUI+server mode. ##### `OnLifecycle(Action handler): IDisposable where T : SessionLifecycleEvent` Subscribe to session lifecycle events. Pass a derived type to filter by kind, or `SessionLifecycleEvent` to receive every lifecycle event. Returns an `IDisposable` that unsubscribes when disposed. ```csharp // Receive every lifecycle event: using var subscription = client.OnLifecycle(evt => { Console.WriteLine($"Session {evt.SessionId}: {evt.Type}"); }); // Only receive foreground events: using var foreground = client.OnLifecycle(evt => { Console.WriteLine($"Session {evt.SessionId} is now in foreground"); }); ``` **Lifecycle Event Types:** - `SessionCreatedEvent` — A new session was created - `SessionDeletedEvent` — A session was deleted - `SessionUpdatedEvent` — A session was updated - `SessionForegroundEvent` — A session became the foreground session in TUI - `SessionBackgroundEvent` — A session is no longer the foreground session --- ### CopilotSession Represents a single conversation session. #### Properties - `SessionId` - The unique identifier for this session - `WorkspacePath` - Path to the session workspace directory when infinite sessions are enabled. Contains `checkpoints/`, `plan.md`, and `files/` subdirectories. Null if infinite sessions are disabled. #### Methods ##### `SendAsync(MessageOptions options): Task` Send a message to the session. **Options:** - `Prompt` - The message/prompt to send - `Attachments` - File attachments - `Mode` - Delivery mode ("enqueue" or "immediate") - `Source` - Optional message origin: `MessageSource.User`, `MessageSource.System`, or `MessageSource.Agent(id)`. Omitted by default, preserving the runtime's default user behavior. Returns the message ID. Use `MessageSource.System` for application-generated system context and `MessageSource.Agent(id)` for messages from an identified agent. This marks the message's origin; it does not replace the session's system prompt or change delivery mode. `SendAndWaitAsync` accepts the same option and still waits for session idle, returning null if no assistant message was received. ```csharp await session.SendAsync(new MessageOptions { Prompt = "The background build completed successfully.", Source = MessageSource.System, }); await session.SendAndWaitAsync(new MessageOptions { Prompt = "The review found no blocking issues.", Source = MessageSource.Agent("reviewer"), }); ``` Agent sources serialize as `agent-`. Pass the agent ID without adding a prefix. The SDK preserves its case and whitespace and rejects null IDs. ##### `On(Action handler): IDisposable` Subscribe to session events. Returns a disposable to unsubscribe. ```csharp var subscription = session.On(evt => { Console.WriteLine($"Event: {evt.Type}"); }); // Later... subscription.Dispose(); ``` ##### `AbortAsync(): Task` Abort the currently processing message in this session. ##### `GetEventsAsync(): Task>` Get all events/messages from this session. ##### `DisposeAsync(): ValueTask` Close the session and release in-memory resources. Session data on disk is preserved — the conversation can be resumed later via `ResumeSessionAsync()`. To permanently delete session data, use `client.DeleteSessionAsync()`. ```csharp // Preferred: automatic cleanup via await using await using var session = await client.CreateSessionAsync(config); // session is automatically disposed when leaving scope // Alternative: explicit dispose var session2 = await client.CreateSessionAsync(config); await session2.DisposeAsync(); ``` --- ## Auto routing tiers Change the Auto routing preference without changing the selected model. The runtime does not apply the preference immediately: it records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives. Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure. Read the authoritative committed, pending, and activating preferences at any time through the session's `model.getCurrent` RPC method. ```csharp var result = await session.SetAutoTierAsync(AutoTier.Intelligence); if (result.Status == ModelSwitchAutoTierStatus.Pending) { // Accepted, but not yet in effect. } // Return to the provider's default Auto routing. await session.SetAutoTierAsync(null); ``` `SetModelAsync` accepts the same preference through `SetModelOptions.AutoTier`, which stages the tier atomically with selecting `auto`. Set `ResetAutoTier` instead to return to provider-default routing; the two options are mutually exclusive. See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for the full lifecycle rules. ## Event Types Sessions emit various events during processing. Each event type is a class that inherits from `SessionEvent`: - `UserMessageEvent` - User message added - `AssistantMessageEvent` - Assistant response - `ToolExecutionStartEvent` - Tool execution started - `ToolExecutionCompleteEvent` - Tool execution completed - `SessionStartEvent` - Session started - `SessionIdleEvent` - Session is idle - `SessionErrorEvent` - Session error occurred - And more... Use pattern matching to handle specific event types: ```csharp session.On(evt => { switch (evt) { case AssistantMessageEvent msg: Console.WriteLine(msg.Data.Content); break; case SessionErrorEvent err: Console.WriteLine($"Error: {err.Data.Message}"); break; } }); ``` ## Image Support The SDK supports image attachments via the `Attachments` parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment: ```csharp // File attachment — runtime reads from disk await session.SendAsync(new MessageOptions { Prompt = "What's in this image?", Attachments = new List { new AttachmentFile { Path = "/path/to/image.jpg", DisplayName = "image.jpg", } } }); // Blob attachment — provide base64 data directly await session.SendAsync(new MessageOptions { Prompt = "What's in this image?", Attachments = new List { new AttachmentBlob { Data = base64ImageData, MimeType = "image/png", } } }); ``` Supported image formats include JPG, PNG, GIF, and other common image types. The agent's `view` tool can also read images directly from the filesystem, so you can also ask questions like: ```csharp await session.SendAsync(new MessageOptions { Prompt = "What does the most recent jpg in this directory portray?" }); ``` ## Streaming Enable streaming to receive assistant response chunks as they're generated: ```csharp var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", Streaming = true }); // Use TaskCompletionSource to wait for completion var done = new TaskCompletionSource(); session.On(evt => { switch (evt) { case AssistantMessageDeltaEvent delta: // Streaming message chunk - print incrementally Console.Write(delta.Data.DeltaContent); break; case AssistantReasoningDeltaEvent reasoningDelta: // Streaming reasoning chunk (if model supports reasoning) Console.Write(reasoningDelta.Data.DeltaContent); break; case AssistantMessageEvent msg: // Final message - complete content Console.WriteLine("\n--- Final message ---"); Console.WriteLine(msg.Data.Content); break; case AssistantReasoningEvent reasoningEvt: // Final reasoning content (if model supports reasoning) Console.WriteLine("--- Reasoning ---"); Console.WriteLine(reasoningEvt.Data.Content); break; case SessionIdleEvent: // Session finished processing done.SetResult(); break; } }); await session.SendAsync(new MessageOptions { Prompt = "Tell me a short story" }); await done.Task; // Wait for streaming to complete ``` When `Streaming = true`: - `AssistantMessageDeltaEvent` events are sent with `DeltaContent` containing incremental text - `AssistantReasoningDeltaEvent` events are sent with `DeltaContent` for reasoning/chain-of-thought (model-dependent) - Accumulate `DeltaContent` values to build the full response progressively - The final `AssistantMessageEvent` and `AssistantReasoningEvent` events contain the complete content Note: `AssistantMessageEvent` and `AssistantReasoningEvent` (final events) are always sent regardless of streaming setting. ## Infinite Sessions By default, sessions use **infinite sessions** which automatically manage context window limits through background compaction and persist state to a workspace directory. ```csharp // Default: infinite sessions enabled with default thresholds var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" }); // Access the workspace path for checkpoints and files Console.WriteLine(session.WorkspacePath); // => ~/.copilot/session-state/{sessionId}/ // Custom thresholds var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", InfiniteSessions = new InfiniteSessionConfig { Enabled = true, BackgroundCompactionThreshold = 0.80, // Start compacting at 80% context usage BufferExhaustionThreshold = 0.95 // Block at 95% until compaction completes } }); // Disable infinite sessions var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", InfiniteSessions = new InfiniteSessionConfig { Enabled = false } }); ``` When enabled, sessions emit compaction events: - `SessionCompactionStartEvent` - Background compaction started - `SessionCompactionCompleteEvent` - Compaction finished (includes token counts) ## Memory Sessions can opt into persistent memory, allowing the agent to read and write memory across turns. Memory is configured per session and applies to both `CreateSessionAsync` and `ResumeSessionAsync`. For more background, see [About GitHub Copilot Memory](https://docs.github.com/en/copilot/concepts/agents/copilot-memory). ```csharp var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", Memory = new MemoryConfiguration { Enabled = true } }); ``` When `Memory` is left unset, no memory configuration is sent and the runtime default applies. In the default `CopilotClientMode.CopilotCli` the SDK leaves `Memory` unset so the runtime applies its own default, while `CopilotClientMode.Empty` defaults `Memory` to disabled unless you set it explicitly. ## Advanced Usage ### Manual Server Control ```csharp var client = new CopilotClient(new CopilotClientOptions { AutoStart = false }); // Start manually await client.StartAsync(); // Use client... // Stop manually await client.StopAsync(); ``` ### Tools You can let the CLI call back into your process when the model needs capabilities you own. Use `CopilotTool.DefineTool` for type-safe tool definitions: ```csharp using Microsoft.Extensions.AI; using System.ComponentModel; var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", Tools = [ CopilotTool.DefineTool( async ([Description("Issue identifier")] string id) => { var issue = await FetchIssueAsync(id); return issue; }, factoryOptions: new AIFunctionFactoryOptions { Name = "lookup_issue", Description = "Fetch issue details from our tracker", }), ] }); ``` When Copilot invokes `lookup_issue`, the client automatically runs your handler and responds to the CLI. Handlers can return any JSON-serializable value (automatically wrapped), or a `ToolResultAIContent` wrapping a `ToolResultObject` for full control over result metadata. Include a `ToolInvocation` parameter in your handler if you need the session ID, tool call ID, tool name, or raw arguments. #### Overriding Built-in Tools If you register a tool with the same name as a built-in CLI tool (e.g. `edit_file`, `read_file`), the runtime will return an error unless you explicitly opt in with `CopilotToolOptions.OverridesBuiltInTool`. This flag signals that you intend to replace the built-in tool with your custom implementation. ```csharp var editFile = CopilotTool.DefineTool( async ([Description("File path")] string path, [Description("New content")] string content) => { // your logic }, toolOptions: new CopilotToolOptions { OverridesBuiltInTool = true }, factoryOptions: new AIFunctionFactoryOptions { Name = "edit_file", Description = "Custom file editor with project-specific validation", }); var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", Tools = [editFile], }); ``` #### Skipping Permission Prompts Set `CopilotToolOptions.SkipPermission` to allow a tool to execute without triggering a permission prompt: ```csharp var safeLookup = CopilotTool.DefineTool( async ([Description("Lookup ID")] string id) => { // your logic }, toolOptions: new CopilotToolOptions { SkipPermission = true }, factoryOptions: new AIFunctionFactoryOptions { Name = "safe_lookup", Description = "A read-only lookup that needs no confirmation", }); ``` `DefineTool` delegates to `AIFunctionFactory.Create`, so advanced `AIFunctionFactoryOptions` remain available through the overload that accepts both `AIFunctionFactoryOptions` and `CopilotToolOptions`. If you want to use `AIFunctionFactory.Create` directly, you can set `skip_permission` in the tool's `AdditionalProperties`. #### Deferring Tools Set `CopilotToolOptions.Defer` to control whether a tool may be loaded lazily via tool search rather than always pre-loaded. Use `CopilotToolDefer.Auto` to allow the tool to be deferred and surfaced through tool search, or `CopilotToolDefer.Never` to force it to always be pre-loaded. Defaults to `CopilotToolDefer.Auto`. ```csharp var lookupIssue = CopilotTool.DefineTool( async ([Description("Issue ID")] string id) => { // your logic }, toolOptions: new CopilotToolOptions { Defer = CopilotToolDefer.Auto }, factoryOptions: new AIFunctionFactoryOptions { Name = "lookup_issue", Description = "Fetch issue details", }); ``` ## Commands Register slash commands so that users of the CLI's TUI can invoke custom actions via `/commandName`. Each command has a `Name`, optional `Description`, and a `Handler` called when the user executes it. ```csharp var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", OnPermissionRequest = PermissionHandler.ApproveAll, Commands = [ new CommandDefinition { Name = "deploy", Description = "Deploy the app to production", Handler = async (context) => { Console.WriteLine($"Deploying with args: {context.Args}"); // Do work here — any thrown error is reported back to the CLI }, }, ], }); ``` When the user types `/deploy staging` in the CLI, the SDK receives a `command.execute` event, routes it to your handler, and automatically responds to the CLI. If the handler throws, the error message is forwarded. Commands are sent to the CLI on both `CreateSessionAsync` and `ResumeSessionAsync`, so you can update the command set when resuming. ## UI Elicitation When the session has elicitation support — either from the CLI's TUI or from another client that registered an `OnElicitationRequest` handler (see [Elicitation Requests](#elicitation-requests)) — the SDK can request interactive form dialogs from the user. The `session.Ui` object provides convenience methods built on a single generic elicitation RPC. > **Capability check:** Elicitation is only available when at least one connected participant advertises support. Always check `session.Capabilities.Ui?.Elicitation` before calling UI methods — this property updates automatically as participants join and leave. ```csharp var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", OnPermissionRequest = PermissionHandler.ApproveAll, }); if (session.Capabilities.Ui?.Elicitation == true) { // Confirm dialog — returns boolean bool ok = await session.Ui.ConfirmAsync("Deploy to production?"); // Selection dialog — returns selected value or null string? env = await session.Ui.SelectAsync("Pick environment", ["production", "staging", "dev"]); // Text input — returns string or null string? name = await session.Ui.InputAsync("Project name:", new UiInputOptions { Title = "Name", MinLength = 1, MaxLength = 50, }); // Generic elicitation with full schema control ElicitationResult result = await session.Ui.ElicitAsync(new ElicitationParams { Message = "Configure deployment", RequestedSchema = new ElicitationSchema { Type = "object", Properties = new Dictionary { ["region"] = new Dictionary { ["type"] = "string", ["enum"] = new[] { "us-east", "eu-west" }, }, ["dryRun"] = new Dictionary { ["type"] = "boolean", ["default"] = true, }, }, Required = ["region"], }, }); // result.Action: Accept, Decline, or Cancel // result.Content: { "region": "us-east", "dryRun": true } (when accepted) } ``` All UI methods throw if elicitation is not supported by the host. ### System Message Customization Control the system prompt using `SystemMessage` in session config: ```csharp var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", SystemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Append, Content = @" - Always check for security vulnerabilities - Suggest performance improvements when applicable " } }); ``` #### Customize Mode Use `Mode = SystemMessageMode.Customize` to selectively override individual sections of the prompt while preserving the rest: ```csharp var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", SystemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Customize, Sections = new Dictionary { [SystemMessageSection.Tone] = new() { Action = SectionOverrideAction.Replace, Content = "Respond in a warm, professional tone. Be thorough in explanations." }, [SystemMessageSection.CodeChangeRules] = new() { Action = SectionOverrideAction.Remove }, [SystemMessageSection.Guidelines] = new() { Action = SectionOverrideAction.Append, Content = "\n* Always cite data sources" }, }, Content = "Focus on financial analysis and reporting." } }); ``` Available section IDs are defined as static properties on the `SystemMessageSection` struct: `Preamble`, `Identity`, `Tone`, `ToolEfficiency`, `EnvironmentContext`, `CodeChangeRules`, `Guidelines`, `Safety`, `ToolInstructions`, `CustomInstructions`, `RuntimeInstructions`, `LastInstructions`. `Identity` and `ToolInstructions` are section groups that target a collection of related sub-sections as a unit; use `Preamble` to target just the identity preamble. Each section override supports five actions: `Replace`, `Remove`, `Append`, `Prepend`, and `Preserve` (a no-op that opts an individually-addressable section out of a group-level `Remove`). Unknown section IDs are handled gracefully: content is appended to additional instructions, and `Remove` overrides are silently ignored. #### Replace Mode For full control (removes all guardrails), use `Mode = SystemMessageMode.Replace`: ```csharp var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", SystemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Replace, Content = "You are a helpful assistant." } }); ``` ### Multiple Sessions ```csharp var session1 = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" }); var session2 = await client.CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-4.5" }); // Both sessions are independent await session1.SendAsync(new MessageOptions { Prompt = "Hello from session 1" }); await session2.SendAsync(new MessageOptions { Prompt = "Hello from session 2" }); ``` ### File Attachments ```csharp await session.SendAsync(new MessageOptions { Prompt = "Analyze this file", Attachments = new List { new AttachmentFile { Path = "/path/to/file.cs", DisplayName = "My File", } } }); ``` ### Bring Your Own Key (BYOK) Use a custom API provider: ```csharp var session = await client.CreateSessionAsync(new SessionConfig { Provider = new ProviderConfig { Type = "openai", BaseUrl = "https://api.openai.com/v1", ApiKey = "your-api-key" } }); ``` ## Telemetry The SDK supports OpenTelemetry for distributed tracing. Provide a `Telemetry` config to enable trace export and automatic W3C Trace Context propagation. ```csharp var client = new CopilotClient(new CopilotClientOptions { Telemetry = new TelemetryConfig { OtlpEndpoint = "http://localhost:4318", }, }); ``` **TelemetryConfig properties:** - `OtlpEndpoint` - OTLP HTTP endpoint URL - `OtlpProtocol` - OTLP HTTP protocol for all signals (`"http/json"` or `"http/protobuf"`) - `FilePath` - File path for JSON-lines trace output - `ExporterType` - `"otlp-http"` or `"file"` - `SourceName` - Instrumentation scope name - `CaptureContent` - Whether to capture message content Trace context (`traceparent`/`tracestate`) is automatically propagated between the SDK and CLI on `CreateSessionAsync`, `ResumeSessionAsync`, and `SendAsync` calls, and inbound when the CLI invokes tool handlers. No extra dependencies — uses built-in `System.Diagnostics.Activity`. ## Permission Handling An `OnPermissionRequest` handler is optional when you create or resume a session. When provided, it is called before the agent executes each tool (file writes, shell commands, custom tools, etc.) and returns a decision. When omitted, permission requests are emitted as events and left pending for the consumer to resolve with the pending permission RPC. ### Approve All (simplest) Use the built-in `PermissionHandler.ApproveAll` helper to approve ordinary permission requests automatically: ```csharp using GitHub.Copilot; var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", OnPermissionRequest = PermissionHandler.ApproveAll, }); ``` When `EnableManagedSettings` is true for the session, `ApproveAll` throws on the first permission request. Use a custom handler for managed sessions; request-level `ManagedApprovalRequired` remains available for human-facing confirmation logic. ### Custom Permission Handler Provide your own permission handler (`Func>`) to inspect each request and apply custom logic. Check `ManagedApprovalRequired` before any automatic approval: ```csharp var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", OnPermissionRequest = async (request, invocation) => { if (request.ManagedApprovalRequired is true) { return PermissionDecision.NoResult(); } // Pattern-match on the discriminated PermissionRequest union to access // per-kind fields (FullCommandText, Path, ToolName, …). return request switch { PermissionRequestShell s => PermissionDecision.Reject($"Refusing shell: {s.FullCommandText}"), _ => PermissionDecision.ApproveOnce(), }; } }); ``` ### Permission Decisions The handler returns a `PermissionDecision`. Use the static factories for common cases (returned types are the strongly-typed variant classes — full IntelliSense via `PermissionDecision.`): | Factory | Meaning | | -------------------------------------- | -------------------------------------------------------------------------------------------- | | `PermissionDecision.ApproveOnce()` | Allow this single request | | `PermissionDecision.Reject(feedback)` | Deny the request, optionally forwarding feedback to the LLM | | `PermissionDecision.UserNotAvailable()`| Deny the request because no user is available to confirm it | | `PermissionDecision.NoResult()` | Decline to respond, allowing another connected client to answer instead | For richer decisions that need an `Approval` payload — `PermissionDecisionApproveForSession`, `PermissionDecisionApproveForLocation`, `PermissionDecisionApprovePermanently` — instantiate the variant class directly. ### Resuming Sessions You may pass `OnPermissionRequest` when resuming a session too: ```csharp var session = await client.ResumeSessionAsync("session-id", new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); ``` ### Per-Tool Skip Permission To let a specific custom tool bypass the permission prompt entirely, set `SkipPermission = true` in `CopilotToolOptions`. See [Skipping Permission Prompts](#skipping-permission-prompts) under Tools. ## User Input Requests Enable the legacy question-and-answer `ask_user` tool by providing an `OnUserInputRequest` handler: ```csharp var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", OnUserInputRequest = async (request, invocation) => { // request.Question - The question to ask // request.Choices - Optional list of choices for multiple choice // request.AllowFreeform - Whether freeform input is allowed (default: true) Console.WriteLine($"Agent asks: {request.Question}"); if (request.Choices?.Count > 0) { Console.WriteLine($"Choices: {string.Join(", ", request.Choices)}"); } // Return the user's response return new UserInputResponse { Answer = "User's answer here", WasFreeform = true // Whether the answer was freeform (not from choices) }; } }); ``` ## Session Hooks Hook into session lifecycle events by providing handlers in the `Hooks` configuration: ```csharp var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", Hooks = new SessionHooks { // Called before each tool execution OnPreToolUse = async (input, invocation) => { Console.WriteLine($"About to run tool: {input.ToolName}"); // Return permission decision and optionally modify args return new PreToolUseHookOutput { PermissionDecision = "allow", // "allow", "deny", or "ask" ModifiedArgs = input.ToolArgs, // Optionally modify tool arguments AdditionalContext = "Extra context for the model" }; }, // Called after each tool execution OnPostToolUse = async (input, invocation) => { Console.WriteLine($"Tool {input.ToolName} completed"); return new PostToolUseHookOutput { AdditionalContext = "Post-execution notes" }; }, // Called when a tool execution result was a failure. OnPostToolUse only // fires on success, so register OnPostToolUseFailure to observe failed // tool calls. The CLI extracts the failure message and passes it as // input.Error. OnPostToolUseFailure = async (input, invocation) => { Console.WriteLine($"Tool {input.ToolName} failed: {input.Error}"); return new PostToolUseFailureHookOutput { AdditionalContext = $"Retry guidance for {input.ToolName}" }; }, // Called when user submits a prompt OnUserPromptSubmitted = async (input, invocation) => { Console.WriteLine($"User prompt: {input.Prompt}"); return new UserPromptSubmittedHookOutput { ModifiedPrompt = input.Prompt // Optionally modify the prompt }; }, // Called when session starts OnSessionStart = async (input, invocation) => { Console.WriteLine($"Session started from: {input.Source}"); // "startup", "resume", "new" return new SessionStartHookOutput { AdditionalContext = "Session initialization context" }; }, // Called when session ends OnSessionEnd = async (input, invocation) => { Console.WriteLine($"Session ended: {input.Reason}"); return null; }, // Called when an error occurs OnErrorOccurred = async (input, invocation) => { Console.WriteLine($"Error in {input.ErrorContext}: {input.Error}"); return new ErrorOccurredHookOutput { ErrorHandling = "retry" // "retry", "skip", or "abort" }; } } }); ``` **Available hooks:** - `OnPreToolUse` - Intercept tool calls before execution. Can allow/deny or modify arguments. - `OnPostToolUse` - Process tool results after successful execution. Can modify results or add context. - `OnPostToolUseFailure` - Observe failed tool executions and inject extra context to guide the model's next step. - `OnUserPromptSubmitted` - Intercept user prompts. Can modify the prompt before processing. - `OnSessionStart` - Run logic when a session starts or resumes. - `OnSessionEnd` - Cleanup or logging when session ends. - `OnErrorOccurred` - Handle errors with retry/skip/abort strategies. ## Elicitation Requests Register an `OnElicitationRequest` handler to let your client act as an elicitation provider — presenting form-based UI dialogs on behalf of the agent. When provided, the server notifies your client whenever a tool or MCP server needs structured user input. ```csharp var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", OnPermissionRequest = PermissionHandler.ApproveAll, AskUserVariant = AskUserVariant.Elicitation, OnElicitationRequest = async (context) => { // context.SessionId - Session that triggered the request // context.Message - Description of what information is needed // context.RequestedSchema - JSON Schema describing the form fields // context.Mode - "form" (structured input) or "url" (browser redirect) // context.ElicitationSource - Origin of the request (e.g. MCP server name) Console.WriteLine($"Elicitation from {context.ElicitationSource}: {context.Message}"); // Present UI to the user and collect their response... return new ElicitationResult { Action = SessionUiElicitationResultAction.Accept, Content = new Dictionary { ["region"] = "us-east", ["dryRun"] = true, }, }; }, }); // The session now reports elicitation capability Console.WriteLine(session.Capabilities.Ui?.Elicitation); // True ``` When `OnElicitationRequest` is provided, the SDK sends `RequestElicitation = true` during session create/resume, which enables `session.Capabilities.Ui.Elicitation` on the session. In multi-client scenarios: - If no connected client was previously providing an elicitation capability, but a new client joins that can, all clients will receive a `capabilities.changed` event to notify them that elicitation is now possible. The SDK automatically updates `session.Capabilities` when these events arrive. - Similarly, if the last elicitation provider disconnects, all clients receive a `capabilities.changed` event indicating elicitation is no longer available. - The server fans out elicitation requests to **all** connected clients that registered a handler — the first response wins. ## Error Handling ```csharp try { var session = await client.CreateSessionAsync(); await session.SendAsync(new MessageOptions { Prompt = "Hello" }); } catch (IOException ex) { Console.Error.WriteLine($"Communication Error: {ex.Message}"); } catch (Exception ex) { Console.Error.WriteLine($"Error: {ex.Message}"); } ``` ## Development Development requires [.NET SDK 10+](https://dotnet.microsoft.com/download) and a supported [Node.js version](../nodejs/README.md#prerequisites). From the repository root: ```bash cd nodejs npm ci ``` ```bash cd test/harness npm ci ``` ```bash cd dotnet dotnet test ``` ## License MIT