Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 74 additions & 64 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ namespace GitHub.Copilot.SDK;
/// await using var client = new CopilotClient();
///
/// // Create a session
/// await using var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-4" });
/// await using var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll, Model = "gpt-4" });
///
/// // Handle events
/// using var subscription = session.On(evt =>
Expand Down Expand Up @@ -340,10 +340,9 @@ private async Task CleanupConnectionAsync(List<Exception>? errors)
/// <summary>
/// Creates a new Copilot session with the specified configuration.
/// </summary>
/// <param name="config">Configuration for the session. If null, default settings are used.</param>
/// <param name="config">Configuration for the session, including the required <see cref="SessionConfig.OnPermissionRequest"/> handler.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
/// <returns>A task that resolves to provide the <see cref="CopilotSession"/>.</returns>
/// <exception cref="InvalidOperationException">Thrown when the client is not connected and AutoStart is disabled, or when a session with the same ID already exists.</exception>
/// <remarks>
/// Sessions maintain conversation state, handle events, and manage tool execution.
/// If the client is not connected and <see cref="CopilotClientOptions.AutoStart"/> is enabled (default),
Expand All @@ -352,21 +351,29 @@ private async Task CleanupConnectionAsync(List<Exception>? errors)
/// <example>
/// <code>
/// // Basic session
/// var session = await client.CreateSessionAsync();
/// var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });
///
/// // Session with model and tools
/// var session = await client.CreateSessionAsync(new SessionConfig
/// var session = await client.CreateSessionAsync(new()
/// {
/// OnPermissionRequest = PermissionHandler.ApproveAll,
/// Model = "gpt-4",
/// Tools = [AIFunctionFactory.Create(MyToolMethod)]
/// });
/// </code>
/// </example>
public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = null, CancellationToken cancellationToken = default)
public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, CancellationToken cancellationToken = default)
{
if (config.OnPermissionRequest == null)
{
throw new ArgumentException(
"An OnPermissionRequest handler is required when creating a session. " +
"For example, to allow all permissions, use CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });");
}

var connection = await EnsureConnectedAsync(cancellationToken);

var hasHooks = config?.Hooks != null && (
var hasHooks = config.Hooks != null && (
config.Hooks.OnPreToolUse != null ||
config.Hooks.OnPostToolUse != null ||
config.Hooks.OnUserPromptSubmitted != null ||
Expand All @@ -375,42 +382,39 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = nul
config.Hooks.OnErrorOccurred != null);

var request = new CreateSessionRequest(
config?.Model,
config?.SessionId,
config?.ClientName,
config?.ReasoningEffort,
config?.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
config?.SystemMessage,
config?.AvailableTools,
config?.ExcludedTools,
config?.Provider,
config.Model,
config.SessionId,
config.ClientName,
config.ReasoningEffort,
config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
config.SystemMessage,
config.AvailableTools,
config.ExcludedTools,
config.Provider,
(bool?)true,
config?.OnUserInputRequest != null ? true : null,
config.OnUserInputRequest != null ? true : null,
hasHooks ? true : null,
config?.WorkingDirectory,
config?.Streaming == true ? true : null,
config?.McpServers,
config.WorkingDirectory,
config.Streaming == true ? true : null,
config.McpServers,
"direct",
config?.CustomAgents,
config?.ConfigDir,
config?.SkillDirectories,
config?.DisabledSkills,
config?.InfiniteSessions);
config.CustomAgents,
config.ConfigDir,
config.SkillDirectories,
config.DisabledSkills,
config.InfiniteSessions);

var response = await InvokeRpcAsync<CreateSessionResponse>(
connection.Rpc, "session.create", [request], cancellationToken);

var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath);
session.RegisterTools(config?.Tools ?? []);
if (config?.OnPermissionRequest != null)
{
session.RegisterPermissionHandler(config.OnPermissionRequest);
}
if (config?.OnUserInputRequest != null)
session.RegisterTools(config.Tools ?? []);
session.RegisterPermissionHandler(config.OnPermissionRequest);
if (config.OnUserInputRequest != null)
{
session.RegisterUserInputHandler(config.OnUserInputRequest);
}
if (config?.Hooks != null)
if (config.Hooks != null)
{
session.RegisterHooks(config.Hooks);
}
Expand All @@ -427,9 +431,10 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = nul
/// Resumes an existing Copilot session with the specified configuration.
/// </summary>
/// <param name="sessionId">The ID of the session to resume.</param>
/// <param name="config">Configuration for the resumed session. If null, default settings are used.</param>
/// <param name="config">Configuration for the resumed session, including the required <see cref="ResumeSessionConfig.OnPermissionRequest"/> handler.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
/// <returns>A task that resolves to provide the <see cref="CopilotSession"/>.</returns>
/// <exception cref="ArgumentException">Thrown when <see cref="ResumeSessionConfig.OnPermissionRequest"/> is not set.</exception>
/// <exception cref="InvalidOperationException">Thrown when the session does not exist or the client is not connected.</exception>
/// <remarks>
/// This allows you to continue a previous conversation, maintaining all conversation history.
Expand All @@ -438,20 +443,28 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = nul
/// <example>
/// <code>
/// // Resume a previous session
/// var session = await client.ResumeSessionAsync("session-123");
/// var session = await client.ResumeSessionAsync("session-123", new() { OnPermissionRequest = PermissionHandler.ApproveAll });
///
/// // Resume with new tools
/// var session = await client.ResumeSessionAsync("session-123", new ResumeSessionConfig
/// var session = await client.ResumeSessionAsync("session-123", new()
/// {
/// OnPermissionRequest = PermissionHandler.ApproveAll,
/// Tools = [AIFunctionFactory.Create(MyNewToolMethod)]
/// });
/// </code>
/// </example>
public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSessionConfig? config = null, CancellationToken cancellationToken = default)
public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSessionConfig config, CancellationToken cancellationToken = default)
{
if (config.OnPermissionRequest == null)
{
throw new ArgumentException(
"An OnPermissionRequest handler is required when resuming a session. " +
"For example, to allow all permissions, use new() { OnPermissionRequest = PermissionHandler.ApproveAll }.");
}

var connection = await EnsureConnectedAsync(cancellationToken);

var hasHooks = config?.Hooks != null && (
var hasHooks = config.Hooks != null && (
config.Hooks.OnPreToolUse != null ||
config.Hooks.OnPostToolUse != null ||
config.Hooks.OnUserPromptSubmitted != null ||
Expand All @@ -461,42 +474,39 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes

var request = new ResumeSessionRequest(
sessionId,
config?.ClientName,
config?.Model,
config?.ReasoningEffort,
config?.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
config?.SystemMessage,
config?.AvailableTools,
config?.ExcludedTools,
config?.Provider,
config.ClientName,
config.Model,
config.ReasoningEffort,
config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
config.SystemMessage,
config.AvailableTools,
config.ExcludedTools,
config.Provider,
(bool?)true,
config?.OnUserInputRequest != null ? true : null,
config.OnUserInputRequest != null ? true : null,
hasHooks ? true : null,
config?.WorkingDirectory,
config?.ConfigDir,
config?.DisableResume == true ? true : null,
config?.Streaming == true ? true : null,
config?.McpServers,
config.WorkingDirectory,
config.ConfigDir,
config.DisableResume == true ? true : null,
config.Streaming == true ? true : null,
config.McpServers,
"direct",
config?.CustomAgents,
config?.SkillDirectories,
config?.DisabledSkills,
config?.InfiniteSessions);
config.CustomAgents,
config.SkillDirectories,
config.DisabledSkills,
config.InfiniteSessions);

var response = await InvokeRpcAsync<ResumeSessionResponse>(
connection.Rpc, "session.resume", [request], cancellationToken);

var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath);
session.RegisterTools(config?.Tools ?? []);
if (config?.OnPermissionRequest != null)
{
session.RegisterPermissionHandler(config.OnPermissionRequest);
}
if (config?.OnUserInputRequest != null)
session.RegisterTools(config.Tools ?? []);
session.RegisterPermissionHandler(config.OnPermissionRequest);
if (config.OnUserInputRequest != null)
{
session.RegisterUserInputHandler(config.OnUserInputRequest);
}
if (config?.Hooks != null)
if (config.Hooks != null)
{
session.RegisterHooks(config.Hooks);
}
Expand All @@ -516,7 +526,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
/// <code>
/// if (client.State == ConnectionState.Connected)
/// {
/// var session = await client.CreateSessionAsync();
/// var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });
/// }
/// </code>
/// </example>
Expand Down Expand Up @@ -630,7 +640,7 @@ public async Task<List<ModelInfo>> ListModelsAsync(CancellationToken cancellatio
/// var lastId = await client.GetLastSessionIdAsync();
/// if (lastId != null)
/// {
/// var session = await client.ResumeSessionAsync(lastId);
/// var session = await client.ResumeSessionAsync(lastId, new() { OnPermissionRequest = PermissionHandler.ApproveAll });
/// }
/// </code>
/// </example>
Expand Down
6 changes: 3 additions & 3 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ namespace GitHub.Copilot.SDK;
/// </remarks>
/// <example>
/// <code>
/// await using var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-4" });
/// await using var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll, Model = "gpt-4" });
///
/// // Subscribe to events
/// using var subscription = session.On(evt =>
Expand Down Expand Up @@ -557,10 +557,10 @@ await InvokeRpcAsync<object>(
/// <example>
/// <code>
/// // Using 'await using' for automatic disposal
/// await using var session = await client.CreateSessionAsync();
/// await using var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });
///
/// // Or manually dispose
/// var session2 = await client.CreateSessionAsync();
/// var session2 = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });
/// // ... use the session ...
/// await session2.DisposeAsync();
/// </code>
Expand Down
6 changes: 3 additions & 3 deletions dotnet/test/AskUserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public async Task Should_Invoke_User_Input_Handler_When_Model_Uses_Ask_User_Tool
{
var userInputRequests = new List<UserInputRequest>();
CopilotSession? session = null;
session = await Client.CreateSessionAsync(new SessionConfig
session = await CreateSessionAsync(new SessionConfig
{
OnUserInputRequest = (request, invocation) =>
{
Expand Down Expand Up @@ -49,7 +49,7 @@ public async Task Should_Receive_Choices_In_User_Input_Request()
{
var userInputRequests = new List<UserInputRequest>();

var session = await Client.CreateSessionAsync(new SessionConfig
var session = await CreateSessionAsync(new SessionConfig
{
OnUserInputRequest = (request, invocation) =>
{
Expand Down Expand Up @@ -82,7 +82,7 @@ public async Task Should_Handle_Freeform_User_Input_Response()
var userInputRequests = new List<UserInputRequest>();
var freeformAnswer = "This is my custom freeform answer that was not in the choices";

var session = await Client.CreateSessionAsync(new SessionConfig
var session = await CreateSessionAsync(new SessionConfig
{
OnUserInputRequest = (request, invocation) =>
{
Expand Down
34 changes: 31 additions & 3 deletions dotnet/test/ClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public async Task Should_Force_Stop_Without_Cleanup()
{
using var client = new CopilotClient(new CopilotClientOptions());

await client.CreateSessionAsync();
await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll });
await client.ForceStopAsync();

Assert.Equal(ConnectionState.Disconnected, client.State);
Expand Down Expand Up @@ -220,7 +220,7 @@ public void Should_Throw_When_UseLoggedInUser_Used_With_CliUrl()
public async Task Should_Not_Throw_When_Disposing_Session_After_Stopping_Client()
{
await using var client = new CopilotClient(new CopilotClientOptions());
await using var session = await client.CreateSessionAsync();
await using var session = await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll });

await client.StopAsync();
}
Expand All @@ -247,12 +247,40 @@ public async Task Should_Report_Error_With_Stderr_When_CLI_Fails_To_Start()
// Verify subsequent calls also fail (don't hang)
var ex2 = await Assert.ThrowsAnyAsync<Exception>(async () =>
{
var session = await client.CreateSessionAsync();
var session = await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll });
await session.SendAsync(new MessageOptions { Prompt = "test" });
});
Assert.Contains("exited", ex2.Message, StringComparison.OrdinalIgnoreCase);

// Cleanup - ForceStop should handle the disconnected state gracefully
try { await client.ForceStopAsync(); } catch (Exception) { /* Expected */ }
}

[Fact]
public async Task Should_Throw_When_CreateSession_Called_Without_PermissionHandler()
{
using var client = new CopilotClient(new CopilotClientOptions());

var ex = await Assert.ThrowsAsync<ArgumentException>(async () =>
{
await client.CreateSessionAsync(new SessionConfig());
});

Assert.Contains("OnPermissionRequest", ex.Message);
Assert.Contains("is required", ex.Message);
}

[Fact]
public async Task Should_Throw_When_ResumeSession_Called_Without_PermissionHandler()
{
using var client = new CopilotClient(new CopilotClientOptions());

var ex = await Assert.ThrowsAsync<ArgumentException>(async () =>
{
await client.ResumeSessionAsync("some-session-id", new ResumeSessionConfig());
});

Assert.Contains("OnPermissionRequest", ex.Message);
Assert.Contains("is required", ex.Message);
}
}
4 changes: 2 additions & 2 deletions dotnet/test/CompactionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class CompactionTests(E2ETestFixture fixture, ITestOutputHelper output) :
public async Task Should_Trigger_Compaction_With_Low_Threshold_And_Emit_Events()
{
// Create session with very low compaction thresholds to trigger compaction quickly
var session = await Client.CreateSessionAsync(new SessionConfig
var session = await CreateSessionAsync(new SessionConfig
{
InfiniteSessions = new InfiniteSessionConfig
{
Expand Down Expand Up @@ -84,7 +84,7 @@ await session.SendAndWaitAsync(new MessageOptions
[Fact]
public async Task Should_Not_Emit_Compaction_Events_When_Infinite_Sessions_Disabled()
{
var session = await Client.CreateSessionAsync(new SessionConfig
var session = await CreateSessionAsync(new SessionConfig
{
InfiniteSessions = new InfiniteSessionConfig
{
Expand Down
Loading
Loading