+ * The new model takes effect for the next message. Conversation history is
+ * preserved.
+ *
+ * @param model
+ * the model ID to switch to (e.g., {@code "gpt-4.1"})
+ * @param reasoningEffort
+ * reasoning effort level; {@code null} to use default
+ * @param reasoningSummary
+ * reasoning summary mode ({@code "none"}, {@code "concise"}, or
+ * {@code "detailed"}); {@code null} to use default. Use
+ * {@code "none"} to suppress summary output regardless of whether
+ * reasoning is enabled.
+ * @param modelCapabilities
+ * per-property overrides for model capabilities; {@code null} to use
+ * runtime defaults
+ * @return a future that completes when the model switch is acknowledged
+ * @throws IllegalStateException
+ * if this session has been terminated
+ * @since 1.3.0
+ */
+ public CompletableFuture
+ * This is the baseline (JDK 17+) implementation. When no
+ * user-provided executor is supplied, it falls back to
+ * {@link ForkJoinPool#commonPool()}, which is shared with the rest of the JVM
+ * and therefore never owned by the SDK.
+ *
+ *
+ * Multi-release JAR contract. This class has a sibling variant
+ * at {@code src/main/java25/com/github/copilot/InternalExecutorProvider.java}
+ * that is compiled with {@code --release 25} into {@code META-INF/versions/25/}
+ * and selected automatically by the JVM on JDK 25+. Any change to the
+ * package-private surface of this class
+ * ({@link #InternalExecutorProvider(Executor) constructor}, {@link #get()},
+ * {@link #canBeShutdown()}) must be mirrored in both source
+ * trees. The two implementations must remain behaviourally
+ * interchangeable from the caller's perspective; only the default-executor
+ * strategy and ownership semantics differ.
+ *
+ * @implNote Maintainers: when editing this file, also edit
+ * {@code src/main/java25/com/github/copilot/InternalExecutorProvider.java}.
+ * The packaged JAR is verified at build time (see the
+ * {@code java25-multi-release} profile in {@code pom.xml}) to ensure
+ * the JDK 25 overlay is present.
+ */
+final class InternalExecutorProvider {
+
+ private final Executor executor;
+
+ InternalExecutorProvider(Executor userProvided) {
+ if (userProvided != null) {
+ this.executor = userProvided;
+ } else {
+ this.executor = ForkJoinPool.commonPool();
+ }
+ }
+
+ Executor get() {
+ return executor;
+ }
+
+ boolean canBeShutdown() {
+ // Since we are using ForkJoinPool.commonPool() or user provided only,
+ // we should not attempt to shut it down
+ return false;
+ }
+
+}
diff --git a/src/main/java/com/github/copilot/SessionRequestBuilder.java b/src/main/java/com/github/copilot/SessionRequestBuilder.java
index d9ad69282..52f4e2179 100644
--- a/src/main/java/com/github/copilot/SessionRequestBuilder.java
+++ b/src/main/java/com/github/copilot/SessionRequestBuilder.java
@@ -106,6 +106,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess
request.setModel(config.getModel());
request.setClientName(config.getClientName());
request.setReasoningEffort(config.getReasoningEffort());
+ request.setReasoningSummary(config.getReasoningSummary());
request.setTools(config.getTools());
request.setSystemMessage(config.getSystemMessage());
request.setAvailableTools(config.getAvailableTools());
@@ -124,14 +125,17 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess
}
config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents);
request.setMcpServers(config.getMcpServers());
+ request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage());
request.setCustomAgents(config.getCustomAgents());
request.setDefaultAgent(config.getDefaultAgent());
request.setAgent(config.getAgent());
request.setInfiniteSessions(config.getInfiniteSessions());
request.setSkillDirectories(config.getSkillDirectories());
request.setInstructionDirectories(config.getInstructionDirectories());
+ request.setPluginDirectories(config.getPluginDirectories());
+ request.setLargeOutput(config.getLargeOutput());
request.setDisabledSkills(config.getDisabledSkills());
- request.setConfigDir(config.getConfigDir());
+ request.setConfigDirectory(config.getConfigDirectory());
config.getEnableConfigDiscovery().ifPresent(request::setEnableConfigDiscovery);
request.setModelCapabilities(config.getModelCapabilities());
@@ -144,6 +148,9 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess
if (config.getOnElicitationRequest() != null) {
request.setRequestElicitation(true);
}
+ if (config.isEnableMcpApps()) {
+ request.setRequestMcpApps(true);
+ }
if (config.getOnExitPlanMode() != null) {
request.setRequestExitPlanMode(true);
}
@@ -197,6 +204,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
request.setModel(config.getModel());
request.setClientName(config.getClientName());
request.setReasoningEffort(config.getReasoningEffort());
+ request.setReasoningSummary(config.getReasoningSummary());
request.setTools(config.getTools());
request.setSystemMessage(config.getSystemMessage());
request.setAvailableTools(config.getAvailableTools());
@@ -210,7 +218,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
request.setHooks(true);
}
request.setWorkingDirectory(config.getWorkingDirectory());
- request.setConfigDir(config.getConfigDir());
+ request.setConfigDirectory(config.getConfigDirectory());
config.getEnableConfigDiscovery().ifPresent(request::setEnableConfigDiscovery);
if (config.isDisableResume()) {
request.setDisableResume(true);
@@ -220,11 +228,14 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
}
config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents);
request.setMcpServers(config.getMcpServers());
+ request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage());
request.setCustomAgents(config.getCustomAgents());
request.setDefaultAgent(config.getDefaultAgent());
request.setAgent(config.getAgent());
request.setSkillDirectories(config.getSkillDirectories());
request.setInstructionDirectories(config.getInstructionDirectories());
+ request.setPluginDirectories(config.getPluginDirectories());
+ request.setLargeOutput(config.getLargeOutput());
request.setDisabledSkills(config.getDisabledSkills());
request.setInfiniteSessions(config.getInfiniteSessions());
request.setModelCapabilities(config.getModelCapabilities());
@@ -238,6 +249,9 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
if (config.getOnElicitationRequest() != null) {
request.setRequestElicitation(true);
}
+ if (config.isEnableMcpApps()) {
+ request.setRequestMcpApps(true);
+ }
if (config.getOnExitPlanMode() != null) {
request.setRequestExitPlanMode(true);
}
diff --git a/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java b/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java
index 69464aa72..941467059 100644
--- a/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java
+++ b/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java
@@ -288,9 +288,11 @@ public CopilotClientOptions setEnvironment(Map
+ * Returns {@code null} if no executor has been explicitly set, indicating that
+ * the SDK should use its default executor strategy.
*
- * @return the executor, or {@code null} to use the default
- * {@code ForkJoinPool.commonPool()}
+ * @return the executor, or {@code null} if using SDK defaults
*/
public Executor getExecutor() {
return executor;
@@ -300,15 +302,18 @@ public Executor getExecutor() {
* Sets the executor used for internal asynchronous operations.
*
* When provided, the SDK uses this executor for all internal
- * {@code CompletableFuture} combinators instead of the default
- * {@code ForkJoinPool.commonPool()}. This allows callers to isolate SDK work
- * onto a dedicated thread pool or integrate with container-managed threading.
+ * {@code CompletableFuture} combinators. This allows callers to isolate SDK
+ * work onto a dedicated thread pool or integrate with container-managed
+ * threading.
*
- * Passing {@code null} reverts to the default {@code ForkJoinPool.commonPool()}
- * behavior.
+ * The SDK will not shut down a user-provided executor. If you pass a custom
+ * {@code ExecutorService}, you remain responsible for shutting it down.
+ *
+ * If not set (or set to {@code null}), the SDK uses its default executor:
+ * virtual threads on JDK 25+, {@code ForkJoinPool.commonPool()} on older JDKs.
*
* @param executor
- * the executor to use, or {@code null} for the default
+ * the executor to use, or {@code null} for SDK defaults
* @return this options instance for fluent chaining
*/
public CopilotClientOptions setExecutor(Executor executor) {
diff --git a/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java
index 1354e8c33..b8dddbec7 100644
--- a/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java
+++ b/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java
@@ -36,6 +36,9 @@ public final class CreateSessionRequest {
@JsonProperty("reasoningEffort")
private String reasoningEffort;
+ @JsonProperty("reasoningSummary")
+ private String reasoningSummary;
+
@JsonProperty("tools")
private List
+ * When a tool produces output exceeding {@link #getMaxSizeBytes()}, the SDK
+ * writes the full output to a file in {@link #getOutputDirectory()} and returns
+ * a truncated preview to the model.
+ *
+ * @since 1.3.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class LargeToolOutputConfig {
+
+ @JsonProperty("enabled")
+ private Boolean enabled;
+
+ @JsonProperty("maxSizeBytes")
+ private Long maxSizeBytes;
+
+ @JsonProperty("outputDir")
+ private String outputDirectory;
+
+ /**
+ * Gets whether large tool output handling is enabled.
+ *
+ * @return {@code true} if enabled, {@code false} if disabled, {@code null} for
+ * default
+ */
+ public Boolean getEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Sets whether large tool output handling is enabled. Defaults to {@code true}
+ * when unset.
+ *
+ * @param enabled
+ * {@code true} to enable, {@code false} to disable
+ * @return this config for method chaining
+ */
+ public LargeToolOutputConfig setEnabled(Boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
+ /**
+ * Gets the maximum tool output size in bytes before it is redirected to a file.
+ *
+ * @return the maximum size in bytes, or {@code null} for default
+ */
+ public Long getMaxSizeBytes() {
+ return maxSizeBytes;
+ }
+
+ /**
+ * Sets the maximum tool output size in bytes before it is redirected to a file.
+ *
+ * @param maxSizeBytes
+ * the maximum size in bytes
+ * @return this config for method chaining
+ */
+ public LargeToolOutputConfig setMaxSizeBytes(Long maxSizeBytes) {
+ this.maxSizeBytes = maxSizeBytes;
+ return this;
+ }
+
+ /**
+ * Gets the directory where large tool output files are written.
+ *
+ * @return the output directory path, or {@code null} for default
+ */
+ public String getOutputDirectory() {
+ return outputDirectory;
+ }
+
+ /**
+ * Sets the directory where large tool output files are written.
+ *
+ * @param outputDirectory
+ * the output directory path
+ * @return this config for method chaining
+ */
+ public LargeToolOutputConfig setOutputDirectory(String outputDirectory) {
+ this.outputDirectory = outputDirectory;
+ return this;
+ }
+}
diff --git a/src/main/java/com/github/copilot/rpc/MessageOptions.java b/src/main/java/com/github/copilot/rpc/MessageOptions.java
index a6cd02b0a..c781011ff 100644
--- a/src/main/java/com/github/copilot/rpc/MessageOptions.java
+++ b/src/main/java/com/github/copilot/rpc/MessageOptions.java
@@ -47,6 +47,7 @@ public class MessageOptions {
private String mode;
private AgentMode agentMode;
private Map
+ * If provided, this text is displayed in the conversation timeline UI instead
+ * of the actual prompt text.
+ *
+ * @param displayPrompt
+ * the display prompt text
+ * @return this options instance for method chaining
+ */
+ public MessageOptions setDisplayPrompt(String displayPrompt) {
+ this.displayPrompt = displayPrompt;
+ return this;
+ }
+
/**
* Creates a shallow clone of this {@code MessageOptions} instance.
*
@@ -194,6 +219,7 @@ public MessageOptions clone() {
copy.mode = this.mode;
copy.agentMode = this.agentMode;
copy.requestHeaders = this.requestHeaders != null ? new HashMap<>(this.requestHeaders) : null;
+ copy.displayPrompt = this.displayPrompt;
return copy;
}
diff --git a/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java
index fa28258b3..d254d73eb 100644
--- a/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java
+++ b/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java
@@ -51,22 +51,26 @@ public class ResumeSessionConfig {
private Boolean coauthorEnabled;
private Boolean manageScheduleEnabled;
private String reasoningEffort;
+ private String reasoningSummary;
private ModelCapabilitiesOverride modelCapabilities;
private PermissionHandler onPermissionRequest;
private UserInputHandler onUserInputRequest;
private SessionHooks hooks;
private String workingDirectory;
- private String configDir;
+ private String configDirectory;
private Boolean enableConfigDiscovery;
private boolean disableResume;
private boolean streaming;
private Boolean includeSubAgentStreamingEvents;
private Map
* Override the default configuration directory location.
*
- * @param configDir
+ * @param configDirectory
* the configuration directory path
* @return this config for method chaining
*/
- public ResumeSessionConfig setConfigDir(String configDir) {
- this.configDir = configDir;
+ public ResumeSessionConfig setConfigDirectory(String configDirectory) {
+ this.configDirectory = configDirectory;
return this;
}
@@ -740,6 +768,39 @@ public ResumeSessionConfig setMcpServers(Map
+ * Controls how MCP OAuth tokens are stored for this session:
+ *
+ * Controls how MCP OAuth tokens are stored for this session:
+ *
+ * When {@code true} and the runtime has MCP Apps enabled (via the
+ * {@code MCP_APPS} feature flag or {@code COPILOT_MCP_APPS=true} environment
+ * override), the runtime adds the {@code mcp-apps} capability to the session,
+ * which causes it to advertise the
+ * {@code extensions.io.modelcontextprotocol/ui} extension to MCP servers (so
+ * they expose {@code _meta.ui.resourceUri} on tools) and to expose the
+ * {@code session.rpc.mcp.apps.{listTools,callTool,readResource,
+ * setHostContext,getHostContext,diagnose}} JSON-RPC methods.
+ *
+ * If the runtime gate is off, the opt-in is silently dropped server-side (the
+ * runtime logs a warning); the session is created normally but the MCP Apps
+ * surface is unavailable. Inspect {@link SessionUiCapabilities#getMcpApps()} on
+ * {@link com.github.copilot.CopilotSession#getCapabilities()} to detect this.
+ *
+ * SDK consumers MUST set this to {@code true} only when they have an iframe
+ * renderer that can display {@code ui://} MCP App bundles. Setting it without a
+ * renderer will cause MCP servers to register UI-enabled tool variants the
+ * consumer cannot display.
+ *
+ * @param enableMcpApps
+ * {@code true} to opt into MCP Apps support
+ * @return this config instance for method chaining
+ */
+ public SessionConfig setEnableMcpApps(boolean enableMcpApps) {
+ this.enableMcpApps = enableMcpApps;
+ return this;
+ }
+
/**
* Gets the exit-plan-mode request handler.
*
@@ -1200,6 +1346,7 @@ public SessionConfig clone() {
copy.clientName = this.clientName;
copy.model = this.model;
copy.reasoningEffort = this.reasoningEffort;
+ copy.reasoningSummary = this.reasoningSummary;
copy.tools = this.tools != null ? new ArrayList<>(this.tools) : null;
copy.systemMessage = this.systemMessage;
copy.availableTools = this.availableTools != null ? new ArrayList<>(this.availableTools) : null;
@@ -1225,8 +1372,10 @@ public SessionConfig clone() {
copy.instructionDirectories = this.instructionDirectories != null
? new ArrayList<>(this.instructionDirectories)
: null;
+ copy.pluginDirectories = this.pluginDirectories != null ? new ArrayList<>(this.pluginDirectories) : null;
+ copy.largeOutput = this.largeOutput;
copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null;
- copy.configDir = this.configDir;
+ copy.configDirectory = this.configDirectory;
copy.enableConfigDiscovery = this.enableConfigDiscovery;
copy.modelCapabilities = this.modelCapabilities;
copy.onEvent = this.onEvent;
@@ -1234,6 +1383,7 @@ public SessionConfig clone() {
copy.onElicitationRequest = this.onElicitationRequest;
copy.onExitPlanMode = this.onExitPlanMode;
copy.onAutoModeSwitch = this.onAutoModeSwitch;
+ copy.enableMcpApps = this.enableMcpApps;
copy.gitHubToken = this.gitHubToken;
copy.remoteSession = this.remoteSession;
copy.cloud = this.cloud;
diff --git a/src/main/java/com/github/copilot/rpc/SessionUiCapabilities.java b/src/main/java/com/github/copilot/rpc/SessionUiCapabilities.java
index 015220d0c..1d3397c8f 100644
--- a/src/main/java/com/github/copilot/rpc/SessionUiCapabilities.java
+++ b/src/main/java/com/github/copilot/rpc/SessionUiCapabilities.java
@@ -21,6 +21,9 @@ public class SessionUiCapabilities {
@JsonProperty("elicitation")
private Boolean elicitation;
+ @JsonProperty("mcpApps")
+ private Boolean mcpApps;
+
/**
* Returns whether the host supports interactive elicitation dialogs.
*
@@ -53,4 +56,41 @@ public SessionUiCapabilities clearElicitation() {
return this;
}
+ /**
+ * Returns whether the runtime has accepted the session's MCP Apps (SEP-1865)
+ * opt-in. Present and {@code true} when the consumer set
+ * {@code enableMcpApps=true} on create/resume and the runtime's
+ * {@code MCP_APPS} feature flag (or {@code COPILOT_MCP_APPS=true} env override)
+ * is on. Otherwise empty or {@code false}, indicating the runtime silently
+ * dropped the opt-in.
+ *
+ * @return an {@link Optional} containing the boolean value, or empty if not set
+ */
+ @JsonIgnore
+ public Optional This is the JDK 25+ multi-release variant. It is
+ * compiled with {@code --release 25} into
+ * {@code META-INF/versions/25/com/github/copilot/InternalExecutorProvider.class}
+ * inside the packaged JAR and is automatically loaded in preference to the
+ * baseline class when the JVM runtime feature version is 25 or greater.
+ * When no user-provided executor is supplied, it creates an SDK-owned
+ * {@link Executors#newVirtualThreadPerTaskExecutor() virtual-thread executor}
+ * that is shut down by {@link CopilotClient#close()}.
+ *
+ * Multi-release JAR contract. This class is the
+ * JDK 25 sibling of the baseline implementation at
+ * {@code src/main/java/com/github/copilot/InternalExecutorProvider.java}.
+ * The package-private surface of both classes
+ * ({@link #InternalExecutorProvider(Executor) constructor},
+ * {@link #get()}, {@link #canBeShutdown()}) must be kept in
+ * lock-step; only the default-executor strategy and ownership
+ * semantics differ.
+ *
+ * @implNote
+ * Maintainers: when editing this file, also edit
+ * {@code src/main/java/com/github/copilot/InternalExecutorProvider.java}.
+ * The packaged JAR is verified at build time (see the
+ * {@code java25-multi-release} profile in {@code pom.xml}) to ensure this
+ * overlay class is present.
+ */
+final class InternalExecutorProvider {
+
+ private final Executor executor;
+ private final boolean owned;
+
+ InternalExecutorProvider(Executor userProvided) {
+ if (userProvided != null) {
+ this.executor = userProvided;
+ this.owned = false;
+ } else {
+ this.executor = Executors.newVirtualThreadPerTaskExecutor();
+ this.owned = true;
+ }
+ }
+
+ Executor get() {
+ return executor;
+ }
+
+ boolean canBeShutdown() {
+ // We can only shut down the executor if we created it (i.e., if it's owned)
+ // such as when using Executors.newVirtualThreadPerTaskExecutor(),
+ // which creates an executor that we are responsible for shutting down.
+ return owned;
+ }
+}
diff --git a/src/test/java/com/github/copilot/ConfigCloneTest.java b/src/test/java/com/github/copilot/ConfigCloneTest.java
index f26f67ed9..e40a3048b 100644
--- a/src/test/java/com/github/copilot/ConfigCloneTest.java
+++ b/src/test/java/com/github/copilot/ConfigCloneTest.java
@@ -21,6 +21,7 @@
import com.github.copilot.rpc.DefaultAgentConfig;
import com.github.copilot.rpc.ExitPlanModeResult;
import com.github.copilot.rpc.InfiniteSessionConfig;
+import com.github.copilot.rpc.LargeToolOutputConfig;
import com.github.copilot.rpc.MessageOptions;
import com.github.copilot.rpc.ModelInfo;
import com.github.copilot.rpc.ResumeSessionConfig;
@@ -114,6 +115,10 @@ void sessionConfigCloneBasic() {
original.setSessionId("my-session");
original.setClientName("my-app");
original.setModel("gpt-4o");
+ original.setReasoningSummary("detailed");
+ original.setPluginDirectories(List.of("/plugins/a", "/plugins/b"));
+ original.setLargeOutput(
+ new LargeToolOutputConfig().setEnabled(true).setMaxSizeBytes(1024L).setOutputDirectory("/tmp/out"));
original.setStreaming(true);
SessionConfig cloned = original.clone();
@@ -121,6 +126,9 @@ void sessionConfigCloneBasic() {
assertEquals(original.getSessionId(), cloned.getSessionId());
assertEquals(original.getClientName(), cloned.getClientName());
assertEquals(original.getModel(), cloned.getModel());
+ assertEquals(original.getReasoningSummary(), cloned.getReasoningSummary());
+ assertEquals(original.getPluginDirectories(), cloned.getPluginDirectories());
+ assertEquals(original.getLargeOutput(), cloned.getLargeOutput());
assertEquals(original.isStreaming(), cloned.isStreaming());
}
@@ -162,11 +170,18 @@ void sessionConfigAgentAndOnEventCloned() {
void resumeSessionConfigCloneBasic() {
ResumeSessionConfig original = new ResumeSessionConfig();
original.setModel("o1");
+ original.setReasoningSummary("none");
+ original.setPluginDirectories(List.of("/plugins/r"));
+ original.setLargeOutput(
+ new LargeToolOutputConfig().setEnabled(false).setMaxSizeBytes(2048L).setOutputDirectory("/tmp/resume"));
original.setStreaming(false);
ResumeSessionConfig cloned = original.clone();
assertEquals(original.getModel(), cloned.getModel());
+ assertEquals(original.getReasoningSummary(), cloned.getReasoningSummary());
+ assertEquals(original.getPluginDirectories(), cloned.getPluginDirectories());
+ assertEquals(original.getLargeOutput(), cloned.getLargeOutput());
assertEquals(original.isStreaming(), cloned.isStreaming());
}
@@ -328,8 +343,8 @@ void resumeSessionConfigAllSetters() {
config.setWorkingDirectory("/project/src");
assertEquals("/project/src", config.getWorkingDirectory());
- config.setConfigDir("/home/user/.config/copilot");
- assertEquals("/home/user/.config/copilot", config.getConfigDir());
+ config.setConfigDirectory("/home/user/.config/copilot");
+ assertEquals("/home/user/.config/copilot", config.getConfigDirectory());
config.setSkillDirectories(List.of("/skills/custom"));
assertEquals(List.of("/skills/custom"), config.getSkillDirectories());
diff --git a/src/test/java/com/github/copilot/CopilotSessionTest.java b/src/test/java/com/github/copilot/CopilotSessionTest.java
index 9c74d4946..44a7373ec 100644
--- a/src/test/java/com/github/copilot/CopilotSessionTest.java
+++ b/src/test/java/com/github/copilot/CopilotSessionTest.java
@@ -559,7 +559,7 @@ void testShouldCreateSessionWithCustomConfigDir() throws Exception {
String customConfigDir = ctx.getWorkDir().resolve("custom-config").toString();
SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
- .setConfigDir(customConfigDir);
+ .setConfigDirectory(customConfigDir);
CopilotSession session = client.createSession(config).get();
assertNotNull(session.getSessionId());
diff --git a/src/test/java/com/github/copilot/CreateSessionReKeyEntryTest.java b/src/test/java/com/github/copilot/CreateSessionReKeyEntryTest.java
index 94f4edbdf..156c96848 100644
--- a/src/test/java/com/github/copilot/CreateSessionReKeyEntryTest.java
+++ b/src/test/java/com/github/copilot/CreateSessionReKeyEntryTest.java
@@ -25,8 +25,10 @@
import com.github.copilot.rpc.SessionConfig;
/**
- * Tests for the session-map re-key cleanup paths in CopilotClient when the
- * server returns a different session ID than the client-supplied one.
+ * Tests that CopilotClient rejects session.create responses whose sessionId
+ * differs from the client-supplied one. Re-keying the sessions map at runtime
+ * is intentionally not supported โ the server must honor the client-supplied
+ * sessionId (or generate one when none is supplied).
*/
class CreateSessionReKeyEntryTest {
@@ -177,7 +179,7 @@ private static void injectConnection(CopilotClient client, JsonRpcClient rpc) th
}
@Test
- void createSessionReKeyEntry_successfulReKey_removesOldKeyAndAddsNewKey() throws Exception {
+ void createSession_serverReturnsDifferentSessionId_throwsAndRemovesPreRegisteredEntry() throws Exception {
String clientSessionId = "client-supplied-id";
String serverSessionId = "server-returned-id";
@@ -188,26 +190,26 @@ void createSessionReKeyEntry_successfulReKey_removesOldKeyAndAddsNewKey() throws
var config = new SessionConfig().setSessionId(clientSessionId)
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL);
- CopilotSession session = client.createSession(config).get();
+ ExecutionException ex = assertThrows(ExecutionException.class, () -> client.createSession(config).get());
+ assertNotNull(ex.getCause());
+ assertTrue(ex.getCause().getMessage().contains(serverSessionId),
+ "Error message should mention the server-returned sessionId");
+ assertTrue(ex.getCause().getMessage().contains(clientSessionId),
+ "Error message should mention the client-requested sessionId");
Map
+ * Runs after {@code package}, when {@code target/${finalName}.jar} exists with
+ * its real {@code Multi-Release: true} manifest and (on JDK 25+ builds) the
+ * {@code META-INF/versions/25/} override produced by {@code maven-jar-plugin}.
+ *
+ * The test spawns a child JVM with the packaged JAR plus {@code test-classes}
+ * on the classpath, runs {@link InternalExecutorProviderProbe}, and asserts
+ * that the executor selected for the current runtime matches expectations.
+ */
+class InternalExecutorProviderIT {
+
+ @Test
+ void packagedJarSelectsExecutorPerRuntimeVersion() throws Exception {
+ Path packagedJar = locatePackagedJar();
+ Path testClasses = locateTestClassesDir();
+ String javaBin = locateJavaBinary();
+
+ String classpath = packagedJar.toString() + File.pathSeparator + testClasses.toString();
+ Process process = new ProcessBuilder(javaBin, "-cp", classpath,
+ "com.github.copilot.InternalExecutorProviderProbe").redirectErrorStream(true).start();
+
+ String output;
+ try {
+ output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
+ assertTrue(process.waitFor(30, TimeUnit.SECONDS), "Probe JVM did not exit within 30s. Output:\n" + output);
+ } finally {
+ if (process.isAlive()) {
+ process.destroyForcibly();
+ }
+ }
+
+ assertEquals(0, process.exitValue(), "Probe exited non-zero. Output:\n" + output);
+
+ Map
+ * Lives in the same package as {@link InternalExecutorProvider} so it can use
+ * its package-private API directly, without reflection.
+ *
+ * Output format (key=value, one per line):
+ *
+ *
+ *
+ * If not set and the client is in
+ * {@link com.github.copilot.CopilotClientMode#EMPTY EMPTY} mode, the SDK
+ * defaults to {@code "in-memory"} for safe multitenant behavior. In other modes
+ * this field is left unset.
+ *
+ * @param mcpOAuthTokenStorage
+ * the storage mode
+ * @return this config for method chaining
+ */
+ public ResumeSessionConfig setMcpOAuthTokenStorage(String mcpOAuthTokenStorage) {
+ this.mcpOAuthTokenStorage = mcpOAuthTokenStorage;
+ return this;
+ }
+
/**
* Gets the custom agent configurations.
*
@@ -852,6 +913,48 @@ public ResumeSessionConfig setInstructionDirectories(List
+ *
+ * If not set and the client is in
+ * {@link com.github.copilot.CopilotClientMode#EMPTY EMPTY} mode, the SDK
+ * defaults to {@code "in-memory"} for safe multitenant behavior. In other modes
+ * this field is left unset.
+ *
+ * @param mcpOAuthTokenStorage
+ * the storage mode
+ * @return this config instance for method chaining
+ */
+ public SessionConfig setMcpOAuthTokenStorage(String mcpOAuthTokenStorage) {
+ this.mcpOAuthTokenStorage = mcpOAuthTokenStorage;
+ return this;
+ }
+
/**
* Gets the custom agent configurations.
*
@@ -796,6 +857,48 @@ public SessionConfig setInstructionDirectories(List
+ * feature=<JDK feature version>
+ * canBeShutdown=<true|false>
+ * virtual=<true|false>
+ *
+ */
+final class InternalExecutorProviderProbe {
+
+ private InternalExecutorProviderProbe() {
+ }
+
+ public static void main(String[] args) throws Exception {
+ InternalExecutorProvider provider = new InternalExecutorProvider(null);
+ Executor executor = provider.get();
+ boolean canBeShutdown = provider.canBeShutdown();
+
+ AtomicBoolean virtual = new AtomicBoolean();
+ CountDownLatch latch = new CountDownLatch(1);
+ executor.execute(() -> {
+ try {
+ virtual.set(isCurrentThreadVirtual());
+ } finally {
+ latch.countDown();
+ }
+ });
+
+ try {
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ System.out.println("error=task-timeout");
+ System.exit(2);
+ }
+ } finally {
+ if (executor instanceof ExecutorService es) {
+ es.shutdownNow();
+ }
+ }
+
+ System.out.println("feature=" + Runtime.version().feature());
+ System.out.println("canBeShutdown=" + canBeShutdown);
+ System.out.println("virtual=" + virtual.get());
+ }
+
+ private static boolean isCurrentThreadVirtual() {
+ try {
+ Method isVirtual = Thread.class.getMethod("isVirtual");
+ return (Boolean) isVirtual.invoke(Thread.currentThread());
+ } catch (ReflectiveOperationException e) {
+ return false;
+ }
+ }
+}
diff --git a/src/test/java/com/github/copilot/InternalExecutorProviderTest.java b/src/test/java/com/github/copilot/InternalExecutorProviderTest.java
new file mode 100644
index 000000000..f1d854cb5
--- /dev/null
+++ b/src/test/java/com/github/copilot/InternalExecutorProviderTest.java
@@ -0,0 +1,55 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import java.lang.reflect.Modifier;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ForkJoinPool;
+
+import org.junit.jupiter.api.Test;
+
+import com.github.copilot.rpc.CopilotClientOptions;
+
+class InternalExecutorProviderTest {
+
+ @Test
+ void baseProviderReturnsCommonPool() {
+ Executor executor = new InternalExecutorProvider(null).get();
+
+ assertSame(ForkJoinPool.commonPool(), executor);
+ }
+
+ @Test
+ void userProvidedExecutorIsNotOwned() {
+ Executor executor = ForkJoinPool.commonPool();
+
+ assertFalse(new InternalExecutorProvider(executor).canBeShutdown());
+ }
+
+ @Test
+ void providerIsPackagePrivate() {
+ assertFalse(Modifier.isPublic(InternalExecutorProvider.class.getModifiers()));
+ }
+
+ @Test
+ void clientDoesNotShutDownUserProvidedExecutor() {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ try {
+ try (var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false).setExecutor(executor))) {
+ assertNotNull(client);
+ }
+
+ assertFalse(executor.isShutdown());
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+}
diff --git a/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/src/test/java/com/github/copilot/SessionRequestBuilderTest.java
index 49a4c9c30..9fca584d1 100644
--- a/src/test/java/com/github/copilot/SessionRequestBuilderTest.java
+++ b/src/test/java/com/github/copilot/SessionRequestBuilderTest.java
@@ -21,6 +21,7 @@
import com.github.copilot.rpc.ElicitationResult;
import com.github.copilot.rpc.ElicitationResultAction;
import com.github.copilot.rpc.ExitPlanModeResult;
+import com.github.copilot.rpc.LargeToolOutputConfig;
import com.github.copilot.rpc.ResumeSessionConfig;
import com.github.copilot.rpc.ResumeSessionRequest;
import com.github.copilot.rpc.SessionConfig;
@@ -90,6 +91,23 @@ void testBuildCreateRequestSetsClientName() {
assertEquals("my-app", request.getClientName());
}
+ @Test
+ void testBuildCreateRequestSetsReasoningSummary() {
+ var config = new SessionConfig().setReasoningSummary("concise");
+ CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config);
+ assertEquals("concise", request.getReasoningSummary());
+ }
+
+ @Test
+ void testBuildCreateRequestSetsPluginDirectoriesAndLargeOutput() {
+ var largeOutput = new LargeToolOutputConfig().setEnabled(true).setMaxSizeBytes(1024L)
+ .setOutputDirectory("/tmp/out");
+ var config = new SessionConfig().setPluginDirectories(List.of("/plugins/a")).setLargeOutput(largeOutput);
+ CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config);
+ assertEquals(List.of("/plugins/a"), request.getPluginDirectories());
+ assertEquals(largeOutput, request.getLargeOutput());
+ }
+
@Test
void testBuildCreateRequestForwardsEnableSessionTelemetryWhenFalse() {
var config = new SessionConfig().setEnableSessionTelemetry(false);
@@ -104,6 +122,26 @@ void testBuildCreateRequestOmitsEnableSessionTelemetryWhenNotSet() {
assertNull(request.getEnableSessionTelemetry());
}
+ @Test
+ void testBuildCreateRequestPassesThroughNullMcpOAuthTokenStorage() {
+ var config = new SessionConfig();
+ CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config);
+ assertNull(request.getMcpOAuthTokenStorage());
+ }
+
+ @Test
+ void testBuildCreateRequestForwardsExplicitMcpOAuthTokenStorage() {
+ var config = new SessionConfig().setMcpOAuthTokenStorage("persistent");
+ CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config);
+ assertEquals("persistent", request.getMcpOAuthTokenStorage());
+ }
+
+ @Test
+ void testBuildCreateRequestNullConfigHasNullMcpOAuthTokenStorage() {
+ CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(null);
+ assertNull(request.getMcpOAuthTokenStorage());
+ }
+
// =========================================================================
// buildResumeRequest
// =========================================================================
@@ -212,6 +250,43 @@ void testBuildResumeRequestSetsClientName() {
assertEquals("my-app", request.getClientName());
}
+ @Test
+ void testBuildResumeRequestPassesThroughNullMcpOAuthTokenStorage() {
+ var config = new ResumeSessionConfig();
+ ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-11", config);
+ assertNull(request.getMcpOAuthTokenStorage());
+ }
+
+ @Test
+ void testBuildResumeRequestForwardsExplicitMcpOAuthTokenStorage() {
+ var config = new ResumeSessionConfig().setMcpOAuthTokenStorage("persistent");
+ ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-12", config);
+ assertEquals("persistent", request.getMcpOAuthTokenStorage());
+ }
+
+ @Test
+ void testBuildResumeRequestNullConfigHasNullMcpOAuthTokenStorage() {
+ ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-13", null);
+ assertNull(request.getMcpOAuthTokenStorage());
+ }
+
+ @Test
+ void testBuildResumeRequestSetsReasoningSummary() {
+ var config = new ResumeSessionConfig().setReasoningSummary("none");
+ ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-14", config);
+ assertEquals("none", request.getReasoningSummary());
+ }
+
+ @Test
+ void testBuildResumeRequestSetsPluginDirectoriesAndLargeOutput() {
+ var largeOutput = new LargeToolOutputConfig().setEnabled(false).setMaxSizeBytes(2048L)
+ .setOutputDirectory("/tmp/resume");
+ var config = new ResumeSessionConfig().setPluginDirectories(List.of("/plugins/r")).setLargeOutput(largeOutput);
+ ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-15", config);
+ assertEquals(List.of("/plugins/r"), request.getPluginDirectories());
+ assertEquals(largeOutput, request.getLargeOutput());
+ }
+
// =========================================================================
// configureSession (ResumeSessionConfig overload)
// =========================================================================