Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
node-version: 22
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: "17"
java-version: "25"
distribution: "microsoft"
cache: "maven"

Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/publish-maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ jobs:

- uses: ./.github/actions/setup-copilot

- name: Set up JDK 17
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: "17"
java-version: "25"
distribution: "microsoft"
cache: "maven"
server-id: central
Expand Down Expand Up @@ -253,4 +253,3 @@ jobs:
-f publish_as_latest=true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

4 changes: 2 additions & 2 deletions .github/workflows/publish-snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ jobs:

- uses: ./.github/actions/setup-copilot

- name: Set up JDK 17
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: "17"
java-version: "25"
distribution: "microsoft"
cache: "maven"
server-id: central
Expand Down
5 changes: 1 addition & 4 deletions .github/workflows/run-smoke-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,7 @@ jobs:

**Critical override — do NOT run the jar:** Stop after the `mvn --no-snapshot-updates clean package` build succeeds. Do NOT execute Step 4 (java -jar) or Step 5 (verify exit code) from the prompt. The workflow will run the jar in a separate deterministic step to guarantee the exit code propagates correctly.

**Critical override — enable Virtual Threads for JDK 25:** After creating the Java source file from the README "Quick Start" section but BEFORE building, you must modify the source file to enable virtual thread support. The Quick Start code contains inline comments that start with `// JDK 25+:` — these are instructions. Find every such comment and follow what it says (comment out lines it says to comment out, uncomment lines it says to uncomment). Add any imports required by the newly uncommented code (e.g. `java.util.concurrent.Executors`).
Also set `maven.compiler.source` and `maven.compiler.target` to `25` in the `pom.xml`.

Follow steps 1-3 only: create the `smoke-test/` directory, create `pom.xml` and the Java source file exactly as specified, apply the JDK 25 virtual thread modifications described above, and build with `mvn --no-snapshot-updates clean package` (no SNAPSHOT updates and without `-U`).
Follow steps 1-3 only: create the `smoke-test/` directory, create `pom.xml` and the Java source file exactly as specified, and build with `mvn --no-snapshot-updates clean package` (no SNAPSHOT updates and without `-U`). Do not modify the Quick Start source for JDK 25; the SDK automatically selects its JDK 25 defaults from the multi-release JAR.

If any step fails, exit with a non-zero exit code. Do not silently fix errors.
PROMPT_EOF
Expand Down
12 changes: 0 additions & 12 deletions .vscode/mcp.json

This file was deleted.

11 changes: 2 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A

### Requirements

