Skip to content

Commit ff18f7e

Browse files
authored
Add custom agent reasoning effort across SDKs (#1981)
* 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 * 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 * 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 * 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 * 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
1 parent 50f8456 commit ff18f7e

14 files changed

Lines changed: 231 additions & 4 deletions

File tree

docs/features/custom-agents.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,14 @@ try (var client = new CopilotClient()) {
253253
| `mcpServers` | `object` | | MCP server configurations specific to this agent |
254254
| `infer` | `boolean` | | Whether the runtime can auto-select this agent (default: `true`) |
255255
| `skills` | `string[]` | | Skill names to preload into the agent's context at startup |
256+
| `model` | `string` | | Model identifier to use while this agent runs |
257+
| `reasoningEffort` | `string` | | Reasoning effort to use while this agent runs. When omitted, no override is sent and the backend chooses its default |
256258

257259
> [!TIP]
258260
> A good `description` helps the runtime match user intent to the right agent. Be specific about the agent's expertise and capabilities.
259261
262+
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`.
263+
260264
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.
261265

262266
| Session Config Property | Type | Description |

dotnet/src/Types.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2657,6 +2657,14 @@ public sealed class CustomAgentConfig
26572657
/// </summary>
26582658
[JsonPropertyName("model")]
26592659
public string? Model { get; set; }
2660+
2661+
/// <summary>
2662+
/// Reasoning effort level for this agent's model.
2663+
/// When omitted, no per-agent override is sent and the backend chooses its
2664+
/// default. The parent session effort is not inherited.
2665+
/// </summary>
2666+
[JsonPropertyName("reasoningEffort")]
2667+
public string? ReasoningEffort { get; set; }
26602668
}
26612669

26622670
/// <summary>

dotnet/test/Unit/ClientSessionLifetimeTests.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,57 @@ public async Task ResumeSessionAsync_Throws_When_Same_Client_Already_Tracks_Sess
204204
AssertSessionCount(client, sessions: 1);
205205
}
206206

207+
[Fact]
208+
public async Task CreateSessionAsync_Serializes_CustomAgent_ReasoningEffort()
209+
{
210+
await using var server = await FakeCopilotServer.StartAsync();
211+
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
212+
await client.StartAsync();
213+
214+
await using var session = await client.CreateSessionAsync(new SessionConfig
215+
{
216+
CustomAgents =
217+
[
218+
new CustomAgentConfig
219+
{
220+
Name = "reasoning-agent",
221+
Prompt = "Think carefully.",
222+
ReasoningEffort = "high"
223+
}
224+
],
225+
OnPermissionRequest = PermissionHandler.ApproveAll
226+
});
227+
228+
var request = Assert.Single(server.Requests, request => request.Method == "session.create");
229+
var agent = Assert.Single(request.Params.GetProperty("customAgents").EnumerateArray());
230+
Assert.Equal("high", agent.GetProperty("reasoningEffort").GetString());
231+
}
232+
233+
[Fact]
234+
public async Task CreateSessionAsync_Omits_CustomAgent_ReasoningEffort_When_Unset()
235+
{
236+
await using var server = await FakeCopilotServer.StartAsync();
237+
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
238+
await client.StartAsync();
239+
240+
await using var session = await client.CreateSessionAsync(new SessionConfig
241+
{
242+
CustomAgents =
243+
[
244+
new CustomAgentConfig
245+
{
246+
Name = "default-agent",
247+
Prompt = "Use runtime defaults."
248+
}
249+
],
250+
OnPermissionRequest = PermissionHandler.ApproveAll
251+
});
252+
253+
var request = Assert.Single(server.Requests, request => request.Method == "session.create");
254+
var agent = Assert.Single(request.Params.GetProperty("customAgents").EnumerateArray());
255+
Assert.False(agent.TryGetProperty("reasoningEffort", out _));
256+
}
257+
207258
[Fact]
208259
public async Task CreateSessionAsync_Registers_McpAuth_Interest_Only_When_Handler_Configured()
209260
{

dotnet/test/Unit/CloneTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
8282
IncludeSubAgentStreamingEvents = false,
8383
McpServers = new Dictionary<string, McpServerConfig> { ["server1"] = new McpStdioServerConfig { Command = "echo" } },
8484
McpOAuthTokenStorage = McpOAuthTokenStorageMode.Persistent,
85-
CustomAgents = [new CustomAgentConfig { Name = "agent1", Model = "claude-haiku-4.5" }],
85+
CustomAgents = [new CustomAgentConfig { Name = "agent1", Model = "claude-haiku-4.5", ReasoningEffort = "high" }],
8686
Agent = "agent1",
8787
Capi = new CapiSessionOptions { EnableWebSocketResponses = false },
8888
Cloud = new CloudSessionOptions
@@ -128,6 +128,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
128128
Assert.Equal(original.McpOAuthTokenStorage, clone.McpOAuthTokenStorage);
129129
Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count);
130130
Assert.Equal(original.CustomAgents[0].Model, clone.CustomAgents[0].Model);
131+
Assert.Equal(original.CustomAgents[0].ReasoningEffort, clone.CustomAgents[0].ReasoningEffort);
131132
Assert.Equal(original.Agent, clone.Agent);
132133
Assert.Same(original.Capi, clone.Capi);
133134
Assert.Same(original.Cloud, clone.Cloud);

go/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,10 @@ type CustomAgentConfig struct {
949949
// When set, the runtime will attempt to use this model for the agent,
950950
// falling back to the parent session model if unavailable.
951951
Model string `json:"model,omitempty"`
952+
// ReasoningEffort is the reasoning effort level for this agent's model.
953+
// When empty, no per-agent override is sent and the backend chooses its
954+
// default. The parent session effort is not inherited.
955+
ReasoningEffort string `json:"reasoningEffort,omitempty"`
952956
}
953957

954958
// DefaultAgentConfig configures the default agent (the built-in agent that handles turns when no custom agent is selected).

go/types_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,28 @@ func TestCustomAgentConfig_JSONIncludesModel(t *testing.T) {
152152
}
153153
}
154154

155+
func TestCustomAgentConfig_JSONIncludesReasoningEffort(t *testing.T) {
156+
cfg := CustomAgentConfig{
157+
Name: "reasoning-agent",
158+
Prompt: "Think carefully.",
159+
ReasoningEffort: "high",
160+
}
161+
162+
data, err := json.Marshal(cfg)
163+
if err != nil {
164+
t.Fatalf("failed to marshal CustomAgentConfig: %v", err)
165+
}
166+
167+
var decoded map[string]any
168+
if err := json.Unmarshal(data, &decoded); err != nil {
169+
t.Fatalf("failed to unmarshal CustomAgentConfig: %v", err)
170+
}
171+
172+
if decoded["reasoningEffort"] != "high" {
173+
t.Errorf("expected reasoningEffort 'high', got %v", decoded["reasoningEffort"])
174+
}
175+
}
176+
155177
func TestCustomAgentConfig_JSONIncludesEmptyTools(t *testing.T) {
156178
cfg := CustomAgentConfig{
157179
Name: "no-tools-agent",
@@ -273,6 +295,9 @@ func TestCustomAgentConfig_JSONOmitsModelWhenEmpty(t *testing.T) {
273295
if _, present := decoded["model"]; present {
274296
t.Errorf("expected model to be omitted when empty, got %v", decoded["model"])
275297
}
298+
if _, present := decoded["reasoningEffort"]; present {
299+
t.Errorf("expected reasoningEffort to be omitted when empty, got %v", decoded["reasoningEffort"])
300+
}
276301
}
277302

278303
func TestTool_JSONIncludesEmptyParameters(t *testing.T) {

java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ public class CustomAgentConfig {
6363
@JsonProperty("model")
6464
private String model;
6565

66+
@JsonProperty("reasoningEffort")
67+
private String reasoningEffort;
68+
6669
/**
6770
* Gets the unique identifier name for this agent.
6871
*
@@ -282,4 +285,28 @@ public CustomAgentConfig setModel(String model) {
282285
this.model = model;
283286
return this;
284287
}
288+
289+
/**
290+
* Gets the reasoning effort level for this agent's model.
291+
*
292+
* @return the reasoning effort level, or {@code null} if not set
293+
*/
294+
public String getReasoningEffort() {
295+
return reasoningEffort;
296+
}
297+
298+
/**
299+
* Sets the reasoning effort level for this agent's model.
300+
* <p>
301+
* When omitted, no per-agent override is sent and the backend chooses its
302+
* default. The parent session effort is not inherited.
303+
*
304+
* @param reasoningEffort
305+
* the reasoning effort level
306+
* @return this config for method chaining
307+
*/
308+
public CustomAgentConfig setReasoningEffort(String reasoningEffort) {
309+
this.reasoningEffort = reasoningEffort;
310+
return this;
311+
}
285312
}

java/src/test/java/com/github/copilot/DataObjectCoverageTest.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ void postToolUseHookInputSessionIdRoundTrip() {
186186
assertEquals("session-xyz", input.getSessionId());
187187
}
188188

189-
// ===== CustomAgentConfig model field =====
189+
// ===== CustomAgentConfig model fields =====
190190

191191
@Test
192192
void customAgentConfigModelGetterAndSetter() {
@@ -227,6 +227,36 @@ void customAgentConfigModelOmittedWhenNull() throws Exception {
227227
assertFalse(json.contains("\"model\""));
228228
}
229229

230+
@Test
231+
void customAgentConfigReasoningEffortGetterAndFluentSetter() {
232+
var cfg = new CustomAgentConfig();
233+
assertNull(cfg.getReasoningEffort());
234+
235+
var result = cfg.setReasoningEffort("high");
236+
assertSame(cfg, result);
237+
assertEquals("high", cfg.getReasoningEffort());
238+
}
239+
240+
@Test
241+
void customAgentConfigReasoningEffortSerializationRoundTrip() throws Exception {
242+
var mapper = JsonRpcClient.getObjectMapper();
243+
var cfg = new CustomAgentConfig().setName("reasoning-agent").setReasoningEffort("high");
244+
245+
var json = mapper.writeValueAsString(cfg);
246+
assertTrue(json.contains("\"reasoningEffort\":\"high\""));
247+
248+
var deserialized = mapper.readValue(json, CustomAgentConfig.class);
249+
assertEquals("high", deserialized.getReasoningEffort());
250+
}
251+
252+
@Test
253+
void customAgentConfigReasoningEffortOmittedWhenNull() throws Exception {
254+
var mapper = JsonRpcClient.getObjectMapper();
255+
var json = mapper.writeValueAsString(new CustomAgentConfig().setName("default-agent"));
256+
257+
assertFalse(json.contains("\"reasoningEffort\""));
258+
}
259+
230260
// ===== PermissionRequestResult setRules =====
231261

232262
@Test

nodejs/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,6 +1641,12 @@ export interface CustomAgentConfig {
16411641
* falling back to the parent session model if unavailable.
16421642
*/
16431643
model?: string;
1644+
/**
1645+
* Reasoning effort level for this agent's model.
1646+
* When omitted, no per-agent override is sent and the backend chooses its
1647+
* default. The parent session effort is not inherited.
1648+
*/
1649+
reasoningEffort?: ReasoningEffort;
16441650
}
16451651

16461652
/**

nodejs/test/client.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2281,9 +2281,10 @@ describe("CopilotClient", () => {
22812281
const payload = spy.mock.calls.find((c) => c[0] === "session.create")![1] as any;
22822282
expect(payload.agent).toBe("test-agent");
22832283
expect(payload.customAgents).toEqual([expect.objectContaining({ name: "test-agent" })]);
2284+
expect(payload.customAgents[0].reasoningEffort).toBeUndefined();
22842285
});
22852286

2286-
it("forwards custom agent model in session.create request", async () => {
2287+
it("forwards custom agent model and reasoning effort in session.create request", async () => {
22872288
const client = new CopilotClient();
22882289
await client.start();
22892290
onTestFinished(() => stopClient(client));
@@ -2296,13 +2297,18 @@ describe("CopilotClient", () => {
22962297
name: "model-agent",
22972298
prompt: "You are a model agent.",
22982299
model: "claude-haiku-4.5",
2300+
reasoningEffort: "high",
22992301
},
23002302
],
23012303
});
23022304

23032305
const payload = spy.mock.calls.find((c) => c[0] === "session.create")![1] as any;
23042306
expect(payload.customAgents).toEqual([
2305-
expect.objectContaining({ name: "model-agent", model: "claude-haiku-4.5" }),
2307+
expect.objectContaining({
2308+
name: "model-agent",
2309+
model: "claude-haiku-4.5",
2310+
reasoningEffort: "high",
2311+
}),
23062312
]);
23072313
});
23082314

0 commit comments

Comments
 (0)