* Update .NET Copilot SDK cookbook: fix On<T> breaking change, add in-process runtime, tool search, and message provenance recipes - Fix breaking change: Session.On is now generic (On<T> where T : SessionEvent). Updated all recipes that used the old non-generic session.On(evt => ...) pattern: error-handling, pr-visualization, managing-local-files, accessibility-report, ralph-loop (both .md and .cs where applicable). - Add new 'In-Process Runtime' recipe (in-process-runtime.md + recipe/in-process-runtime.cs) demonstrating RuntimeConnection.ForInProcess(), live-tested. - Document tool search / deferred loading (SessionConfig.ToolSearch / ToolSearchConfig) in pr-visualization.md, live-tested. - Document typed message provenance (MessageOptions.Source / MessageSource) in error-handling.md, live-tested. - Fix dotnet/README.md recipe index: it was missing Ralph Loop and Accessibility Report entries; also added the new In-Process Runtime entry. - Add In-Process Runtime entry to the top-level cookbook/copilot-sdk/README.md .NET section. - Update dotnet/recipe/README.md runnable-examples table with the new recipe. All existing .NET cookbook recipes were rebuilt and rechecked against the current GitHub Copilot SDK (Copilot CLI 1.0.85); no other breaking changes were found. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update cookbook model names to current supported models - Replace stale/nonexistent model IDs (gpt-5, claude-sonnet-4.5, claude-opus-4.6, gpt-5.1-codex-mini) with current values from the supported AI models list - Prefer Model = "auto" wherever a specific model isn't load-bearing to the recipe's teaching point - ralph-loop: pin gpt-5.3-codex (current codex-family GA model), since a dedicated coding model is a deliberate, documented choice for the autonomous loop - multiple-sessions: reshape the model narrative so it demonstrates a real explicit-override use case (A/B testing via claude-sonnet-5) instead of two sessions sharing the same hardcoded model Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use SendAndWaitAsync in in-process runtime recipe Replace the event-based TaskCompletionSource pattern with SendAndWaitAsync so the example waits for the final assistant response, propagates session errors, and uses the SDK timeout behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
6.1 KiB
Error Handling Patterns
Handle errors gracefully in your Copilot SDK applications.
Runnable example: recipe/error-handling.cs
dotnet run recipe/error-handling.cs
Example scenario
You need to handle various error conditions like connection failures, timeouts, and invalid responses.
Basic try-catch
using GitHub.Copilot;
var client = new CopilotClient();
try
{
await client.StartAsync();
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "auto",
OnPermissionRequest = PermissionHandler.ApproveAll
});
var done = new TaskCompletionSource<string>();
session.On<SessionEvent>(evt =>
{
if (evt is AssistantMessageEvent msg)
{
done.SetResult(msg.Data.Content);
}
});
await session.SendAsync(new MessageOptions { Prompt = "Hello!" });
var response = await done.Task;
Console.WriteLine(response);
await session.DisposeAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
finally
{
await client.StopAsync();
}
Session.Onis now generic:On<T>(Action<T> handler) where T : SessionEvent. The type argument can no longer be inferred from a lambda that only pattern-matches inside the body, so calls likesession.On(evt => { if (evt is AssistantMessageEvent msg) ... })fail to compile withCS0411. Either specifyOn<SessionEvent>and pattern-match inside (as above), or subscribe directly to the concrete event type you care about, e.g.session.On<AssistantMessageEvent>(evt => Console.WriteLine(evt.Data?.Content)).
Handling specific error types
try
{
await client.StartAsync();
}
catch (FileNotFoundException)
{
Console.WriteLine("Copilot CLI not found. Please install it first.");
}
catch (HttpRequestException ex) when (ex.Message.Contains("connection"))
{
Console.WriteLine("Could not connect to Copilot CLI server.");
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex.Message}");
}
Timeout handling
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "auto",
OnPermissionRequest = PermissionHandler.ApproveAll
});
try
{
var done = new TaskCompletionSource<string>();
session.On<SessionEvent>(evt =>
{
if (evt is AssistantMessageEvent msg)
{
done.SetResult(msg.Data.Content);
}
});
await session.SendAsync(new MessageOptions { Prompt = "Complex question..." });
// Wait with timeout (30 seconds)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var response = await done.Task.WaitAsync(cts.Token);
Console.WriteLine(response);
}
catch (OperationCanceledException)
{
Console.WriteLine("Request timed out");
}
Aborting a request
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "auto",
OnPermissionRequest = PermissionHandler.ApproveAll
});
// Start a request
await session.SendAsync(new MessageOptions { Prompt = "Write a very long story..." });
// Abort it after some condition
await Task.Delay(5000);
await session.AbortAsync();
Console.WriteLine("Request aborted");
Graceful shutdown
Console.CancelKeyPress += async (sender, e) =>
{
e.Cancel = true;
Console.WriteLine("Shutting down...");
try
{
await client.StopAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Cleanup error: {ex.Message}");
}
Environment.Exit(0);
};
In 1.0,
StopAsync()throws if it encounters errors during cleanup rather than returning a list of cleanup errors, so wrap it in a try/catch to log failures instead of letting them crash shutdown. UseForceStopAsync()if a graceful stop takes too long.
Using await using for automatic disposal
await using var client = new CopilotClient();
await client.StartAsync();
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "auto",
OnPermissionRequest = PermissionHandler.ApproveAll
});
// ... do work ...
// client.StopAsync() is automatically called when exiting scope
Tagging message provenance
Since v1.0.14, MessageOptions has a Source property so you can tag why a message was sent —
useful when errors or unexpected turns show up in transcripts and you need to tell human input
apart from messages an automated system (a webhook handler, a scheduled job, another agent) sent
on the user's behalf.
using GitHub.Copilot;
// Ordinary human input (also the default when Source is omitted).
await session.SendAsync(new MessageOptions
{
Prompt = "What changed in the last release?",
Source = MessageSource.User
});
// A message injected by your own system rather than typed by a person,
// e.g. a scheduled health check or automated retry.
await session.SendAsync(new MessageOptions
{
Prompt = "Re-run the failed step and report the result.",
Source = MessageSource.System
});
// A message sent by another agent or automation acting on the user's behalf,
// tagged with a caller-supplied identifier.
await session.SendAsync(new MessageOptions
{
Prompt = "Summarize the open incidents.",
Source = MessageSource.Agent("incident-bot")
});
MessageSource is a closed record with User, System, and a MessageSource.Agent(string id)
factory (serialized as "agent-<id>"). Tagging a source only records provenance on the message —
it does not change how the message is delivered, replace the session's system prompt, or grant
extra permissions. When Source is omitted, the field is left unset and the runtime treats the
message as ordinary user input.
Best practices
Permission handling is opt-in. If a session may need tool, file, or system access, set OnPermissionRequest explicitly when creating it.
- Always clean up: Use try-finally or
await usingto ensureStopAsync()is called - Handle connection errors: The CLI might not be installed or running
- Set appropriate timeouts: Use
CancellationTokenfor long-running requests - Log errors: Capture error details for debugging