From 00a8d382faaecc638d4c553d627fd6d10a66ae0c Mon Sep 17 00:00:00 2001 From: MarkXian Date: Fri, 14 Aug 2026 11:33:16 +0800 Subject: [PATCH] Expose large output config on custom agents --- dotnet/src/Types.cs | 7 +++ dotnet/test/Unit/SerializationTests.cs | 25 +++++++++++ go/types.go | 3 ++ go/types_test.go | 38 ++++++++++++++++ .../github/copilot/rpc/CustomAgentConfig.java | 26 +++++++++++ .../copilot/DataObjectCoverageTest.java | 18 ++++++++ nodejs/src/client.ts | 9 ++-- nodejs/src/types.ts | 5 +++ nodejs/test/client.test.ts | 34 +++++++++++++++ python/copilot/client.py | 2 + python/copilot/session.py | 3 ++ python/test_client.py | 43 +++++++++++++++++++ rust/src/types.rs | 25 +++++++++++ 13 files changed, 235 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index c0810b387..383049d41 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2796,6 +2796,13 @@ public sealed class CustomAgentConfig /// [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } + + /// + /// Large tool output handling for this agent. + /// + /// When omitted, no agent-specific large output override is sent. + [JsonPropertyName("largeOutput")] + public LargeToolOutputConfig? LargeOutput { get; set; } } /// diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 6edf16809..6a03bb756 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -444,6 +444,31 @@ public void SessionRequests_CanSerializePluginDirectoriesAndLargeOutput_WithSdkO Assert.Equal("/tmp/large-output", resumeLargeOutput.GetProperty("outputDir").GetString()); } + [Fact] + public void CustomAgentConfig_CanSerializeLargeOutput_WithSdkOptions() + { + var options = GetSerializerOptions(); + var agent = new CustomAgentConfig + { + Name = "large-output-agent", + Prompt = "Handle large outputs.", + LargeOutput = new LargeToolOutputConfig + { + Enabled = false, + MaxSizeBytes = 2048, + OutputDirectory = "/tmp/agent-large-output", + }, + }; + + var json = JsonSerializer.Serialize(agent, options); + using var document = JsonDocument.Parse(json); + var largeOutput = document.RootElement.GetProperty("largeOutput"); + + Assert.False(largeOutput.GetProperty("enabled").GetBoolean()); + Assert.Equal(2048, largeOutput.GetProperty("maxSizeBytes").GetInt64()); + Assert.Equal("/tmp/agent-large-output", largeOutput.GetProperty("outputDir").GetString()); + } + [Fact] public void SessionRequests_CanSerializeMemory_WithSdkOptions() { diff --git a/go/types.go b/go/types.go index c0b34586d..8881437b8 100644 --- a/go/types.go +++ b/go/types.go @@ -1043,6 +1043,9 @@ type CustomAgentConfig struct { // When empty, the runtime resolves model configuration, then inherits the // parent effort only for the same model. ReasoningEffort string `json:"reasoningEffort,omitempty"` + // LargeOutput configures large tool output handling for this agent. When + // nil, no agent-specific large output override is sent. + LargeOutput *LargeToolOutputConfig `json:"largeOutput,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 4195464b3..24b4dde72 100644 --- a/go/types_test.go +++ b/go/types_test.go @@ -186,6 +186,44 @@ func TestCustomAgentConfig_JSONIncludesReasoningEffort(t *testing.T) { } } +func TestCustomAgentConfig_JSONIncludesLargeOutput(t *testing.T) { + enabled := false + maxSizeBytes := int64(2048) + cfg := CustomAgentConfig{ + Name: "large-output-agent", + Prompt: "Handle large outputs.", + LargeOutput: &LargeToolOutputConfig{ + Enabled: &enabled, + MaxSizeBytes: &maxSizeBytes, + OutputDirectory: "/tmp/agent-large-output", + }, + } + + 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) + } + + largeOutput, ok := decoded["largeOutput"].(map[string]any) + if !ok { + t.Fatalf("expected largeOutput object, got %v", decoded["largeOutput"]) + } + if largeOutput["enabled"] != false { + t.Errorf("expected enabled false, got %v", largeOutput["enabled"]) + } + if largeOutput["maxSizeBytes"] != float64(2048) { + t.Errorf("expected maxSizeBytes 2048, got %v", largeOutput["maxSizeBytes"]) + } + if largeOutput["outputDir"] != "/tmp/agent-large-output" { + t.Errorf("expected outputDir '/tmp/agent-large-output', got %v", largeOutput["outputDir"]) + } +} + func TestCustomAgentConfig_JSONIncludesEmptyTools(t *testing.T) { cfg := CustomAgentConfig{ Name: "no-tools-agent", diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java index 62de19b6a..19e866d96 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java @@ -66,6 +66,9 @@ public class CustomAgentConfig { @JsonProperty("reasoningEffort") private String reasoningEffort; + @JsonProperty("largeOutput") + private LargeToolOutputConfig largeOutput; + /** * Gets the unique identifier name for this agent. * @@ -309,4 +312,27 @@ public CustomAgentConfig setReasoningEffort(String reasoningEffort) { this.reasoningEffort = reasoningEffort; return this; } + + /** + * Gets the large tool output handling configuration for this agent. + * + * @return the large output configuration, or {@code null} if not set + */ + public LargeToolOutputConfig getLargeOutput() { + return largeOutput; + } + + /** + * Sets the large tool output handling configuration for this agent. + *

