From 391e129d20a0e7a4a0c7c28f3069d9fc5f6d8e0e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:38:56 +0000 Subject: [PATCH 1/3] Initial plan From 2a9764cff5cc4e51ab5bdf6c52b318d54dccfa35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:06:09 +0000 Subject: [PATCH 2/3] Port reference implementation sync: McpServerConfig types, ModelCapabilitiesOverride, agent skills, per-request headers Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/514da8aa-3336-46ca-b39a-48faabcbb354 Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- .lastmerge | 2 +- .../github/copilot/sdk/CopilotSession.java | 46 ++++ .../copilot/sdk/SessionRequestBuilder.java | 6 + .../sdk/json/CreateSessionRequest.java | 50 +++- .../copilot/sdk/json/CustomAgentConfig.java | 36 ++- .../copilot/sdk/json/McpHttpServerConfig.java | 106 ++++++++ .../copilot/sdk/json/McpServerConfig.java | 88 +++++++ .../sdk/json/McpStdioServerConfig.java | 155 ++++++++++++ .../copilot/sdk/json/MessageOptions.java | 29 +++ .../sdk/json/ModelCapabilitiesOverride.java | 235 ++++++++++++++++++ .../copilot/sdk/json/ProviderConfig.java | 30 +++ .../copilot/sdk/json/ResumeSessionConfig.java | 86 ++++++- .../sdk/json/ResumeSessionRequest.java | 50 +++- .../copilot/sdk/json/SendMessageRequest.java | 14 ++ .../copilot/sdk/json/SessionConfig.java | 98 +++++++- .../copilot/sdk/ClosedSessionGuardTest.java | 64 ++--- .../copilot/sdk/CopilotSessionTest.java | 4 +- .../github/copilot/sdk/McpAndAgentsTest.java | 24 +- .../com/github/copilot/sdk/SkillsTest.java | 75 ++++++ 19 files changed, 1134 insertions(+), 64 deletions(-) create mode 100644 src/main/java/com/github/copilot/sdk/json/McpHttpServerConfig.java create mode 100644 src/main/java/com/github/copilot/sdk/json/McpServerConfig.java create mode 100644 src/main/java/com/github/copilot/sdk/json/McpStdioServerConfig.java create mode 100644 src/main/java/com/github/copilot/sdk/json/ModelCapabilitiesOverride.java diff --git a/.lastmerge b/.lastmerge index 83feb636c..4255f7b13 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -c3fa6cbfb83d4a20b7912b1a17013d48f5a277a1 +922959f4a7b83509c3620d4881733c6c5677f00c diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index aaf2f4345..a4f2a5b51 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -460,6 +460,7 @@ public CompletableFuture send(MessageOptions options) { request.setPrompt(options.getPrompt()); request.setAttachments(options.getAttachments()); request.setMode(options.getMode()); + request.setRequestHeaders(options.getRequestHeaders()); return rpc.invoke("session.send", request, SendMessageResponse.class).thenApply(SendMessageResponse::messageId); } @@ -1552,6 +1553,51 @@ public CompletableFuture setModel(String model, String reasoningEffort) { .thenApply(r -> null); } + /** + * Changes the model for this session with optional reasoning effort and + * capability overrides. + *

+ * The new model takes effect for the next message. Conversation history is + * preserved. + * + *

{@code
+     * session.setModel("claude-sonnet-4.5", null,
+     * 		new ModelCapabilitiesOverride().setSupports(new ModelCapabilitiesOverride.Supports().setVision(false)))
+     * 		.get();
+     * }
+ * + * @param model + * the model ID to switch to (e.g., {@code "gpt-4.1"}) + * @param reasoningEffort + * reasoning effort level (e.g., {@code "low"}, {@code "medium"}, + * {@code "high"}, {@code "xhigh"}); {@code null} to use default + * @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, + com.github.copilot.sdk.json.ModelCapabilitiesOverride modelCapabilities) { + ensureNotTerminated(); + SessionModelSwitchToParams.SessionModelSwitchToParamsModelCapabilities generatedCapabilities = null; + if (modelCapabilities != null) { + SessionModelSwitchToParams.SessionModelSwitchToParamsModelCapabilities.SessionModelSwitchToParamsModelCapabilitiesSupports supports = null; + if (modelCapabilities.getSupports() != null) { + var s = modelCapabilities.getSupports(); + supports = new SessionModelSwitchToParams.SessionModelSwitchToParamsModelCapabilities.SessionModelSwitchToParamsModelCapabilitiesSupports( + s.getVision(), s.getReasoningEffort()); + } + generatedCapabilities = new SessionModelSwitchToParams.SessionModelSwitchToParamsModelCapabilities(supports, + null); + } + return getRpc().model + .switchTo(new SessionModelSwitchToParams(sessionId, model, reasoningEffort, generatedCapabilities)) + .thenApply(r -> null); + } + /** * Changes the model for this session. *

diff --git a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java index d74bbfaf3..f20b0a21f 100644 --- a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java +++ b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java @@ -115,6 +115,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setHooks(config.getHooks() != null && config.getHooks().hasHooks() ? true : null); request.setWorkingDirectory(config.getWorkingDirectory()); request.setStreaming(config.isStreaming() ? true : null); + request.setIncludeSubAgentStreamingEvents(config.getIncludeSubAgentStreamingEvents()); request.setMcpServers(config.getMcpServers()); request.setCustomAgents(config.getCustomAgents()); request.setAgent(config.getAgent()); @@ -122,6 +123,8 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setSkillDirectories(config.getSkillDirectories()); request.setDisabledSkills(config.getDisabledSkills()); request.setConfigDir(config.getConfigDir()); + request.setEnableConfigDiscovery(config.getEnableConfigDiscovery()); + request.setModelCapabilities(config.getModelCapabilities()); if (config.getCommands() != null && !config.getCommands().isEmpty()) { var wireCommands = config.getCommands().stream() @@ -185,14 +188,17 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setHooks(config.getHooks() != null && config.getHooks().hasHooks() ? true : null); request.setWorkingDirectory(config.getWorkingDirectory()); request.setConfigDir(config.getConfigDir()); + request.setEnableConfigDiscovery(config.getEnableConfigDiscovery()); request.setDisableResume(config.isDisableResume() ? true : null); request.setStreaming(config.isStreaming() ? true : null); + request.setIncludeSubAgentStreamingEvents(config.getIncludeSubAgentStreamingEvents()); request.setMcpServers(config.getMcpServers()); request.setCustomAgents(config.getCustomAgents()); request.setAgent(config.getAgent()); request.setSkillDirectories(config.getSkillDirectories()); request.setDisabledSkills(config.getDisabledSkills()); request.setInfiniteSessions(config.getInfiniteSessions()); + request.setModelCapabilities(config.getModelCapabilities()); if (config.getCommands() != null && !config.getCommands().isEmpty()) { var wireCommands = config.getCommands().stream() diff --git a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java index d030631de..25e777d1a 100644 --- a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java @@ -67,8 +67,11 @@ public final class CreateSessionRequest { @JsonProperty("streaming") private Boolean streaming; + @JsonProperty("includeSubAgentStreamingEvents") + private Boolean includeSubAgentStreamingEvents; + @JsonProperty("mcpServers") - private Map mcpServers; + private Map mcpServers; @JsonProperty("envValueMode") private String envValueMode; @@ -91,12 +94,18 @@ public final class CreateSessionRequest { @JsonProperty("configDir") private String configDir; + @JsonProperty("enableConfigDiscovery") + private Boolean enableConfigDiscovery; + @JsonProperty("commands") private List commands; @JsonProperty("requestElicitation") private Boolean requestElicitation; + @JsonProperty("modelCapabilities") + private ModelCapabilitiesOverride modelCapabilities; + /** Gets the model name. @return the model */ public String getModel() { return model; @@ -240,12 +249,12 @@ public void setStreaming(Boolean streaming) { } /** Gets MCP servers. @return the servers map */ - public Map getMcpServers() { + public Map getMcpServers() { return mcpServers == null ? null : Collections.unmodifiableMap(mcpServers); } /** Sets MCP servers. @param mcpServers the servers map */ - public void setMcpServers(Map mcpServers) { + public void setMcpServers(Map mcpServers) { this.mcpServers = mcpServers; } @@ -319,6 +328,29 @@ public void setConfigDir(String configDir) { this.configDir = configDir; } + /** Gets enable config discovery flag. @return the flag */ + public Boolean getEnableConfigDiscovery() { + return enableConfigDiscovery; + } + + /** Sets enable config discovery flag. @param enableConfigDiscovery the flag */ + public void setEnableConfigDiscovery(Boolean enableConfigDiscovery) { + this.enableConfigDiscovery = enableConfigDiscovery; + } + + /** Gets include sub-agent streaming events flag. @return the flag */ + public Boolean getIncludeSubAgentStreamingEvents() { + return includeSubAgentStreamingEvents; + } + + /** + * Sets include sub-agent streaming events flag. @param + * includeSubAgentStreamingEvents the flag + */ + public void setIncludeSubAgentStreamingEvents(Boolean includeSubAgentStreamingEvents) { + this.includeSubAgentStreamingEvents = includeSubAgentStreamingEvents; + } + /** Gets the commands wire definitions. @return the commands */ public List getCommands() { return commands == null ? null : Collections.unmodifiableList(commands); @@ -338,4 +370,16 @@ public Boolean getRequestElicitation() { public void setRequestElicitation(Boolean requestElicitation) { this.requestElicitation = requestElicitation; } + + /** Gets the model capabilities override. @return the override */ + public ModelCapabilitiesOverride getModelCapabilities() { + return modelCapabilities; + } + + /** + * Sets the model capabilities override. @param modelCapabilities the override + */ + public void setModelCapabilities(ModelCapabilitiesOverride modelCapabilities) { + this.modelCapabilities = modelCapabilities; + } } diff --git a/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java b/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java index 99731ddfb..1421db603 100644 --- a/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java @@ -50,11 +50,14 @@ public class CustomAgentConfig { private String prompt; @JsonProperty("mcpServers") - private Map mcpServers; + private Map mcpServers; @JsonProperty("infer") private Boolean infer; + @JsonProperty("skills") + private List skills; + /** * Gets the unique identifier name for this agent. * @@ -175,7 +178,7 @@ public CustomAgentConfig setPrompt(String prompt) { * * @return the MCP servers map */ - public Map getMcpServers() { + public Map getMcpServers() { return mcpServers == null ? null : Collections.unmodifiableMap(mcpServers); } @@ -186,7 +189,7 @@ public Map getMcpServers() { * the MCP server configurations * @return this config for method chaining */ - public CustomAgentConfig setMcpServers(Map mcpServers) { + public CustomAgentConfig setMcpServers(Map mcpServers) { this.mcpServers = mcpServers; return this; } @@ -211,4 +214,31 @@ public CustomAgentConfig setInfer(Boolean infer) { this.infer = infer; return this; } + + /** + * Gets the list of skill names to preload into this agent's context. + * + * @return the list of skill names, or {@code null} if not set + */ + public List getSkills() { + return skills == null ? null : Collections.unmodifiableList(skills); + } + + /** + * Sets the list of skill names to preload into this agent's context. + *

+ * When set, the full content of each listed skill is eagerly injected into the + * agent's context at startup. Skills are resolved by name from the session's + * configured skill directories + * ({@link SessionConfig#setSkillDirectories(List)}). When omitted, no skills + * are injected (opt-in model). + * + * @param skills + * the list of skill names to preload + * @return this config for method chaining + */ + public CustomAgentConfig setSkills(List skills) { + this.skills = skills; + return this; + } } diff --git a/src/main/java/com/github/copilot/sdk/json/McpHttpServerConfig.java b/src/main/java/com/github/copilot/sdk/json/McpHttpServerConfig.java new file mode 100644 index 000000000..7017db3d2 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/McpHttpServerConfig.java @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Configuration for a remote HTTP/SSE MCP (Model Context Protocol) server. + *

+ * Use this to configure an MCP server that communicates over HTTP or + * Server-Sent Events (SSE). + * + *

Example Usage

+ * + *
{@code
+ * var server = new McpHttpServerConfig().setUrl("https://mcp.example.com/sse").setTools(List.of("*"));
+ *
+ * var config = new SessionConfig().setMcpServers(Map.of("remote-server", server));
+ * }
+ * + * @see McpServerConfig + * @see SessionConfig#setMcpServers(java.util.Map) + * @since 1.3.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class McpHttpServerConfig extends McpServerConfig { + + @JsonProperty("type") + private final String type = "http"; + + @JsonProperty("url") + private String url; + + @JsonProperty("headers") + private Map headers; + + /** + * Gets the server type discriminator. + * + * @return always {@code "http"} + */ + public String getType() { + return type; + } + + /** + * Gets the URL of the remote server. + * + * @return the server URL + */ + public String getUrl() { + return url; + } + + /** + * Sets the URL of the remote server. + * + * @param url + * the server URL + * @return this config for method chaining + */ + public McpHttpServerConfig setUrl(String url) { + this.url = url; + return this; + } + + /** + * Gets the optional HTTP headers to include in requests. + * + * @return the headers map, or {@code null} + */ + public Map getHeaders() { + return headers == null ? null : Collections.unmodifiableMap(headers); + } + + /** + * Sets optional HTTP headers to include in requests to this server. + * + * @param headers + * the headers map + * @return this config for method chaining + */ + public McpHttpServerConfig setHeaders(Map headers) { + this.headers = headers; + return this; + } + + @Override + public McpHttpServerConfig setTools(List tools) { + super.setTools(tools); + return this; + } + + @Override + public McpHttpServerConfig setTimeout(Integer timeout) { + super.setTimeout(timeout); + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/McpServerConfig.java b/src/main/java/com/github/copilot/sdk/json/McpServerConfig.java new file mode 100644 index 000000000..7cf39af6b --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/McpServerConfig.java @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Abstract base class for MCP (Model Context Protocol) server configurations. + *

+ * Use one of the concrete subclasses to configure MCP servers: + *

    + *
  • {@link McpStdioServerConfig} — for local/stdio-based MCP servers
  • + *
  • {@link McpHttpServerConfig} — for remote HTTP/SSE-based MCP servers
  • + *
+ * + * @see McpStdioServerConfig + * @see McpHttpServerConfig + * @see SessionConfig#setMcpServers(java.util.Map) + * @since 1.3.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true, defaultImpl = McpStdioServerConfig.class) +@JsonSubTypes({@JsonSubTypes.Type(value = McpStdioServerConfig.class, name = "stdio"), + @JsonSubTypes.Type(value = McpStdioServerConfig.class, name = "local"), + @JsonSubTypes.Type(value = McpHttpServerConfig.class, name = "http"), + @JsonSubTypes.Type(value = McpHttpServerConfig.class, name = "sse")}) +public abstract class McpServerConfig { + + @JsonProperty("tools") + private List tools; + + @JsonProperty("timeout") + private Integer timeout; + + /** + * Gets the list of tools to include from this server. + *

+ * An empty list means none; use {@code "*"} to include all tools. + * + * @return the list of tool names, or {@code null} if not set + */ + public List getTools() { + return tools == null ? null : Collections.unmodifiableList(tools); + } + + /** + * Sets the list of tools to include from this server. + *

+ * An empty list means none; use {@code "*"} to include all tools. + * + * @param tools + * the list of tool names, or {@code null} + * @return this config for method chaining + */ + public McpServerConfig setTools(List tools) { + this.tools = tools; + return this; + } + + /** + * Gets the optional timeout in milliseconds for tool calls to this server. + * + * @return the timeout in milliseconds, or {@code null} for the default + */ + public Integer getTimeout() { + return timeout; + } + + /** + * Sets an optional timeout in milliseconds for tool calls to this server. + * + * @param timeout + * the timeout in milliseconds, or {@code null} for the default + * @return this config for method chaining + */ + public McpServerConfig setTimeout(Integer timeout) { + this.timeout = timeout; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/McpStdioServerConfig.java b/src/main/java/com/github/copilot/sdk/json/McpStdioServerConfig.java new file mode 100644 index 000000000..900034be6 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/McpStdioServerConfig.java @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Configuration for a local/stdio MCP (Model Context Protocol) server. + *

+ * Use this to configure an MCP server that is launched as a local subprocess + * and communicates via standard input/output. + * + *

Example Usage

+ * + *
{@code
+ * var server = new McpStdioServerConfig().setCommand("npx")
+ * 		.setArgs(List.of("-y", "@modelcontextprotocol/server-filesystem", "/path")).setTools(List.of("*"));
+ *
+ * var config = new SessionConfig().setMcpServers(Map.of("filesystem", server));
+ * }
+ * + * @see McpServerConfig + * @see SessionConfig#setMcpServers(java.util.Map) + * @since 1.3.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class McpStdioServerConfig extends McpServerConfig { + + @JsonProperty("type") + private final String type = "stdio"; + + @JsonProperty("command") + private String command; + + @JsonProperty("args") + private List args; + + @JsonProperty("env") + private Map env; + + @JsonProperty("workingDirectory") + private String workingDirectory; + + /** + * Gets the server type discriminator. + * + * @return always {@code "stdio"} + */ + public String getType() { + return type; + } + + /** + * Gets the command to run the MCP server. + * + * @return the command + */ + public String getCommand() { + return command; + } + + /** + * Sets the command to run the MCP server. + * + * @param command + * the command + * @return this config for method chaining + */ + public McpStdioServerConfig setCommand(String command) { + this.command = command; + return this; + } + + /** + * Gets the arguments to pass to the command. + * + * @return the arguments list, or {@code null} + */ + public List getArgs() { + return args == null ? null : Collections.unmodifiableList(args); + } + + /** + * Sets the arguments to pass to the command. + * + * @param args + * the arguments list + * @return this config for method chaining + */ + public McpStdioServerConfig setArgs(List args) { + this.args = args; + return this; + } + + /** + * Gets the environment variables to pass to the server. + * + * @return the environment variables map, or {@code null} + */ + public Map getEnv() { + return env == null ? null : Collections.unmodifiableMap(env); + } + + /** + * Sets the environment variables to pass to the server. + * + * @param env + * the environment variables map + * @return this config for method chaining + */ + public McpStdioServerConfig setEnv(Map env) { + this.env = env; + return this; + } + + /** + * Gets the working directory for the server process. + * + * @return the working directory path, or {@code null} + */ + public String getWorkingDirectory() { + return workingDirectory; + } + + /** + * Sets the working directory for the server process. + * + * @param workingDirectory + * the working directory path + * @return this config for method chaining + */ + public McpStdioServerConfig setWorkingDirectory(String workingDirectory) { + this.workingDirectory = workingDirectory; + return this; + } + + @Override + public McpStdioServerConfig setTools(List tools) { + super.setTools(tools); + return this; + } + + @Override + public McpStdioServerConfig setTimeout(Integer timeout) { + super.setTimeout(timeout); + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/MessageOptions.java b/src/main/java/com/github/copilot/sdk/json/MessageOptions.java index c2a92014e..21909d576 100644 --- a/src/main/java/com/github/copilot/sdk/json/MessageOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/MessageOptions.java @@ -6,7 +6,9 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import com.fasterxml.jackson.annotation.JsonInclude; @@ -43,6 +45,7 @@ public class MessageOptions { private String prompt; private List attachments; private String mode; + private Map requestHeaders; /** * Gets the message prompt. @@ -123,6 +126,31 @@ public String getMode() { return mode; } + /** + * Gets the custom per-turn HTTP headers for outbound model requests. + * + * @return the headers map, or {@code null} if not set + */ + public Map getRequestHeaders() { + return requestHeaders == null ? null : Collections.unmodifiableMap(requestHeaders); + } + + /** + * Sets custom per-turn HTTP headers for outbound model requests. + *

+ * These headers are included in the model API request for this specific message + * turn. Use this to pass per-request authentication, tracing, or custom + * metadata. + * + * @param requestHeaders + * the headers map + * @return this options instance for method chaining + */ + public MessageOptions setRequestHeaders(Map requestHeaders) { + this.requestHeaders = requestHeaders; + return this; + } + /** * Creates a shallow clone of this {@code MessageOptions} instance. *

@@ -139,6 +167,7 @@ public MessageOptions clone() { copy.prompt = this.prompt; copy.attachments = this.attachments != null ? new ArrayList<>(this.attachments) : null; copy.mode = this.mode; + copy.requestHeaders = this.requestHeaders != null ? new HashMap<>(this.requestHeaders) : null; return copy; } diff --git a/src/main/java/com/github/copilot/sdk/json/ModelCapabilitiesOverride.java b/src/main/java/com/github/copilot/sdk/json/ModelCapabilitiesOverride.java new file mode 100644 index 000000000..18701ad67 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ModelCapabilitiesOverride.java @@ -0,0 +1,235 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Per-property overrides for model capabilities, deep-merged over runtime + * defaults. + *

+ * Use this to override specific model capabilities when creating a session or + * switching models with {@link com.github.copilot.sdk.CopilotSession#setModel}. + * Only non-null fields are applied; unset fields retain their runtime defaults. + * + *

Example: Disable vision for a session

+ * + *
{@code
+ * var config = new SessionConfig().setModel("claude-sonnet-4.5").setModelCapabilities(
+ * 		new ModelCapabilitiesOverride().setSupports(new ModelCapabilitiesOverride.Supports().setVision(false)));
+ * }
+ * + *

Example: Override capabilities when switching models

+ * + *
{@code
+ * session.setModel("claude-sonnet-4.5", null,
+ * 		new ModelCapabilitiesOverride().setSupports(new ModelCapabilitiesOverride.Supports().setVision(true))).get();
+ * }
+ * + * @see com.github.copilot.sdk.CopilotSession#setModel(String, String, + * ModelCapabilitiesOverride) + * @see SessionConfig#setModelCapabilities(ModelCapabilitiesOverride) + * @since 1.3.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelCapabilitiesOverride { + + @JsonProperty("supports") + private Supports supports; + + @JsonProperty("limits") + private Limits limits; + + /** + * Gets the feature flag overrides. + * + * @return the supports overrides, or {@code null} if not set + */ + public Supports getSupports() { + return supports; + } + + /** + * Sets the feature flag overrides. + * + * @param supports + * the supports overrides + * @return this instance for method chaining + */ + public ModelCapabilitiesOverride setSupports(Supports supports) { + this.supports = supports; + return this; + } + + /** + * Gets the token limit overrides. + * + * @return the limits overrides, or {@code null} if not set + */ + public Limits getLimits() { + return limits; + } + + /** + * Sets the token limit overrides. + * + * @param limits + * the limits overrides + * @return this instance for method chaining + */ + public ModelCapabilitiesOverride setLimits(Limits limits) { + this.limits = limits; + return this; + } + + /** + * Feature flag overrides for model capabilities. + *

+ * Set a field to {@code true} or {@code false} to override that capability; + * leave it {@code null} to use the runtime default. + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Supports { + + @JsonProperty("vision") + private Boolean vision; + + @JsonProperty("reasoningEffort") + private Boolean reasoningEffort; + + /** + * Gets the vision override. + * + * @return {@code true} to enable vision, {@code false} to disable, or + * {@code null} to use the runtime default + */ + public Boolean getVision() { + return vision; + } + + /** + * Sets whether vision (image input) is enabled. + * + * @param vision + * {@code true} to enable, {@code false} to disable, or {@code null} + * to use the runtime default + * @return this instance for method chaining + */ + public Supports setVision(Boolean vision) { + this.vision = vision; + return this; + } + + /** + * Gets the reasoning effort override. + * + * @return {@code true} to enable reasoning effort, {@code false} to disable, or + * {@code null} to use the runtime default + */ + public Boolean getReasoningEffort() { + return reasoningEffort; + } + + /** + * Sets whether reasoning effort configuration is enabled. + * + * @param reasoningEffort + * {@code true} to enable, {@code false} to disable, or {@code null} + * to use the runtime default + * @return this instance for method chaining + */ + public Supports setReasoningEffort(Boolean reasoningEffort) { + this.reasoningEffort = reasoningEffort; + return this; + } + } + + /** + * Token limit overrides for model capabilities. + *

+ * Set a field to override that limit; leave it {@code null} to use the runtime + * default. + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Limits { + + @JsonProperty("max_prompt_tokens") + private Integer maxPromptTokens; + + @JsonProperty("max_output_tokens") + private Integer maxOutputTokens; + + @JsonProperty("max_context_window_tokens") + private Integer maxContextWindowTokens; + + /** + * Gets the maximum prompt tokens override. + * + * @return the override value, or {@code null} to use the runtime default + */ + public Integer getMaxPromptTokens() { + return maxPromptTokens; + } + + /** + * Sets the maximum number of tokens in a prompt. + * + * @param maxPromptTokens + * the override value, or {@code null} to use the runtime default + * @return this instance for method chaining + */ + public Limits setMaxPromptTokens(Integer maxPromptTokens) { + this.maxPromptTokens = maxPromptTokens; + return this; + } + + /** + * Gets the maximum output tokens override. + * + * @return the override value, or {@code null} to use the runtime default + */ + public Integer getMaxOutputTokens() { + return maxOutputTokens; + } + + /** + * Sets the maximum number of output tokens. + * + * @param maxOutputTokens + * the override value, or {@code null} to use the runtime default + * @return this instance for method chaining + */ + public Limits setMaxOutputTokens(Integer maxOutputTokens) { + this.maxOutputTokens = maxOutputTokens; + return this; + } + + /** + * Gets the maximum context window tokens override. + * + * @return the override value, or {@code null} to use the runtime default + */ + public Integer getMaxContextWindowTokens() { + return maxContextWindowTokens; + } + + /** + * Sets the maximum total context window size in tokens. + * + * @param maxContextWindowTokens + * the override value, or {@code null} to use the runtime default + * @return this instance for method chaining + */ + public Limits setMaxContextWindowTokens(Integer maxContextWindowTokens) { + this.maxContextWindowTokens = maxContextWindowTokens; + return this; + } + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java b/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java index 96c70cf12..803400db2 100644 --- a/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java @@ -7,6 +7,9 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.Map; + /** * Configuration for a custom API provider (BYOK - Bring Your Own Key). *

@@ -51,6 +54,9 @@ public class ProviderConfig { @JsonProperty("azure") private AzureOptions azure; + @JsonProperty("headers") + private Map headers; + /** * Gets the provider type. * @@ -195,4 +201,28 @@ public ProviderConfig setAzure(AzureOptions azure) { this.azure = azure; return this; } + + /** + * Gets the custom HTTP headers for outbound provider requests. + * + * @return the headers map, or {@code null} if not set + */ + public Map getHeaders() { + return headers == null ? null : Collections.unmodifiableMap(headers); + } + + /** + * Sets custom HTTP headers to include in outbound provider requests. + *

+ * Use this to pass additional authentication headers or custom metadata to the + * provider API. + * + * @param headers + * the headers map + * @return this config for method chaining + */ + public ProviderConfig setHeaders(Map headers) { + this.headers = headers; + return this; + } } diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java index 2836cfd36..c3199c317 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java @@ -44,14 +44,17 @@ public class ResumeSessionConfig { private List excludedTools; private ProviderConfig provider; private String reasoningEffort; + private ModelCapabilitiesOverride modelCapabilities; private PermissionHandler onPermissionRequest; private UserInputHandler onUserInputRequest; private SessionHooks hooks; private String workingDirectory; private String configDir; + private Boolean enableConfigDiscovery; private boolean disableResume; private boolean streaming; - private Map mcpServers; + private Boolean includeSubAgentStreamingEvents; + private Map mcpServers; private List customAgents; private String agent; private List skillDirectories; @@ -356,6 +359,80 @@ public ResumeSessionConfig setConfigDir(String configDir) { return this; } + /** + * Gets whether automatic configuration discovery is enabled. + * + * @return {@code true} to enable discovery, {@code false} to disable, or + * {@code null} to use the runtime default + */ + public Boolean getEnableConfigDiscovery() { + return enableConfigDiscovery; + } + + /** + * Sets whether to automatically discover MCP server configurations and skill + * directories from the working directory. + *

+ * When {@code true}, the CLI scans the working directory for {@code .mcp.json}, + * {@code .vscode/mcp.json} and skill directories, and merges them with + * explicitly provided configurations. + * + * @param enableConfigDiscovery + * {@code true} to enable discovery, {@code false} to disable, or + * {@code null} to use the runtime default + * @return this config for method chaining + */ + public ResumeSessionConfig setEnableConfigDiscovery(Boolean enableConfigDiscovery) { + this.enableConfigDiscovery = enableConfigDiscovery; + return this; + } + + /** + * Gets whether sub-agent streaming events are included. + * + * @return {@code true} to include sub-agent streaming events, {@code false} to + * suppress them, or {@code null} to use the runtime default + */ + public Boolean getIncludeSubAgentStreamingEvents() { + return includeSubAgentStreamingEvents; + } + + /** + * Sets whether to include sub-agent streaming events in the event stream. + * + * @param includeSubAgentStreamingEvents + * {@code true} to include streaming events, {@code false} to + * suppress + * @return this config for method chaining + */ + public ResumeSessionConfig setIncludeSubAgentStreamingEvents(Boolean includeSubAgentStreamingEvents) { + this.includeSubAgentStreamingEvents = includeSubAgentStreamingEvents; + return this; + } + + /** + * Gets the model capabilities override. + * + * @return the model capabilities override, or {@code null} if not set + */ + public ModelCapabilitiesOverride getModelCapabilities() { + return modelCapabilities; + } + + /** + * Sets per-property overrides for model capabilities, deep-merged over runtime + * defaults. + * + * @param modelCapabilities + * the model capabilities override + * @return this config for method chaining + * @see ModelCapabilitiesOverride + */ + public ResumeSessionConfig setModelCapabilities(ModelCapabilitiesOverride modelCapabilities) { + this.modelCapabilities = modelCapabilities; + return this; + } + /** * Returns whether the resume event is disabled. * @@ -405,7 +482,7 @@ public ResumeSessionConfig setStreaming(boolean streaming) { * * @return the MCP servers map */ - public Map getMcpServers() { + public Map getMcpServers() { return mcpServers == null ? null : Collections.unmodifiableMap(mcpServers); } @@ -416,7 +493,7 @@ public Map getMcpServers() { * the MCP servers configuration map * @return this config for method chaining */ - public ResumeSessionConfig setMcpServers(Map mcpServers) { + public ResumeSessionConfig setMcpServers(Map mcpServers) { this.mcpServers = mcpServers; return this; } @@ -629,13 +706,16 @@ public ResumeSessionConfig clone() { copy.excludedTools = this.excludedTools != null ? new ArrayList<>(this.excludedTools) : null; copy.provider = this.provider; copy.reasoningEffort = this.reasoningEffort; + 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.enableConfigDiscovery = this.enableConfigDiscovery; copy.disableResume = this.disableResume; copy.streaming = this.streaming; + copy.includeSubAgentStreamingEvents = this.includeSubAgentStreamingEvents; copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; copy.agent = this.agent; diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java index 7be9a6281..77d00b24d 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java @@ -68,14 +68,20 @@ public final class ResumeSessionRequest { @JsonProperty("configDir") private String configDir; + @JsonProperty("enableConfigDiscovery") + private Boolean enableConfigDiscovery; + @JsonProperty("disableResume") private Boolean disableResume; @JsonProperty("streaming") private Boolean streaming; + @JsonProperty("includeSubAgentStreamingEvents") + private Boolean includeSubAgentStreamingEvents; + @JsonProperty("mcpServers") - private Map mcpServers; + private Map mcpServers; @JsonProperty("envValueMode") private String envValueMode; @@ -101,6 +107,9 @@ public final class ResumeSessionRequest { @JsonProperty("requestElicitation") private Boolean requestElicitation; + @JsonProperty("modelCapabilities") + private ModelCapabilitiesOverride modelCapabilities; + /** Gets the session ID. @return the session ID */ public String getSessionId() { return sessionId; @@ -246,6 +255,16 @@ public void setConfigDir(String configDir) { this.configDir = configDir; } + /** Gets enable config discovery flag. @return the flag */ + public Boolean getEnableConfigDiscovery() { + return enableConfigDiscovery; + } + + /** Sets enable config discovery flag. @param enableConfigDiscovery the flag */ + public void setEnableConfigDiscovery(Boolean enableConfigDiscovery) { + this.enableConfigDiscovery = enableConfigDiscovery; + } + /** Gets disable resume flag. @return the flag */ public Boolean getDisableResume() { return disableResume; @@ -266,13 +285,26 @@ public void setStreaming(Boolean streaming) { this.streaming = streaming; } + /** Gets include sub-agent streaming events flag. @return the flag */ + public Boolean getIncludeSubAgentStreamingEvents() { + return includeSubAgentStreamingEvents; + } + + /** + * Sets include sub-agent streaming events flag. @param + * includeSubAgentStreamingEvents the flag + */ + public void setIncludeSubAgentStreamingEvents(Boolean includeSubAgentStreamingEvents) { + this.includeSubAgentStreamingEvents = includeSubAgentStreamingEvents; + } + /** Gets MCP servers. @return the servers map */ - public Map getMcpServers() { + public Map getMcpServers() { return mcpServers == null ? null : Collections.unmodifiableMap(mcpServers); } /** Sets MCP servers. @param mcpServers the servers map */ - public void setMcpServers(Map mcpServers) { + public void setMcpServers(Map mcpServers) { this.mcpServers = mcpServers; } @@ -358,4 +390,16 @@ public Boolean getRequestElicitation() { public void setRequestElicitation(Boolean requestElicitation) { this.requestElicitation = requestElicitation; } + + /** Gets the model capabilities override. @return the override */ + public ModelCapabilitiesOverride getModelCapabilities() { + return modelCapabilities; + } + + /** + * Sets the model capabilities override. @param modelCapabilities the override + */ + public void setModelCapabilities(ModelCapabilitiesOverride modelCapabilities) { + this.modelCapabilities = modelCapabilities; + } } diff --git a/src/main/java/com/github/copilot/sdk/json/SendMessageRequest.java b/src/main/java/com/github/copilot/sdk/json/SendMessageRequest.java index 32f43a7ef..2ef39770f 100644 --- a/src/main/java/com/github/copilot/sdk/json/SendMessageRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/SendMessageRequest.java @@ -6,6 +6,7 @@ import java.util.Collections; import java.util.List; +import java.util.Map; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -36,6 +37,9 @@ public final class SendMessageRequest { @JsonProperty("mode") private String mode; + @JsonProperty("requestHeaders") + private Map requestHeaders; + /** Gets the session ID. @return the session ID */ public String getSessionId() { return sessionId; @@ -75,4 +79,14 @@ public String getMode() { public void setMode(String mode) { this.mode = mode; } + + /** Gets the per-turn request headers. @return the headers map */ + public Map getRequestHeaders() { + return requestHeaders == null ? null : Collections.unmodifiableMap(requestHeaders); + } + + /** Sets the per-turn request headers. @param requestHeaders the headers map */ + public void setRequestHeaders(Map requestHeaders) { + this.requestHeaders = requestHeaders; + } } diff --git a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java index 50e463e5a..997da5116 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java @@ -50,13 +50,16 @@ public class SessionConfig { private SessionHooks hooks; private String workingDirectory; private boolean streaming; - private Map mcpServers; + private Boolean includeSubAgentStreamingEvents; + private Map mcpServers; private List customAgents; private String agent; private InfiniteSessionConfig infiniteSessions; private List skillDirectories; private List disabledSkills; private String configDir; + private Boolean enableConfigDiscovery; + private ModelCapabilitiesOverride modelCapabilities; private Consumer onEvent; private List commands; private ElicitationHandler onElicitationRequest; @@ -401,7 +404,7 @@ public SessionConfig setStreaming(boolean streaming) { * * @return the MCP servers map */ - public Map getMcpServers() { + public Map getMcpServers() { return mcpServers == null ? null : Collections.unmodifiableMap(mcpServers); } @@ -415,7 +418,7 @@ public Map getMcpServers() { * the MCP servers configuration map * @return this config instance for method chaining */ - public SessionConfig setMcpServers(Map mcpServers) { + public SessionConfig setMcpServers(Map mcpServers) { this.mcpServers = mcpServers; return this; } @@ -568,6 +571,92 @@ public SessionConfig setConfigDir(String configDir) { return this; } + /** + * Gets whether automatic configuration discovery is enabled. + * + * @return {@code true} to enable discovery, {@code false} to disable, or + * {@code null} to use the runtime default + */ + public Boolean getEnableConfigDiscovery() { + return enableConfigDiscovery; + } + + /** + * Sets whether to automatically discover MCP server configurations and skill + * directories from the working directory. + *

+ * When {@code true}, the CLI scans the working directory for {@code .mcp.json}, + * {@code .vscode/mcp.json} and skill directories, and merges them with + * explicitly provided {@link #setMcpServers(Map)} and + * {@link #setSkillDirectories(List)}, with explicit values taking precedence on + * name collision. + * + * @param enableConfigDiscovery + * {@code true} to enable discovery, {@code false} to disable, or + * {@code null} to use the runtime default + * @return this config instance for method chaining + */ + public SessionConfig setEnableConfigDiscovery(Boolean enableConfigDiscovery) { + this.enableConfigDiscovery = enableConfigDiscovery; + return this; + } + + /** + * Gets whether sub-agent streaming events are included. + * + * @return {@code true} to include sub-agent streaming events, {@code false} to + * suppress them, or {@code null} to use the runtime default + */ + public Boolean getIncludeSubAgentStreamingEvents() { + return includeSubAgentStreamingEvents; + } + + /** + * Sets whether to include sub-agent streaming events in the event stream. + *

+ * When {@code true}, streaming delta events from sub-agents (e.g., + * {@code assistant.message_delta} with {@code agentId} set) are forwarded to + * this connection. When {@code false}, only non-streaming sub-agent events and + * {@code subagent.*} lifecycle events are forwarded; streaming deltas from + * sub-agents are suppressed. Default: {@code true}. + * + * @param includeSubAgentStreamingEvents + * {@code true} to include streaming events, {@code false} to + * suppress + * @return this config instance for method chaining + */ + public SessionConfig setIncludeSubAgentStreamingEvents(Boolean includeSubAgentStreamingEvents) { + this.includeSubAgentStreamingEvents = includeSubAgentStreamingEvents; + return this; + } + + /** + * Gets the model capabilities override. + * + * @return the model capabilities override, or {@code null} if not set + */ + public ModelCapabilitiesOverride getModelCapabilities() { + return modelCapabilities; + } + + /** + * Sets per-property overrides for model capabilities, deep-merged over runtime + * defaults. + *

+ * Use this to override specific model capabilities (such as vision support) for + * this session. Only non-null fields in the override are applied; unset fields + * retain their runtime defaults. + * + * @param modelCapabilities + * the model capabilities override + * @return this config instance for method chaining + * @see ModelCapabilitiesOverride + */ + public SessionConfig setModelCapabilities(ModelCapabilitiesOverride modelCapabilities) { + this.modelCapabilities = modelCapabilities; + return this; + } + /** * Gets the event handler registered before the session.create RPC is issued. * @@ -675,6 +764,7 @@ public SessionConfig clone() { copy.hooks = this.hooks; copy.workingDirectory = this.workingDirectory; copy.streaming = this.streaming; + copy.includeSubAgentStreamingEvents = this.includeSubAgentStreamingEvents; copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; copy.agent = this.agent; @@ -682,6 +772,8 @@ public SessionConfig clone() { copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null; copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; copy.configDir = this.configDir; + copy.enableConfigDiscovery = this.enableConfigDiscovery; + copy.modelCapabilities = this.modelCapabilities; copy.onEvent = this.onEvent; copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; copy.onElicitationRequest = this.onElicitationRequest; diff --git a/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java b/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java index 72d088a12..edc503f94 100644 --- a/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java +++ b/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java @@ -52,8 +52,8 @@ void testSendStringThrowsAfterTermination() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -72,8 +72,8 @@ void testSendOptionsThrowsAfterTermination() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -92,8 +92,8 @@ void testSendAndWaitStringThrowsAfterTermination() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -112,8 +112,8 @@ void testSendAndWaitOptionsThrowsAfterTermination() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -132,8 +132,8 @@ void testSendAndWaitWithTimeoutThrowsAfterTermination() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -152,8 +152,8 @@ void testOnConsumerThrowsAfterTermination() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -174,8 +174,8 @@ void testOnTypedConsumerThrowsAfterTermination() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -196,8 +196,8 @@ void testGetMessagesThrowsAfterTermination() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -216,8 +216,8 @@ void testAbortThrowsAfterTermination() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -236,8 +236,8 @@ void testSetEventErrorHandlerThrowsAfterTermination() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -258,8 +258,8 @@ void testSetEventErrorPolicyThrowsAfterTermination() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -278,8 +278,8 @@ void testGetSessionIdWorksAfterTermination() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); String sessionIdBeforeClose = session.getSessionId(); session.close(); @@ -297,8 +297,8 @@ void testGetWorkspacePathWorksAfterTermination() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); String pathBeforeClose = session.getWorkspacePath(); session.close(); @@ -315,8 +315,8 @@ void testCloseIsIdempotent() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); // First close should succeed assertDoesNotThrow(() -> session.close()); @@ -337,8 +337,8 @@ void testTryWithResourcesDoubleClose() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); try (session) { // Manual close within try-with-resources @@ -362,8 +362,8 @@ void testSetModelThrowsAfterTermination() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.close(); assertThrows(IllegalStateException.class, () -> { diff --git a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java index 1e94c22a2..0a5cfcaed 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java @@ -71,8 +71,8 @@ void testShouldReceiveSessionEvents_createAndDestroy() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); assertNotNull(session.getSessionId()); assertTrue(session.getSessionId().matches("^[a-f0-9-]+$")); diff --git a/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java b/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java index 5a362fe4e..2c91dadfe 100644 --- a/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java +++ b/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java @@ -8,7 +8,6 @@ import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterAll; @@ -17,6 +16,8 @@ import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.json.CustomAgentConfig; +import com.github.copilot.sdk.json.McpServerConfig; +import com.github.copilot.sdk.json.McpStdioServerConfig; import com.github.copilot.sdk.json.MessageOptions; import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.ResumeSessionConfig; @@ -46,14 +47,9 @@ static void teardown() throws Exception { } } - // Helper method to create an MCP local server configuration - private Map createLocalMcpServer(String command, List args) { - var server = new HashMap(); - server.put("type", "local"); - server.put("command", command); - server.put("args", args); - server.put("tools", List.of("*")); - return server; + // Helper method to create an MCP stdio server configuration + private McpStdioServerConfig createLocalMcpServer(String command, List args) { + return new McpStdioServerConfig().setCommand(command).setArgs(args).setTools(List.of("*")); } // ============ MCP Server Tests ============ @@ -68,7 +64,7 @@ private Map createLocalMcpServer(String command, List ar void testShouldAcceptMcpServerConfigurationOnSessionCreate() throws Exception { ctx.configureForTest("mcp_and_agents", "should_accept_mcp_server_configuration_on_session_create"); - var mcpServers = new HashMap(); + var mcpServers = new HashMap(); mcpServers.put("test-server", createLocalMcpServer("echo", List.of("hello"))); try (CopilotClient client = ctx.createClient()) { @@ -108,7 +104,7 @@ void testShouldAcceptMcpServerConfigurationOnSessionResume() throws Exception { session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); // Resume with MCP servers - var mcpServers = new HashMap(); + var mcpServers = new HashMap(); mcpServers.put("test-server", createLocalMcpServer("echo", List.of("hello"))); CopilotSession session2 = client.resumeSession(sessionId, new ResumeSessionConfig() @@ -139,7 +135,7 @@ void testShouldHandleMultipleMcpServers() throws Exception { // count ctx.configureForTest("mcp_and_agents", "should_accept_mcp_server_configuration_on_session_create"); - var mcpServers = new HashMap(); + var mcpServers = new HashMap(); mcpServers.put("server1", createLocalMcpServer("echo", List.of("server1"))); mcpServers.put("server2", createLocalMcpServer("echo", List.of("server2"))); @@ -261,7 +257,7 @@ void testShouldAcceptCustomAgentWithMcpServers() throws Exception { // Use combined snapshot since this uses both MCP servers and custom agents ctx.configureForTest("mcp_and_agents", "should_accept_both_mcp_servers_and_custom_agents"); - var agentMcpServers = new HashMap(); + var agentMcpServers = new HashMap(); agentMcpServers.put("agent-server", createLocalMcpServer("echo", List.of("agent-mcp"))); List customAgents = List.of(new CustomAgentConfig().setName("mcp-agent") @@ -315,7 +311,7 @@ void testShouldAcceptMultipleCustomAgents() throws Exception { void testShouldAcceptBothMcpServersAndCustomAgents() throws Exception { ctx.configureForTest("mcp_and_agents", "should_accept_both_mcp_servers_and_custom_agents"); - var mcpServers = new HashMap(); + var mcpServers = new HashMap(); mcpServers.put("shared-server", createLocalMcpServer("echo", List.of("shared"))); List customAgents = List.of(new CustomAgentConfig().setName("combined-agent") diff --git a/src/test/java/com/github/copilot/sdk/SkillsTest.java b/src/test/java/com/github/copilot/sdk/SkillsTest.java index 7d27af3b8..6cf34044f 100644 --- a/src/test/java/com/github/copilot/sdk/SkillsTest.java +++ b/src/test/java/com/github/copilot/sdk/SkillsTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.CustomAgentConfig; import com.github.copilot.sdk.json.MessageOptions; import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.SessionConfig; @@ -158,4 +159,78 @@ void testShouldNotApplySkillWhenDisabledViaDisabledSkills() throws Exception { session.close(); } } + + /** + * Verifies that an agent with a Skills field can preload and invoke the skill. + * + * @see Snapshot: skills/should_allow_agent_with_skills_to_invoke_skill + */ + @Test + void testShouldAllowAgentWithSkillsToInvokeSkill() throws Exception { + ctx.configureForTest("skills", "should_allow_agent_with_skills_to_invoke_skill"); + + Path skillsDirPath = createSkillDir(); + + var agent = new CustomAgentConfig().setName("skill-agent").setDescription("An agent with access to test-skill") + .setPrompt("You are a helpful test agent.").setSkills(List.of("test-skill")); + + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSkillDirectories(List.of(skillsDirPath.toString())).setCustomAgents(List.of(agent)) + .setAgent("skill-agent"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + // The agent has Skills = ["test-skill"], so the skill content is preloaded + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Say hello briefly using the test skill.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains(SKILL_MARKER), + "Response should contain skill marker '" + SKILL_MARKER + "': " + response.getData().content()); + + session.close(); + } + } + + /** + * Verifies that an agent without a Skills field does not get skill content + * injected. + * + * @see Snapshot: skills/should_not_provide_skills_to_agent_without_skills_field + */ + @Test + void testShouldNotProvideSkillsToAgentWithoutSkillsField() throws Exception { + ctx.configureForTest("skills", "should_not_provide_skills_to_agent_without_skills_field"); + + Path skillsDirPath = createSkillDir(); + + var agent = new CustomAgentConfig().setName("no-skill-agent").setDescription("An agent without skills access") + .setPrompt("You are a helpful test agent."); + + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSkillDirectories(List.of(skillsDirPath.toString())).setCustomAgents(List.of(agent)) + .setAgent("no-skill-agent"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + // The agent has no Skills field, so no skill content is injected + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Say hello briefly using the test skill.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertFalse(response.getData().content().contains(SKILL_MARKER), + "Response should NOT contain skill marker when agent has no Skills field: " + + response.getData().content()); + + session.close(); + } + } } From def54493bd17b619464c26b4589647a5ed01824e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 00:28:45 +0000 Subject: [PATCH 3/3] Fix limits mapping in setModel and fix import order in ProviderConfig Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/c48ff7e5-7e42-43e6-a3df-42fe0004aa91 Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- src/main/java/com/github/copilot/sdk/CopilotSession.java | 7 ++++++- .../java/com/github/copilot/sdk/json/ProviderConfig.java | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index a4f2a5b51..b54b2cbf3 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -1590,8 +1590,13 @@ public CompletableFuture setModel(String model, String reasoningEffort, supports = new SessionModelSwitchToParams.SessionModelSwitchToParamsModelCapabilities.SessionModelSwitchToParamsModelCapabilitiesSupports( s.getVision(), s.getReasoningEffort()); } + SessionModelSwitchToParams.SessionModelSwitchToParamsModelCapabilities.SessionModelSwitchToParamsModelCapabilitiesLimits limits = null; + if (modelCapabilities.getLimits() != null) { + limits = new ObjectMapper().convertValue(modelCapabilities.getLimits(), + SessionModelSwitchToParams.SessionModelSwitchToParamsModelCapabilities.SessionModelSwitchToParamsModelCapabilitiesLimits.class); + } generatedCapabilities = new SessionModelSwitchToParams.SessionModelSwitchToParamsModelCapabilities(supports, - null); + limits); } return getRpc().model .switchTo(new SessionModelSwitchToParams(sessionId, model, reasoningEffort, generatedCapabilities)) diff --git a/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java b/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java index 803400db2..3b2995681 100644 --- a/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java @@ -4,12 +4,12 @@ package com.github.copilot.sdk.json; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + /** * Configuration for a custom API provider (BYOK - Bring Your Own Key). *