From 5c5312e85dad0072589502b75f15fd634515e98d Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 28 May 2026 13:14:07 -0400 Subject: [PATCH 1/3] Config parity across SDKs: add largeOutput, pluginDirectories, spell out directory Bring the six language SDKs (.NET, Node, Python, Go, Java, Rust) into closer alignment on session-configuration surface area: - Expose `largeOutput` / `LargeToolOutputConfig` on session create and resume in every SDK so callers can configure the CLI's large tool-output handling (enabled, maxSizeBytes, outputDirectory). Wire field stays `largeOutput.outputDir` for compatibility with the runtime. - Expose `pluginDirectories` on session create and resume in every SDK, documented as an explicit opt-in that loads plugin agents and rules even when `enableConfigDiscovery` is false. Defaults to null/none, which is the safe choice for multi-tenant hosts. - Spell out `Dir` -> `Directory` in public APIs for consistency with the rest of Types.cs (e.g. SkillDirectories, InstructionDirectories): `configDir` -> `configDirectory`, `outputDir` -> `outputDirectory`. The wire JSON names (`configDir`, `outputDir`) are preserved via language-specific serializer annotations, so this is a source-only rename. - Node SDK also re-exports `ReasoningSummary` and adds `reasoningSummary` support on resume, matching what the other SDKs already had. Notes for reviewers: - `args`/`toolArgs`/`modifiedArgs` are intentionally left abbreviated, matching the wire field names and the shell/process/CLI argument idiom used elsewhere (RuntimeConnection, MCP stdio, hook inputs). - New unit tests cover serialization (wire field stays `outputDir` / `configDir`), clone semantics for the new fields, and round-trips through CreateSessionRequest / ResumeSessionConfig in every SDK. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 20 +- dotnet/src/Types.cs | 65 ++++- dotnet/test/E2E/ClientE2ETests.cs | 2 +- dotnet/test/E2E/ClientOptionsE2ETests.cs | 2 +- dotnet/test/E2E/SessionE2ETests.cs | 2 +- dotnet/test/Unit/CloneTests.cs | 41 ++- dotnet/test/Unit/SerializationTests.cs | 71 +++++ go/client.go | 10 +- go/client_test.go | 104 ++++++++ go/internal/e2e/session_e2e_test.go | 2 +- go/session.go | 4 + go/types.go | 55 +++- .../com/github/copilot/CopilotSession.java | 34 ++- .../github/copilot/SessionRequestBuilder.java | 10 +- .../copilot/rpc/CreateSessionRequest.java | 51 +++- .../copilot/rpc/LargeToolOutputConfig.java | 91 +++++++ .../copilot/rpc/ResumeSessionConfig.java | 85 +++++- .../copilot/rpc/ResumeSessionRequest.java | 51 +++- .../com/github/copilot/rpc/SessionConfig.java | 85 +++++- .../com/github/copilot/ConfigCloneTest.java | 17 +- .../github/copilot/CopilotSessionTest.java | 2 +- .../copilot/SessionRequestBuilderTest.java | 43 +++ nodejs/src/client.ts | 27 +- nodejs/src/index.ts | 1 + nodejs/src/session.ts | 2 + nodejs/src/types.ts | 63 ++++- nodejs/test/client.test.ts | 84 +++++- nodejs/test/e2e/session.e2e.test.ts | 2 +- python/copilot/__init__.py | 4 + python/copilot/client.py | 60 ++++- python/copilot/session.py | 28 ++ python/e2e/test_session_e2e.py | 2 +- python/test_client.py | 86 +++++- rust/src/session.rs | 2 +- rust/src/types.rs | 248 ++++++++++++++++-- rust/src/wire.rs | 16 +- rust/tests/e2e/session.rs | 2 +- rust/tests/session_test.rs | 21 +- 38 files changed, 1398 insertions(+), 97 deletions(-) create mode 100644 java/src/main/java/com/github/copilot/rpc/LargeToolOutputConfig.java diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 57d55f227..f315c41db 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -852,6 +852,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance localSessionId, config.ClientName, config.ReasoningEffort, + config.ReasoningSummary, config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), wireSystemMessage, toolFilter.AvailableTools, @@ -871,7 +872,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.CustomAgents, config.DefaultAgent, config.Agent, - config.ConfigDir, + config.ConfigDirectory, config.EnableConfigDiscovery, config.SkillDirectories, config.DisabledSkills, @@ -885,6 +886,8 @@ public async Task CreateSessionAsync(SessionConfig config, Cance RemoteSession: config.RemoteSession, Cloud: config.Cloud, InstructionDirectories: config.InstructionDirectories, + PluginDirectories: config.PluginDirectories, + LargeOutput: config.LargeOutput, Canvases: config.Canvases, RequestCanvasRenderer: config.RequestCanvasRenderer, RequestExtensions: config.RequestExtensions, @@ -1032,6 +1035,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.ClientName, config.Model, config.ReasoningEffort, + config.ReasoningSummary, config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), wireSystemMessage, toolFilter.AvailableTools, @@ -1044,7 +1048,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.OnAutoModeSwitchRequest != null ? true : null, hasHooks ? true : null, config.WorkingDirectory, - config.ConfigDir, + config.ConfigDirectory, config.EnableConfigDiscovery, config.SuppressResumeEvent is true ? true : null, config.Streaming is true ? true : null, @@ -1066,6 +1070,8 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes RemoteSession: config.RemoteSession, ContinuePendingWork: config.ContinuePendingWork, InstructionDirectories: config.InstructionDirectories, + PluginDirectories: config.PluginDirectories, + LargeOutput: config.LargeOutput, Canvases: config.Canvases, RequestCanvasRenderer: config.RequestCanvasRenderer, RequestExtensions: config.RequestExtensions, @@ -2145,6 +2151,7 @@ internal record CreateSessionRequest( string? SessionId, string? ClientName, string? ReasoningEffort, + ReasoningSummary? ReasoningSummary, IList? Tools, SystemMessageConfig? SystemMessage, IList? AvailableTools, @@ -2164,7 +2171,7 @@ internal record CreateSessionRequest( IList? CustomAgents, DefaultAgentConfig? DefaultAgent, string? Agent, - string? ConfigDir, + [property: JsonPropertyName("configDir")] string? ConfigDirectory, bool? EnableConfigDiscovery, IList? SkillDirectories, IList? DisabledSkills, @@ -2178,6 +2185,8 @@ internal record CreateSessionRequest( RemoteSessionMode? RemoteSession = null, CloudSessionOptions? Cloud = null, IList? InstructionDirectories = null, + IList? PluginDirectories = null, + LargeToolOutputConfig? LargeOutput = null, #pragma warning disable GHCP001 IList? Canvases = null, bool? RequestCanvasRenderer = null, @@ -2216,6 +2225,7 @@ internal record ResumeSessionRequest( string? ClientName, string? Model, string? ReasoningEffort, + ReasoningSummary? ReasoningSummary, IList? Tools, SystemMessageConfig? SystemMessage, IList? AvailableTools, @@ -2228,7 +2238,7 @@ internal record ResumeSessionRequest( bool? RequestAutoModeSwitch, bool? Hooks, string? WorkingDirectory, - string? ConfigDir, + [property: JsonPropertyName("configDir")] string? ConfigDirectory, bool? EnableConfigDiscovery, bool? SuppressResumeEvent, bool? Streaming, @@ -2250,6 +2260,8 @@ internal record ResumeSessionRequest( RemoteSessionMode? RemoteSession = null, bool? ContinuePendingWork = null, IList? InstructionDirectories = null, + IList? PluginDirectories = null, + LargeToolOutputConfig? LargeOutput = null, #pragma warning disable GHCP001 IList? Canvases = null, bool? RequestCanvasRenderer = null, diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 1f8c475f5..73f388af0 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2293,6 +2293,37 @@ public sealed class InfiniteSessionConfig public double? BufferExhaustionThreshold { get; set; } } +/// +/// Configuration for handling large tool outputs. +/// +/// +/// When a tool produces output exceeding the configured size, the output is +/// written to a temp file and a reference is returned to the model instead of +/// returning to it the full payload. +/// +public sealed class LargeToolOutputConfig +{ + /// + /// Whether large output handling is enabled. + /// + /// The default value is . + [JsonPropertyName("enabled")] + public bool? Enabled { get; set; } + + /// + /// Maximum size in bytes before output is written to a temp file. + /// + [JsonPropertyName("maxSizeBytes")] + public long? MaxSizeBytes { get; set; } + + /// + /// Directory to write temp files to. + /// + /// The default value is the OS temp directory. + [JsonPropertyName("outputDir")] + public string? OutputDirectory { get; set; } +} + /// /// GitHub repository metadata to associate with a cloud session. /// @@ -2340,7 +2371,7 @@ protected SessionConfigBase(SessionConfigBase? other) AvailableTools = other.AvailableTools is not null ? [.. other.AvailableTools] : null; ClientName = other.ClientName; Commands = other.Commands is not null ? [.. other.Commands] : null; - ConfigDir = other.ConfigDir; + ConfigDirectory = other.ConfigDirectory; CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null; DefaultAgent = other.DefaultAgent; Agent = other.Agent; @@ -2349,6 +2380,7 @@ protected SessionConfigBase(SessionConfigBase? other) ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null; Hooks = other.Hooks; InfiniteSessions = other.InfiniteSessions; + LargeOutput = other.LargeOutput; McpServers = other.McpServers is not null ? (other.McpServers is Dictionary dict ? new Dictionary(dict, dict.Comparer) @@ -2369,6 +2401,7 @@ protected SessionConfigBase(SessionConfigBase? other) CoauthorEnabled = other.CoauthorEnabled; ManageScheduleEnabled = other.ManageScheduleEnabled; ReasoningEffort = other.ReasoningEffort; + ReasoningSummary = other.ReasoningSummary; CreateSessionFsProvider = other.CreateSessionFsProvider; GitHubToken = other.GitHubToken; RemoteSession = other.RemoteSession; @@ -2380,6 +2413,7 @@ protected SessionConfigBase(SessionConfigBase? other) CanvasHandler = other.CanvasHandler; #pragma warning restore GHCP001 SkillDirectories = other.SkillDirectories is not null ? [.. other.SkillDirectories] : null; + PluginDirectories = other.PluginDirectories is not null ? [.. other.PluginDirectories] : null; InstructionDirectories = other.InstructionDirectories is not null ? [.. other.InstructionDirectories] : null; Streaming = other.Streaming; IncludeSubAgentStreamingEvents = other.IncludeSubAgentStreamingEvents; @@ -2401,6 +2435,14 @@ protected SessionConfigBase(SessionConfigBase? other) /// public string? ReasoningEffort { get; set; } + /// + /// Reasoning summary mode for models that support configurable reasoning summaries. + /// + /// + /// Use to suppress summary output regardless of whether reasoning is enabled. + /// + public ReasoningSummary? ReasoningSummary { get; set; } + /// Per-property overrides for model capabilities, deep-merged over runtime defaults. public ModelCapabilitiesOverride? ModelCapabilities { get; set; } @@ -2408,7 +2450,7 @@ protected SessionConfigBase(SessionConfigBase? other) /// Override the default configuration directory location. /// When specified, the session will use this directory for storing config and state. /// - public string? ConfigDir { get; set; } + public string? ConfigDirectory { get; set; } /// /// When , automatically discovers MCP server configurations @@ -2557,6 +2599,17 @@ protected SessionConfigBase(SessionConfigBase? other) /// Directories to load skills from. public IList? SkillDirectories { get; set; } + /// + /// Local filesystem paths to Open Plugins-format directories + /// (https://open-plugins.com/) to load for this session. + /// + /// + /// Relative paths resolve against (or the + /// runtime cwd if unset). Treated as an explicit opt-in: plugin agents + /// and rules load even when is false. + /// + public IList? PluginDirectories { get; set; } + /// Additional directories to search for custom instruction files. public IList? InstructionDirectories { get; set; } @@ -2569,6 +2622,14 @@ protected SessionConfigBase(SessionConfigBase? other) /// public InfiniteSessionConfig? InfiniteSessions { get; set; } + /// + /// Configuration for handling large tool outputs. When a tool produces + /// output exceeding the configured size, the output is written to a temp + /// file and a reference is returned to the model instead of the full + /// payload. + /// + public LargeToolOutputConfig? LargeOutput { get; set; } + /// /// Optional event handler registered on the session before the session.create / session.resume /// RPC is issued, ensuring early events are delivered. diff --git a/dotnet/test/E2E/ClientE2ETests.cs b/dotnet/test/E2E/ClientE2ETests.cs index 836a2c155..9972e3b33 100644 --- a/dotnet/test/E2E/ClientE2ETests.cs +++ b/dotnet/test/E2E/ClientE2ETests.cs @@ -1,4 +1,4 @@ -/*--------------------------------------------------------------------------------------------- +/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ diff --git a/dotnet/test/E2E/ClientOptionsE2ETests.cs b/dotnet/test/E2E/ClientOptionsE2ETests.cs index 95ddeee75..0257c57db 100644 --- a/dotnet/test/E2E/ClientOptionsE2ETests.cs +++ b/dotnet/test/E2E/ClientOptionsE2ETests.cs @@ -1,4 +1,4 @@ -/*--------------------------------------------------------------------------------------------- +/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ diff --git a/dotnet/test/E2E/SessionE2ETests.cs b/dotnet/test/E2E/SessionE2ETests.cs index fa56415b5..7825f479c 100644 --- a/dotnet/test/E2E/SessionE2ETests.cs +++ b/dotnet/test/E2E/SessionE2ETests.cs @@ -550,7 +550,7 @@ public async Task SendAndWait_Throws_OperationCanceledException_When_Token_Cance public async Task Should_Create_Session_With_Custom_Config_Dir() { var customConfigDir = Path.Join(Ctx.HomeDir, "custom-config"); - var session = await CreateSessionAsync(new SessionConfig { ConfigDir = customConfigDir }); + var session = await CreateSessionAsync(new SessionConfig { ConfigDirectory = customConfigDir }); Assert.Matches(@"^[a-f0-9-]+$", session.SessionId); diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index 184c13b18..59efc244d 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -1,4 +1,4 @@ -/*--------------------------------------------------------------------------------------------- +/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ @@ -68,7 +68,8 @@ public void SessionConfig_Clone_CopiesAllProperties() ClientName = "my-app", Model = "gpt-4", ReasoningEffort = "high", - ConfigDir = "/config", + ReasoningSummary = ReasoningSummary.Detailed, + ConfigDirectory = "/config", AvailableTools = ["tool1", "tool2"], ExcludedTools = ["tool3"], WorkingDirectory = "/workspace", @@ -91,6 +92,8 @@ public void SessionConfig_Clone_CopiesAllProperties() SkillDirectories = ["/skills"], InstructionDirectories = ["/instructions"], DisabledSkills = ["skill1"], + PluginDirectories = ["/plugins"], + LargeOutput = new LargeToolOutputConfig { Enabled = true, MaxSizeBytes = 2048, OutputDirectory = "/tmp/out" }, OnExitPlanModeRequest = static (_, _) => Task.FromResult(new ExitPlanModeResult()), OnAutoModeSwitchRequest = static (_, _) => Task.FromResult(AutoModeSwitchResponse.No), }; @@ -101,7 +104,8 @@ public void SessionConfig_Clone_CopiesAllProperties() Assert.Equal(original.ClientName, clone.ClientName); Assert.Equal(original.Model, clone.Model); Assert.Equal(original.ReasoningEffort, clone.ReasoningEffort); - Assert.Equal(original.ConfigDir, clone.ConfigDir); + Assert.Equal(original.ReasoningSummary, clone.ReasoningSummary); + Assert.Equal(original.ConfigDirectory, clone.ConfigDirectory); Assert.Equal(original.AvailableTools, clone.AvailableTools); Assert.Equal(original.ExcludedTools, clone.ExcludedTools); Assert.Equal(original.WorkingDirectory, clone.WorkingDirectory); @@ -117,6 +121,8 @@ public void SessionConfig_Clone_CopiesAllProperties() Assert.Equal(original.SkillDirectories, clone.SkillDirectories); Assert.Equal(original.InstructionDirectories, clone.InstructionDirectories); Assert.Equal(original.DisabledSkills, clone.DisabledSkills); + Assert.Equal(original.PluginDirectories, clone.PluginDirectories); + Assert.Same(original.LargeOutput, clone.LargeOutput); Assert.Same(original.OnExitPlanModeRequest, clone.OnExitPlanModeRequest); Assert.Same(original.OnAutoModeSwitchRequest, clone.OnAutoModeSwitchRequest); } @@ -356,6 +362,35 @@ public void ResumeSessionConfig_Clone_CopiesContinuePendingWork() Assert.True(clone.ContinuePendingWork); } + [Fact] + public void ResumeSessionConfig_Clone_CopiesReasoningSummary() + { + var original = new ResumeSessionConfig + { + ReasoningSummary = ReasoningSummary.None, + }; + + var clone = original.Clone(); + + Assert.Equal(original.ReasoningSummary, clone.ReasoningSummary); + } + + [Fact] + public void ResumeSessionConfig_Clone_CopiesPluginDirectoriesAndLargeOutput() + { + var largeOutput = new LargeToolOutputConfig { Enabled = false, MaxSizeBytes = 4096, OutputDirectory = "/tmp/resume" }; + var original = new ResumeSessionConfig + { + PluginDirectories = ["/resume/plugins"], + LargeOutput = largeOutput, + }; + + var clone = original.Clone(); + + Assert.Equal(original.PluginDirectories, clone.PluginDirectories); + Assert.Same(original.LargeOutput, clone.LargeOutput); + } + [Fact] public void ResumeSessionConfig_Clone_PreservesContinuePendingWorkDefault() { diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index c1e406104..321ff61fe 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -171,6 +171,77 @@ public void ResumeSessionRequest_CanSerializeInstructionDirectories_WithSdkOptio Assert.Equal("C:\\resume-instructions", root.GetProperty("instructionDirectories")[0].GetString()); } + [Fact] + public void SessionRequests_CanSerializeReasoningSummary_WithSdkOptions() + { + var options = GetSerializerOptions(); + var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); + var createRequest = CreateInternalRequest( + createRequestType, + ("SessionId", "session-id"), + ("ReasoningSummary", ReasoningSummary.Detailed)); + + var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); + using var createDocument = JsonDocument.Parse(createJson); + Assert.Equal("detailed", createDocument.RootElement.GetProperty("reasoningSummary").GetString()); + + var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); + var resumeRequest = CreateInternalRequest( + resumeRequestType, + ("SessionId", "session-id"), + ("ReasoningSummary", ReasoningSummary.None)); + + var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); + using var resumeDocument = JsonDocument.Parse(resumeJson); + Assert.Equal("none", resumeDocument.RootElement.GetProperty("reasoningSummary").GetString()); + } + + [Fact] + public void SessionRequests_CanSerializePluginDirectoriesAndLargeOutput_WithSdkOptions() + { + var options = GetSerializerOptions(); + var pluginDirs = new List { "/tmp/plugins/a", "/tmp/plugins/b" }; + var largeOutput = new LargeToolOutputConfig + { + Enabled = true, + MaxSizeBytes = 1024, + OutputDirectory = "/tmp/large-output", + }; + + var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); + var createRequest = CreateInternalRequest( + createRequestType, + ("SessionId", "session-id"), + ("PluginDirectories", pluginDirs), + ("LargeOutput", largeOutput)); + + var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); + using var createDocument = JsonDocument.Parse(createJson); + var createRoot = createDocument.RootElement; + Assert.Equal("/tmp/plugins/a", createRoot.GetProperty("pluginDirectories")[0].GetString()); + Assert.Equal("/tmp/plugins/b", createRoot.GetProperty("pluginDirectories")[1].GetString()); + var createLargeOutput = createRoot.GetProperty("largeOutput"); + Assert.True(createLargeOutput.GetProperty("enabled").GetBoolean()); + Assert.Equal(1024, createLargeOutput.GetProperty("maxSizeBytes").GetInt64()); + Assert.Equal("/tmp/large-output", createLargeOutput.GetProperty("outputDir").GetString()); + + var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); + var resumeRequest = CreateInternalRequest( + resumeRequestType, + ("SessionId", "session-id"), + ("PluginDirectories", pluginDirs), + ("LargeOutput", largeOutput)); + + var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); + using var resumeDocument = JsonDocument.Parse(resumeJson); + var resumeRoot = resumeDocument.RootElement; + Assert.Equal("/tmp/plugins/a", resumeRoot.GetProperty("pluginDirectories")[0].GetString()); + var resumeLargeOutput = resumeRoot.GetProperty("largeOutput"); + Assert.True(resumeLargeOutput.GetProperty("enabled").GetBoolean()); + Assert.Equal(1024, resumeLargeOutput.GetProperty("maxSizeBytes").GetInt64()); + Assert.Equal("/tmp/large-output", resumeLargeOutput.GetProperty("outputDir").GetString()); + } + [Fact] public void CreateSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptions() { diff --git a/go/client.go b/go/client.go index bb1b905c4..60a92d1cb 100644 --- a/go/client.go +++ b/go/client.go @@ -604,7 +604,8 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.Model = config.Model req.ClientName = config.ClientName req.ReasoningEffort = config.ReasoningEffort - req.ConfigDir = config.ConfigDir + req.ReasoningSummary = config.ReasoningSummary + req.ConfigDir = config.ConfigDirectory if config.EnableConfigDiscovery { req.EnableConfigDiscovery = Bool(true) } @@ -633,9 +634,11 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.DefaultAgent = config.DefaultAgent req.Agent = config.Agent req.SkillDirectories = config.SkillDirectories + req.PluginDirectories = config.PluginDirectories req.InstructionDirectories = config.InstructionDirectories req.DisabledSkills = config.DisabledSkills req.InfiniteSessions = config.InfiniteSessions + req.LargeOutput = config.LargeOutput req.GitHubToken = config.GitHubToken req.RemoteSession = config.RemoteSession req.Cloud = config.Cloud @@ -900,6 +903,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.ClientName = config.ClientName req.Model = config.Model req.ReasoningEffort = config.ReasoningEffort + req.ReasoningSummary = config.ReasoningSummary systemMessage := c.systemMessageForMode(config.SystemMessage) wireSystemMessage, transformCallbacks := extractTransformCallbacks(systemMessage) req.SystemMessage = wireSystemMessage @@ -940,7 +944,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.Hooks = Bool(true) } req.WorkingDirectory = config.WorkingDirectory - req.ConfigDir = config.ConfigDir + req.ConfigDir = config.ConfigDirectory if config.EnableConfigDiscovery { req.EnableConfigDiscovery = Bool(true) } @@ -956,9 +960,11 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.DefaultAgent = config.DefaultAgent req.Agent = config.Agent req.SkillDirectories = config.SkillDirectories + req.PluginDirectories = config.PluginDirectories req.InstructionDirectories = config.InstructionDirectories req.DisabledSkills = config.DisabledSkills req.InfiniteSessions = config.InfiniteSessions + req.LargeOutput = config.LargeOutput req.GitHubToken = config.GitHubToken req.RemoteSession = config.RemoteSession req.Canvases = config.Canvases diff --git a/go/client_test.go b/go/client_test.go index 39358a72a..6e619bbf5 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -402,6 +402,110 @@ func TestResumeSessionRequest_ClientName(t *testing.T) { }) } +func TestSessionRequests_ReasoningSummary(t *testing.T) { + t.Run("create includes reasoningSummary in JSON when set", func(t *testing.T) { + req := createSessionRequest{ReasoningSummary: ReasoningSummaryConcise} + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + if m["reasoningSummary"] != "concise" { + t.Errorf("Expected reasoningSummary to be 'concise', got %v", m["reasoningSummary"]) + } + }) + + t.Run("resume includes reasoningSummary in JSON when set", func(t *testing.T) { + req := resumeSessionRequest{SessionID: "s1", ReasoningSummary: ReasoningSummaryNone} + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + if m["reasoningSummary"] != "none" { + t.Errorf("Expected reasoningSummary to be 'none', got %v", m["reasoningSummary"]) + } + }) +} + +func TestSessionRequests_PluginDirectoriesAndLargeOutput(t *testing.T) { + pluginDirs := []string{"/tmp/plugins/a", "/tmp/plugins/b"} + enabled := true + maxBytes := 1024 + largeOutput := &LargeToolOutputConfig{ + Enabled: &enabled, + MaxSizeBytes: &maxBytes, + OutputDirectory: "/tmp/large-output", + } + + expectedLargeOutput := map[string]any{ + "enabled": true, + "maxSizeBytes": float64(1024), + "outputDir": "/tmp/large-output", + } + expectedPluginDirs := []any{"/tmp/plugins/a", "/tmp/plugins/b"} + + t.Run("create includes pluginDirectories and largeOutput in JSON when set", func(t *testing.T) { + req := createSessionRequest{PluginDirectories: pluginDirs, LargeOutput: largeOutput} + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + if !reflect.DeepEqual(m["pluginDirectories"], expectedPluginDirs) { + t.Errorf("Expected pluginDirectories %v, got %v", expectedPluginDirs, m["pluginDirectories"]) + } + if !reflect.DeepEqual(m["largeOutput"], expectedLargeOutput) { + t.Errorf("Expected largeOutput %v, got %v", expectedLargeOutput, m["largeOutput"]) + } + }) + + t.Run("resume includes pluginDirectories and largeOutput in JSON when set", func(t *testing.T) { + req := resumeSessionRequest{SessionID: "s1", PluginDirectories: pluginDirs, LargeOutput: largeOutput} + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + if !reflect.DeepEqual(m["pluginDirectories"], expectedPluginDirs) { + t.Errorf("Expected pluginDirectories %v, got %v", expectedPluginDirs, m["pluginDirectories"]) + } + if !reflect.DeepEqual(m["largeOutput"], expectedLargeOutput) { + t.Errorf("Expected largeOutput %v, got %v", expectedLargeOutput, m["largeOutput"]) + } + }) + + t.Run("create omits pluginDirectories and largeOutput when nil", func(t *testing.T) { + req := createSessionRequest{} + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + if _, ok := m["pluginDirectories"]; ok { + t.Errorf("Expected pluginDirectories to be omitted") + } + if _, ok := m["largeOutput"]; ok { + t.Errorf("Expected largeOutput to be omitted") + } + }) +} + func TestCreateSessionRequest_Agent(t *testing.T) { t.Run("includes agent in JSON when set", func(t *testing.T) { req := createSessionRequest{Agent: "test-agent"} diff --git a/go/internal/e2e/session_e2e_test.go b/go/internal/e2e/session_e2e_test.go index c2b9f57c9..4343355a4 100644 --- a/go/internal/e2e/session_e2e_test.go +++ b/go/internal/e2e/session_e2e_test.go @@ -760,7 +760,7 @@ func TestSessionE2E(t *testing.T) { customConfigDir := ctx.HomeDir + "/custom-config" session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - ConfigDir: customConfigDir, + ConfigDirectory: customConfigDir, }) if err != nil { t.Fatalf("Failed to create session with custom config dir: %v", err) diff --git a/go/session.go b/go/session.go index cd35a741c..e242cc1cf 100644 --- a/go/session.go +++ b/go/session.go @@ -1456,6 +1456,9 @@ func (s *Session) Abort(ctx context.Context) error { type SetModelOptions struct { // ReasoningEffort sets the reasoning effort level for the new model (e.g., "low", "medium", "high", "xhigh"). ReasoningEffort *string + // ReasoningSummary sets the reasoning summary mode for the new model. + // Use ReasoningSummaryNone to suppress summary output regardless of whether reasoning is enabled. + ReasoningSummary *ReasoningSummary // ModelCapabilities overrides individual model capabilities resolved by the runtime. // Only non-nil fields are applied over the runtime-resolved capabilities. ModelCapabilities *rpc.ModelCapabilitiesOverride @@ -1476,6 +1479,7 @@ func (s *Session) SetModel(ctx context.Context, model string, opts *SetModelOpti params := &rpc.ModelSwitchToRequest{ModelID: model} if opts != nil { params.ReasoningEffort = opts.ReasoningEffort + params.ReasoningSummary = opts.ReasoningSummary params.ModelCapabilities = opts.ModelCapabilities } _, err := s.RPC.Model.SwitchTo(ctx, params) diff --git a/go/types.go b/go/types.go index 3a8f2e45e..b48463208 100644 --- a/go/types.go +++ b/go/types.go @@ -844,6 +844,21 @@ type InfiniteSessionConfig struct { BufferExhaustionThreshold *float64 `json:"bufferExhaustionThreshold,omitempty"` } +// LargeToolOutputConfig configures handling of large tool outputs. When a tool +// produces output exceeding the configured size, the output is written to a +// temp file and a reference is returned to the model instead of the full +// payload. +type LargeToolOutputConfig struct { + // Enabled controls whether large output handling is enabled. Default: true. + Enabled *bool `json:"enabled,omitempty"` + // MaxSizeBytes is the maximum size in bytes before output is written to a + // temp file. Default: 50KB. + MaxSizeBytes *int `json:"maxSizeBytes,omitempty"` + // OutputDirectory is the directory to write temp files to. Defaults to the OS + // temp directory. + OutputDirectory string `json:"outputDir,omitempty"` +} + // SessionFsCapabilities declares optional provider capabilities. type SessionFsCapabilities struct { // Sqlite indicates whether the provider supports SQLite query/exists operations. @@ -876,9 +891,12 @@ type SessionConfig struct { // Valid values: "low", "medium", "high", "xhigh" // Only applies to models where capabilities.supports.reasoningEffort is true. ReasoningEffort string - // ConfigDir overrides the default configuration directory location. + // ReasoningSummary mode for models that support configurable reasoning summaries. + // Use ReasoningSummaryNone to suppress summary output regardless of whether reasoning is enabled. + ReasoningSummary ReasoningSummary + // ConfigDirectory overrides the default configuration directory location. // When specified, the session will use this directory for storing config and state. - ConfigDir string + ConfigDirectory string // EnableConfigDiscovery, when true, automatically discovers MCP server configurations // (e.g. .mcp.json, .vscode/mcp.json) and skill directories from the working directory // and merges them with any explicitly provided MCPServers and SkillDirectories, with @@ -957,6 +975,12 @@ type SessionConfig struct { Agent string // SkillDirectories is a list of directories to load skills from SkillDirectories []string + // PluginDirectories is a list of local filesystem paths to Open Plugins-format + // directories (https://open-plugins.com/) to load for this session. + // Relative paths resolve against WorkingDirectory (or the runtime cwd if unset). + // Treated as an explicit opt-in: plugin agents and rules load even when + // EnableConfigDiscovery is false. + PluginDirectories []string // InstructionDirectories is a list of additional directories to search for custom instruction files InstructionDirectories []string // DisabledSkills is a list of skill names to disable @@ -964,6 +988,10 @@ type SessionConfig struct { // InfiniteSessions configures infinite sessions for persistent workspaces and automatic compaction. // When enabled (default), sessions automatically manage context limits and persist state. InfiniteSessions *InfiniteSessionConfig + // LargeOutput configures handling of large tool outputs. When a tool produces + // output exceeding the configured size, the output is written to a temp file + // and a reference is returned to the model instead of the full payload. + LargeOutput *LargeToolOutputConfig // OnEvent is an optional event handler that is registered on the session before // the session.create RPC is issued. This guarantees that early events emitted // by the CLI during session creation (e.g. session.start) are delivered to the @@ -1190,6 +1218,9 @@ type ResumeSessionConfig struct { // ReasoningEffort level for models that support it. // Valid values: "low", "medium", "high", "xhigh" ReasoningEffort string + // ReasoningSummary mode for models that support configurable reasoning summaries. + // Use ReasoningSummaryNone to suppress summary output regardless of whether reasoning is enabled. + ReasoningSummary ReasoningSummary // OnPermissionRequest is an optional handler for permission requests from the server. // When nil, permission requests are surfaced as events and left pending for the // consumer to resolve via pending permission RPCs. @@ -1201,8 +1232,8 @@ type ResumeSessionConfig struct { // WorkingDirectory is the working directory for the session. // Tool operations will be relative to this directory. WorkingDirectory string - // ConfigDir overrides the default configuration directory location. - ConfigDir string + // ConfigDirectory overrides the default configuration directory location. + ConfigDirectory string // EnableConfigDiscovery, when true, automatically discovers MCP server configurations // (e.g. .mcp.json, .vscode/mcp.json) and skill directories from the working directory // and merges them with any explicitly provided MCPServers and SkillDirectories, with @@ -1233,12 +1264,22 @@ type ResumeSessionConfig struct { Agent string // SkillDirectories is a list of directories to load skills from SkillDirectories []string + // PluginDirectories is a list of local filesystem paths to Open Plugins-format + // directories (https://open-plugins.com/) to load for this session. + // Relative paths resolve against WorkingDirectory (or the runtime cwd if unset). + // Treated as an explicit opt-in: plugin agents and rules load even when + // EnableConfigDiscovery is false. + PluginDirectories []string // InstructionDirectories is a list of additional directories to search for custom instruction files InstructionDirectories []string // DisabledSkills is a list of skill names to disable DisabledSkills []string // InfiniteSessions configures infinite sessions for persistent workspaces and automatic compaction. InfiniteSessions *InfiniteSessionConfig + // LargeOutput configures handling of large tool outputs. When a tool produces + // output exceeding the configured size, the output is written to a temp file + // and a reference is returned to the model instead of the full payload. + LargeOutput *LargeToolOutputConfig // GitHubToken is an optional per-session GitHub token used for authentication. // When provided, the session authenticates as the token's owner instead of // using the global client-level auth. @@ -1501,6 +1542,7 @@ type createSessionRequest struct { SessionID string `json:"sessionId,omitempty"` ClientName string `json:"clientName,omitempty"` ReasoningEffort string `json:"reasoningEffort,omitempty"` + ReasoningSummary ReasoningSummary `json:"reasoningSummary,omitempty"` Tools []Tool `json:"tools,omitempty"` SystemMessage *SystemMessageConfig `json:"systemMessage,omitempty"` AvailableTools []string `json:"availableTools"` @@ -1529,9 +1571,11 @@ type createSessionRequest struct { ConfigDir string `json:"configDir,omitempty"` EnableConfigDiscovery *bool `json:"enableConfigDiscovery,omitempty"` SkillDirectories []string `json:"skillDirectories,omitempty"` + PluginDirectories []string `json:"pluginDirectories,omitempty"` InstructionDirectories []string `json:"instructionDirectories,omitempty"` DisabledSkills []string `json:"disabledSkills,omitempty"` InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` + LargeOutput *LargeToolOutputConfig `json:"largeOutput,omitempty"` Commands []wireCommand `json:"commands,omitempty"` RequestElicitation *bool `json:"requestElicitation,omitempty"` GitHubToken string `json:"gitHubToken,omitempty"` @@ -1564,6 +1608,7 @@ type resumeSessionRequest struct { ClientName string `json:"clientName,omitempty"` Model string `json:"model,omitempty"` ReasoningEffort string `json:"reasoningEffort,omitempty"` + ReasoningSummary ReasoningSummary `json:"reasoningSummary,omitempty"` Tools []Tool `json:"tools,omitempty"` SystemMessage *SystemMessageConfig `json:"systemMessage,omitempty"` AvailableTools []string `json:"availableTools"` @@ -1594,9 +1639,11 @@ type resumeSessionRequest struct { DefaultAgent *DefaultAgentConfig `json:"defaultAgent,omitempty"` Agent string `json:"agent,omitempty"` SkillDirectories []string `json:"skillDirectories,omitempty"` + PluginDirectories []string `json:"pluginDirectories,omitempty"` InstructionDirectories []string `json:"instructionDirectories,omitempty"` DisabledSkills []string `json:"disabledSkills,omitempty"` InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` + LargeOutput *LargeToolOutputConfig `json:"largeOutput,omitempty"` Commands []wireCommand `json:"commands,omitempty"` RequestElicitation *bool `json:"requestElicitation,omitempty"` GitHubToken string `json:"gitHubToken,omitempty"` diff --git a/java/src/main/java/com/github/copilot/CopilotSession.java b/java/src/main/java/com/github/copilot/CopilotSession.java index eed070bd5..acd58ae32 100644 --- a/java/src/main/java/com/github/copilot/CopilotSession.java +++ b/java/src/main/java/com/github/copilot/CopilotSession.java @@ -1729,6 +1729,35 @@ public CompletableFuture setModel(String model, String reasoningEffort) { */ public CompletableFuture setModel(String model, String reasoningEffort, com.github.copilot.rpc.ModelCapabilitiesOverride modelCapabilities) { + return setModel(model, reasoningEffort, null, modelCapabilities); + } + + /** + * Changes the model for this session with optional reasoning effort, reasoning + * summary mode, and capability overrides. + *