- Java 17 or later. **JDK 25 recommended**. Selecting JDK 25 enables the use of virtual threads, as shown in the [Quick Start](#quick-start).
- Java 17 or later. **JDK 25 recommended**. 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`)

### Maven
Expand Down Expand Up @@ -70,23 +70,16 @@ implementation 'com.github:copilot-sdk-java:1.0.0-beta-java.4'
import com.github.copilot.sdk.CopilotClient;
import com.github.copilot.sdk.generated.AssistantMessageEvent;
import com.github.copilot.sdk.generated.SessionUsageInfoEvent;
import com.github.copilot.sdk.json.CopilotClientOptions;
import com.github.copilot.sdk.json.MessageOptions;
import com.github.copilot.sdk.json.PermissionHandler;
import com.github.copilot.sdk.json.SessionConfig;

import java.util.concurrent.Executors;

public class CopilotSDK {
public static void main(String[] args) throws Exception {
var lastMessage = new String[]{null};

// Create and start client
try (var client = new CopilotClient()) { // JDK 25+: comment out this line
// JDK 25+: uncomment the following 3 lines for virtual thread support
// var options = new CopilotClientOptions()
// .setExecutor(Executors.newVirtualThreadPerTaskExecutor());
// try (var client = new CopilotClient(options)) {
try (var client = new CopilotClient()) {
client.start().get();

// Create a session
Expand Down
46 changes: 46 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,10 @@
<configuration>
<dataFile>${project.build.directory}/jacoco-test-results/sdk-tests.exec</dataFile>
<outputDirectory>${project.reporting.outputDirectory}/jacoco-coverage</outputDirectory>
<excludes>
<!-- Exclude multi-release classes to avoid duplicate class analysis -->
<exclude>META-INF/versions/**/*.class</exclude>
</excludes>
</configuration>
</execution>
</executions>
Expand Down Expand Up @@ -693,6 +697,48 @@
<surefire.jvm.args>-XX:+EnableDynamicAgentLoading</surefire.jvm.args>
</properties>
</profile>
<profile>
<id>java25-multi-release</id>
<activation>
<jdk>[25,)</jdk>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile-java25</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>25</release>
<useIncrementalCompilation>false</useIncrementalCompilation>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/main/java25</compileSourceRoot>
</compileSourceRoots>
<multiReleaseOutput>true</multiReleaseOutput>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<!-- Skip git-clone + npm install of the copilot-sdk test harness -->
<profile>
<id>skip-test-harness</id>
Expand Down
41 changes: 24 additions & 17 deletions src/main/java/com/github/copilot/sdk/CopilotClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
Expand Down Expand Up @@ -78,6 +79,8 @@ public final class CopilotClient implements AutoCloseable {
public static final int AUTOCLOSEABLE_TIMEOUT_SECONDS = 10;
private static final int FORCE_KILL_TIMEOUT_SECONDS = 10;
private final CopilotClientOptions options;
private final Executor executor;
private final ExecutorService ownedExecutor;
private final CliServerManager serverManager;
private final LifecycleEventManager lifecycleManager = new LifecycleEventManager();
private final Map<String, CopilotSession> sessions = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -153,6 +156,11 @@ public CopilotClient(CopilotClientOptions options) {
this.optionsPort = null;
}

Executor providedExecutor = this.options.getExecutor();
this.executor = providedExecutor != null ? providedExecutor : DefaultExecutorProvider.INSTANCE.get();
this.ownedExecutor = providedExecutor == null && this.executor instanceof ExecutorService executorService
? executorService
: null;
Comment on lines +161 to +163
this.serverManager = new CliServerManager(this.options);
this.serverManager.setConnectionToken(this.effectiveConnectionToken);
}
Expand All @@ -176,11 +184,9 @@ public CompletableFuture<Void> start() {
private CompletableFuture<Connection> startCore() {
LOG.fine("Starting Copilot client");

Executor exec = options.getExecutor();
Executor exec = executor;
try {
return exec != null
? CompletableFuture.supplyAsync(this::startCoreBody, exec)
: CompletableFuture.supplyAsync(this::startCoreBody);
return CompletableFuture.supplyAsync(this::startCoreBody, exec);
} catch (RejectedExecutionException e) {
return CompletableFuture.failedFuture(e);
}
Expand Down Expand Up @@ -209,8 +215,7 @@ private Connection startCoreBody() {
Connection connection = new Connection(rpc, process, new ServerRpc(rpc::invoke));

// Register handlers for server-to-client calls
RpcHandlerDispatcher dispatcher = new RpcHandlerDispatcher(sessions, lifecycleManager::dispatch,
options.getExecutor());
RpcHandlerDispatcher dispatcher = new RpcHandlerDispatcher(sessions, lifecycleManager::dispatch, executor);
dispatcher.registerHandlers(rpc);

// Verify protocol version
Expand Down Expand Up @@ -308,7 +313,7 @@ private static boolean isUnsupportedConnectMethod(JsonRpcException ex) {
*/
public CompletableFuture<Void> stop() {
var closeFutures = new ArrayList<CompletableFuture<Void>>();
Executor exec = options.getExecutor();
Executor exec = executor;

for (CopilotSession session : new ArrayList<>(sessions.values())) {
Runnable closeTask = () -> {
Expand All @@ -320,9 +325,7 @@ public CompletableFuture<Void> stop() {
};
CompletableFuture<Void> future;
try {
future = exec != null
? CompletableFuture.runAsync(closeTask, exec)
: CompletableFuture.runAsync(closeTask);
future = CompletableFuture.runAsync(closeTask, exec);
} catch (RejectedExecutionException e) {
LOG.log(Level.WARNING, "Executor rejected session close task; closing inline", e);
closeTask.run();
Expand All @@ -344,7 +347,7 @@ public CompletableFuture<Void> stop() {
public CompletableFuture<Void> forceStop() {
disposed = true;
sessions.clear();
return cleanupConnection();
return cleanupConnection().whenComplete((ignored, error) -> shutdownOwnedExecutor());
}

private CompletableFuture<Void> cleanupConnection() {
Expand Down Expand Up @@ -436,9 +439,7 @@ public CompletableFuture<CopilotSession> createSession(SessionConfig config) {

long setupNanos = System.nanoTime();
var session = new CopilotSession(sessionId, connection.rpc);
if (options.getExecutor() != null) {
session.setExecutor(options.getExecutor());
}
session.setExecutor(executor);
SessionRequestBuilder.configureSession(session, config);
sessions.put(sessionId, session);
LoggingHelpers.logTiming(LOG, Level.FINE,
Expand Down Expand Up @@ -524,9 +525,7 @@ public CompletableFuture<CopilotSession> resumeSession(String sessionId, ResumeS
// Register the session before the RPC call to avoid missing early events.
long setupNanos = System.nanoTime();
var session = new CopilotSession(sessionId, connection.rpc);
if (options.getExecutor() != null) {
session.setExecutor(options.getExecutor());
}
session.setExecutor(executor);
SessionRequestBuilder.configureSession(session, config);
sessions.put(sessionId, session);
LoggingHelpers.logTiming(LOG, Level.FINE,
Expand Down Expand Up @@ -924,6 +923,14 @@ public void close() {
stop().get(AUTOCLOSEABLE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (Exception e) {
LOG.log(Level.FINE, "Error during close", e);
} finally {
shutdownOwnedExecutor();
}
}

private void shutdownOwnedExecutor() {
if (ownedExecutor != null) {
ownedExecutor.shutdown();
}
}

Expand Down
18 changes: 18 additions & 0 deletions src/main/java/com/github/copilot/sdk/DefaultExecutorProvider.java
Original file line number Diff line number Diff line change
@@ -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.ForkJoinPool;

enum DefaultExecutorProvider {
INSTANCE;

private final Executor executor = ForkJoinPool.commonPool();

Executor get() {
return executor;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,11 @@ public CopilotClientOptions setEnvironment(Map<String, String> environment) {

/**
* Gets the executor used for internal asynchronous operations.
* <p>
* 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;
Expand All @@ -299,15 +301,18 @@ public Executor getExecutor() {
* Sets the executor used for internal asynchronous operations.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 1 addition & 1 deletion src/site/markdown/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading