diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs
index 664b35d9..d024071d 100644
--- a/dotnet/src/Types.cs
+++ b/dotnet/src/Types.cs
@@ -24,6 +24,34 @@ public enum ConnectionState
public class CopilotClientOptions
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CopilotClientOptions() { }
+
+ ///
+ /// Initializes a new instance of the class
+ /// by copying the properties of the specified instance.
+ ///
+ protected CopilotClientOptions(CopilotClientOptions? other)
+ {
+ if (other is null) return;
+
+ AutoRestart = other.AutoRestart;
+ AutoStart = other.AutoStart;
+ CliArgs = (string[]?)other.CliArgs?.Clone();
+ CliPath = other.CliPath;
+ CliUrl = other.CliUrl;
+ Cwd = other.Cwd;
+ Environment = other.Environment;
+ GithubToken = other.GithubToken;
+ Logger = other.Logger;
+ LogLevel = other.LogLevel;
+ Port = other.Port;
+ UseLoggedInUser = other.UseLoggedInUser;
+ UseStdio = other.UseStdio;
+ }
+
///
/// Path to the Copilot CLI executable. If not specified, uses the bundled CLI from the SDK.
///
@@ -53,6 +81,17 @@ public class CopilotClientOptions
/// Default: true (but defaults to false when GithubToken is provided).
///
public bool? UseLoggedInUser { get; set; }
+
+ ///
+ /// Creates a shallow clone of this instance.
+ ///
+ ///
+ /// Mutable collection properties are copied into new collection instances so that modifications
+ /// to those collections on the clone do not affect the original.
+ /// Other reference-type properties (for example delegates and the logger) are not
+ /// deep-cloned; the original and the clone will share those objects.
+ ///
+ public virtual CopilotClientOptions Clone() => new(this);
}
public class ToolBinaryResult
@@ -692,6 +731,42 @@ public class InfiniteSessionConfig
public class SessionConfig
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SessionConfig() { }
+
+ ///
+ /// Initializes a new instance of the class
+ /// by copying the properties of the specified instance.
+ ///
+ protected SessionConfig(SessionConfig? other)
+ {
+ if (other is null) return;
+
+ AvailableTools = other.AvailableTools is not null ? [.. other.AvailableTools] : null;
+ ConfigDir = other.ConfigDir;
+ CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null;
+ DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
+ ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null;
+ Hooks = other.Hooks;
+ InfiniteSessions = other.InfiniteSessions;
+ McpServers = other.McpServers is not null
+ ? new Dictionary(other.McpServers, other.McpServers.Comparer)
+ : null;
+ Model = other.Model;
+ OnPermissionRequest = other.OnPermissionRequest;
+ OnUserInputRequest = other.OnUserInputRequest;
+ Provider = other.Provider;
+ ReasoningEffort = other.ReasoningEffort;
+ SessionId = other.SessionId;
+ SkillDirectories = other.SkillDirectories is not null ? [.. other.SkillDirectories] : null;
+ Streaming = other.Streaming;
+ SystemMessage = other.SystemMessage;
+ Tools = other.Tools is not null ? [.. other.Tools] : null;
+ WorkingDirectory = other.WorkingDirectory;
+ }
+
public string? SessionId { get; set; }
public string? Model { get; set; }
@@ -769,10 +844,58 @@ public class SessionConfig
/// When enabled (default), sessions automatically manage context limits and persist state.
///
public InfiniteSessionConfig? InfiniteSessions { get; set; }
+
+ ///
+ /// Creates a shallow clone of this instance.
+ ///
+ ///
+ /// Mutable collection properties are copied into new collection instances so that modifications
+ /// to those collections on the clone do not affect the original.
+ /// Other reference-type properties (for example provider configuration, system messages,
+ /// hooks, infinite session configuration, and delegates) are not deep-cloned; the original
+ /// and the clone will share those nested objects, and changes to them may affect both.
+ ///
+ public virtual SessionConfig Clone() => new(this);
}
public class ResumeSessionConfig
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ResumeSessionConfig() { }
+
+ ///
+ /// Initializes a new instance of the class
+ /// by copying the properties of the specified instance.
+ ///
+ protected ResumeSessionConfig(ResumeSessionConfig? other)
+ {
+ if (other is null) return;
+
+ AvailableTools = other.AvailableTools is not null ? [.. other.AvailableTools] : null;
+ ConfigDir = other.ConfigDir;
+ CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null;
+ DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
+ DisableResume = other.DisableResume;
+ ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null;
+ Hooks = other.Hooks;
+ InfiniteSessions = other.InfiniteSessions;
+ McpServers = other.McpServers is not null
+ ? new Dictionary(other.McpServers, other.McpServers.Comparer)
+ : null;
+ Model = other.Model;
+ OnPermissionRequest = other.OnPermissionRequest;
+ OnUserInputRequest = other.OnUserInputRequest;
+ Provider = other.Provider;
+ ReasoningEffort = other.ReasoningEffort;
+ SkillDirectories = other.SkillDirectories is not null ? [.. other.SkillDirectories] : null;
+ Streaming = other.Streaming;
+ SystemMessage = other.SystemMessage;
+ Tools = other.Tools is not null ? [.. other.Tools] : null;
+ WorkingDirectory = other.WorkingDirectory;
+ }
+
///
/// Model to use for this session. Can change the model when resuming.
///
@@ -870,13 +993,54 @@ public class ResumeSessionConfig
/// Infinite session configuration for persistent workspaces and automatic compaction.
///
public InfiniteSessionConfig? InfiniteSessions { get; set; }
+
+ ///
+ /// Creates a shallow clone of this instance.
+ ///
+ ///
+ /// Mutable collection properties are copied into new collection instances so that modifications
+ /// to those collections on the clone do not affect the original.
+ /// Other reference-type properties (for example provider configuration, system messages,
+ /// hooks, infinite session configuration, and delegates) are not deep-cloned; the original
+ /// and the clone will share those nested objects, and changes to them may affect both.
+ ///
+ public virtual ResumeSessionConfig Clone() => new(this);
}
public class MessageOptions
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MessageOptions() { }
+
+ ///
+ /// Initializes a new instance of the class
+ /// by copying the properties of the specified instance.
+ ///
+ protected MessageOptions(MessageOptions? other)
+ {
+ if (other is null) return;
+
+ Attachments = other.Attachments is not null ? [.. other.Attachments] : null;
+ Mode = other.Mode;
+ Prompt = other.Prompt;
+ }
+
public string Prompt { get; set; } = string.Empty;
public List? Attachments { get; set; }
public string? Mode { get; set; }
+
+ ///
+ /// Creates a shallow clone of this instance.
+ ///
+ ///
+ /// Mutable collection properties are copied into new collection instances so that modifications
+ /// to those collections on the clone do not affect the original.
+ /// Other reference-type properties (for example attachment items) are not deep-cloned;
+ /// the original and the clone will share those nested objects.
+ ///
+ public virtual MessageOptions Clone() => new(this);
}
public delegate void SessionEventHandler(SessionEvent sessionEvent);
diff --git a/dotnet/test/CloneTests.cs b/dotnet/test/CloneTests.cs
new file mode 100644
index 00000000..10ad0205
--- /dev/null
+++ b/dotnet/test/CloneTests.cs
@@ -0,0 +1,243 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+using Microsoft.Extensions.AI;
+using Xunit;
+
+namespace GitHub.Copilot.SDK.Test;
+
+public class CloneTests
+{
+ [Fact]
+ public void CopilotClientOptions_Clone_CopiesAllProperties()
+ {
+ var original = new CopilotClientOptions
+ {
+ CliPath = "/usr/bin/copilot",
+ CliArgs = ["--verbose", "--debug"],
+ Cwd = "/home/user",
+ Port = 8080,
+ UseStdio = false,
+ CliUrl = "http://localhost:8080",
+ LogLevel = "debug",
+ AutoStart = false,
+ AutoRestart = false,
+ Environment = new Dictionary { ["KEY"] = "value" },
+ GithubToken = "ghp_test",
+ UseLoggedInUser = false,
+ };
+
+ var clone = original.Clone();
+
+ Assert.Equal(original.CliPath, clone.CliPath);
+ Assert.Equal(original.CliArgs, clone.CliArgs);
+ Assert.Equal(original.Cwd, clone.Cwd);
+ Assert.Equal(original.Port, clone.Port);
+ Assert.Equal(original.UseStdio, clone.UseStdio);
+ Assert.Equal(original.CliUrl, clone.CliUrl);
+ Assert.Equal(original.LogLevel, clone.LogLevel);
+ Assert.Equal(original.AutoStart, clone.AutoStart);
+ Assert.Equal(original.AutoRestart, clone.AutoRestart);
+ Assert.Equal(original.Environment, clone.Environment);
+ Assert.Equal(original.GithubToken, clone.GithubToken);
+ Assert.Equal(original.UseLoggedInUser, clone.UseLoggedInUser);
+ }
+
+ [Fact]
+ public void CopilotClientOptions_Clone_CollectionsAreIndependent()
+ {
+ var original = new CopilotClientOptions
+ {
+ CliArgs = ["--verbose"],
+ };
+
+ var clone = original.Clone();
+
+ // Mutate clone array
+ clone.CliArgs![0] = "--quiet";
+
+ // Original is unaffected
+ Assert.Equal("--verbose", original.CliArgs![0]);
+ }
+
+ [Fact]
+ public void CopilotClientOptions_Clone_EnvironmentIsShared()
+ {
+ var env = new Dictionary { ["key"] = "value" };
+ var original = new CopilotClientOptions { Environment = env };
+
+ var clone = original.Clone();
+
+ Assert.Same(original.Environment, clone.Environment);
+ }
+
+ [Fact]
+ public void SessionConfig_Clone_CopiesAllProperties()
+ {
+ var original = new SessionConfig
+ {
+ SessionId = "test-session",
+ Model = "gpt-4",
+ ReasoningEffort = "high",
+ ConfigDir = "/config",
+ AvailableTools = ["tool1", "tool2"],
+ ExcludedTools = ["tool3"],
+ WorkingDirectory = "/workspace",
+ Streaming = true,
+ McpServers = new Dictionary { ["server1"] = new object() },
+ CustomAgents = [new CustomAgentConfig { Name = "agent1" }],
+ SkillDirectories = ["/skills"],
+ DisabledSkills = ["skill1"],
+ };
+
+ var clone = original.Clone();
+
+ Assert.Equal(original.SessionId, clone.SessionId);
+ Assert.Equal(original.Model, clone.Model);
+ Assert.Equal(original.ReasoningEffort, clone.ReasoningEffort);
+ Assert.Equal(original.ConfigDir, clone.ConfigDir);
+ Assert.Equal(original.AvailableTools, clone.AvailableTools);
+ Assert.Equal(original.ExcludedTools, clone.ExcludedTools);
+ Assert.Equal(original.WorkingDirectory, clone.WorkingDirectory);
+ Assert.Equal(original.Streaming, clone.Streaming);
+ Assert.Equal(original.McpServers.Count, clone.McpServers!.Count);
+ Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count);
+ Assert.Equal(original.SkillDirectories, clone.SkillDirectories);
+ Assert.Equal(original.DisabledSkills, clone.DisabledSkills);
+ }
+
+ [Fact]
+ public void SessionConfig_Clone_CollectionsAreIndependent()
+ {
+ var original = new SessionConfig
+ {
+ AvailableTools = ["tool1"],
+ ExcludedTools = ["tool2"],
+ McpServers = new Dictionary { ["s1"] = new object() },
+ CustomAgents = [new CustomAgentConfig { Name = "a1" }],
+ SkillDirectories = ["/skills"],
+ DisabledSkills = ["skill1"],
+ };
+
+ var clone = original.Clone();
+
+ // Mutate clone collections
+ clone.AvailableTools!.Add("tool99");
+ clone.ExcludedTools!.Add("tool99");
+ clone.McpServers!["s2"] = new object();
+ clone.CustomAgents!.Add(new CustomAgentConfig { Name = "a2" });
+ clone.SkillDirectories!.Add("/more");
+ clone.DisabledSkills!.Add("skill99");
+
+ // Original is unaffected
+ Assert.Single(original.AvailableTools!);
+ Assert.Single(original.ExcludedTools!);
+ Assert.Single(original.McpServers!);
+ Assert.Single(original.CustomAgents!);
+ Assert.Single(original.SkillDirectories!);
+ Assert.Single(original.DisabledSkills!);
+ }
+
+ [Fact]
+ public void SessionConfig_Clone_PreservesMcpServersComparer()
+ {
+ var servers = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["server"] = new object() };
+ var original = new SessionConfig { McpServers = servers };
+
+ var clone = original.Clone();
+
+ Assert.True(clone.McpServers!.ContainsKey("SERVER")); // case-insensitive lookup works
+ }
+
+ [Fact]
+ public void ResumeSessionConfig_Clone_CollectionsAreIndependent()
+ {
+ var original = new ResumeSessionConfig
+ {
+ AvailableTools = ["tool1"],
+ ExcludedTools = ["tool2"],
+ McpServers = new Dictionary { ["s1"] = new object() },
+ CustomAgents = [new CustomAgentConfig { Name = "a1" }],
+ SkillDirectories = ["/skills"],
+ DisabledSkills = ["skill1"],
+ };
+
+ var clone = original.Clone();
+
+ // Mutate clone collections
+ clone.AvailableTools!.Add("tool99");
+ clone.ExcludedTools!.Add("tool99");
+ clone.McpServers!["s2"] = new object();
+ clone.CustomAgents!.Add(new CustomAgentConfig { Name = "a2" });
+ clone.SkillDirectories!.Add("/more");
+ clone.DisabledSkills!.Add("skill99");
+
+ // Original is unaffected
+ Assert.Single(original.AvailableTools!);
+ Assert.Single(original.ExcludedTools!);
+ Assert.Single(original.McpServers!);
+ Assert.Single(original.CustomAgents!);
+ Assert.Single(original.SkillDirectories!);
+ Assert.Single(original.DisabledSkills!);
+ }
+
+ [Fact]
+ public void ResumeSessionConfig_Clone_PreservesMcpServersComparer()
+ {
+ var servers = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["server"] = new object() };
+ var original = new ResumeSessionConfig { McpServers = servers };
+
+ var clone = original.Clone();
+
+ Assert.True(clone.McpServers!.ContainsKey("SERVER"));
+ }
+
+ [Fact]
+ public void MessageOptions_Clone_CopiesAllProperties()
+ {
+ var original = new MessageOptions
+ {
+ Prompt = "Hello",
+ Attachments = [new UserMessageDataAttachmentsItemFile { Path = "/test.txt", DisplayName = "test.txt" }],
+ Mode = "chat",
+ };
+
+ var clone = original.Clone();
+
+ Assert.Equal(original.Prompt, clone.Prompt);
+ Assert.Equal(original.Mode, clone.Mode);
+ Assert.Single(clone.Attachments!);
+ }
+
+ [Fact]
+ public void MessageOptions_Clone_AttachmentsAreIndependent()
+ {
+ var original = new MessageOptions
+ {
+ Attachments = [new UserMessageDataAttachmentsItemFile { Path = "/test.txt", DisplayName = "test.txt" }],
+ };
+
+ var clone = original.Clone();
+
+ clone.Attachments!.Add(new UserMessageDataAttachmentsItemFile { Path = "/other.txt", DisplayName = "other.txt" });
+
+ Assert.Single(original.Attachments!);
+ }
+
+ [Fact]
+ public void Clone_WithNullCollections_ReturnsNullCollections()
+ {
+ var original = new SessionConfig();
+
+ var clone = original.Clone();
+
+ Assert.Null(clone.AvailableTools);
+ Assert.Null(clone.ExcludedTools);
+ Assert.Null(clone.McpServers);
+ Assert.Null(clone.CustomAgents);
+ Assert.Null(clone.SkillDirectories);
+ Assert.Null(clone.DisabledSkills);
+ Assert.Null(clone.Tools);
+ }
+}