From 2f43b384b4396f3e836ad0c72c8cd6049e03b0bb Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 14 Jul 2026 15:59:24 +0200 Subject: [PATCH 1/5] Add custom agent reasoning effort Expose an optional per-agent reasoning effort across every SDK binding while preserving omission semantics and the reasoningEffort wire name. Add focused serialization, cloning, builder, and DTO coverage plus custom-agent documentation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 32727fc4-2ac8-41eb-a68a-e74674ecf155 --- docs/features/custom-agents.md | 4 +++ dotnet/src/Types.cs | 7 ++++ dotnet/test/Unit/CloneTests.cs | 3 +- dotnet/test/Unit/SerializationTests.cs | 31 +++++++++++++++++ go/types.go | 3 ++ go/types_test.go | 25 ++++++++++++++ .../github/copilot/rpc/CustomAgentConfig.java | 26 +++++++++++++++ .../copilot/DataObjectCoverageTest.java | 32 +++++++++++++++++- nodejs/src/types.ts | 5 +++ nodejs/test/client.test.ts | 10 ++++-- python/copilot/client.py | 2 ++ python/copilot/session.py | 2 ++ python/test_client.py | 26 +++++++++++++++ rust/src/types.rs | 33 +++++++++++++++++++ 14 files changed, 205 insertions(+), 4 deletions(-) diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index e43aca1b80..8cec242268 100644 --- a/docs/features/custom-agents.md +++ b/docs/features/custom-agents.md @@ -253,10 +253,14 @@ try (var client = new CopilotClient()) { | `mcpServers` | `object` | | MCP server configurations specific to this agent | | `infer` | `boolean` | | Whether the runtime can auto-select this agent (default: `true`) | | `skills` | `string[]` | | Skill names to preload into the agent's context at startup | +| `model` | `string` | | Model identifier to use while this agent runs | +| `reasoningEffort` | `string` | | Reasoning effort to use while this agent runs | > [!TIP] > A good `description` helps the runtime match user intent to the right agent. Be specific about the agent's expertise and capabilities. +Set `model` and `reasoningEffort` to override the parent session's model settings while a custom agent runs. Omit either property to leave that setting unspecified; the SDK does not add a per-agent default. Python uses `reasoning_effort`, .NET uses `ReasoningEffort`, Go uses `ReasoningEffort`, Java uses `setReasoningEffort`, and Rust uses `with_reasoning_effort`. + In addition to per-agent configuration above, you can set `agent` on the **session config** itself to pre-select which custom agent is active when the session starts. See [Selecting an Agent at Session Creation](#selecting-an-agent-at-session-creation) below. | Session Config Property | Type | Description | diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index f893baf5e5..35792997e7 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2657,6 +2657,13 @@ public sealed class CustomAgentConfig /// [JsonPropertyName("model")] public string? Model { get; set; } + + /// + /// Reasoning effort level for this agent's model. + /// When omitted, no per-agent reasoning effort is sent to the runtime. + /// + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } } /// diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index 425b580a1b..ec509ab169 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -82,7 +82,7 @@ public void SessionConfig_Clone_CopiesAllProperties() IncludeSubAgentStreamingEvents = false, McpServers = new Dictionary { ["server1"] = new McpStdioServerConfig { Command = "echo" } }, McpOAuthTokenStorage = McpOAuthTokenStorageMode.Persistent, - CustomAgents = [new CustomAgentConfig { Name = "agent1", Model = "claude-haiku-4.5" }], + CustomAgents = [new CustomAgentConfig { Name = "agent1", Model = "claude-haiku-4.5", ReasoningEffort = "high" }], Agent = "agent1", Capi = new CapiSessionOptions { EnableWebSocketResponses = false }, Cloud = new CloudSessionOptions @@ -128,6 +128,7 @@ public void SessionConfig_Clone_CopiesAllProperties() Assert.Equal(original.McpOAuthTokenStorage, clone.McpOAuthTokenStorage); Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count); Assert.Equal(original.CustomAgents[0].Model, clone.CustomAgents[0].Model); + Assert.Equal(original.CustomAgents[0].ReasoningEffort, clone.CustomAgents[0].ReasoningEffort); Assert.Equal(original.Agent, clone.Agent); Assert.Same(original.Capi, clone.Capi); Assert.Same(original.Cloud, clone.Cloud); diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index ae7e885138..0782295892 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -16,6 +16,37 @@ namespace GitHub.Copilot.Test.Unit; /// public class SerializationTests { + [Fact] + public void CustomAgentConfig_SerializesReasoningEffort_WithSdkOptions() + { + var options = GetSerializerOptions(); + var original = new CustomAgentConfig + { + Name = "reasoning-agent", + Prompt = "Think carefully.", + ReasoningEffort = "high" + }; + + var json = JsonSerializer.Serialize(original, options); + using var document = JsonDocument.Parse(json); + Assert.Equal("high", document.RootElement.GetProperty("reasoningEffort").GetString()); + } + + [Fact] + public void CustomAgentConfig_OmitsReasoningEffortWhenUnset_WithSdkOptions() + { + var options = GetSerializerOptions(); + var original = new CustomAgentConfig + { + Name = "default-agent", + Prompt = "Use runtime defaults." + }; + + var json = JsonSerializer.Serialize(original, options); + using var document = JsonDocument.Parse(json); + Assert.False(document.RootElement.TryGetProperty("reasoningEffort", out _)); + } + [Fact] public void ProviderConfig_CanSerializeHeaders_WithSdkOptions() { diff --git a/go/types.go b/go/types.go index c9dd71d19b..1c76c117f0 100644 --- a/go/types.go +++ b/go/types.go @@ -949,6 +949,9 @@ type CustomAgentConfig struct { // When set, the runtime will attempt to use this model for the agent, // falling back to the parent session model if unavailable. Model string `json:"model,omitempty"` + // ReasoningEffort is the reasoning effort level for this agent's model. + // When empty, no per-agent reasoning effort is sent to the runtime. + ReasoningEffort string `json:"reasoningEffort,omitempty"` } // DefaultAgentConfig configures the default agent (the built-in agent that handles turns when no custom agent is selected). diff --git a/go/types_test.go b/go/types_test.go index b0349de292..a76ebaad40 100644 --- a/go/types_test.go +++ b/go/types_test.go @@ -152,6 +152,28 @@ func TestCustomAgentConfig_JSONIncludesModel(t *testing.T) { } } +func TestCustomAgentConfig_JSONIncludesReasoningEffort(t *testing.T) { + cfg := CustomAgentConfig{ + Name: "reasoning-agent", + Prompt: "Think carefully.", + ReasoningEffort: "high", + } + + data, err := json.Marshal(cfg) + if err != nil { + t.Fatalf("failed to marshal CustomAgentConfig: %v", err) + } + + var decoded map[string]any + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("failed to unmarshal CustomAgentConfig: %v", err) + } + + if decoded["reasoningEffort"] != "high" { + t.Errorf("expected reasoningEffort 'high', got %v", decoded["reasoningEffort"]) + } +} + func TestCustomAgentConfig_JSONIncludesEmptyTools(t *testing.T) { cfg := CustomAgentConfig{ Name: "no-tools-agent", @@ -273,6 +295,9 @@ func TestCustomAgentConfig_JSONOmitsModelWhenEmpty(t *testing.T) { if _, present := decoded["model"]; present { t.Errorf("expected model to be omitted when empty, got %v", decoded["model"]) } + if _, present := decoded["reasoningEffort"]; present { + t.Errorf("expected reasoningEffort to be omitted when empty, got %v", decoded["reasoningEffort"]) + } } func TestTool_JSONIncludesEmptyParameters(t *testing.T) { diff --git a/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java b/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java index 5136f77786..35e128f762 100644 --- a/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java @@ -63,6 +63,9 @@ public class CustomAgentConfig { @JsonProperty("model") private String model; + @JsonProperty("reasoningEffort") + private String reasoningEffort; + /** * Gets the unique identifier name for this agent. * @@ -282,4 +285,27 @@ public CustomAgentConfig setModel(String model) { this.model = model; return this; } + + /** + * Gets the reasoning effort level for this agent's model. + * + * @return the reasoning effort level, or {@code null} if not set + */ + public String getReasoningEffort() { + return reasoningEffort; + } + + /** + * Sets the reasoning effort level for this agent's model. + *

+ * When omitted, no per-agent reasoning effort is sent to the runtime. + * + * @param reasoningEffort + * the reasoning effort level + * @return this config for method chaining + */ + public CustomAgentConfig setReasoningEffort(String reasoningEffort) { + this.reasoningEffort = reasoningEffort; + return this; + } } diff --git a/java/src/test/java/com/github/copilot/DataObjectCoverageTest.java b/java/src/test/java/com/github/copilot/DataObjectCoverageTest.java index ece824234b..f38a038368 100644 --- a/java/src/test/java/com/github/copilot/DataObjectCoverageTest.java +++ b/java/src/test/java/com/github/copilot/DataObjectCoverageTest.java @@ -186,7 +186,7 @@ void postToolUseHookInputSessionIdRoundTrip() { assertEquals("session-xyz", input.getSessionId()); } - // ===== CustomAgentConfig model field ===== + // ===== CustomAgentConfig model fields ===== @Test void customAgentConfigModelGetterAndSetter() { @@ -227,6 +227,36 @@ void customAgentConfigModelOmittedWhenNull() throws Exception { assertFalse(json.contains("\"model\"")); } + @Test + void customAgentConfigReasoningEffortGetterAndFluentSetter() { + var cfg = new CustomAgentConfig(); + assertNull(cfg.getReasoningEffort()); + + var result = cfg.setReasoningEffort("high"); + assertSame(cfg, result); + assertEquals("high", cfg.getReasoningEffort()); + } + + @Test + void customAgentConfigReasoningEffortSerializationRoundTrip() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var cfg = new CustomAgentConfig().setName("reasoning-agent").setReasoningEffort("high"); + + var json = mapper.writeValueAsString(cfg); + assertTrue(json.contains("\"reasoningEffort\":\"high\"")); + + var deserialized = mapper.readValue(json, CustomAgentConfig.class); + assertEquals("high", deserialized.getReasoningEffort()); + } + + @Test + void customAgentConfigReasoningEffortOmittedWhenNull() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(new CustomAgentConfig().setName("default-agent")); + + assertFalse(json.contains("\"reasoningEffort\"")); + } + // ===== PermissionRequestResult setRules ===== @Test diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 12ab0860b0..b90f7e1da8 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1632,6 +1632,11 @@ export interface CustomAgentConfig { * falling back to the parent session model if unavailable. */ model?: string; + /** + * Reasoning effort level for this agent's model. + * When omitted, the SDK does not send a per-agent reasoning effort. + */ + reasoningEffort?: ReasoningEffort; } /** diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 07be2b95e3..695ffe0f07 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -2216,9 +2216,10 @@ describe("CopilotClient", () => { const payload = spy.mock.calls.find((c) => c[0] === "session.create")![1] as any; expect(payload.agent).toBe("test-agent"); expect(payload.customAgents).toEqual([expect.objectContaining({ name: "test-agent" })]); + expect(payload.customAgents[0].reasoningEffort).toBeUndefined(); }); - it("forwards custom agent model in session.create request", async () => { + it("forwards custom agent model and reasoning effort in session.create request", async () => { const client = new CopilotClient(); await client.start(); onTestFinished(() => stopClient(client)); @@ -2231,13 +2232,18 @@ describe("CopilotClient", () => { name: "model-agent", prompt: "You are a model agent.", model: "claude-haiku-4.5", + reasoningEffort: "high", }, ], }); const payload = spy.mock.calls.find((c) => c[0] === "session.create")![1] as any; expect(payload.customAgents).toEqual([ - expect.objectContaining({ name: "model-agent", model: "claude-haiku-4.5" }), + expect.objectContaining({ + name: "model-agent", + model: "claude-haiku-4.5", + reasoningEffort: "high", + }), ]); }); diff --git a/python/copilot/client.py b/python/copilot/client.py index 94eca4f89c..f2f7e3cb35 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -3736,6 +3736,8 @@ def _convert_custom_agent_to_wire_format( wire_agent["skills"] = agent["skills"] if "model" in agent: wire_agent["model"] = agent["model"] + if "reasoning_effort" in agent: + wire_agent["reasoningEffort"] = agent["reasoning_effort"] return wire_agent def _convert_default_agent_to_wire_format( diff --git a/python/copilot/session.py b/python/copilot/session.py index d0f402ef79..50666fcd64 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -1080,6 +1080,8 @@ class CustomAgentConfig(TypedDict, total=False): skills: NotRequired[list[str]] # Model identifier (e.g. "claude-haiku-4.5"); runtime falls back to parent model if unavailable model: NotRequired[str] + # Reasoning effort for this agent's model; omit to leave it unspecified + reasoning_effort: NotRequired[ReasoningEffort] class DefaultAgentConfig(TypedDict, total=False): diff --git a/python/test_client.py b/python/test_client.py index 9cb00d809f..e1d8d9522a 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -2245,6 +2245,32 @@ def test_model_field_is_omitted_when_absent(self): wire = client._convert_custom_agent_to_wire_format(agent) assert "model" not in wire + def test_reasoning_effort_is_forwarded_in_camel_case(self): + from copilot.client import CopilotClient + from copilot.session import CustomAgentConfig + + client = CopilotClient.__new__(CopilotClient) + agent: CustomAgentConfig = { + "name": "reasoning-agent", + "prompt": "Think carefully.", + "reasoning_effort": "high", + } + wire = client._convert_custom_agent_to_wire_format(agent) + assert wire["reasoningEffort"] == "high" + assert "reasoning_effort" not in wire + + def test_reasoning_effort_is_omitted_when_absent(self): + from copilot.client import CopilotClient + from copilot.session import CustomAgentConfig + + client = CopilotClient.__new__(CopilotClient) + agent: CustomAgentConfig = { + "name": "default-agent", + "prompt": "Use runtime defaults.", + } + wire = client._convert_custom_agent_to_wire_format(agent) + assert "reasoningEffort" not in wire + class TestPostToolUseFailureHookDispatch: """Unit tests for the postToolUseFailure handler dispatch.""" diff --git a/rust/src/types.rs b/rust/src/types.rs index 81eac81ca2..9afcf6349d 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -628,6 +628,11 @@ pub struct CustomAgentConfig { /// falling back to the parent session model if unavailable. #[serde(default, skip_serializing_if = "Option::is_none")] pub model: Option, + /// Reasoning effort level for this agent's model. + /// + /// When unset, no per-agent reasoning effort is sent to the runtime. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, } impl CustomAgentConfig { @@ -695,6 +700,12 @@ impl CustomAgentConfig { self.model = Some(model.into()); self } + + /// Set the reasoning effort level for this agent's model. + pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into) -> Self { + self.reasoning_effort = Some(reasoning_effort.into()); + self + } } /// Configures the default (built-in) agent that handles turns when no @@ -5469,6 +5480,28 @@ mod tests { assert!(wire.get("model").is_none()); } + #[test] + fn custom_agent_config_builder_with_reasoning_effort() { + let agent = + CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); + assert_eq!(agent.reasoning_effort.as_deref(), Some("high")); + } + + #[test] + fn custom_agent_config_serializes_reasoning_effort() { + let agent = + CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); + let wire = serde_json::to_value(&agent).unwrap(); + assert_eq!(wire["reasoningEffort"], "high"); + } + + #[test] + fn custom_agent_config_omits_reasoning_effort_when_none() { + let agent = CustomAgentConfig::new("default-agent", "prompt"); + let wire = serde_json::to_value(&agent).unwrap(); + assert!(wire.get("reasoningEffort").is_none()); + } + #[test] #[should_panic(expected = "tool parameter schema must be a JSON object")] fn tool_with_parameters_panics_on_non_object_value() { From 379484e096c91a0e87af9b0d6fa35122757b9bfb Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 14 Jul 2026 16:02:58 +0200 Subject: [PATCH 2/5] Clarify custom agent effort inheritance Document that omitted per-agent reasoning effort inherits an explicit parent session effort, while omission at both levels leaves the backend to choose. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 32727fc4-2ac8-41eb-a68a-e74674ecf155 --- docs/features/custom-agents.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index 8cec242268..0ac079d154 100644 --- a/docs/features/custom-agents.md +++ b/docs/features/custom-agents.md @@ -254,12 +254,12 @@ try (var client = new CopilotClient()) { | `infer` | `boolean` | | Whether the runtime can auto-select this agent (default: `true`) | | `skills` | `string[]` | | Skill names to preload into the agent's context at startup | | `model` | `string` | | Model identifier to use while this agent runs | -| `reasoningEffort` | `string` | | Reasoning effort to use while this agent runs | +| `reasoningEffort` | `string` | | Reasoning effort to use while this agent runs. Inherits an explicit parent session effort when omitted | > [!TIP] > A good `description` helps the runtime match user intent to the right agent. Be specific about the agent's expertise and capabilities. -Set `model` and `reasoningEffort` to override the parent session's model settings while a custom agent runs. Omit either property to leave that setting unspecified; the SDK does not add a per-agent default. Python uses `reasoning_effort`, .NET uses `ReasoningEffort`, Go uses `ReasoningEffort`, Java uses `setReasoningEffort`, and Rust uses `with_reasoning_effort`. +Set `model` and `reasoningEffort` to override the parent session's model settings while a custom agent runs. When `reasoningEffort` is omitted, the runtime currently inherits an explicitly configured parent session effort. If the parent session effort is also omitted, no effort is sent and the backend chooses. The SDK does not add a per-agent default. Python uses `reasoning_effort`, .NET uses `ReasoningEffort`, Go uses `ReasoningEffort`, Java uses `setReasoningEffort`, and Rust uses `with_reasoning_effort`. In addition to per-agent configuration above, you can set `agent` on the **session config** itself to pre-select which custom agent is active when the session starts. See [Selecting an Agent at Session Creation](#selecting-an-agent-at-session-creation) below. From 8ce00798293cd83e499e4fa2511a5c310f7761c3 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 14 Jul 2026 16:08:21 +0200 Subject: [PATCH 3/5] Clarify custom agent effort API docs Align every binding's CustomAgentConfig description with runtime inheritance semantics for omitted reasoning effort. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 32727fc4-2ac8-41eb-a68a-e74674ecf155 --- dotnet/src/Types.cs | 3 ++- go/types.go | 3 ++- .../main/java/com/github/copilot/rpc/CustomAgentConfig.java | 3 ++- nodejs/src/types.ts | 3 ++- python/copilot/session.py | 3 ++- rust/src/types.rs | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 35792997e7..72339bbc3d 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2660,7 +2660,8 @@ public sealed class CustomAgentConfig ///

