Skip to content
Draft
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
20 changes: 11 additions & 9 deletions 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 21+ recommended**the SDK automatically uses virtual threads for its internal I/O on Java 21+, and any virtual-thread-based custom executor examples using `Executors.newVirtualThreadPerTaskExecutor()` also require Java 21+ (see [Virtual Threads](#virtual-threads)).
- GitHub Copilot CLI 1.0.17 or later installed and in `PATH` (or provide custom `cliPath`)

### Maven
Expand Down Expand Up @@ -69,23 +69,17 @@ implementation 'com.github:copilot-sdk-java:0.2.1-java.1'
import com.github.copilot.sdk.CopilotClient;
import com.github.copilot.sdk.events.AssistantMessageEvent;
import com.github.copilot.sdk.events.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)) {
// On Java 21+, the SDK automatically uses virtual threads for internal I/O.
try (var client = new CopilotClient()) {
client.start().get();

// Create a session
Expand Down Expand Up @@ -143,6 +137,14 @@ jbang https://github.com/github/copilot-sdk-java/blob/latest/jbang-example.java
- [MCP Servers Integration](https://github.github.io/copilot-sdk-java/latest/mcp.html)
- [Cookbook](src/site/markdown/cookbook/) — Practical recipes for common use cases

## Virtual Threads

When running on **Java 21+**, the SDK automatically uses [virtual threads](https://openjdk.org/jeps/444) for its internal I/O threads (JSON-RPC reader loop, CLI stderr forwarding). This is implemented via a [Multi-Release JAR (JEP 238)](https://openjdk.org/jeps/238) — no configuration or code changes are required on your part.

On Java 17–20, the SDK falls back to standard platform (daemon) threads.

> **Note:** The `ScheduledExecutorService` used for `sendAndWait` timeouts always uses platform threads, because the JDK does not provide a virtual-thread-based scheduled executor.

## Projects Using This SDK

| Project | Description |
Expand Down
1 change: 1 addition & 0 deletions jbang-example.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
class CopilotSDK {
public static void main(String[] args) throws Exception {
// Create and start client
// On Java 21+, the SDK automatically uses virtual threads for internal I/O.
try (var client = new CopilotClient()) {
client.start().get();

Expand Down
30 changes: 30 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
<archive>
<manifestEntries>
<Automatic-Module-Name>com.github.copilot.sdk.java</Automatic-Module-Name>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</archive>
</configuration>
Expand Down Expand Up @@ -261,6 +262,11 @@
<version>2.44.5</version>
<configuration>
<java>
<includes>
<include>src/main/java/**/*.java</include>
<include>src/main/java21/**/*.java</include>
<include>src/test/java/**/*.java</include>
</includes>
<eclipse>
<version>4.33</version>
</eclipse>
Expand Down Expand Up @@ -556,6 +562,30 @@
<properties>
<surefire.jvm.args>-XX:+EnableDynamicAgentLoading</surefire.jvm.args>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile-java21</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>21</release>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/main/java21</compileSourceRoot>
</compileSourceRoots>
<multiReleaseOutput>true</multiReleaseOutput>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<!-- Skip git-clone + npm install of the copilot-sdk test harness -->
<profile>
Expand Down
3 changes: 1 addition & 2 deletions src/main/java/com/github/copilot/sdk/CliServerManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ JsonRpcClient connectToServer(Process process, String tcpHost, Integer tcpPort)
}

private void startStderrReader(Process process) {
var stderrThread = new Thread(() -> {
var stderrThread = ThreadFactoryProvider.newThread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
String line;
Expand All @@ -186,7 +186,6 @@ private void startStderrReader(Process process) {
LOG.log(Level.FINE, "Error reading stderr", e);
}
}, "cli-stderr-reader");
stderrThread.setDaemon(true);
stderrThread.start();
}

Expand Down
7 changes: 7 additions & 0 deletions src/main/java/com/github/copilot/sdk/CopilotClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@
* provides methods to create and manage conversation sessions. It can either
* spawn a CLI server process or connect to an existing server.
* <p>
* <b>Threading:</b> On Java 21+, the SDK automatically uses virtual threads for
* its internal I/O threads (JSON-RPC reader, CLI stderr forwarding). On Java
* 17–20, standard platform threads are used. The
* {@link java.util.concurrent.ScheduledExecutorService} used for
* {@code sendAndWait} timeouts always uses platform threads because the JDK
* does not provide a virtual-thread-based scheduled executor.
* <p>
* Example usage:
*
* <pre>{@code
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/github/copilot/sdk/CopilotSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@
* session data on disk — the conversation can be resumed later via
* {@link CopilotClient#resumeSession}. To permanently delete session data, use
* {@link CopilotClient#deleteSession}.
* <p>
* <b>Threading:</b> The {@link java.util.concurrent.ScheduledExecutorService}
* used for {@code sendAndWait} timeouts always uses platform threads regardless
* of the Java version, because the JDK does not provide a virtual-thread-based
* scheduled executor.
*
* <h2>Example Usage</h2>
*
Expand Down
7 changes: 1 addition & 6 deletions src/main/java/com/github/copilot/sdk/JsonRpcClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
import java.util.logging.Level;
Expand Down Expand Up @@ -57,11 +56,7 @@ private JsonRpcClient(InputStream inputStream, OutputStream outputStream, Socket
this.outputStream = outputStream;
this.socket = socket;
this.process = process;
this.readerExecutor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "jsonrpc-reader");
t.setDaemon(true);
return t;
});
this.readerExecutor = ThreadFactoryProvider.newSingleThreadExecutor("jsonrpc-reader");
startReader();
}

Expand Down
68 changes: 68 additions & 0 deletions src/main/java/com/github/copilot/sdk/ThreadFactoryProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.sdk;

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

/**
* Provides thread factories for the SDK's internal thread creation.
* <p>
* On Java 17, this class returns standard platform-thread factories. On Java
* 21+, the multi-release JAR overlay replaces this class with one that returns
* virtual-thread factories, giving the SDK lightweight threads for its
* I/O-bound JSON-RPC communication without any user configuration.
* <p>
* The {@link java.util.concurrent.ScheduledExecutorService} used for
* {@code sendAndWait} timeouts in {@link CopilotSession} is <em>not</em>
* affected, because the JDK offers no virtual-thread-based scheduled executor.
*
* @since 0.2.2-java.1
*/
final class ThreadFactoryProvider {

private ThreadFactoryProvider() {
}
Comment on lines +24 to +27
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOG is declared but never used in this class. Please remove it (and any now-unneeded logging import) to avoid dead code and future confusion.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed unused LOG field and java.util.logging.Logger import from both ThreadFactoryProvider files (baseline and Java 21 overlay). Also fixed the Java 21 overlay to use Executors.newSingleThreadExecutor(...) instead of newThreadPerTaskExecutor(...) for a true single-thread executor, and replaced the empty verification section in advanced.md with actionable guidance. See commit 9d456f6.


/**
* Creates a new daemon thread with the given name and runnable.
*
* @param runnable
* the task to run
* @param name
* the thread name for debuggability
* @return the new (unstarted) thread
*/
static Thread newThread(Runnable runnable, String name) {
Thread t = new Thread(runnable, name);
t.setDaemon(true);
return t;
}

/**
* Creates a single-thread executor suitable for the JSON-RPC reader loop.
*
* @param name
* the thread name for debuggability
* @return a single-thread {@link ExecutorService}
*/
static ExecutorService newSingleThreadExecutor(String name) {
return Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, name);
t.setDaemon(true);
return t;
});
}

/**
* Returns {@code true} when this class uses virtual threads (Java 21+
* multi-release overlay), {@code false} for platform threads.
*
* @return whether virtual threads are in use
*/
static boolean isVirtualThreads() {
return false;
}
}
58 changes: 58 additions & 0 deletions src/main/java21/com/github/copilot/sdk/ThreadFactoryProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.sdk;

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

/**
* Java 21+ override that uses virtual threads for the SDK's internal thread
* creation.
* <p>
* This class is placed under {@code META-INF/versions/21/} in the multi-release
* JAR and replaces the baseline {@code ThreadFactoryProvider} when running on
* Java 21 or later.
*
* @since 0.2.2-java.1
*/
final class ThreadFactoryProvider {

private ThreadFactoryProvider() {
}
Comment on lines +20 to +23
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOG is declared but never used in this class. Please remove it (and any now-unneeded logging import) to avoid dead code and future confusion.

Copilot uses AI. Check for mistakes.

/**
* Creates a new virtual thread with the given name and runnable.
*
* @param runnable
* the task to run
* @param name
* the thread name for debuggability
* @return the new (unstarted) virtual thread
*/
static Thread newThread(Runnable runnable, String name) {
return Thread.ofVirtual().name(name).unstarted(runnable);
}

/**
* Creates a single-thread executor backed by a virtual-thread factory for the
* JSON-RPC reader loop.
*
* @param name
* the thread name for debuggability
* @return a single-thread virtual-thread-backed {@link ExecutorService}
*/
static ExecutorService newSingleThreadExecutor(String name) {
return Executors.newSingleThreadExecutor(Thread.ofVirtual().name(name).factory());
}

/**
* Returns {@code true} — this is the virtual-thread overlay.
*
* @return {@code true}
*/
static boolean isVirtualThreads() {
return true;
}
}
27 changes: 27 additions & 0 deletions src/site/markdown/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ This guide covers advanced scenarios for extending and customizing your Copilot
- [Session Capabilities](#Session_Capabilities)
- [Outgoing Elicitation via session.getUi()](#Outgoing_Elicitation_via_session.getUi)
- [Getting Session Metadata by ID](#Getting_Session_Metadata_by_ID)
- [Virtual Threads (Java 21+)](#Virtual_Threads_Java_21)

---

Expand Down Expand Up @@ -1237,6 +1238,32 @@ This is more efficient than `listSessions()` when you already know the session I

---

## Virtual Threads (Java 21+)

When running on **Java 21 or later**, the SDK automatically uses [virtual threads (JEP 444)](https://openjdk.org/jeps/444) for its internal I/O threads. This is implemented via a [Multi-Release JAR (JEP 238)](https://openjdk.org/jeps/238) — no configuration or code changes are required.

### What Uses Virtual Threads

| Component | Thread name | Java 17–20 | Java 21+ |
|-----------|------------|-------------|----------|
| JSON-RPC reader loop | `jsonrpc-reader` | Platform (daemon) | Virtual |
| CLI stderr forwarding | `cli-stderr-reader` | Platform (daemon) | Virtual |
| `sendAndWait` timeouts | `sendAndWait-timeout` | Platform (daemon) | Platform (daemon) |

The `sendAndWait` timeout scheduler always uses platform threads because the JDK does not provide a virtual-thread-based `ScheduledExecutorService`.

### Performance Implications

Virtual threads are lightweight and scheduled by the JVM on a shared `ForkJoinPool` carrier pool. For the I/O-bound JSON-RPC communication this SDK performs, virtual threads reduce memory footprint and improve scalability when many concurrent sessions are active.

### How It Works

The SDK JAR includes `Multi-Release: true` in its manifest. On Java 21+, the JVM loads the `ThreadFactoryProvider` class from `META-INF/versions/21/`, which uses `Thread.ofVirtual()`. On earlier JVMs, the baseline class under the main class path is loaded, which creates standard platform threads.

### Verifying Virtual Thread Usage

Thread names are preserved for debuggability regardless of thread type. On Java 21+, a thread dump will show `jsonrpc-reader` and `cli-stderr-reader` as virtual threads rather than platform threads. You can verify this via `jcmd <pid> Thread.dump_to_file -format=json <file>` or your IDE's thread inspector.

## Next Steps

- 📖 **[Documentation](documentation.html)** - Core concepts, events, streaming, models, tool filtering, reasoning effort
Expand Down
2 changes: 1 addition & 1 deletion src/site/markdown/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Copilot: In Tokyo it's 75°F and sunny. Great day to be outside!

Before you begin, make sure you have:

- **Java 17+** installed
- **Java 17+** installed (**Java 21+ recommended** for automatic [virtual thread](https://openjdk.org/jeps/444) support)
- **GitHub Copilot CLI** installed and authenticated ([Installation guide](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli))

Verify the CLI is working:
Expand Down
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 (**Java 21+ recommended** for automatic [virtual thread](https://openjdk.org/jeps/444) support)
- GitHub Copilot CLI 1.0.17 or later installed and in PATH (or provide custom `cliPath`)

### Installation
Expand Down
Loading
Loading