+ * The new model takes effect for the next message. Conversation history is + * preserved. + * + * @param model + * the model ID to switch to (e.g., {@code "gpt-4.1"}) + * @param reasoningEffort + * reasoning effort level; {@code null} to use default + * @param reasoningSummary + * reasoning summary mode ({@code "none"}, {@code "concise"}, or + * {@code "detailed"}); {@code null} to use default. Use + * {@code "none"} to suppress summary output regardless of whether + * reasoning is enabled. + * @param modelCapabilities + * per-property overrides for model capabilities; {@code null} to use + * runtime defaults + * @return a future that completes when the model switch is acknowledged + * @throws IllegalStateException + * if this session has been terminated + * @since 1.3.0 + */ + public CompletableFuture setModel(String model, String reasoningEffort, String reasoningSummary, + com.github.copilot.rpc.ModelCapabilitiesOverride modelCapabilities) { ensureNotTerminated(); ModelCapabilitiesOverride generatedCapabilities = null; if (modelCapabilities != null) { @@ -1745,9 +1774,12 @@ public CompletableFuture setModel(String model, String reasoningEffort, } generatedCapabilities = new ModelCapabilitiesOverride(supports, limits); } + var generatedReasoningSummary = reasoningSummary == null ? null + : com.github.copilot.generated.rpc.ReasoningSummary.fromValue(reasoningSummary); return getRpc().model .switchTo( - new SessionModelSwitchToParams(sessionId, model, reasoningEffort, null, generatedCapabilities)) + new SessionModelSwitchToParams(sessionId, model, reasoningEffort, generatedReasoningSummary, + generatedCapabilities)) .thenApply(r -> null); } diff --git a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java index d9ad69282..ffb065ea6 100644 --- a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -106,6 +106,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setModel(config.getModel()); request.setClientName(config.getClientName()); request.setReasoningEffort(config.getReasoningEffort()); + request.setReasoningSummary(config.getReasoningSummary()); request.setTools(config.getTools()); request.setSystemMessage(config.getSystemMessage()); request.setAvailableTools(config.getAvailableTools()); @@ -130,8 +131,10 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setInfiniteSessions(config.getInfiniteSessions()); request.setSkillDirectories(config.getSkillDirectories()); request.setInstructionDirectories(config.getInstructionDirectories()); + request.setPluginDirectories(config.getPluginDirectories()); + request.setLargeOutput(config.getLargeOutput()); request.setDisabledSkills(config.getDisabledSkills()); - request.setConfigDir(config.getConfigDir()); + request.setConfigDirectory(config.getConfigDirectory()); config.getEnableConfigDiscovery().ifPresent(request::setEnableConfigDiscovery); request.setModelCapabilities(config.getModelCapabilities()); @@ -197,6 +200,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setModel(config.getModel()); request.setClientName(config.getClientName()); request.setReasoningEffort(config.getReasoningEffort()); + request.setReasoningSummary(config.getReasoningSummary()); request.setTools(config.getTools()); request.setSystemMessage(config.getSystemMessage()); request.setAvailableTools(config.getAvailableTools()); @@ -210,7 +214,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setHooks(true); } request.setWorkingDirectory(config.getWorkingDirectory()); - request.setConfigDir(config.getConfigDir()); + request.setConfigDirectory(config.getConfigDirectory()); config.getEnableConfigDiscovery().ifPresent(request::setEnableConfigDiscovery); if (config.isDisableResume()) { request.setDisableResume(true); @@ -225,6 +229,8 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setAgent(config.getAgent()); request.setSkillDirectories(config.getSkillDirectories()); request.setInstructionDirectories(config.getInstructionDirectories()); + request.setPluginDirectories(config.getPluginDirectories()); + request.setLargeOutput(config.getLargeOutput()); request.setDisabledSkills(config.getDisabledSkills()); request.setInfiniteSessions(config.getInfiniteSessions()); request.setModelCapabilities(config.getModelCapabilities()); diff --git a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index 1354e8c33..d14bb35ca 100644 --- a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -36,6 +36,9 @@ public final class CreateSessionRequest { @JsonProperty("reasoningEffort") private String reasoningEffort; + @JsonProperty("reasoningSummary") + private String reasoningSummary; + @JsonProperty("tools") private List tools; @@ -99,11 +102,17 @@ public final class CreateSessionRequest { @JsonProperty("instructionDirectories") private List instructionDirectories; + @JsonProperty("pluginDirectories") + private List pluginDirectories; + + @JsonProperty("largeOutput") + private LargeToolOutputConfig largeOutput; + @JsonProperty("disabledSkills") private List disabledSkills; @JsonProperty("configDir") - private String configDir; + private String configDirectory; @JsonProperty("enableConfigDiscovery") private Boolean enableConfigDiscovery; @@ -174,6 +183,16 @@ public void setReasoningEffort(String reasoningEffort) { this.reasoningEffort = reasoningEffort; } + /** Gets the reasoning summary mode. @return the reasoning summary mode */ + public String getReasoningSummary() { + return reasoningSummary; + } + + /** Sets the reasoning summary mode. @param reasoningSummary the reasoning summary mode */ + public void setReasoningSummary(String reasoningSummary) { + this.reasoningSummary = reasoningSummary; + } + /** Gets the tools. @return the tool definitions */ public List getTools() { return tools == null ? null : Collections.unmodifiableList(tools); @@ -418,6 +437,26 @@ public void setInstructionDirectories(List instructionDirectories) { this.instructionDirectories = instructionDirectories; } + /** Gets plugin directories. @return the plugin directories */ + public List getPluginDirectories() { + return pluginDirectories == null ? null : Collections.unmodifiableList(pluginDirectories); + } + + /** Sets plugin directories. @param pluginDirectories the directories */ + public void setPluginDirectories(List pluginDirectories) { + this.pluginDirectories = pluginDirectories; + } + + /** Gets large output config. @return the large output config */ + public LargeToolOutputConfig getLargeOutput() { + return largeOutput; + } + + /** Sets large output config. @param largeOutput the large output config */ + public void setLargeOutput(LargeToolOutputConfig largeOutput) { + this.largeOutput = largeOutput; + } + /** Gets disabled skills. @return the disabled skill names */ public List getDisabledSkills() { return disabledSkills == null ? null : Collections.unmodifiableList(disabledSkills); @@ -429,13 +468,13 @@ public void setDisabledSkills(List disabledSkills) { } /** Gets config directory. @return the config directory path */ - public String getConfigDir() { - return configDir; + public String getConfigDirectory() { + return configDirectory; } - /** Sets config directory. @param configDir the config directory path */ - public void setConfigDir(String configDir) { - this.configDir = configDir; + /** Sets config directory. @param configDirectory the config directory path */ + public void setConfigDirectory(String configDirectory) { + this.configDirectory = configDirectory; } /** Gets enable config discovery flag. @return the flag */ diff --git a/java/src/main/java/com/github/copilot/rpc/LargeToolOutputConfig.java b/java/src/main/java/com/github/copilot/rpc/LargeToolOutputConfig.java new file mode 100644 index 000000000..50a3cf4dd --- /dev/null +++ b/java/src/main/java/com/github/copilot/rpc/LargeToolOutputConfig.java @@ -0,0 +1,91 @@ +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Configuration for large tool output handling. + *

+ * When a tool produces output exceeding {@link #getMaxSizeBytes()}, the SDK + * writes the full output to a file in {@link #getOutputDirectory()} and returns a + * truncated preview to the model. + * + * @since 1.3.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class LargeToolOutputConfig { + + @JsonProperty("enabled") + private Boolean enabled; + + @JsonProperty("maxSizeBytes") + private Long maxSizeBytes; + + @JsonProperty("outputDir") + private String outputDirectory; + + /** + * Gets whether large tool output handling is enabled. + * + * @return {@code true} if enabled, {@code false} if disabled, {@code null} for + * default + */ + public Boolean getEnabled() { + return enabled; + } + + /** + * Sets whether large tool output handling is enabled. Defaults to {@code true} + * when unset. + * + * @param enabled + * {@code true} to enable, {@code false} to disable + * @return this config for method chaining + */ + public LargeToolOutputConfig setEnabled(Boolean enabled) { + this.enabled = enabled; + return this; + } + + /** + * Gets the maximum tool output size in bytes before it is redirected to a file. + * + * @return the maximum size in bytes, or {@code null} for default + */ + public Long getMaxSizeBytes() { + return maxSizeBytes; + } + + /** + * Sets the maximum tool output size in bytes before it is redirected to a file. + * + * @param maxSizeBytes + * the maximum size in bytes + * @return this config for method chaining + */ + public LargeToolOutputConfig setMaxSizeBytes(Long maxSizeBytes) { + this.maxSizeBytes = maxSizeBytes; + return this; + } + + /** + * Gets the directory where large tool output files are written. + * + * @return the output directory path, or {@code null} for default + */ + public String getOutputDirectory() { + return outputDirectory; + } + + /** + * Sets the directory where large tool output files are written. + * + * @param outputDirectory + * the output directory path + * @return this config for method chaining + */ + public LargeToolOutputConfig setOutputDirectory(String outputDirectory) { + this.outputDirectory = outputDirectory; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index fa28258b3..32349be09 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -51,12 +51,13 @@ public class ResumeSessionConfig { private Boolean coauthorEnabled; private Boolean manageScheduleEnabled; private String reasoningEffort; + private String reasoningSummary; private ModelCapabilitiesOverride modelCapabilities; private PermissionHandler onPermissionRequest; private UserInputHandler onUserInputRequest; private SessionHooks hooks; private String workingDirectory; - private String configDir; + private String configDirectory; private Boolean enableConfigDiscovery; private boolean disableResume; private boolean streaming; @@ -67,6 +68,8 @@ public class ResumeSessionConfig { private String agent; private List skillDirectories; private List instructionDirectories; + private List pluginDirectories; + private LargeToolOutputConfig largeOutput; private List disabledSkills; private InfiniteSessionConfig infiniteSessions; private Consumer onEvent; @@ -468,6 +471,29 @@ public ResumeSessionConfig setReasoningEffort(String reasoningEffort) { return this; } + /** + * Gets the reasoning summary mode. + * + * @return the reasoning summary mode ("none", "concise", or "detailed") + */ + public String getReasoningSummary() { + return reasoningSummary; + } + + /** + * Sets the reasoning summary mode for models that support configurable + * reasoning summaries. Use {@code "none"} to suppress summary output + * regardless of whether reasoning is enabled. + * + * @param reasoningSummary + * the reasoning summary mode + * @return this config for method chaining + */ + public ResumeSessionConfig setReasoningSummary(String reasoningSummary) { + this.reasoningSummary = reasoningSummary; + return this; + } + /** * Gets the permission request handler. * @@ -560,8 +586,8 @@ public ResumeSessionConfig setWorkingDirectory(String workingDirectory) { * * @return the configuration directory path */ - public String getConfigDir() { - return configDir; + public String getConfigDirectory() { + return configDirectory; } /** @@ -569,12 +595,12 @@ public String getConfigDir() { *

* Override the default configuration directory location. * - * @param configDir + * @param configDirectory * the configuration directory path * @return this config for method chaining */ - public ResumeSessionConfig setConfigDir(String configDir) { - this.configDir = configDir; + public ResumeSessionConfig setConfigDirectory(String configDirectory) { + this.configDirectory = configDirectory; return this; } @@ -852,6 +878,48 @@ public ResumeSessionConfig setInstructionDirectories(List instructionDir return this; } + /** + * Gets the plugin directories to load Open Plugin definitions from. + * + * @return the list of plugin directory paths + */ + public List getPluginDirectories() { + return pluginDirectories == null ? null : Collections.unmodifiableList(pluginDirectories); + } + + /** + * Sets the plugin directories to load Open Plugin definitions from. + * + * @param pluginDirectories + * the list of plugin directory paths + * @return this config for method chaining + */ + public ResumeSessionConfig setPluginDirectories(List pluginDirectories) { + this.pluginDirectories = pluginDirectories; + return this; + } + + /** + * Gets the configuration for large tool output handling. + * + * @return the large output config, or {@code null} for default + */ + public LargeToolOutputConfig getLargeOutput() { + return largeOutput; + } + + /** + * Sets the configuration for large tool output handling. + * + * @param largeOutput + * the large output config + * @return this config for method chaining + */ + public ResumeSessionConfig setLargeOutput(LargeToolOutputConfig largeOutput) { + this.largeOutput = largeOutput; + return this; + } + /** * Gets the disabled skills. * @@ -1104,12 +1172,13 @@ public ResumeSessionConfig clone() { copy.provider = this.provider; copy.enableSessionTelemetry = this.enableSessionTelemetry; copy.reasoningEffort = this.reasoningEffort; + copy.reasoningSummary = this.reasoningSummary; copy.modelCapabilities = this.modelCapabilities; copy.onPermissionRequest = this.onPermissionRequest; copy.onUserInputRequest = this.onUserInputRequest; copy.hooks = this.hooks; copy.workingDirectory = this.workingDirectory; - copy.configDir = this.configDir; + copy.configDirectory = this.configDirectory; copy.enableConfigDiscovery = this.enableConfigDiscovery; copy.disableResume = this.disableResume; copy.streaming = this.streaming; @@ -1122,6 +1191,8 @@ public ResumeSessionConfig clone() { copy.instructionDirectories = this.instructionDirectories != null ? new ArrayList<>(this.instructionDirectories) : null; + copy.pluginDirectories = this.pluginDirectories != null ? new ArrayList<>(this.pluginDirectories) : null; + copy.largeOutput = this.largeOutput; copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; copy.infiniteSessions = this.infiniteSessions; copy.onEvent = this.onEvent; diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index 4321a24aa..78e8dfeed 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -38,6 +38,9 @@ public final class ResumeSessionRequest { @JsonProperty("reasoningEffort") private String reasoningEffort; + @JsonProperty("reasoningSummary") + private String reasoningSummary; + @JsonProperty("tools") private List tools; @@ -72,7 +75,7 @@ public final class ResumeSessionRequest { private String workingDirectory; @JsonProperty("configDir") - private String configDir; + private String configDirectory; @JsonProperty("enableConfigDiscovery") private Boolean enableConfigDiscovery; @@ -107,6 +110,12 @@ public final class ResumeSessionRequest { @JsonProperty("instructionDirectories") private List instructionDirectories; + @JsonProperty("pluginDirectories") + private List pluginDirectories; + + @JsonProperty("largeOutput") + private LargeToolOutputConfig largeOutput; + @JsonProperty("disabledSkills") private List disabledSkills; @@ -176,6 +185,16 @@ public void setReasoningEffort(String reasoningEffort) { this.reasoningEffort = reasoningEffort; } + /** Gets the reasoning summary mode. @return the reasoning summary mode */ + public String getReasoningSummary() { + return reasoningSummary; + } + + /** Sets the reasoning summary mode. @param reasoningSummary the reasoning summary mode */ + public void setReasoningSummary(String reasoningSummary) { + this.reasoningSummary = reasoningSummary; + } + /** Gets the tools. @return the tool definitions */ public List getTools() { return tools == null ? null : Collections.unmodifiableList(tools); @@ -323,13 +342,13 @@ public void setWorkingDirectory(String workingDirectory) { } /** Gets config directory. @return the config directory */ - public String getConfigDir() { - return configDir; + public String getConfigDirectory() { + return configDirectory; } - /** Sets config directory. @param configDir the config directory */ - public void setConfigDir(String configDir) { - this.configDir = configDir; + /** Sets config directory. @param configDirectory the config directory */ + public void setConfigDirectory(String configDirectory) { + this.configDirectory = configDirectory; } /** Gets enable config discovery flag. @return the flag */ @@ -478,6 +497,26 @@ public void setInstructionDirectories(List instructionDirectories) { this.instructionDirectories = instructionDirectories; } + /** Gets plugin directories. @return the plugin directories */ + public List getPluginDirectories() { + return pluginDirectories == null ? null : Collections.unmodifiableList(pluginDirectories); + } + + /** Sets plugin directories. @param pluginDirectories the directories */ + public void setPluginDirectories(List pluginDirectories) { + this.pluginDirectories = pluginDirectories; + } + + /** Gets large output config. @return the large output config */ + public LargeToolOutputConfig getLargeOutput() { + return largeOutput; + } + + /** Sets large output config. @param largeOutput the large output config */ + public void setLargeOutput(LargeToolOutputConfig largeOutput) { + this.largeOutput = largeOutput; + } + /** Gets disabled skills. @return the disabled skill names */ public List getDisabledSkills() { return disabledSkills == null ? null : Collections.unmodifiableList(disabledSkills); diff --git a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java index 2a42df610..b97abb616 100644 --- a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -42,6 +42,7 @@ public class SessionConfig { private String clientName; private String model; private String reasoningEffort; + private String reasoningSummary; private List tools; private SystemMessageConfig systemMessage; private List availableTools; @@ -65,8 +66,10 @@ public class SessionConfig { private InfiniteSessionConfig infiniteSessions; private List skillDirectories; private List instructionDirectories; + private List pluginDirectories; + private LargeToolOutputConfig largeOutput; private List disabledSkills; - private String configDir; + private String configDirectory; private Boolean enableConfigDiscovery; private ModelCapabilitiesOverride modelCapabilities; private Consumer onEvent; @@ -171,6 +174,29 @@ public SessionConfig setReasoningEffort(String reasoningEffort) { return this; } + /** + * Gets the reasoning summary mode. + * + * @return the reasoning summary mode ("none", "concise", or "detailed") + */ + public String getReasoningSummary() { + return reasoningSummary; + } + + /** + * Sets the reasoning summary mode for models that support configurable + * reasoning summaries. Use {@code "none"} to suppress summary output + * regardless of whether reasoning is enabled. + * + * @param reasoningSummary + * the reasoning summary mode + * @return this config instance for method chaining + */ + public SessionConfig setReasoningSummary(String reasoningSummary) { + this.reasoningSummary = reasoningSummary; + return this; + } + /** * Gets the custom tools for this session. * @@ -796,6 +822,48 @@ public SessionConfig setInstructionDirectories(List instructionDirectori return this; } + /** + * Gets the plugin directories to load Open Plugin definitions from. + * + * @return the list of plugin directory paths + */ + public List getPluginDirectories() { + return pluginDirectories == null ? null : Collections.unmodifiableList(pluginDirectories); + } + + /** + * Sets the plugin directories to load Open Plugin definitions from. + * + * @param pluginDirectories + * the list of plugin directory paths + * @return this config instance for method chaining + */ + public SessionConfig setPluginDirectories(List pluginDirectories) { + this.pluginDirectories = pluginDirectories; + return this; + } + + /** + * Gets the configuration for large tool output handling. + * + * @return the large output config, or {@code null} for default + */ + public LargeToolOutputConfig getLargeOutput() { + return largeOutput; + } + + /** + * Sets the configuration for large tool output handling. + * + * @param largeOutput + * the large output config + * @return this config instance for method chaining + */ + public SessionConfig setLargeOutput(LargeToolOutputConfig largeOutput) { + this.largeOutput = largeOutput; + return this; + } + /** * Gets the disabled skill names. * @@ -825,8 +893,8 @@ public SessionConfig setDisabledSkills(List disabledSkills) { * * @return the config directory path */ - public String getConfigDir() { - return configDir; + public String getConfigDirectory() { + return configDirectory; } /** @@ -835,12 +903,12 @@ public String getConfigDir() { * This allows using a specific directory for session configuration instead of * the default location. * - * @param configDir + * @param configDirectory * the configuration directory path * @return this config instance for method chaining */ - public SessionConfig setConfigDir(String configDir) { - this.configDir = configDir; + public SessionConfig setConfigDirectory(String configDirectory) { + this.configDirectory = configDirectory; return this; } @@ -1200,6 +1268,7 @@ public SessionConfig clone() { copy.clientName = this.clientName; copy.model = this.model; copy.reasoningEffort = this.reasoningEffort; + copy.reasoningSummary = this.reasoningSummary; copy.tools = this.tools != null ? new ArrayList<>(this.tools) : null; copy.systemMessage = this.systemMessage; copy.availableTools = this.availableTools != null ? new ArrayList<>(this.availableTools) : null; @@ -1225,8 +1294,10 @@ public SessionConfig clone() { copy.instructionDirectories = this.instructionDirectories != null ? new ArrayList<>(this.instructionDirectories) : null; + copy.pluginDirectories = this.pluginDirectories != null ? new ArrayList<>(this.pluginDirectories) : null; + copy.largeOutput = this.largeOutput; copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; - copy.configDir = this.configDir; + copy.configDirectory = this.configDirectory; copy.enableConfigDiscovery = this.enableConfigDiscovery; copy.modelCapabilities = this.modelCapabilities; copy.onEvent = this.onEvent; diff --git a/java/src/test/java/com/github/copilot/ConfigCloneTest.java b/java/src/test/java/com/github/copilot/ConfigCloneTest.java index f26f67ed9..ad08e5353 100644 --- a/java/src/test/java/com/github/copilot/ConfigCloneTest.java +++ b/java/src/test/java/com/github/copilot/ConfigCloneTest.java @@ -21,6 +21,7 @@ import com.github.copilot.rpc.DefaultAgentConfig; import com.github.copilot.rpc.ExitPlanModeResult; import com.github.copilot.rpc.InfiniteSessionConfig; +import com.github.copilot.rpc.LargeToolOutputConfig; import com.github.copilot.rpc.MessageOptions; import com.github.copilot.rpc.ModelInfo; import com.github.copilot.rpc.ResumeSessionConfig; @@ -114,6 +115,9 @@ void sessionConfigCloneBasic() { original.setSessionId("my-session"); original.setClientName("my-app"); original.setModel("gpt-4o"); + original.setReasoningSummary("detailed"); + original.setPluginDirectories(List.of("/plugins/a", "/plugins/b")); + original.setLargeOutput(new LargeToolOutputConfig().setEnabled(true).setMaxSizeBytes(1024L).setOutputDirectory("/tmp/out")); original.setStreaming(true); SessionConfig cloned = original.clone(); @@ -121,6 +125,9 @@ void sessionConfigCloneBasic() { assertEquals(original.getSessionId(), cloned.getSessionId()); assertEquals(original.getClientName(), cloned.getClientName()); assertEquals(original.getModel(), cloned.getModel()); + assertEquals(original.getReasoningSummary(), cloned.getReasoningSummary()); + assertEquals(original.getPluginDirectories(), cloned.getPluginDirectories()); + assertEquals(original.getLargeOutput(), cloned.getLargeOutput()); assertEquals(original.isStreaming(), cloned.isStreaming()); } @@ -162,11 +169,17 @@ void sessionConfigAgentAndOnEventCloned() { void resumeSessionConfigCloneBasic() { ResumeSessionConfig original = new ResumeSessionConfig(); original.setModel("o1"); + original.setReasoningSummary("none"); + original.setPluginDirectories(List.of("/plugins/r")); + original.setLargeOutput(new LargeToolOutputConfig().setEnabled(false).setMaxSizeBytes(2048L).setOutputDirectory("/tmp/resume")); original.setStreaming(false); ResumeSessionConfig cloned = original.clone(); assertEquals(original.getModel(), cloned.getModel()); + assertEquals(original.getReasoningSummary(), cloned.getReasoningSummary()); + assertEquals(original.getPluginDirectories(), cloned.getPluginDirectories()); + assertEquals(original.getLargeOutput(), cloned.getLargeOutput()); assertEquals(original.isStreaming(), cloned.isStreaming()); } @@ -328,8 +341,8 @@ void resumeSessionConfigAllSetters() { config.setWorkingDirectory("/project/src"); assertEquals("/project/src", config.getWorkingDirectory()); - config.setConfigDir("/home/user/.config/copilot"); - assertEquals("/home/user/.config/copilot", config.getConfigDir()); + config.setConfigDirectory("/home/user/.config/copilot"); + assertEquals("/home/user/.config/copilot", config.getConfigDirectory()); config.setSkillDirectories(List.of("/skills/custom")); assertEquals(List.of("/skills/custom"), config.getSkillDirectories()); diff --git a/java/src/test/java/com/github/copilot/CopilotSessionTest.java b/java/src/test/java/com/github/copilot/CopilotSessionTest.java index 9c74d4946..44a7373ec 100644 --- a/java/src/test/java/com/github/copilot/CopilotSessionTest.java +++ b/java/src/test/java/com/github/copilot/CopilotSessionTest.java @@ -559,7 +559,7 @@ void testShouldCreateSessionWithCustomConfigDir() throws Exception { String customConfigDir = ctx.getWorkDir().resolve("custom-config").toString(); SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setConfigDir(customConfigDir); + .setConfigDirectory(customConfigDir); CopilotSession session = client.createSession(config).get(); assertNotNull(session.getSessionId()); diff --git a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index 49a4c9c30..df1dfe749 100644 --- a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -21,6 +21,7 @@ import com.github.copilot.rpc.ElicitationResult; import com.github.copilot.rpc.ElicitationResultAction; import com.github.copilot.rpc.ExitPlanModeResult; +import com.github.copilot.rpc.LargeToolOutputConfig; import com.github.copilot.rpc.ResumeSessionConfig; import com.github.copilot.rpc.ResumeSessionRequest; import com.github.copilot.rpc.SessionConfig; @@ -90,6 +91,27 @@ void testBuildCreateRequestSetsClientName() { assertEquals("my-app", request.getClientName()); } + @Test + void testBuildCreateRequestSetsReasoningSummary() { + var config = new SessionConfig().setReasoningSummary("concise"); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + assertEquals("concise", request.getReasoningSummary()); + } + + @Test + void testBuildCreateRequestSetsPluginDirectoriesAndLargeOutput() { + var largeOutput = new LargeToolOutputConfig() + .setEnabled(true) + .setMaxSizeBytes(1024L) + .setOutputDirectory("/tmp/out"); + var config = new SessionConfig() + .setPluginDirectories(List.of("/plugins/a")) + .setLargeOutput(largeOutput); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + assertEquals(List.of("/plugins/a"), request.getPluginDirectories()); + assertEquals(largeOutput, request.getLargeOutput()); + } + @Test void testBuildCreateRequestForwardsEnableSessionTelemetryWhenFalse() { var config = new SessionConfig().setEnableSessionTelemetry(false); @@ -212,6 +234,27 @@ void testBuildResumeRequestSetsClientName() { assertEquals("my-app", request.getClientName()); } + @Test + void testBuildResumeRequestSetsReasoningSummary() { + var config = new ResumeSessionConfig().setReasoningSummary("none"); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-11", config); + assertEquals("none", request.getReasoningSummary()); + } + + @Test + void testBuildResumeRequestSetsPluginDirectoriesAndLargeOutput() { + var largeOutput = new LargeToolOutputConfig() + .setEnabled(false) + .setMaxSizeBytes(2048L) + .setOutputDirectory("/tmp/resume"); + var config = new ResumeSessionConfig() + .setPluginDirectories(List.of("/plugins/r")) + .setLargeOutput(largeOutput); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-12", config); + assertEquals(List.of("/plugins/r"), request.getPluginDirectories()); + assertEquals(largeOutput, request.getLargeOutput()); + } + // ========================================================================= // configureSession (ResumeSessionConfig overload) // ========================================================================= diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 7f3cbe8e4..510fe0d87 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -49,6 +49,7 @@ import type { GetAuthStatusResponse, GetStatusResponse, InternalRuntimeConnection, + LargeToolOutputConfig, MCPServerConfig, ModelInfo, ResumeSessionConfig, @@ -133,6 +134,22 @@ function toWireCustomAgents(agents: CustomAgentConfig[] | undefined): unknown[] }); } +/** + * Convert a {@link LargeToolOutputConfig} from the public API shape + * (`outputDirectory`) to the wire shape (`outputDir`). + */ +function toWireLargeOutput( + config: LargeToolOutputConfig | undefined +): Record | undefined { + if (!config) return undefined; + const { outputDirectory, ...rest } = config; + const wire: Record = { ...rest }; + if (outputDirectory !== undefined) { + wire.outputDir = outputDirectory; + } + return wire; +} + function toolFilterListToArray(value: string[] | ToolSet | undefined): string[] | undefined { if (value === undefined) { return undefined; @@ -1072,6 +1089,7 @@ export class CopilotClient { sessionId: localSessionId, clientName: config.clientName, reasoningEffort: config.reasoningEffort, + reasoningSummary: config.reasoningSummary, tools: config.tools?.map((tool) => ({ name: tool.name, description: tool.description, @@ -1094,6 +1112,7 @@ export class CopilotClient { provider: config.provider, enableSessionTelemetry: config.enableSessionTelemetry, modelCapabilities: config.modelCapabilities, + largeOutput: toWireLargeOutput(config.largeOutput), requestPermission: !!config.onPermissionRequest, requestUserInput: !!config.onUserInputRequest, requestElicitation: !!config.onElicitationRequest, @@ -1108,9 +1127,10 @@ export class CopilotClient { customAgents: toWireCustomAgents(config.customAgents), defaultAgent: config.defaultAgent, agent: config.agent, - configDir: config.configDir, + configDir: config.configDirectory, enableConfigDiscovery: config.enableConfigDiscovery, skillDirectories: config.skillDirectories, + pluginDirectories: config.pluginDirectories, instructionDirectories: config.instructionDirectories, disabledSkills: config.disabledSkills, infiniteSessions: config.infiniteSessions, @@ -1238,6 +1258,7 @@ export class CopilotClient { clientName: config.clientName, model: config.model, reasoningEffort: config.reasoningEffort, + reasoningSummary: config.reasoningSummary, systemMessage: wireSystemMessage, availableTools: toolFilterOptions.availableTools, excludedTools: toolFilterOptions.excludedTools, @@ -1260,6 +1281,7 @@ export class CopilotClient { })), provider: config.provider, modelCapabilities: config.modelCapabilities, + largeOutput: toWireLargeOutput(config.largeOutput), requestPermission: config.onPermissionRequest !== defaultJoinSessionPermissionHandler, requestUserInput: !!config.onUserInputRequest, @@ -1268,7 +1290,7 @@ export class CopilotClient { requestAutoModeSwitch: !!config.onAutoModeSwitchRequest, hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), workingDirectory: config.workingDirectory, - configDir: config.configDir, + configDir: config.configDirectory, enableConfigDiscovery: config.enableConfigDiscovery, streaming: config.streaming, includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true, @@ -1278,6 +1300,7 @@ export class CopilotClient { defaultAgent: config.defaultAgent, agent: config.agent, skillDirectories: config.skillDirectories, + pluginDirectories: config.pluginDirectories, instructionDirectories: config.instructionDirectories, disabledSkills: config.disabledSkills, infiniteSessions: config.infiniteSessions, diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 7211a1bc5..c044f2b94 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -73,6 +73,7 @@ export type { GetAuthStatusResponse, GetStatusResponse, InfiniteSessionConfig, + LargeToolOutputConfig, UiInputOptions, MCPStdioServerConfig, MCPHTTPServerConfig, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 4bd3f3546..b3d99271b 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -31,6 +31,7 @@ import type { PermissionHandler, PermissionRequest, ReasoningEffort, + ReasoningSummary, ModelCapabilitiesOverride, SectionTransformFn, SessionCapabilities, @@ -1170,6 +1171,7 @@ export class CopilotSession { model: string, options?: { reasoningEffort?: ReasoningEffort; + reasoningSummary?: ReasoningSummary; modelCapabilities?: ModelCapabilitiesOverride; } ): Promise { diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 3dfc78e8a..d0b7828a0 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -9,13 +9,17 @@ // Import and re-export generated session event types import type { Canvas } from "./canvas.js"; import type { SessionFsProvider } from "./sessionFsProvider.js"; -import type { SessionEvent as GeneratedSessionEvent } from "./generated/session-events.js"; +import type { + ReasoningSummary, + SessionEvent as GeneratedSessionEvent, +} from "./generated/session-events.js"; import type { CopilotSession } from "./session.js"; import type { RemoteSessionMode } from "./generated/rpc.js"; import type { OpenCanvasInstance } from "./generated/rpc.js"; import type { ToolSet } from "./toolSet.js"; export type { RemoteSessionMode } from "./generated/rpc.js"; export type SessionEvent = GeneratedSessionEvent; +export type { ReasoningSummary } from "./generated/session-events.js"; export type { SessionFsProvider } from "./sessionFsProvider.js"; export { createSessionFsAdapter } from "./sessionFsProvider.js"; export type { SessionFsFileInfo } from "./sessionFsProvider.js"; @@ -1494,6 +1498,32 @@ export interface InfiniteSessionConfig { bufferExhaustionThreshold?: number; } +/** + * Configuration for handling large tool outputs. + * + * When a tool produces output exceeding the configured size, the output is + * written to a temp file and a reference is returned to the model instead of + * the full payload. + */ +export interface LargeToolOutputConfig { + /** + * Whether large output handling is enabled. + * @default true + */ + enabled?: boolean; + + /** + * Maximum size in bytes before output is written to a temp file. + * @default 51200 + */ + maxSizeBytes?: number; + + /** + * Directory to write temp files to. Defaults to the OS temp directory. + */ + outputDirectory?: string; +} + /** * Valid reasoning effort levels for models that support it. */ @@ -1533,14 +1563,28 @@ export interface SessionConfigBase { */ reasoningEffort?: ReasoningEffort; + /** + * Reasoning summary mode for models that support configurable reasoning summaries. + * Use "none" to suppress summary output regardless of whether reasoning is enabled. + */ + reasoningSummary?: ReasoningSummary; + /** Per-property overrides for model capabilities, deep-merged over runtime defaults. */ modelCapabilities?: ModelCapabilitiesOverride; + /** + * Configuration for handling large tool outputs. When a tool produces + * output exceeding the configured size, the output is written to a temp + * file and a reference is returned to the model instead of the full + * payload. + */ + largeOutput?: LargeToolOutputConfig; + /** * Override the default configuration directory location. * When specified, the session will use this directory for storing config and state. */ - configDir?: string; + configDirectory?: string; /** * When true, automatically discovers MCP server configurations (e.g. `.mcp.json`, @@ -1778,6 +1822,21 @@ export interface SessionConfigBase { */ skillDirectories?: string[]; + /** + * Local filesystem paths to Open Plugins-format directories + * (https://open-plugins.com/) to load for this session. + * + * Relative paths resolve against `workingDirectory` (or the runtime cwd if + * unset); absolute paths are recommended. Invalid entries are logged and + * skipped. + * + * Treated as an explicit opt-in: plugin agents and rules load even when + * {@link SessionConfigBase.enableConfigDiscovery} is false. Loaded assets + * slot between project (cwd) sources and personal/home sources in the + * session-wide precedence order. + */ + pluginDirectories?: string[]; + /** * Additional directories to search for custom instruction files. */ diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index e3b0f6932..c8d6a5fd8 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -106,6 +106,82 @@ describe("CopilotClient", () => { expect(payload.openCanvasInstances).toBeUndefined(); }); + it("forwards reasoningSummary in session.create and session.resume", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + const session = await client.createSession({ + onPermissionRequest: approveAll, + reasoningSummary: "concise", + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + reasoningSummary: "none", + }); + + const createPayload = spy.mock.calls.find(([method]) => method === "session.create")![1] as any; + const resumePayload = spy.mock.calls.find(([method]) => method === "session.resume")![1] as any; + expect(createPayload.reasoningSummary).toBe("concise"); + expect(resumePayload.reasoningSummary).toBe("none"); + }); + + it("forwards pluginDirectories and largeOutput in session.create and session.resume", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + const pluginDirs = ["/tmp/plugins/a", "/tmp/plugins/b"]; + const largeOutput = { + enabled: true, + maxSizeBytes: 1024, + outputDirectory: "/tmp/large-output", + }; + const expectedWireLargeOutput = { + enabled: true, + maxSizeBytes: 1024, + outputDir: "/tmp/large-output", + }; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + pluginDirectories: pluginDirs, + largeOutput, + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + pluginDirectories: pluginDirs, + largeOutput, + }); + + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + const resumePayload = spy.mock.calls.find( + ([method]) => method === "session.resume" + )![1] as any; + expect(createPayload.pluginDirectories).toEqual(pluginDirs); + expect(createPayload.largeOutput).toEqual(expectedWireLargeOutput); + expect(resumePayload.pluginDirectories).toEqual(pluginDirs); + expect(resumePayload.largeOutput).toEqual(expectedWireLargeOutput); + }); + it("routes canvas.action.invoke to registered canvas action handlers via clientSessionApis", async () => { const canvas = createCanvas({ id: "counter", @@ -664,7 +740,7 @@ describe("CopilotClient", () => { spy.mockRestore(); }); - it("sends reasoningEffort with session.model.switchTo when provided", async () => { + it("sends reasoning options with session.model.switchTo when provided", async () => { const client = new CopilotClient(); await client.start(); onTestFinished(() => client.forceStop()); @@ -678,12 +754,16 @@ describe("CopilotClient", () => { throw new Error(`Unexpected method: ${method}`); }); - await session.setModel("claude-sonnet-4.6", { reasoningEffort: "high" }); + await session.setModel("claude-sonnet-4.6", { + reasoningEffort: "high", + reasoningSummary: "detailed", + }); expect(spy).toHaveBeenCalledWith("session.model.switchTo", { sessionId: session.sessionId, modelId: "claude-sonnet-4.6", reasoningEffort: "high", + reasoningSummary: "detailed", }); spy.mockRestore(); diff --git a/nodejs/test/e2e/session.e2e.test.ts b/nodejs/test/e2e/session.e2e.test.ts index acd863b9b..55a064ab4 100644 --- a/nodejs/test/e2e/session.e2e.test.ts +++ b/nodejs/test/e2e/session.e2e.test.ts @@ -587,7 +587,7 @@ describe("Sessions", async () => { }); const session = await client.createSession({ onPermissionRequest: approveAll, - configDir: customConfigDir, + configDirectory: customConfigDir, }); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index af5db5747..5f51cf021 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -86,6 +86,7 @@ ExitPlanModeResult, InfiniteSessionConfig, InputOptions, + LargeToolOutputConfig, MCPHTTPServerConfig, MCPServerConfig, MCPStdioServerConfig, @@ -105,6 +106,7 @@ PreToolUseHookInput, PreToolUseHookOutput, ProviderConfig, + ReasoningSummary, SessionCapabilities, SessionEndHandler, SessionEndHookInput, @@ -181,6 +183,7 @@ "GetStatusResponse", "InfiniteSessionConfig", "InputOptions", + "LargeToolOutputConfig", "LogLevel", "MCPHTTPServerConfig", "MCPServerConfig", @@ -215,6 +218,7 @@ "PreToolUseHookInput", "PreToolUseHookOutput", "ProviderConfig", + "ReasoningSummary", "RemoteSessionMode", "RuntimeConnection", "SessionBackgroundEvent", diff --git a/python/copilot/client.py b/python/copilot/client.py index c878129a3..3ac352a6d 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -25,7 +25,7 @@ import threading import time import uuid -from collections.abc import Awaitable, Callable, Sequence +from collections.abc import Awaitable, Callable, Mapping, Sequence from dataclasses import dataclass from datetime import UTC, datetime from pathlib import Path @@ -76,9 +76,11 @@ ElicitationHandler, ExitPlanModeHandler, InfiniteSessionConfig, + LargeToolOutputConfig, MCPServerConfig, ProviderConfig, ReasoningEffort, + ReasoningSummary, SectionTransformFn, SessionFsConfig, SessionHooks, @@ -154,6 +156,18 @@ def _mcp_servers_to_wire( return wire +def _large_output_to_wire(config: Mapping[str, Any]) -> dict[str, Any]: + """Convert a ``LargeToolOutputConfig`` mapping to wire format.""" + wire: dict[str, Any] = {} + if "enabled" in config: + wire["enabled"] = config["enabled"] + if "max_size_bytes" in config: + wire["maxSizeBytes"] = config["max_size_bytes"] + if "output_directory" in config: + wire["outputDir"] = config["output_directory"] + return wire + + class TelemetryConfig(TypedDict, total=False): """Configuration for OpenTelemetry integration with the Copilot CLI.""" @@ -1539,6 +1553,7 @@ async def create_session( session_id: str | None = None, client_name: str | None = None, reasoning_effort: ReasoningEffort | None = None, + reasoning_summary: ReasoningSummary | None = None, tools: list[Tool] | None = None, system_message: SystemMessageConfig | None = None, available_tools: list[str] | ToolSet | None = None, @@ -1559,12 +1574,14 @@ async def create_session( custom_agents: list[CustomAgentConfig] | None = None, default_agent: DefaultAgentConfig | dict[str, Any] | None = None, agent: str | None = None, - config_dir: str | None = None, + config_directory: str | None = None, enable_config_discovery: bool | None = None, skill_directories: list[str] | None = None, + plugin_directories: list[str] | None = None, instruction_directories: list[str] | None = None, disabled_skills: list[str] | None = None, infinite_sessions: InfiniteSessionConfig | None = None, + large_output: LargeToolOutputConfig | None = None, on_event: Callable[[SessionEvent], None] | None = None, commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, @@ -1595,6 +1612,9 @@ async def create_session( session_id: Optional session ID. If not provided, a UUID is generated. client_name: Optional client name for identification. reasoning_effort: Reasoning effort level for the model. + reasoning_summary: Reasoning summary mode for supported models. + Use ``"none"`` to suppress summary output regardless of whether + reasoning is enabled. tools: Custom tools to register with the session. system_message: System message configuration. available_tools: Allowlist of tools to enable. When specified, only @@ -1628,7 +1648,7 @@ async def create_session( default_agent: Configuration for the default agent, including tool visibility controls. agent: Agent to use for the session. - config_dir: Override for the configuration directory. + config_directory: Override for the configuration directory. enable_config_discovery: When True, automatically discovers MCP server configurations (e.g. ``.mcp.json``, ``.vscode/mcp.json``) and skill directories from the working directory and merges them with any @@ -1703,6 +1723,8 @@ async def create_session( payload["clientName"] = client_name if reasoning_effort: payload["reasoningEffort"] = reasoning_effort + if reasoning_summary: + payload["reasoningSummary"] = reasoning_summary if tool_defs: payload["tools"] = tool_defs @@ -1798,8 +1820,8 @@ async def create_session( payload["agent"] = agent # Add config directory override if provided - if config_dir: - payload["configDir"] = config_dir + if config_directory: + payload["configDir"] = config_directory # Add config discovery flag if provided if enable_config_discovery is not None: @@ -1809,6 +1831,10 @@ async def create_session( if skill_directories: payload["skillDirectories"] = skill_directories + # Add plugin directories configuration if provided + if plugin_directories: + payload["pluginDirectories"] = plugin_directories + # Add instruction directories configuration if provided if instruction_directories is not None: payload["instructionDirectories"] = instruction_directories @@ -1832,6 +1858,9 @@ async def create_session( ] payload["infiniteSessions"] = wire_config + if large_output is not None: + payload["largeOutput"] = _large_output_to_wire(large_output) + if canvases: payload["canvases"] = [c.to_dict() for c in canvases] if request_canvas_renderer is not None: @@ -2016,6 +2045,7 @@ async def resume_session( model: str | None = None, client_name: str | None = None, reasoning_effort: ReasoningEffort | None = None, + reasoning_summary: ReasoningSummary | None = None, tools: list[Tool] | None = None, system_message: SystemMessageConfig | None = None, available_tools: list[str] | ToolSet | None = None, @@ -2036,12 +2066,14 @@ async def resume_session( custom_agents: list[CustomAgentConfig] | None = None, default_agent: DefaultAgentConfig | dict[str, Any] | None = None, agent: str | None = None, - config_dir: str | None = None, + config_directory: str | None = None, enable_config_discovery: bool | None = None, skill_directories: list[str] | None = None, + plugin_directories: list[str] | None = None, instruction_directories: list[str] | None = None, disabled_skills: list[str] | None = None, infinite_sessions: InfiniteSessionConfig | None = None, + large_output: LargeToolOutputConfig | None = None, on_event: Callable[[SessionEvent], None] | None = None, commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, @@ -2073,6 +2105,9 @@ async def resume_session( model: The model to use for the resumed session. client_name: Optional client name for identification. reasoning_effort: Reasoning effort level for the model. + reasoning_summary: Reasoning summary mode for supported models. + Use ``"none"`` to suppress summary output regardless of whether + reasoning is enabled. tools: Custom tools to register with the session. system_message: System message configuration. available_tools: Allowlist of tools to enable. When specified, only @@ -2106,7 +2141,7 @@ async def resume_session( default_agent: Configuration for the default agent, including tool visibility controls. agent: Agent to use for the session. - config_dir: Override for the configuration directory. + config_directory: Override for the configuration directory. enable_config_discovery: When True, automatically discovers MCP server configurations (e.g. ``.mcp.json``, ``.vscode/mcp.json``) and skill directories from the working directory and merges them with any @@ -2183,6 +2218,8 @@ async def resume_session( payload["model"] = model if reasoning_effort: payload["reasoningEffort"] = reasoning_effort + if reasoning_summary: + payload["reasoningSummary"] = reasoning_summary if tool_defs: payload["tools"] = tool_defs wire_system_message, transform_callbacks = _extract_transform_callbacks(system_message) @@ -2239,8 +2276,8 @@ async def resume_session( if working_directory: payload["workingDirectory"] = working_directory - if config_dir: - payload["configDir"] = config_dir + if config_directory: + payload["configDir"] = config_directory if enable_config_discovery is not None: payload["enableConfigDiscovery"] = enable_config_discovery @@ -2265,6 +2302,8 @@ async def resume_session( payload["agent"] = agent if skill_directories: payload["skillDirectories"] = skill_directories + if plugin_directories: + payload["pluginDirectories"] = plugin_directories if instruction_directories is not None: payload["instructionDirectories"] = instruction_directories if disabled_skills: @@ -2284,6 +2323,9 @@ async def resume_session( ] payload["infiniteSessions"] = wire_config + if large_output is not None: + payload["largeOutput"] = _large_output_to_wire(large_output) + if canvases: payload["canvases"] = [c.to_dict() for c in canvases] if open_canvases: diff --git a/python/copilot/session.py b/python/copilot/session.py index 527a8a092..5beab450c 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -64,6 +64,7 @@ ExternalToolRequestedData, PermissionRequest, PermissionRequestedData, + ReasoningSummary as _RpcReasoningSummary, SessionErrorData, SessionEvent, SessionIdleData, @@ -86,6 +87,7 @@ # ============================================================================ ReasoningEffort = Literal["low", "medium", "high", "xhigh"] +ReasoningSummary = Literal["none", "concise", "detailed"] SessionFsConventions = Literal["posix", "windows"] @@ -948,6 +950,23 @@ class InfiniteSessionConfig(TypedDict, total=False): buffer_exhaustion_threshold: float +class LargeToolOutputConfig(TypedDict, total=False): + """ + Configuration for handling large tool outputs. + + When a tool produces output exceeding the configured size, the output is + written to a temp file and a reference is returned to the model instead of + the full payload. + """ + + # Whether large output handling is enabled. Default True. + enabled: bool + # Maximum size in bytes before output is written to a temp file. Default 50KB. + max_size_bytes: int + # Directory to write temp files to. Defaults to the OS temp directory. + output_directory: str + + # ============================================================================ # Session Configuration # ============================================================================ @@ -2341,6 +2360,7 @@ async def set_model( model: str, *, reasoning_effort: str | None = None, + reasoning_summary: ReasoningSummary | None = None, model_capabilities: ModelCapabilitiesOverride | None = None, ) -> None: """ @@ -2353,6 +2373,9 @@ async def set_model( model: Model ID to switch to (e.g., "gpt-4.1", "claude-sonnet-4"). reasoning_effort: Optional reasoning effort level for the new model (e.g., "low", "medium", "high", "xhigh"). + reasoning_summary: Optional reasoning summary mode for supported + models. Use "none" to suppress summary output regardless of + whether reasoning is enabled. model_capabilities: Override individual model capabilities resolved by the runtime. Raises: @@ -2373,6 +2396,11 @@ async def set_model( ModelSwitchToRequest( model_id=model, reasoning_effort=reasoning_effort, + reasoning_summary=( + _RpcReasoningSummary(reasoning_summary) + if reasoning_summary is not None + else None + ), model_capabilities=rpc_caps, ) ) diff --git a/python/e2e/test_session_e2e.py b/python/e2e/test_session_e2e.py index f1f7cda63..69e166801 100644 --- a/python/e2e/test_session_e2e.py +++ b/python/e2e/test_session_e2e.py @@ -579,7 +579,7 @@ async def test_should_create_session_with_custom_config_dir(self, ctx: E2ETestCo custom_config_dir = os.path.join(ctx.home_dir, "custom-config") session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, config_dir=custom_config_dir + on_permission_request=PermissionHandler.approve_all, config_directory=custom_config_dir ) assert session.session_id diff --git a/python/test_client.py b/python/test_client.py index cb4f8f0ef..1d2919459 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -104,6 +104,89 @@ async def mock_request(method, params, **kwargs): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_create_and_resume_session_forward_reasoning_summary(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured = {} + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method in ("session.create", "session.resume"): + result = {"sessionId": params.get("sessionId") or "session-1"} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + return {} + + client._client.request = mock_request + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + reasoning_summary="concise", + ) + await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + reasoning_summary="none", + ) + + assert captured["session.create"]["reasoningSummary"] == "concise" + assert captured["session.resume"]["reasoningSummary"] == "none" + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_create_and_resume_session_forward_plugin_directories_and_large_output(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured = {} + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method in ("session.create", "session.resume"): + result = {"sessionId": params.get("sessionId") or "session-1"} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + return {} + + client._client.request = mock_request + + plugin_dirs = ["/tmp/plugins/a", "/tmp/plugins/b"] + large_output = { + "enabled": True, + "max_size_bytes": 1024, + "output_directory": "/tmp/large-output", + } + expected_large_output_wire = { + "enabled": True, + "maxSizeBytes": 1024, + "outputDir": "/tmp/large-output", + } + + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + plugin_directories=plugin_dirs, + large_output=large_output, + ) + await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + plugin_directories=plugin_dirs, + large_output=large_output, + ) + + assert captured["session.create"]["pluginDirectories"] == plugin_dirs + assert captured["session.create"]["largeOutput"] == expected_large_output_wire + assert captured["session.resume"]["pluginDirectories"] == plugin_dirs + assert captured["session.resume"]["largeOutput"] == expected_large_output_wire + finally: + await client.force_stop() + class TestURLParsing: def test_parse_port_only_url(self): @@ -965,9 +1048,10 @@ async def mock_request(method, params, **kwargs): return await original_request(method, params, **kwargs) client._client.request = mock_request - await session.set_model("gpt-4.1") + await session.set_model("gpt-4.1", reasoning_summary="detailed") assert captured["session.model.switchTo"]["sessionId"] == session.session_id assert captured["session.model.switchTo"]["modelId"] == "gpt-4.1" + assert captured["session.model.switchTo"]["reasoningSummary"] == "detailed" finally: await client.force_stop() diff --git a/rust/src/session.rs b/rust/src/session.rs index 93e48ac40..1fcf433a5 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -523,7 +523,7 @@ impl Session { let request = ModelSwitchToRequest { model_id: model.to_string(), reasoning_effort: opts.reasoning_effort, - reasoning_summary: None, + reasoning_summary: opts.reasoning_summary, model_capabilities: opts.model_capabilities, }; self.rpc().model().switch_to(request).await?; diff --git a/rust/src/types.rs b/rust/src/types.rs index 97d726994..bc5ad40e6 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -14,6 +14,7 @@ use serde_json::Value; use crate::canvas::{CanvasDeclaration, CanvasHandler}; use crate::generated::api_types::OpenCanvasInstance; +use crate::generated::session_events::ReasoningSummary; use crate::handler::{ AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, PermissionHandler, UserInputHandler, @@ -673,6 +674,54 @@ pub struct DefaultAgentConfig { pub excluded_tools: Option>, } +/// Configuration for large tool output handling. +/// +/// When a tool produces output exceeding [`max_size_bytes`](Self::max_size_bytes), +/// the SDK writes the full output to a file in [`output_directory`](Self::output_directory) +/// and returns a truncated preview to the model. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct LargeToolOutputConfig { + /// Whether large tool output handling is enabled. Defaults to `true` on the CLI. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, + /// Maximum tool output size in bytes before it is redirected to a file. + /// Defaults to 50KB on the CLI. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_size_bytes: Option, + /// Directory where large tool output files are written. Defaults to + /// the OS temp directory on the CLI. + #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")] + pub output_directory: Option, +} + +impl LargeToolOutputConfig { + /// Construct an empty [`LargeToolOutputConfig`]; all fields default to + /// unset (the CLI applies its own defaults). + pub fn new() -> Self { + Self::default() + } + + /// Toggle large tool output handling on or off. + pub fn with_enabled(mut self, enabled: bool) -> Self { + self.enabled = Some(enabled); + self + } + + /// Set the maximum tool output size in bytes before it is redirected to a file. + pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self { + self.max_size_bytes = Some(max_size_bytes); + self + } + + /// Set the directory where large tool output files are written. + pub fn with_output_directory>(mut self, output_directory: P) -> Self { + self.output_directory = Some(output_directory.into()); + self + } +} + /// Configures infinite sessions: persistent workspaces with automatic /// context-window compaction. /// @@ -1104,6 +1153,10 @@ pub struct SessionConfig { pub client_name: Option, /// Reasoning effort level (e.g. `"low"`, `"medium"`, `"high"`). pub reasoning_effort: Option, + /// Reasoning summary mode for models that support configurable + /// reasoning summaries. Use [`ReasoningSummary::None`] to suppress + /// summary output regardless of whether reasoning is enabled. + pub reasoning_summary: Option, /// Enable streaming token deltas via `assistant.message_delta` events. pub streaming: Option, /// Custom system message configuration. @@ -1136,6 +1189,10 @@ pub struct SessionConfig { /// Additional directories to search for custom instruction files. /// Forwarded to the CLI; not the same as [`skill_directories`](Self::skill_directories). pub instruction_directories: Option>, + /// Open Plugin directory paths passed through to the CLI. + pub plugin_directories: Option>, + /// Configuration for large tool output handling, forwarded to the CLI. + pub large_output: Option, /// Skill names to disable. Skills in this set will not be available /// even if found in skill directories. pub disabled_skills: Option>, @@ -1172,7 +1229,7 @@ pub struct SessionConfig { pub model_capabilities: Option, /// Override the default configuration directory location. When set, /// the session uses this directory for storing config and state. - pub config_dir: Option, + pub config_directory: Option, /// Working directory for the session. Tool operations resolve /// relative paths against this directory. pub working_directory: Option, @@ -1259,6 +1316,7 @@ impl std::fmt::Debug for SessionConfig { .field("model", &self.model) .field("client_name", &self.client_name) .field("reasoning_effort", &self.reasoning_effort) + .field("reasoning_summary", &self.reasoning_summary) .field("streaming", &self.streaming) .field("system_message", &self.system_message) .field("tools", &self.tools) @@ -1276,6 +1334,8 @@ impl std::fmt::Debug for SessionConfig { .field("enable_config_discovery", &self.enable_config_discovery) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) + .field("plugin_directories", &self.plugin_directories) + .field("large_output", &self.large_output) .field("disabled_skills", &self.disabled_skills) .field("hooks", &self.hooks) .field("custom_agents", &self.custom_agents) @@ -1285,7 +1345,7 @@ impl std::fmt::Debug for SessionConfig { .field("provider", &self.provider) .field("enable_session_telemetry", &self.enable_session_telemetry) .field("model_capabilities", &self.model_capabilities) - .field("config_dir", &self.config_dir) + .field("config_directory", &self.config_directory) .field("working_directory", &self.working_directory) .field( "github_token", @@ -1346,6 +1406,7 @@ impl Default for SessionConfig { model: None, client_name: None, reasoning_effort: None, + reasoning_summary: None, streaming: None, system_message: None, tools: None, @@ -1360,6 +1421,8 @@ impl Default for SessionConfig { enable_config_discovery: None, skill_directories: None, instruction_directories: None, + plugin_directories: None, + large_output: None, disabled_skills: None, hooks: None, custom_agents: None, @@ -1369,7 +1432,7 @@ impl Default for SessionConfig { provider: None, enable_session_telemetry: None, model_capabilities: None, - config_dir: None, + config_directory: None, working_directory: None, github_token: None, remote_session: None, @@ -1467,6 +1530,7 @@ impl SessionConfig { model: self.model, client_name: self.client_name, reasoning_effort: self.reasoning_effort, + reasoning_summary: self.reasoning_summary, streaming: self.streaming, system_message: self.system_message, tools: self.tools, @@ -1488,6 +1552,8 @@ impl SessionConfig { hooks: hooks_flag, skill_directories: self.skill_directories, instruction_directories: self.instruction_directories, + plugin_directories: self.plugin_directories, + large_output: self.large_output, disabled_skills: self.disabled_skills, custom_agents: self.custom_agents, default_agent: self.default_agent, @@ -1496,7 +1562,7 @@ impl SessionConfig { provider: self.provider, enable_session_telemetry: self.enable_session_telemetry, model_capabilities: self.model_capabilities, - config_dir: self.config_dir, + config_dir: self.config_directory, working_directory: self.working_directory, github_token: self.github_token, remote_session: self.remote_session, @@ -1648,6 +1714,12 @@ impl SessionConfig { self } + /// Set [`reasoning_summary`](Self::reasoning_summary). + pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self { + self.reasoning_summary = Some(summary); + self + } + /// Enable streaming token deltas via `assistant.message_delta` events. pub fn with_streaming(mut self, streaming: bool) -> Self { self.streaming = Some(streaming); @@ -1753,6 +1825,22 @@ impl SessionConfig { self } + /// Set Open Plugin directory paths passed through to the CLI on session create. + pub fn with_plugin_directories(mut self, paths: I) -> Self + where + I: IntoIterator, + P: Into, + { + self.plugin_directories = Some(paths.into_iter().map(Into::into).collect()); + self + } + + /// Set the [`LargeToolOutputConfig`] forwarded to the CLI on session create. + pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self { + self.large_output = Some(config); + self + } + /// Set the names of skills to disable (overrides skill discovery). pub fn with_disabled_skills(mut self, names: I) -> Self where @@ -1816,8 +1904,8 @@ impl SessionConfig { } /// Override the default configuration directory location. - pub fn with_config_dir(mut self, dir: impl Into) -> Self { - self.config_dir = Some(dir.into()); + pub fn with_config_directory(mut self, dir: impl Into) -> Self { + self.config_directory = Some(dir.into()); self } @@ -1900,6 +1988,10 @@ pub struct ResumeSessionConfig { pub client_name: Option, /// Desired reasoning effort to apply after resuming the session. pub reasoning_effort: Option, + /// Reasoning summary mode to apply after resuming the session. Use + /// [`ReasoningSummary::None`] to suppress summary output regardless of + /// whether reasoning is enabled. + pub reasoning_summary: Option, /// Enable streaming token deltas. pub streaming: Option, /// Re-supply the system message so the agent retains workspace context @@ -1933,6 +2025,10 @@ pub struct ResumeSessionConfig { /// Additional directories to search for custom instruction files on /// resume. Forwarded to the CLI; not the same as [`skill_directories`](Self::skill_directories). pub instruction_directories: Option>, + /// Open Plugin directory paths passed through to the CLI on resume. + pub plugin_directories: Option>, + /// Configuration for large tool output handling, forwarded to the CLI on resume. + pub large_output: Option, /// Skill names to disable on resume. pub disabled_skills: Option>, /// Enable session hooks on resume. @@ -1958,7 +2054,7 @@ pub struct ResumeSessionConfig { /// Per-property model capability overrides on resume. pub model_capabilities: Option, /// Override the default configuration directory location on resume. - pub config_dir: Option, + pub config_directory: Option, /// Per-session working directory on resume. pub working_directory: Option, /// Per-session GitHub token on resume. See @@ -2026,6 +2122,7 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("session_id", &self.session_id) .field("client_name", &self.client_name) .field("reasoning_effort", &self.reasoning_effort) + .field("reasoning_summary", &self.reasoning_summary) .field("streaming", &self.streaming) .field("system_message", &self.system_message) .field("tools", &self.tools) @@ -2044,6 +2141,8 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("enable_config_discovery", &self.enable_config_discovery) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) + .field("plugin_directories", &self.plugin_directories) + .field("large_output", &self.large_output) .field("disabled_skills", &self.disabled_skills) .field("hooks", &self.hooks) .field("custom_agents", &self.custom_agents) @@ -2053,7 +2152,7 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("provider", &self.provider) .field("enable_session_telemetry", &self.enable_session_telemetry) .field("model_capabilities", &self.model_capabilities) - .field("config_dir", &self.config_dir) + .field("config_directory", &self.config_directory) .field("working_directory", &self.working_directory) .field( "github_token", @@ -2151,6 +2250,7 @@ impl ResumeSessionConfig { session_id: self.session_id, client_name: self.client_name, reasoning_effort: self.reasoning_effort, + reasoning_summary: self.reasoning_summary, streaming: self.streaming, system_message: self.system_message, tools: self.tools, @@ -2173,6 +2273,8 @@ impl ResumeSessionConfig { hooks: hooks_flag, skill_directories: self.skill_directories, instruction_directories: self.instruction_directories, + plugin_directories: self.plugin_directories, + large_output: self.large_output, disabled_skills: self.disabled_skills, custom_agents: self.custom_agents, default_agent: self.default_agent, @@ -2181,7 +2283,7 @@ impl ResumeSessionConfig { provider: self.provider, enable_session_telemetry: self.enable_session_telemetry, model_capabilities: self.model_capabilities, - config_dir: self.config_dir, + config_dir: self.config_directory, working_directory: self.working_directory, github_token: self.github_token, remote_session: self.remote_session, @@ -2218,6 +2320,7 @@ impl ResumeSessionConfig { session_id, client_name: None, reasoning_effort: None, + reasoning_summary: None, streaming: None, system_message: None, tools: None, @@ -2233,6 +2336,8 @@ impl ResumeSessionConfig { enable_config_discovery: None, skill_directories: None, instruction_directories: None, + plugin_directories: None, + large_output: None, disabled_skills: None, hooks: None, custom_agents: None, @@ -2242,7 +2347,7 @@ impl ResumeSessionConfig { provider: None, enable_session_telemetry: None, model_capabilities: None, - config_dir: None, + config_directory: None, working_directory: None, github_token: None, remote_session: None, @@ -2366,6 +2471,12 @@ impl ResumeSessionConfig { self } + /// Set [`reasoning_summary`](Self::reasoning_summary). + pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self { + self.reasoning_summary = Some(summary); + self + } + /// Enable streaming token deltas via `assistant.message_delta` events. pub fn with_streaming(mut self, streaming: bool) -> Self { self.streaming = Some(streaming); @@ -2478,6 +2589,22 @@ impl ResumeSessionConfig { self } + /// Set Open Plugin directory paths passed through to the CLI on resume. + pub fn with_plugin_directories(mut self, paths: I) -> Self + where + I: IntoIterator, + P: Into, + { + self.plugin_directories = Some(paths.into_iter().map(Into::into).collect()); + self + } + + /// Set the [`LargeToolOutputConfig`] forwarded to the CLI on resume. + pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self { + self.large_output = Some(config); + self + } + /// Set the names of skills to disable on resume. pub fn with_disabled_skills(mut self, names: I) -> Self where @@ -2539,8 +2666,8 @@ impl ResumeSessionConfig { } /// Override the default configuration directory location on resume. - pub fn with_config_dir(mut self, dir: impl Into) -> Self { - self.config_dir = Some(dir.into()); + pub fn with_config_directory(mut self, dir: impl Into) -> Self { + self.config_directory = Some(dir.into()); self } @@ -2772,6 +2899,10 @@ pub struct SetModelOptions { /// Reasoning effort for the new model (e.g. `"low"`, `"medium"`, /// `"high"`, `"xhigh"`). pub reasoning_effort: Option, + /// Reasoning summary mode for the new model. Use + /// [`ReasoningSummary::None`] to suppress summary output regardless of + /// whether reasoning is enabled. + pub reasoning_summary: Option, /// Override individual model capabilities resolved by the runtime. Only /// fields set on the override are applied; the rest fall back to the /// runtime-resolved values for the model. @@ -2785,6 +2916,12 @@ impl SetModelOptions { self } + /// Set [`reasoning_summary`](Self::reasoning_summary). + pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self { + self.reasoning_summary = Some(summary); + self + } + /// Set [`model_capabilities`](Self::model_capabilities). pub fn with_model_capabilities( mut self, @@ -3747,9 +3884,10 @@ mod tests { use super::{ AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition, AttachmentSelectionRange, ConnectionState, CustomAgentConfig, DeliveryMode, ExtensionInfo, - GitHubReferenceType, InfiniteSessionConfig, ProviderConfig, ResumeSessionConfig, - SessionConfig, SessionEvent, SessionId, SystemMessageConfig, Tool, ToolBinaryResult, - ToolResult, ToolResultExpanded, ToolResultResponse, ensure_attachment_display_names, + GitHubReferenceType, InfiniteSessionConfig, LargeToolOutputConfig, ProviderConfig, + ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, + SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded, + ToolResultResponse, ensure_attachment_display_names, }; use crate::generated::session_events::TypedSessionEvent; @@ -3903,11 +4041,12 @@ mod tests { use super::{CloudSessionOptions, CloudSessionRepository}; let mut cfg = SessionConfig::default(); - cfg.config_dir = Some(PathBuf::from("/tmp/cfg")); + cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); cfg.working_directory = Some(PathBuf::from("/tmp/work")); cfg.github_token = Some("ghs_secret".to_string()); cfg.include_sub_agent_streaming_events = Some(false); cfg.enable_session_telemetry = Some(false); + cfg.reasoning_summary = Some(ReasoningSummary::Concise); cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export); cfg.cloud = Some(CloudSessionOptions::with_repository( CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"), @@ -3923,6 +4062,7 @@ mod tests { assert_eq!(wire_json["gitHubToken"], "ghs_secret"); assert_eq!(wire_json["includeSubAgentStreamingEvents"], false); assert_eq!(wire_json["enableSessionTelemetry"], false); + assert_eq!(wire_json["reasoningSummary"], "concise"); assert_eq!(wire_json["remoteSession"], "export"); assert_eq!(wire_json["cloud"]["repository"]["owner"], "github"); assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk"); @@ -3935,20 +4075,52 @@ mod tests { let empty_json = serde_json::to_value(&empty_wire).unwrap(); assert!(empty_json.get("gitHubToken").is_none()); assert!(empty_json.get("enableSessionTelemetry").is_none()); + assert!(empty_json.get("reasoningSummary").is_none()); assert!(empty_json.get("remoteSession").is_none()); assert!(empty_json.get("cloud").is_none()); } + #[test] + fn session_config_into_wire_serializes_plugin_directories_and_large_output() { + use std::path::PathBuf; + + let mut cfg = SessionConfig::default(); + cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins")]); + cfg.large_output = Some( + LargeToolOutputConfig::new() + .with_enabled(true) + .with_max_size_bytes(1024) + .with_output_directory(PathBuf::from("/tmp/large-output")), + ); + + let (wire, _) = cfg + .into_wire(Some(SessionId::from("sess-1"))) + .expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins"); + assert_eq!(wire_json["largeOutput"]["enabled"], true); + assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024); + assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output"); + + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("empty"))) + .expect("default has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("pluginDirectories").is_none()); + assert!(empty_json.get("largeOutput").is_none()); + } + #[test] fn resume_session_config_into_wire_serializes_bucket_b_fields() { use std::path::PathBuf; let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); cfg.working_directory = Some(PathBuf::from("/tmp/work")); - cfg.config_dir = Some(PathBuf::from("/tmp/cfg")); + cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); cfg.github_token = Some("ghs_secret".to_string()); cfg.include_sub_agent_streaming_events = Some(true); cfg.enable_session_telemetry = Some(false); + cfg.reasoning_summary = Some(ReasoningSummary::Detailed); cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On); let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); @@ -3959,6 +4131,7 @@ mod tests { assert_eq!(wire_json["gitHubToken"], "ghs_secret"); assert_eq!(wire_json["includeSubAgentStreamingEvents"], true); assert_eq!(wire_json["enableSessionTelemetry"], false); + assert_eq!(wire_json["reasoningSummary"], "detailed"); assert_eq!(wire_json["remoteSession"], "on"); // Unset remote_session is omitted on the wire. @@ -3966,9 +4139,38 @@ mod tests { .into_wire() .expect("default resume has no duplicate handlers"); let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("reasoningSummary").is_none()); assert!(empty_json.get("remoteSession").is_none()); } + #[test] + fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() { + use std::path::PathBuf; + + let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); + cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]); + cfg.large_output = Some( + LargeToolOutputConfig::new() + .with_enabled(false) + .with_max_size_bytes(2048) + .with_output_directory(PathBuf::from("/tmp/large-output-r")), + ); + + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r"); + assert_eq!(wire_json["largeOutput"]["enabled"], false); + assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048); + assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r"); + + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("default resume has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("pluginDirectories").is_none()); + assert!(empty_json.get("largeOutput").is_none()); + } + #[test] fn session_config_builder_composes() { use std::collections::HashMap; @@ -3978,6 +4180,7 @@ mod tests { .with_model("claude-sonnet-4") .with_client_name("test-app") .with_reasoning_effort("medium") + .with_reasoning_summary(ReasoningSummary::Concise) .with_streaming(true) .with_tools([Tool::new("greet")]) .with_available_tools(["bash", "view"]) @@ -3987,7 +4190,7 @@ mod tests { .with_skill_directories([PathBuf::from("/tmp/skills")]) .with_disabled_skills(["broken-skill"]) .with_agent("researcher") - .with_config_dir(PathBuf::from("/tmp/config")) + .with_config_directory(PathBuf::from("/tmp/config")) .with_working_directory(PathBuf::from("/tmp/work")) .with_github_token("ghp_test") .with_enable_session_telemetry(false) @@ -3998,6 +4201,7 @@ mod tests { assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4")); assert_eq!(cfg.client_name.as_deref(), Some("test-app")); assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium")); + assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise)); assert_eq!(cfg.streaming, Some(true)); assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1)); assert_eq!( @@ -4019,7 +4223,7 @@ mod tests { Some(&["broken-skill".to_string()][..]) ); assert_eq!(cfg.agent.as_deref(), Some("researcher")); - assert_eq!(cfg.config_dir, Some(PathBuf::from("/tmp/config"))); + assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); assert_eq!(cfg.enable_session_telemetry, Some(false)); @@ -4036,6 +4240,7 @@ mod tests { let cfg = ResumeSessionConfig::new(SessionId::from("sess-2")) .with_client_name("test-app") + .with_reasoning_summary(ReasoningSummary::None) .with_streaming(true) .with_tools([Tool::new("greet")]) .with_available_tools(["bash", "view"]) @@ -4045,7 +4250,7 @@ mod tests { .with_skill_directories([PathBuf::from("/tmp/skills")]) .with_disabled_skills(["broken-skill"]) .with_agent("researcher") - .with_config_dir(PathBuf::from("/tmp/config")) + .with_config_directory(PathBuf::from("/tmp/config")) .with_working_directory(PathBuf::from("/tmp/work")) .with_github_token("ghp_test") .with_enable_session_telemetry(false) @@ -4056,6 +4261,7 @@ mod tests { assert_eq!(cfg.session_id.as_str(), "sess-2"); assert_eq!(cfg.client_name.as_deref(), Some("test-app")); + assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None)); assert_eq!(cfg.streaming, Some(true)); assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1)); assert_eq!( @@ -4077,7 +4283,7 @@ mod tests { Some(&["broken-skill".to_string()][..]) ); assert_eq!(cfg.agent.as_deref(), Some("researcher")); - assert_eq!(cfg.config_dir, Some(PathBuf::from("/tmp/config"))); + assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); assert_eq!(cfg.enable_session_telemetry, Some(false)); diff --git a/rust/src/wire.rs b/rust/src/wire.rs index 15d137760..c5d7eb632 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -22,9 +22,11 @@ use crate::canvas::CanvasDeclaration; use crate::generated::api_types::{ ModelCapabilitiesOverride, OpenCanvasInstance, RemoteSessionMode, }; +use crate::generated::session_events::ReasoningSummary; use crate::types::{ CloudSessionOptions, CustomAgentConfig, DefaultAgentConfig, ExtensionInfo, - InfiniteSessionConfig, McpServerConfig, ProviderConfig, SessionId, SystemMessageConfig, Tool, + InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, ProviderConfig, SessionId, + SystemMessageConfig, Tool, }; /// Wire representation of a slash command (name + description only). The @@ -51,6 +53,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub streaming: Option, #[serde(skip_serializing_if = "Option::is_none")] pub system_message: Option, @@ -87,6 +91,10 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub instruction_directories: Option>, #[serde(skip_serializing_if = "Option::is_none")] + pub plugin_directories: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub large_output: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub disabled_skills: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub custom_agents: Option>, @@ -128,6 +136,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub streaming: Option, #[serde(skip_serializing_if = "Option::is_none")] pub system_message: Option, @@ -165,6 +175,10 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub instruction_directories: Option>, #[serde(skip_serializing_if = "Option::is_none")] + pub plugin_directories: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub large_output: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub disabled_skills: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub custom_agents: Option>, diff --git a/rust/tests/e2e/session.rs b/rust/tests/e2e/session.rs index 8e2e65a46..67ee48489 100644 --- a/rust/tests/e2e/session.rs +++ b/rust/tests/e2e/session.rs @@ -836,7 +836,7 @@ async fn should_create_session_with_custom_config_dir() { let session = client .create_session( ctx.approve_all_session_config() - .with_config_dir(custom_config_dir), + .with_config_directory(custom_config_dir), ) .await .expect("create session"); diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index b8868ba37..85d9a6f3f 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -11,6 +11,7 @@ use github_copilot_sdk::generated::api_types::{ CanvasInstanceAvailability, CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult, OpenCanvasInstance, }; +use github_copilot_sdk::generated::session_events::ReasoningSummary; use github_copilot_sdk::handler::{ ApproveAllHandler, AutoModeSwitchHandler, AutoModeSwitchResponse, ElicitationHandler, ExitPlanModeHandler, ExitPlanModeResult, UserInputHandler, UserInputResponse, @@ -18,7 +19,7 @@ use github_copilot_sdk::handler::{ use github_copilot_sdk::types::{ CommandContext, CommandDefinition, CommandHandler, DeliveryMode, ElicitationRequest, ElicitationResult, ExitPlanModeData, ExtensionInfo, MessageOptions, RequestId, SessionConfig, - SessionId, Tool, ToolInvocation, ToolResult, + SessionId, SetModelOptions, Tool, ToolInvocation, ToolResult, }; use github_copilot_sdk::{Client, tool}; use serde_json::Value; @@ -1283,12 +1284,24 @@ async fn set_model_sends_switch_to_request() { let handle = tokio::spawn({ let session = session.clone(); - async move { session.set_model("claude-sonnet-4", None).await.unwrap() } + async move { + session + .set_model( + "claude-sonnet-4", + Some( + SetModelOptions::default() + .with_reasoning_summary(ReasoningSummary::Detailed), + ), + ) + .await + .unwrap() + } }); let request = server.read_request().await; assert_eq!(request["method"], "session.model.switchTo"); assert_eq!(request["params"]["modelId"], "claude-sonnet-4"); + assert_eq!(request["params"]["reasoningSummary"], "detailed"); server .respond( &request, @@ -3075,7 +3088,7 @@ fn session_config_serializes_bucket_b_fields() { let mut cfg = SessionConfig::default(); cfg.session_id = Some(SessionId::from("custom-id")); - cfg.config_dir = Some(PathBuf::from("/tmp/cfg")); + cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); cfg.working_directory = Some(PathBuf::from("/tmp/work")); cfg.github_token = Some("ghs_secret".to_string()); cfg.include_sub_agent_streaming_events = Some(false); @@ -3102,7 +3115,7 @@ fn resume_session_config_serializes_bucket_b_fields() { let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); cfg.working_directory = Some(PathBuf::from("/tmp/work")); - cfg.config_dir = Some(PathBuf::from("/tmp/cfg")); + cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); cfg.github_token = Some("ghs_secret".to_string()); cfg.include_sub_agent_streaming_events = Some(true); cfg.enable_session_telemetry = Some(false); From ab81060419a425c0a71265172d0e1b0d63ecb530 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 13:41:36 -0400 Subject: [PATCH 2/3] Fix CI: formatters and linters across SDKs - python: ruff isort fix (separate aliased import) - nodejs: prettier format on test/client.test.ts - go: gofmt alignment in client_test.go and session_e2e_test.go - rust: clippy field_reassign_with_default fix in types.rs test - java: spotless:apply across 10 files (line wrap, javadoc multi-line) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- go/client_test.go | 6 +++--- go/internal/e2e/session_e2e_test.go | 2 +- .../com/github/copilot/CopilotSession.java | 10 ++++------ .../copilot/rpc/CreateSessionRequest.java | 5 ++++- .../copilot/rpc/LargeToolOutputConfig.java | 4 ++-- .../copilot/rpc/ResumeSessionConfig.java | 4 ++-- .../copilot/rpc/ResumeSessionRequest.java | 5 ++++- .../com/github/copilot/rpc/SessionConfig.java | 4 ++-- .../com/github/copilot/ConfigCloneTest.java | 6 ++++-- .../copilot/SessionRequestBuilderTest.java | 16 ++++------------ nodejs/test/client.test.ts | 8 ++++++-- python/copilot/session.py | 4 +++- rust/src/types.rs | 18 ++++++++++-------- 13 files changed, 49 insertions(+), 43 deletions(-) diff --git a/go/client_test.go b/go/client_test.go index 6e619bbf5..6340a7370 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -439,9 +439,9 @@ func TestSessionRequests_PluginDirectoriesAndLargeOutput(t *testing.T) { enabled := true maxBytes := 1024 largeOutput := &LargeToolOutputConfig{ - Enabled: &enabled, - MaxSizeBytes: &maxBytes, - OutputDirectory: "/tmp/large-output", + Enabled: &enabled, + MaxSizeBytes: &maxBytes, + OutputDirectory: "/tmp/large-output", } expectedLargeOutput := map[string]any{ diff --git a/go/internal/e2e/session_e2e_test.go b/go/internal/e2e/session_e2e_test.go index 4343355a4..f73bef7ea 100644 --- a/go/internal/e2e/session_e2e_test.go +++ b/go/internal/e2e/session_e2e_test.go @@ -760,7 +760,7 @@ func TestSessionE2E(t *testing.T) { customConfigDir := ctx.HomeDir + "/custom-config" session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - ConfigDirectory: customConfigDir, + ConfigDirectory: customConfigDir, }) if err != nil { t.Fatalf("Failed to create session with custom config dir: %v", err) diff --git a/java/src/main/java/com/github/copilot/CopilotSession.java b/java/src/main/java/com/github/copilot/CopilotSession.java index acd58ae32..64c02d8b8 100644 --- a/java/src/main/java/com/github/copilot/CopilotSession.java +++ b/java/src/main/java/com/github/copilot/CopilotSession.java @@ -1774,13 +1774,11 @@ public CompletableFuture setModel(String model, String reasoningEffort, St } generatedCapabilities = new ModelCapabilitiesOverride(supports, limits); } - var generatedReasoningSummary = reasoningSummary == null ? null + var generatedReasoningSummary = reasoningSummary == null + ? null : com.github.copilot.generated.rpc.ReasoningSummary.fromValue(reasoningSummary); - return getRpc().model - .switchTo( - new SessionModelSwitchToParams(sessionId, model, reasoningEffort, generatedReasoningSummary, - generatedCapabilities)) - .thenApply(r -> null); + return getRpc().model.switchTo(new SessionModelSwitchToParams(sessionId, model, reasoningEffort, + generatedReasoningSummary, generatedCapabilities)).thenApply(r -> null); } /** diff --git a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index d14bb35ca..9778be921 100644 --- a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -188,7 +188,10 @@ public String getReasoningSummary() { return reasoningSummary; } - /** Sets the reasoning summary mode. @param reasoningSummary the reasoning summary mode */ + /** + * Sets the reasoning summary mode. @param reasoningSummary the reasoning + * summary mode + */ public void setReasoningSummary(String reasoningSummary) { this.reasoningSummary = reasoningSummary; } diff --git a/java/src/main/java/com/github/copilot/rpc/LargeToolOutputConfig.java b/java/src/main/java/com/github/copilot/rpc/LargeToolOutputConfig.java index 50a3cf4dd..1694761e2 100644 --- a/java/src/main/java/com/github/copilot/rpc/LargeToolOutputConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/LargeToolOutputConfig.java @@ -7,8 +7,8 @@ * Configuration for large tool output handling. *

* When a tool produces output exceeding {@link #getMaxSizeBytes()}, the SDK - * writes the full output to a file in {@link #getOutputDirectory()} and returns a - * truncated preview to the model. + * writes the full output to a file in {@link #getOutputDirectory()} and returns + * a truncated preview to the model. * * @since 1.3.0 */ diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index 32349be09..6d56d35af 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -482,8 +482,8 @@ public String getReasoningSummary() { /** * Sets the reasoning summary mode for models that support configurable - * reasoning summaries. Use {@code "none"} to suppress summary output - * regardless of whether reasoning is enabled. + * reasoning summaries. Use {@code "none"} to suppress summary output regardless + * of whether reasoning is enabled. * * @param reasoningSummary * the reasoning summary mode diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index 78e8dfeed..fa9e2da8e 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -190,7 +190,10 @@ public String getReasoningSummary() { return reasoningSummary; } - /** Sets the reasoning summary mode. @param reasoningSummary the reasoning summary mode */ + /** + * Sets the reasoning summary mode. @param reasoningSummary the reasoning + * summary mode + */ public void setReasoningSummary(String reasoningSummary) { this.reasoningSummary = reasoningSummary; } diff --git a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java index b97abb616..155ce165d 100644 --- a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -185,8 +185,8 @@ public String getReasoningSummary() { /** * Sets the reasoning summary mode for models that support configurable - * reasoning summaries. Use {@code "none"} to suppress summary output - * regardless of whether reasoning is enabled. + * reasoning summaries. Use {@code "none"} to suppress summary output regardless + * of whether reasoning is enabled. * * @param reasoningSummary * the reasoning summary mode diff --git a/java/src/test/java/com/github/copilot/ConfigCloneTest.java b/java/src/test/java/com/github/copilot/ConfigCloneTest.java index ad08e5353..e40a3048b 100644 --- a/java/src/test/java/com/github/copilot/ConfigCloneTest.java +++ b/java/src/test/java/com/github/copilot/ConfigCloneTest.java @@ -117,7 +117,8 @@ void sessionConfigCloneBasic() { original.setModel("gpt-4o"); original.setReasoningSummary("detailed"); original.setPluginDirectories(List.of("/plugins/a", "/plugins/b")); - original.setLargeOutput(new LargeToolOutputConfig().setEnabled(true).setMaxSizeBytes(1024L).setOutputDirectory("/tmp/out")); + original.setLargeOutput( + new LargeToolOutputConfig().setEnabled(true).setMaxSizeBytes(1024L).setOutputDirectory("/tmp/out")); original.setStreaming(true); SessionConfig cloned = original.clone(); @@ -171,7 +172,8 @@ void resumeSessionConfigCloneBasic() { original.setModel("o1"); original.setReasoningSummary("none"); original.setPluginDirectories(List.of("/plugins/r")); - original.setLargeOutput(new LargeToolOutputConfig().setEnabled(false).setMaxSizeBytes(2048L).setOutputDirectory("/tmp/resume")); + original.setLargeOutput( + new LargeToolOutputConfig().setEnabled(false).setMaxSizeBytes(2048L).setOutputDirectory("/tmp/resume")); original.setStreaming(false); ResumeSessionConfig cloned = original.clone(); diff --git a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index df1dfe749..b10e6bd8e 100644 --- a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -100,13 +100,9 @@ void testBuildCreateRequestSetsReasoningSummary() { @Test void testBuildCreateRequestSetsPluginDirectoriesAndLargeOutput() { - var largeOutput = new LargeToolOutputConfig() - .setEnabled(true) - .setMaxSizeBytes(1024L) + var largeOutput = new LargeToolOutputConfig().setEnabled(true).setMaxSizeBytes(1024L) .setOutputDirectory("/tmp/out"); - var config = new SessionConfig() - .setPluginDirectories(List.of("/plugins/a")) - .setLargeOutput(largeOutput); + var config = new SessionConfig().setPluginDirectories(List.of("/plugins/a")).setLargeOutput(largeOutput); CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); assertEquals(List.of("/plugins/a"), request.getPluginDirectories()); assertEquals(largeOutput, request.getLargeOutput()); @@ -243,13 +239,9 @@ void testBuildResumeRequestSetsReasoningSummary() { @Test void testBuildResumeRequestSetsPluginDirectoriesAndLargeOutput() { - var largeOutput = new LargeToolOutputConfig() - .setEnabled(false) - .setMaxSizeBytes(2048L) + var largeOutput = new LargeToolOutputConfig().setEnabled(false).setMaxSizeBytes(2048L) .setOutputDirectory("/tmp/resume"); - var config = new ResumeSessionConfig() - .setPluginDirectories(List.of("/plugins/r")) - .setLargeOutput(largeOutput); + var config = new ResumeSessionConfig().setPluginDirectories(List.of("/plugins/r")).setLargeOutput(largeOutput); ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-12", config); assertEquals(List.of("/plugins/r"), request.getPluginDirectories()); assertEquals(largeOutput, request.getLargeOutput()); diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index c8d6a5fd8..ac29c6aa1 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -128,8 +128,12 @@ describe("CopilotClient", () => { reasoningSummary: "none", }); - const createPayload = spy.mock.calls.find(([method]) => method === "session.create")![1] as any; - const resumePayload = spy.mock.calls.find(([method]) => method === "session.resume")![1] as any; + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + const resumePayload = spy.mock.calls.find( + ([method]) => method === "session.resume" + )![1] as any; expect(createPayload.reasoningSummary).toBe("concise"); expect(resumePayload.reasoningSummary).toBe("none"); }); diff --git a/python/copilot/session.py b/python/copilot/session.py index 5beab450c..95a74c9c6 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -64,12 +64,14 @@ ExternalToolRequestedData, PermissionRequest, PermissionRequestedData, - ReasoningSummary as _RpcReasoningSummary, SessionErrorData, SessionEvent, SessionIdleData, session_event_from_dict, ) +from .generated.session_events import ( + ReasoningSummary as _RpcReasoningSummary, +) from .tools import Tool, ToolHandler, ToolInvocation, ToolResult logger = logging.getLogger(__name__) diff --git a/rust/src/types.rs b/rust/src/types.rs index bc5ad40e6..6534d8bee 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -4084,14 +4084,16 @@ mod tests { fn session_config_into_wire_serializes_plugin_directories_and_large_output() { use std::path::PathBuf; - let mut cfg = SessionConfig::default(); - cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins")]); - cfg.large_output = Some( - LargeToolOutputConfig::new() - .with_enabled(true) - .with_max_size_bytes(1024) - .with_output_directory(PathBuf::from("/tmp/large-output")), - ); + let cfg = SessionConfig { + plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]), + large_output: Some( + LargeToolOutputConfig::new() + .with_enabled(true) + .with_max_size_bytes(1024) + .with_output_directory(PathBuf::from("/tmp/large-output")), + ), + ..Default::default() + }; let (wire, _) = cfg .into_wire(Some(SessionId::from("sess-1"))) From c6c4eb85c29d84fc346a992e2afc5e88e587fbe0 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 13:49:49 -0400 Subject: [PATCH 3/3] Go: type MaxSizeBytes as *int64 to match other SDKs Reviewer flagged that `*int` is architecture-dependent and narrower than the u64/long/Long used in the other SDKs. Switch to `*int64` (matches Java `Long` and .NET `long?`) and update the test literal cast. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- go/client_test.go | 2 +- go/types.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go/client_test.go b/go/client_test.go index 6340a7370..6e4e935cf 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -437,7 +437,7 @@ func TestSessionRequests_ReasoningSummary(t *testing.T) { func TestSessionRequests_PluginDirectoriesAndLargeOutput(t *testing.T) { pluginDirs := []string{"/tmp/plugins/a", "/tmp/plugins/b"} enabled := true - maxBytes := 1024 + maxBytes := int64(1024) largeOutput := &LargeToolOutputConfig{ Enabled: &enabled, MaxSizeBytes: &maxBytes, diff --git a/go/types.go b/go/types.go index b48463208..f66c6ffcb 100644 --- a/go/types.go +++ b/go/types.go @@ -853,7 +853,7 @@ type LargeToolOutputConfig struct { Enabled *bool `json:"enabled,omitempty"` // MaxSizeBytes is the maximum size in bytes before output is written to a // temp file. Default: 50KB. - MaxSizeBytes *int `json:"maxSizeBytes,omitempty"` + MaxSizeBytes *int64 `json:"maxSizeBytes,omitempty"` // OutputDirectory is the directory to write temp files to. Defaults to the OS // temp directory. OutputDirectory string `json:"outputDir,omitempty"`