/// Reasoning effort level for this agent's model. - /// When omitted, no per-agent reasoning effort is sent to the runtime. + /// When omitted, the runtime inherits an explicit parent session effort. + /// If the parent effort is also omitted, the backend chooses the effort. /// [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } diff --git a/go/types.go b/go/types.go index 1c76c117f0..e23b2166bf 100644 --- a/go/types.go +++ b/go/types.go @@ -950,7 +950,8 @@ type CustomAgentConfig struct { // falling back to the parent session model if unavailable. Model string `json:"model,omitempty"` // ReasoningEffort is the reasoning effort level for this agent's model. - // When empty, no per-agent reasoning effort is sent to the runtime. + // When empty, the runtime inherits an explicit parent session effort. + // If the parent effort is also empty, the backend chooses the effort. ReasoningEffort string `json:"reasoningEffort,omitempty"` } diff --git a/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java b/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java index 35e128f762..0b50862ccd 100644 --- a/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java @@ -298,7 +298,8 @@ public String getReasoningEffort() { /** * Sets the reasoning effort level for this agent's model. *

- * When omitted, no per-agent reasoning effort is sent to the runtime. + * When omitted, the runtime inherits an explicit parent session effort. If the + * parent effort is also omitted, the backend chooses the effort. * * @param reasoningEffort * the reasoning effort level diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index b90f7e1da8..03e42124bd 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1634,7 +1634,8 @@ export interface CustomAgentConfig { model?: string; /** * Reasoning effort level for this agent's model. - * When omitted, the SDK does not send a per-agent reasoning effort. + * When omitted, the runtime inherits an explicit parent session effort. + * If the parent effort is also omitted, the backend chooses the effort. */ reasoningEffort?: ReasoningEffort; } diff --git a/python/copilot/session.py b/python/copilot/session.py index 50666fcd64..ce23379ebf 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -1080,7 +1080,8 @@ class CustomAgentConfig(TypedDict, total=False): skills: NotRequired[list[str]] # Model identifier (e.g. "claude-haiku-4.5"); runtime falls back to parent model if unavailable model: NotRequired[str] - # Reasoning effort for this agent's model; omit to leave it unspecified + # Reasoning effort for this agent's model. When omitted, inherits an explicit + # parent session effort; if that is also omitted, the backend chooses. reasoning_effort: NotRequired[ReasoningEffort] diff --git a/rust/src/types.rs b/rust/src/types.rs index 9afcf6349d..b714d011cb 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -630,7 +630,8 @@ pub struct CustomAgentConfig { pub model: Option, /// Reasoning effort level for this agent's model. /// - /// When unset, no per-agent reasoning effort is sent to the runtime. + /// When unset, the runtime inherits an explicit parent session effort. If + /// the parent effort is also unset, the backend chooses the effort. #[serde(default, skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, } From 6557c86d7c47da10c914e57a7b09e80b41a9c323 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 14 Jul 2026 16:12:51 +0200 Subject: [PATCH 4/5] Test custom agent effort through client Capture session.create requests through the public .NET client path instead of reflecting into private serializer options. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 32727fc4-2ac8-41eb-a68a-e74674ecf155 --- .../test/Unit/ClientSessionLifetimeTests.cs | 51 +++++++++++++++++++ dotnet/test/Unit/SerializationTests.cs | 31 ----------- 2 files changed, 51 insertions(+), 31 deletions(-) diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index f736e8dee4..e1143db17c 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -204,6 +204,57 @@ public async Task ResumeSessionAsync_Throws_When_Same_Client_Already_Tracks_Sess AssertSessionCount(client, sessions: 1); } + [Fact] + public async Task CreateSessionAsync_Serializes_CustomAgent_ReasoningEffort() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await client.StartAsync(); + + await using var session = await client.CreateSessionAsync(new SessionConfig + { + CustomAgents = + [ + new CustomAgentConfig + { + Name = "reasoning-agent", + Prompt = "Think carefully.", + ReasoningEffort = "high" + } + ], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var request = Assert.Single(server.Requests, request => request.Method == "session.create"); + var agent = Assert.Single(request.Params.GetProperty("customAgents").EnumerateArray()); + Assert.Equal("high", agent.GetProperty("reasoningEffort").GetString()); + } + + [Fact] + public async Task CreateSessionAsync_Omits_CustomAgent_ReasoningEffort_When_Unset() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await client.StartAsync(); + + await using var session = await client.CreateSessionAsync(new SessionConfig + { + CustomAgents = + [ + new CustomAgentConfig + { + Name = "default-agent", + Prompt = "Use runtime defaults." + } + ], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var request = Assert.Single(server.Requests, request => request.Method == "session.create"); + var agent = Assert.Single(request.Params.GetProperty("customAgents").EnumerateArray()); + Assert.False(agent.TryGetProperty("reasoningEffort", out _)); + } + [Fact] public async Task CreateSessionAsync_Registers_McpAuth_Interest_Only_When_Handler_Configured() { diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 0782295892..ae7e885138 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -16,37 +16,6 @@ namespace GitHub.Copilot.Test.Unit; /// public class SerializationTests { - [Fact] - public void CustomAgentConfig_SerializesReasoningEffort_WithSdkOptions() - { - var options = GetSerializerOptions(); - var original = new CustomAgentConfig - { - Name = "reasoning-agent", - Prompt = "Think carefully.", - ReasoningEffort = "high" - }; - - var json = JsonSerializer.Serialize(original, options); - using var document = JsonDocument.Parse(json); - Assert.Equal("high", document.RootElement.GetProperty("reasoningEffort").GetString()); - } - - [Fact] - public void CustomAgentConfig_OmitsReasoningEffortWhenUnset_WithSdkOptions() - { - var options = GetSerializerOptions(); - var original = new CustomAgentConfig - { - Name = "default-agent", - Prompt = "Use runtime defaults." - }; - - var json = JsonSerializer.Serialize(original, options); - using var document = JsonDocument.Parse(json); - Assert.False(document.RootElement.TryGetProperty("reasoningEffort", out _)); - } - [Fact] public void ProviderConfig_CanSerializeHeaders_WithSdkOptions() { From ca4eab67934d21db14bd4c76b40bd4e217becd18 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 14 Jul 2026 16:16:07 +0200 Subject: [PATCH 5/5] Clarify custom agent effort omission Document that omitted per-agent reasoning effort sends no override and lets the backend choose its default rather than inheriting the parent session effort. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 32727fc4-2ac8-41eb-a68a-e74674ecf155 --- docs/features/custom-agents.md | 4 ++-- dotnet/src/Types.cs | 4 ++-- go/types.go | 4 ++-- .../main/java/com/github/copilot/rpc/CustomAgentConfig.java | 4 ++-- nodejs/src/types.ts | 4 ++-- python/copilot/session.py | 4 ++-- rust/src/types.rs | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index 0ac079d154..3cab77474e 100644 --- a/docs/features/custom-agents.md +++ b/docs/features/custom-agents.md @@ -254,12 +254,12 @@ try (var client = new CopilotClient()) { | `infer` | `boolean` | | Whether the runtime can auto-select this agent (default: `true`) | | `skills` | `string[]` | | Skill names to preload into the agent's context at startup | | `model` | `string` | | Model identifier to use while this agent runs | -| `reasoningEffort` | `string` | | Reasoning effort to use while this agent runs. Inherits an explicit parent session effort when omitted | +| `reasoningEffort` | `string` | | Reasoning effort to use while this agent runs. When omitted, no override is sent and the backend chooses its default | > [!TIP] > A good `description` helps the runtime match user intent to the right agent. Be specific about the agent's expertise and capabilities. -Set `model` and `reasoningEffort` to override the parent session's model settings while a custom agent runs. When `reasoningEffort` is omitted, the runtime currently inherits an explicitly configured parent session effort. If the parent session effort is also omitted, no effort is sent and the backend chooses. The SDK does not add a per-agent default. Python uses `reasoning_effort`, .NET uses `ReasoningEffort`, Go uses `ReasoningEffort`, Java uses `setReasoningEffort`, and Rust uses `with_reasoning_effort`. +Set `model` and `reasoningEffort` to override the parent session's model settings while a custom agent runs. When `reasoningEffort` is omitted, the SDK sends no per-agent override and the backend chooses its default. The parent session effort is not inherited, and the SDK does not add a per-agent default. Python uses `reasoning_effort`, .NET uses `ReasoningEffort`, Go uses `ReasoningEffort`, Java uses `setReasoningEffort`, and Rust uses `with_reasoning_effort`. In addition to per-agent configuration above, you can set `agent` on the **session config** itself to pre-select which custom agent is active when the session starts. See [Selecting an Agent at Session Creation](#selecting-an-agent-at-session-creation) below. diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 72339bbc3d..2bb643c75b 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2660,8 +2660,8 @@ public sealed class CustomAgentConfig ///