+ * When omitted, no agent-specific large output override is sent. + * + * @param largeOutput + * the large output configuration + * @return this config for method chaining + */ + public CustomAgentConfig setLargeOutput(LargeToolOutputConfig largeOutput) { + this.largeOutput = largeOutput; + return this; + } } diff --git a/java/sdk/src/test/java/com/github/copilot/DataObjectCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/DataObjectCoverageTest.java index f95c5bcc5..55f5d7b9f 100644 --- a/java/sdk/src/test/java/com/github/copilot/DataObjectCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/DataObjectCoverageTest.java @@ -278,6 +278,24 @@ void customAgentConfigReasoningEffortOmittedWhenNull() throws Exception { assertFalse(json.contains("\"reasoningEffort\"")); } + @Test + void customAgentConfigLargeOutputSerializationRoundTrip() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var cfg = new CustomAgentConfig().setName("large-output-agent") + .setLargeOutput(new LargeToolOutputConfig().setEnabled(false).setMaxSizeBytes(2048L) + .setOutputDirectory("/tmp/agent-large-output")); + + var json = mapper.writeValueAsString(cfg); + assertTrue(json.contains("\"largeOutput\"")); + assertTrue(json.contains("\"outputDir\":\"/tmp/agent-large-output\"")); + + var deserialized = mapper.readValue(json, CustomAgentConfig.class); + assertNotNull(deserialized.getLargeOutput()); + assertEquals(false, deserialized.getLargeOutput().getEnabled()); + assertEquals(2048L, deserialized.getLargeOutput().getMaxSizeBytes()); + assertEquals("/tmp/agent-large-output", deserialized.getLargeOutput().getOutputDirectory()); + } + // ===== PermissionRequestResult setRules ===== @Test diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 30095186e..c44c7252c 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -248,9 +248,12 @@ function toWireMcpServers( function toWireCustomAgents(agents: CustomAgentConfig[] | undefined): unknown[] | undefined { if (!agents) return undefined; return agents.map((agent) => { - if (!agent.mcpServers) return agent; - const { mcpServers, ...rest } = agent; - return { ...rest, mcpServers: toWireMcpServers(mcpServers) }; + const { mcpServers, largeOutput, ...rest } = agent; + return { + ...rest, + ...(mcpServers ? { mcpServers: toWireMcpServers(mcpServers) } : {}), + ...(largeOutput ? { largeOutput: toWireLargeOutput(largeOutput) } : {}), + }; }); } diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 8149c36da..172c0ad43 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1791,6 +1791,11 @@ export interface CustomAgentConfig { * then inherits the parent effort only if this agent uses the same model. */ reasoningEffort?: ReasoningEffort; + /** + * Large tool output handling for this agent. + * When unset, no agent-specific large output override is sent. + */ + largeOutput?: LargeToolOutputConfig; } /** diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 49ec169c4..2271bb152 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -2646,6 +2646,40 @@ describe("CopilotClient", () => { ]); }); + it("forwards custom agent large output in session.create request", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const spy = vi.spyOn((client as any).connection!, "sendRequest"); + await client.createSession({ + onPermissionRequest: approveAll, + customAgents: [ + { + name: "large-output-agent", + prompt: "You are a large output agent.", + largeOutput: { + enabled: false, + maxSizeBytes: 2048, + outputDirectory: "/tmp/agent-large-output", + }, + }, + ], + }); + + const payload = spy.mock.calls.find((c) => c[0] === "session.create")![1] as any; + expect(payload.customAgents).toEqual([ + expect.objectContaining({ + name: "large-output-agent", + largeOutput: { + enabled: false, + maxSizeBytes: 2048, + outputDir: "/tmp/agent-large-output", + }, + }), + ]); + }); + it("forwards agent in session.resume request", async () => { const client = new CopilotClient(); await client.start(); diff --git a/python/copilot/client.py b/python/copilot/client.py index 6cdd765c3..c91b23369 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -4035,6 +4035,8 @@ def _convert_custom_agent_to_wire_format( wire_agent["model"] = agent["model"] if "reasoning_effort" in agent: wire_agent["reasoningEffort"] = agent["reasoning_effort"] + if "large_output" in agent: + wire_agent["largeOutput"] = _large_output_to_wire(agent["large_output"]) return wire_agent def _convert_default_agent_to_wire_format( diff --git a/python/copilot/session.py b/python/copilot/session.py index 92c24bdd8..0133f70fe 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -1161,6 +1161,9 @@ class CustomAgentConfig(TypedDict, total=False): # Reasoning effort for this agent's model. When omitted, the runtime resolves # model configuration, then inherits the parent effort only for the same model. reasoning_effort: NotRequired[ReasoningEffort] + # Large output handling for this agent. When omitted, no agent-specific + # large output override is sent. + large_output: NotRequired[LargeToolOutputConfig] class DefaultAgentConfig(TypedDict, total=False): diff --git a/python/test_client.py b/python/test_client.py index cf4bdf192..36126746f 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -9,6 +9,7 @@ import os from datetime import UTC, datetime from tempfile import TemporaryDirectory +from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest @@ -1166,6 +1167,48 @@ async def mock_request(method, params, **kwargs): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_custom_agent_large_output_uses_wire_output_dir(self) -> None: + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured: dict[str, Any] = {} + + async def mock_request(method: str, params: dict[str, Any], **kwargs: Any) -> Any: + captured[method] = params + if method == "session.create": + 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 + + await client.create_session( + on_permission_request=PermissionHandler.approve_all, + custom_agents=[ + { + "name": "large-output-agent", + "prompt": "You are a large output agent.", + "large_output": { + "enabled": False, + "max_size_bytes": 2048, + "output_directory": "/tmp/agent-large-output", + }, + } + ], + ) + + assert captured["session.create"]["customAgents"][0]["largeOutput"] == { + "enabled": False, + "maxSizeBytes": 2048, + "outputDir": "/tmp/agent-large-output", + } + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_create_and_resume_session_forward_memory(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) diff --git a/rust/src/types.rs b/rust/src/types.rs index 1946ded08..7330f2f90 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -668,6 +668,11 @@ pub struct CustomAgentConfig { /// parent effort only for the same model. #[serde(default, skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, + /// Large tool output handling for this agent. + /// + /// When unset, no agent-specific large output override is sent. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub large_output: Option, } impl CustomAgentConfig { @@ -741,6 +746,12 @@ impl CustomAgentConfig { self.reasoning_effort = Some(reasoning_effort.into()); self } + + /// Set the large tool output handling policy for this agent. + pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self { + self.large_output = Some(config); + self + } } /// Configures the default (built-in) agent that handles turns when no @@ -5997,6 +6008,20 @@ mod tests { assert!(wire.get("reasoningEffort").is_none()); } + #[test] + fn custom_agent_config_serializes_large_output() { + let agent = CustomAgentConfig::new("large-output-agent", "prompt").with_large_output( + LargeToolOutputConfig::new() + .with_enabled(false) + .with_max_size_bytes(2048) + .with_output_directory("/tmp/agent-large-output"), + ); + let wire = serde_json::to_value(&agent).unwrap(); + assert_eq!(wire["largeOutput"]["enabled"], false); + assert_eq!(wire["largeOutput"]["maxSizeBytes"], 2048); + assert_eq!(wire["largeOutput"]["outputDir"], "/tmp/agent-large-output"); + } + #[test] #[should_panic(expected = "tool parameter schema must be a JSON object")] fn tool_with_parameters_panics_on_non_object_value() {