+ * 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; @@ -299,15 +301,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/java25/com/github/copilot/sdk/DefaultExecutorProvider.java b/src/main/java25/com/github/copilot/sdk/DefaultExecutorProvider.java
new file mode 100644
index 0000000000..c429047f30
--- /dev/null
+++ b/src/main/java25/com/github/copilot/sdk/DefaultExecutorProvider.java
@@ -0,0 +1,18 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+enum DefaultExecutorProvider {
+ INSTANCE;
+
+ private final Executor executor = Executors.newVirtualThreadPerTaskExecutor();
+
+ Executor get() {
+ return executor;
+ }
+}
diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md
index a097da69e4..ef6e2c1cd5 100644
--- a/src/site/markdown/index.md
+++ b/src/site/markdown/index.md
@@ -8,7 +8,7 @@ Welcome to the documentation for the **GitHub Copilot SDK for Java** — a Java
### Requirements
-- Java 17 or later
+- Java 17 or later. On JDK 25 and later, the SDK automatically uses virtual threads for its default internal executor.
- GitHub Copilot CLI 1.0.17 or later installed and in PATH (or provide custom `cliPath`)
### Installation
diff --git a/src/test/java/com/github/copilot/sdk/DefaultExecutorProviderTest.java b/src/test/java/com/github/copilot/sdk/DefaultExecutorProviderTest.java
new file mode 100644
index 0000000000..a7b4fab8e9
--- /dev/null
+++ b/src/test/java/com/github/copilot/sdk/DefaultExecutorProviderTest.java
@@ -0,0 +1,111 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.TimeUnit;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+
+import org.junit.jupiter.api.Test;
+
+class DefaultExecutorProviderTest {
+
+ @Test
+ void testBaseImplementationReturnsForkJoinPool() {
+ Executor executor = DefaultExecutorProvider.INSTANCE.get();
+ assertNotNull(executor);
+ assertEquals(ForkJoinPool.commonPool(), executor);
+ }
+
+ @Test
+ void multiReleaseProviderUsesVirtualThreadsOnJdk25() throws Exception {
+ if (Runtime.version().feature() < 25) {
+ return;
+ }
+
+ Path classes = Path.of("target", "classes");
+ Path baseClass = classes.resolve("com/github/copilot/sdk/DefaultExecutorProvider.class");
+ Path java25Class = classes.resolve("META-INF/versions/25/com/github/copilot/sdk/DefaultExecutorProvider.class");
+ assertTrue(Files.exists(baseClass), "Base DefaultExecutorProvider class must be compiled");
+ assertTrue(Files.exists(java25Class), "JDK 25 build must compile the multi-release executor provider");
+
+ Path jar = Files.createTempFile("copilot-sdk-default-executor", ".jar");
+ try {
+ createProviderJar(jar, baseClass, java25Class);
+
+ try (var loader = new URLClassLoader(new URL[]{jar.toUri().toURL()}, null)) {
+ Class> provider = Class.forName("com.github.copilot.sdk.DefaultExecutorProvider", true, loader);
+ Field instanceField = provider.getDeclaredField("INSTANCE");
+ instanceField.setAccessible(true);
+ Object instance = instanceField.get(null);
+
+ Method getMethod = provider.getDeclaredMethod("get");
+ getMethod.setAccessible(true);
+ Object result = getMethod.invoke(instance);
+
+ assertNotNull(result, "JDK 25 multi-release provider must create a default executor");
+ assertTrue(result instanceof Executor, "Default provider must return an Executor");
+
+ var executor = (Executor) result;
+ try {
+ var ranOnVirtualThread = new CompletableFuture