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
12 changes: 2 additions & 10 deletions java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,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 @@ -69,23 +69,16 @@ implementation 'com.github:copilot-sdk-java:1.0.0-beta-java.4'
import com.github.copilot.CopilotClient;
import com.github.copilot.generated.AssistantMessageEvent;
import com.github.copilot.generated.SessionUsageInfoEvent;
import com.github.copilot.rpc.CopilotClientOptions;
import com.github.copilot.rpc.MessageOptions;
import com.github.copilot.rpc.PermissionHandler;
import com.github.copilot.rpc.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 Expand Up @@ -212,4 +205,3 @@ MIT — see [LICENSE](LICENSE) for details.
[![Star History Chart](https://api.star-history.com/svg?repos=github/copilot-sdk-java&type=Date)](https://www.star-history.com/#github/copilot-sdk-java&Date)

⭐ Drop a star if you find this useful!

46 changes: 46 additions & 0 deletions java/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 @@ -507,6 +511,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>
Comment thread
brunoborges marked this conversation as resolved.
</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
52 changes: 35 additions & 17 deletions java/src/main/java/com/github/copilot/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 : InternalExecutorProvider.create();
this.ownedExecutor = providedExecutor == null && InternalExecutorProvider.isOwned(this.executor)
&& this.executor instanceof ExecutorService executorService ? executorService : null;
Comment thread
brunoborges marked this conversation as resolved.

this.serverManager = new CliServerManager(this.options);
this.serverManager.setConnectionToken(this.effectiveConnectionToken);
}
Expand All @@ -176,11 +184,8 @@ public CompletableFuture<Void> start() {
private CompletableFuture<Connection> startCore() {
LOG.fine("Starting Copilot client");

Executor exec = options.getExecutor();
try {
return exec != null
? CompletableFuture.supplyAsync(this::startCoreBody, exec)
: CompletableFuture.supplyAsync(this::startCoreBody);
return CompletableFuture.supplyAsync(this::startCoreBody, executor);
} catch (RejectedExecutionException e) {
return CompletableFuture.failedFuture(e);
}
Expand Down Expand Up @@ -209,8 +214,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 +312,6 @@ private static boolean isUnsupportedConnectMethod(JsonRpcException ex) {
*/
public CompletableFuture<Void> stop() {
var closeFutures = new ArrayList<CompletableFuture<Void>>();
Executor exec = options.getExecutor();

for (CopilotSession session : new ArrayList<>(sessions.values())) {
Runnable closeTask = () -> {
Expand All @@ -320,9 +323,7 @@ public CompletableFuture<Void> stop() {
};
CompletableFuture<Void> future;
try {
future = exec != null
? CompletableFuture.runAsync(closeTask, exec)
: CompletableFuture.runAsync(closeTask);
future = CompletableFuture.runAsync(closeTask, executor);
} catch (RejectedExecutionException e) {
LOG.log(Level.WARNING, "Executor rejected session close task; closing inline", e);
closeTask.run();
Expand All @@ -344,7 +345,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 +437,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 +523,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 @@ -923,6 +920,27 @@ 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) {
return;
}

ownedExecutor.shutdown();
try {
if (!ownedExecutor.awaitTermination(AUTOCLOSEABLE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
LOG.log(Level.FINE, "Owned executor did not terminate within {0} seconds; forcing shutdown.",
AUTOCLOSEABLE_TIMEOUT_SECONDS);
ownedExecutor.shutdownNow();
}
} catch (InterruptedException e) {
ownedExecutor.shutdownNow();
Thread.currentThread().interrupt();
LOG.log(Level.FINE, "Interrupted while waiting for owned executor to terminate", e);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot;

import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;

final class InternalExecutorProvider {

private InternalExecutorProvider() {
}

static Executor create() {
return ForkJoinPool.commonPool();
}

static boolean isOwned(Executor executor) {
return false;
}
}
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,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

final class InternalExecutorProvider {

private InternalExecutorProvider() {
}

static Executor create() {
return Executors.newVirtualThreadPerTaskExecutor();
}

static boolean isOwned(Executor executor) {
return executor instanceof ExecutorService;
}
}
Loading
Loading