/// Reasoning effort level for this agent's model. - /// When omitted, the runtime inherits an explicit parent session effort. - /// If the parent effort is also omitted, the backend chooses the effort. + /// When omitted, no per-agent override is sent and the backend chooses its + /// default. The parent session effort is not inherited. /// [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } diff --git a/go/types.go b/go/types.go index e23b2166bf..88edb7b3e6 100644 --- a/go/types.go +++ b/go/types.go @@ -950,8 +950,8 @@ type CustomAgentConfig struct { // falling back to the parent session model if unavailable. Model string `json:"model,omitempty"` // ReasoningEffort is the reasoning effort level for this agent's model. - // When empty, the runtime inherits an explicit parent session effort. - // If the parent effort is also empty, the backend chooses the effort. + // When empty, no per-agent override is sent and the backend chooses its + // default. The parent session effort is not inherited. ReasoningEffort string `json:"reasoningEffort,omitempty"` } diff --git a/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java b/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java index 0b50862ccd..3604a1ef5d 100644 --- a/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java @@ -298,8 +298,8 @@ public String getReasoningEffort() { /** * Sets the reasoning effort level for this agent's model. *

- * When omitted, the runtime inherits an explicit parent session effort. If the - * parent effort is also omitted, the backend chooses the effort. + * When omitted, no per-agent override is sent and the backend chooses its + * default. The parent session effort is not inherited. * * @param reasoningEffort * the reasoning effort level diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 03e42124bd..660508ad50 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1634,8 +1634,8 @@ export interface CustomAgentConfig { model?: string; /** * Reasoning effort level for this agent's model. - * When omitted, the runtime inherits an explicit parent session effort. - * If the parent effort is also omitted, the backend chooses the effort. + * When omitted, no per-agent override is sent and the backend chooses its + * default. The parent session effort is not inherited. */ reasoningEffort?: ReasoningEffort; } diff --git a/python/copilot/session.py b/python/copilot/session.py index ce23379ebf..b6736939ac 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -1080,8 +1080,8 @@ class CustomAgentConfig(TypedDict, total=False): skills: NotRequired[list[str]] # Model identifier (e.g. "claude-haiku-4.5"); runtime falls back to parent model if unavailable model: NotRequired[str] - # Reasoning effort for this agent's model. When omitted, inherits an explicit - # parent session effort; if that is also omitted, the backend chooses. + # Reasoning effort for this agent's model. When omitted, no per-agent override + # is sent and the backend chooses its default; the parent effort is not inherited. reasoning_effort: NotRequired[ReasoningEffort] diff --git a/rust/src/types.rs b/rust/src/types.rs index b714d011cb..7907b0c7f2 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -630,8 +630,8 @@ pub struct CustomAgentConfig { pub model: Option, /// Reasoning effort level for this agent's model. /// - /// When unset, the runtime inherits an explicit parent session effort. If - /// the parent effort is also unset, the backend chooses the effort. + /// When unset, no per-agent override is sent and the backend chooses its + /// default. The parent session effort is not inherited. #[serde(default, skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, }