# GitHub Copilot SDK for Java [![Build](https://github.com/github/copilot-sdk/actions/workflows/java-sdk-tests.yml/badge.svg)](https://github.com/github/copilot-sdk/actions/workflows/java-sdk-tests.yml) [![Java 17+](https://img.shields.io/badge/Java-17%2B-blue?logo=openjdk&logoColor=white)](https://openjdk.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) #### Latest release [![GitHub Release Date](https://img.shields.io/github/release-date/github/copilot-sdk)](https://github.com/github/copilot-sdk/releases) [![GitHub Release](https://img.shields.io/github/v/release/github/copilot-sdk)](https://github.com/github/copilot-sdk/releases) [![Maven Central](https://img.shields.io/maven-central/v/com.github/copilot-sdk-java)](https://central.sonatype.com/artifact/com.github/copilot-sdk-java) [![Javadoc](https://javadoc.io/badge2/com.github/copilot-sdk-java/javadoc.svg?q=1)](https://javadoc.io/doc/com.github/copilot-sdk-java/latest/index.html) ## Background Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build AI-powered applications and agentic workflows. The Java SDK tracks the official GitHub Copilot SDK family (TypeScript, Python, Go, .NET, and Rust). ## Prerequisites To use the SDK, you'll need: - Java 17 or later. **JDK 25 recommended**. The distributed jar is a multi-release jar (MR-JAR) and is compiled on JDK 25 with `maven.compiler.release` set to 17. This means, when run on JDK 25 and later, the SDK automatically uses virtual threads for its default internal executor. Managed stdio and TCP connections materialize the platform classifier's `copilot-runtime[.exe]` and adjacent `runtime.node` by default. An explicit `cliPath` or `COPILOT_CLI_PATH` environment variable overrides the bundled runtime. ## Installation ### Maven ```xml com.github copilot-sdk-java 1.0.13 ``` ### Gradle ```groovy implementation 'com.github:copilot-sdk-java:1.0.13' ``` ### Snapshot builds Snapshot builds of the next development version are published to Maven Central Snapshots. To use them, add the snapshot repository and depend on the development version: #### Maven ```xml central-snapshots https://central.sonatype.com/repository/maven-snapshots/ true com.github copilot-sdk-java 1.0.14-SNAPSHOT ``` #### Gradle ```groovy implementation 'com.github:copilot-sdk-java:1.0.14-SNAPSHOT' ``` ## In-process mode (experimental) The SDK supports running the Copilot runtime **in-process** as a native library instead of spawning a separate CLI process. This eliminates process management overhead and simplifies deployment. In-process mode is currently experimental and supported on **linux-x64** (glibc), **linux-arm64** (glibc), **win32-x64**, **win32-arm64**, and **darwin-arm64**. Because in-process mode is experimental, see the [Using experimental APIs](#using-experimental-apis) section for how to opt in. ### Additional dependency Add both the SDK and the platform-specific native runtime to your project: ```xml com.github copilot-sdk-java ${copilot.version} com.github copilot-sdk-java-runtime ${copilot.version} linux-x64 net.java.dev.jna jna 5.19.1 ``` ### Usage Configure the client to use the in-process connection: ```java CopilotClientOptions options = new CopilotClientOptions() .setConnection(RuntimeConnection.forInProcess()); CopilotClient client = new CopilotClient(options); client.start().get(); ``` ## Quick Start ```java import com.github.copilot.CopilotClient; import com.github.copilot.generated.AssistantMessageEvent; import com.github.copilot.generated.SessionUsageInfoEvent; import com.github.copilot.rpc.MessageOptions; import com.github.copilot.rpc.PermissionHandler; import com.github.copilot.rpc.SessionConfig; 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()) { client.start().get(); // Create a session var session = client.createSession( new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("claude-sonnet-4.5")).get(); // Handle assistant message events session.on(AssistantMessageEvent.class, msg -> { lastMessage[0] = msg.getData().content(); System.out.println(lastMessage[0]); }); // Handle session usage info events session.on(SessionUsageInfoEvent.class, usage -> { var data = usage.getData(); System.out.println("\n--- Usage Metrics ---"); System.out.println("Current tokens: " + data.currentTokens().intValue()); System.out.println("Token limit: " + data.tokenLimit().intValue()); System.out.println("Messages count: " + data.messagesLength().intValue()); }); // Send a message var completable = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")); // and wait for completion completable.get(); } boolean success = lastMessage[0] != null && lastMessage[0].contains("4"); System.exit(success ? 0 : -1); } } ``` When targeting MCP tools configured through `setMcpServers(...)`, remember the runtime tool name is `-`. For `setAvailableTools(...)` and `setExcludedTools(...)`, prefer the source-qualified filter form `mcp:-`. For `CustomAgentConfig.setTools(...)` and `DefaultAgentConfig.setExcludedTools(...)`, use `-` directly. `CopilotClientOptions.setCwd(...)` sets the runtime process working directory, which otherwise inherits the current process working directory. `SessionConfig.setWorkingDirectory(...)` sets the session working directory, which otherwise defaults to the runtime process working directory. `SessionConfig.setAskUserVariant(AskUserVariant.ELICITATION)` selects the structured form-based `ask_user` tool when an elicitation handler is also set. The default is `AskUserVariant.LEGACY`. Re-supply the option and handler through `ResumeSessionConfig` on a cold resume. For rotating per-session GitHub credentials, use `SessionConfig.setGitHubTokenProvider(...)` (or the equivalent `ResumeSessionConfig` setter) instead of `setGitHubToken(...)`: ```java var config = new SessionConfig() .setGitHubTokenProvider(args -> acquireForHost(args.host()).thenApply(token -> GitHubTokenProviderResult.token(token, 8 * 60 * 60))) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL); ``` The remaining lifetime is required and must be positive when the callback completes; production GitHub tokens typically last eight hours. A static token and a provider are mutually exclusive. Initial acquisition runs during session creation or resume. Cancellation, provider errors, and invalid token responses reject that operation instead of falling back to ambient authentication. Idle sessions refresh only before their next credential-consuming operation; there is no background refresh timer. ## Message source Use `MessageSource.SYSTEM` for application-generated system context and `MessageSource.agent(id)` for messages from an identified agent: ```java import com.github.copilot.rpc.MessageOptions; import com.github.copilot.rpc.MessageSource; session.send(new MessageOptions() .setPrompt("The background build completed successfully.") .setSource(MessageSource.SYSTEM)).get(); session.sendAndWait(new MessageOptions() .setPrompt("The review found no blocking issues.") .setSource(MessageSource.agent("reviewer"))).get(); ``` Leave `source` unset to omit it from the request and retain the runtime's default user-input behavior, or set `MessageSource.USER` explicitly. Source is independent of delivery mode (`enqueue` or `immediate`) and does not configure the session's system prompt. Agent sources serialize as `agent-`. Pass the agent ID without adding a prefix. The SDK preserves its case and whitespace and rejects null IDs. `sendAndWait` accepts the same source values as `send`. ## Permission Handling `PermissionHandler.APPROVE_ALL` approves requests when managed settings are disabled. When `enableManagedSettings` is true, it completes exceptionally. Custom handlers can inspect `request.getManagedApprovalRequired()` for human-facing confirmation logic. When handling `PermissionRequestedEvent` directly, convert its generated event value with `PermissionRequest.fromJsonValue(event.getData().permissionRequest())` to access the typed metadata. Custom handlers must check managed approval before applying kind-specific automatic decisions: ```java import java.util.concurrent.CompletableFuture; import com.github.copilot.rpc.PermissionHandler; import com.github.copilot.rpc.PermissionRequestResult; PermissionHandler handler = (request, invocation) -> { if (Boolean.TRUE.equals(request.getManagedApprovalRequired())) { return CompletableFuture.completedFuture(PermissionRequestResult.noResult()); } return CompletableFuture.completedFuture(PermissionRequestResult.approveOnce()); }; ``` ## Try it with JBang You can run the SDK without setting up a full Java project, by using [JBang](https://www.jbang.dev/). See the full source of [`jbang-example.java`](sdk/jbang-example.java) for a complete example with more features like session idle handling and usage info events. Or run it directly from the repository: ```bash jbang https://github.com/github/copilot-sdk/blob/main/java/sdk/jbang-example.java ``` ## Annotation-based tools and `ToolInvocation` context When you define tools with `@CopilotTool`, parameters of type `ToolInvocation` are injected as runtime context and are not exposed in the tool schema. `ToolInvocation` can appear before, between, or after schema-visible parameters. ```java import com.github.copilot.rpc.ToolInvocation; import com.github.copilot.tool.CopilotTool; import com.github.copilot.tool.CopilotToolParam; class ProgressTools { @CopilotTool("Reports the current phase and session") public String reportProgress( @CopilotToolParam("Current phase") String phase, ToolInvocation invocation) { return "phase=" + phase + ", sessionId=" + invocation.getSessionId(); } } ``` Position examples: ```java @CopilotTool("Invocation first") public String report(ToolInvocation invocation, @CopilotToolParam("Phase") String phase) { ... } @CopilotTool("Invocation only") public String onlyContext(ToolInvocation invocation) { ... } @CopilotTool("Invocation middle") public String report(@CopilotToolParam("Phase") String phase, ToolInvocation invocation, @CopilotToolParam("Limit") int limit) { ... } ``` ## Inline lambda tool definitions (experimental) For inline tool authoring at the session construction site, use `ToolDefinition.from(...)` with explicit parameter metadata: ```java import com.github.copilot.rpc.ToolDefinition; import com.github.copilot.rpc.ToolDefer; import com.github.copilot.tool.Param; ToolDefinition search = ToolDefinition .from( "search_items", "Searches indexed items by keyword", Param.of(String.class, "keyword", "Search keyword"), keyword -> "Searching for: " + keyword) .skipPermission(true) .defer(ToolDefer.AUTO); ``` ### Parameter metadata with `Param.of(...)` `Param.of(type, name, description)` creates a required parameter. For optional parameters with defaults: ```java Param limit = Param.of(Integer.class, "limit", "Max results", false, "10"); ``` ### Async handlers Use `fromAsync` for asynchronous tool handlers: ```java import java.util.concurrent.CompletableFuture; ToolDefinition fetchData = ToolDefinition.fromAsync( "fetch_data", "Fetches data from remote source", Param.of(String.class, "url", "Data source URL"), url -> CompletableFuture.supplyAsync(() -> fetchRemote(url)) ); ``` ### ToolInvocation context injection Inline tools can access `ToolInvocation` runtime context using `fromWithToolInvocation`: ```java ToolDefinition reportPhase = ToolDefinition.fromWithToolInvocation( "report_phase", "Reports the current phase with invocation context", Param.of(String.class, "phase", "The current phase"), (phase, invocation) -> "phase=" + phase + ", toolCallId=" + invocation.getToolCallId() ); ``` For async with `ToolInvocation`, use `fromAsyncWithToolInvocation`. ### Fluent option modifiers Chain fluent modifiers to set tool options: - `.skipPermission(boolean)` — bypass permission prompts - `.defer(ToolDefer)` — control deferred execution (`AUTO`, `NEVER`) - `.overridesBuiltInTool(boolean)` — shadow built-in tools For design context and decision rationale, see [ADR-006](docs/adr/adr-006-tool-definition-inline.md). ## Auto routing tiers Use `CapiSessionOptions.setAutoTier(...)` to select `AutoTier.EFFICIENCY`, `AutoTier.BALANCE`, or `AutoTier.INTELLIGENCE`. This option is meaningful only with model `auto` (Auto mode V2). It requires a runtime version that supports `capi.autoTier`. ```java import com.github.copilot.rpc.AutoTier; import com.github.copilot.rpc.CapiSessionOptions; import com.github.copilot.rpc.PermissionHandler; import com.github.copilot.rpc.SessionConfig; var config = new SessionConfig() .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("auto") .setCapi(new CapiSessionOptions().setAutoTier(AutoTier.BALANCE)); ``` The same options work with `ResumeSessionConfig.setCapi(...)` and can be combined with `setEnableWebSocketResponses(false)`. The SDK omits an unset (`null`) tier: the runtime chooses its default on create and preserves the persisted/current tier on resume. An explicit tier overrides the persisted tier on cold resume. On resident resume, a different tier requests a safe switch applied after the resume succeeds; it cannot change a turn that is already in flight. The SDK does not choose a default or manage tier persistence. See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for the lifecycle rules. ### Changing the Auto tier during a session Change the Auto routing preference without changing the selected model. The runtime does not apply the preference immediately: it records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives. Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure. Read the authoritative committed, pending, and activating preferences at any time through the session's `model.getCurrent` RPC method. ```java var result = session.setAutoTier(AutoTier.INTELLIGENCE).get(); if (result.status() == ModelSwitchAutoTierStatus.PENDING) { // Accepted, but not yet in effect. } // Return to the provider's default Auto routing. session.setAutoTier(null).get(); ``` `setModel(SetModelOptions)` accepts the same preference through `SetModelOptions.setAutoTier(...)`, which stages the tier atomically with selecting `auto`. Call `setResetAutoTier(true)` instead to return to provider-default routing; the two options are mutually exclusive. ## Session Store `enableSessionStore` on `SessionConfig` enables the cross-session store for search and retrieval across sessions. When unset in the default `CopilotClientMode.COPILOT_CLI` mode, the runtime default applies (enabled). In `CopilotClientMode.EMPTY` mode, defaults to disabled. ## Memory Sessions can opt into persistent memory, allowing the agent to read and write memory across turns. Memory is configured per session and applies to both `createSession` and `resumeSession`. For more background, see [About GitHub Copilot Memory](https://docs.github.com/en/copilot/concepts/agents/copilot-memory). ```java import com.github.copilot.rpc.MemoryConfiguration; import com.github.copilot.rpc.PermissionHandler; import com.github.copilot.rpc.ResumeSessionConfig; import com.github.copilot.rpc.SessionConfig; // Enable memory for a new session var session = client.createSession(new SessionConfig() .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("gpt-5") .setMemory(new MemoryConfiguration().setEnabled(true)) ).get(); // Disable memory for a new session var sessionNoMemory = client.createSession(new SessionConfig() .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("gpt-5") .setMemory(new MemoryConfiguration().setEnabled(false)) ).get(); // Configure memory while resuming var resumed = client.resumeSession(sessionId, new ResumeSessionConfig() .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setMemory(new MemoryConfiguration().setEnabled(true)) ).get(); ``` When `memory` is left unset, no memory configuration is sent and the runtime default applies. In the default `CopilotClientMode.COPILOT_CLI` the SDK leaves `memory` unset so the runtime applies its own default, while `CopilotClientMode.EMPTY` defaults `memory` to disabled unless you set it explicitly. ## Using experimental APIs Some SDK APIs are marked as experimental with `@CopilotExperimental`. These APIs may change or be removed in future versions without notice. By default, referencing an experimental API from your code causes a **compile-time error**: ``` error: Use of experimental API 'ExperimentalType' in field type is not allowed. Add @AllowCopilotExperimental or compiler option -Acopilot.experimental.allowed=true to opt in. ``` To opt in and use experimental APIs, either: - annotate the consuming class, method, or constructor with `@AllowCopilotExperimental`, or - pass the annotation processor option `-Acopilot.experimental.allowed=true` to the Java compiler. ### In code ```java import com.github.copilot.AllowCopilotExperimental; import test.ExperimentalType; @AllowCopilotExperimental public class Consumer { private ExperimentalType field; public ExperimentalType getIt() { return field; } @AllowCopilotExperimental public ExperimentalType echo(ExperimentalType value) { return value; } } ``` ### Maven ```xml org.apache.maven.plugins maven-compiler-plugin -Acopilot.experimental.allowed=true ``` ### Gradle ```groovy tasks.withType(JavaCompile) { options.compilerArgs += ['-Acopilot.experimental.allowed=true'] } ``` ### What the processor catches The processor detects usage of experimental types in **declarations**: | Usage pattern | Caught? | |---|---| | Field declared with experimental type | ✅ | | Method parameter of experimental type | ✅ | | Method return type is experimental | ✅ | | `extends` / `implements` experimental type | ✅ | | `throws` an experimental exception type | ✅ | | Generic type argument is experimental (e.g., `List`) | ✅ | ### Known limitations The processor uses standard JSR 269 annotation processing APIs for maximum portability (works with javac, ECJ/Eclipse, and any compliant compiler). This means it inspects **declarations only**, not expressions inside method bodies. The following patterns are **not caught** by the processor: | Usage pattern | Caught? | Workaround | |---|---|---| | `new ExperimentalType()` in a method body (no field/param declaration) | ❌ | Use the compiler flag for a whole-compilation opt-in | | `ExperimentalType.staticMethod()` inline call | ❌ | Use the compiler flag for a whole-compilation opt-in | | Method reference `ExperimentalType::method` | ❌ | Use the compiler flag for a whole-compilation opt-in | | Local variable with experimental type (including `var` inference) | ❌ | Move the usage into a declaration the processor can see, or use the compiler flag | | Cast to experimental type | ❌ | Use the compiler flag for a whole-compilation opt-in | In practice, these gaps rarely matter: any meaningful use of an experimental SDK type almost always appears in a field declaration, method signature, or type hierarchy — all of which are caught. A purely inline expression with no declaration footprint (e.g., `session.rpc().experimental.foo().join()`) is the only case that would slip through. See [ADR-004](docs/adr/adr-004-copilotexperimental.md) for the design rationale. ### Example ```java import com.github.copilot.CopilotExperimental; // This type is experimental — consumer code that references it // in declarations will fail to compile unless the opt-in flag is provided. @CopilotExperimental public class ExperimentalType { public void doSomething() {} } // Consumer code — compiles only with -Acopilot.experimental.allowed=true import test.ExperimentalType; public class Consumer { private ExperimentalType field; // ← caught: field type public ExperimentalType getIt() { return field; } // ← caught: return type public void setIt(ExperimentalType v) { } // ← caught: parameter type } ``` The gate also applies to individual methods annotated with `@CopilotExperimental` on otherwise stable types. When a type-level annotation is present, all member accesses through that type are considered experimental. `@AllowCopilotExperimental` mirrors the same declaration-level boundary: annotating a class opts in that class and its enclosed declarations, while annotating a method or constructor opts in just that executable signature. ## Projects Using This SDK | Project | Description | | ----------------------------------------------------------------------------- | ------------------------------------------ | | [JMeter Copilot Plugin](https://github.com/brunoborges/jmeter-copilot-plugin) | JMeter plugin for AI-assisted load testing | > Want to add your project? Open a PR! ### Development Setup Requires JDK 25 or later and a supported [Node.js version](../nodejs/README.md#prerequisites) for development. The following steps validate the artifact built with JDK 25 runs on both 25 and 17, preserving the MR-JAR behavior. ```bash # Clone the repository git clone https://github.com/github/copilot-sdk.git cd copilot-sdk/java # Build and test with JDK 25 mvn test-compile jar:jar mvn verify -Dskip.test.harness=true # Set your paths for JDK 17 # Run the JDK 25 built jar with JDK 17 JVM for tests. Do not re-compile the jar. mvn jacoco:prepare-agent@wire-up-coverage-instrumentation antrun:run@print-test-jdk-banner surefire:test failsafe:integration-test failsafe:verify jacoco:report@build-coverage-report-from-tests -Denforcer.skip=true ``` #### Formatting and linting From the repository root, run `just format-java` to apply formatting and `just lint-java` to check formatting and Javadoc. These recipes are also included in `just format` and `just lint`. Without `just`, run the equivalent Maven commands from `java/`: ```bash # Apply formatting mvn -pl sdk spotless:apply # Check formatting and Javadoc mvn -pl sdk spotless:check checkstyle:check ``` CI enforces both checks. Spotless runs explicitly in CI; `mvn verify` alone does not check formatting. #### Development Setup for native embedding Run native-runtime Maven commands from the `java` directory. Native packaging requires Node.js in addition to JDK 25 and Maven because `copilot-native/scripts/fetch-native.mjs` retrieves the pinned runtime package from the corresponding GitHub release. On a native Linux glibc host, Maven activates `native-linux-x64` or `native-linux-arm64` for the matching architecture when `copilot.native.libc=glibc` is set. On Windows x64, Windows ARM64, and Apple Silicon macOS, Maven activates `native-win32-x64`, `native-win32-arm64`, or `native-darwin-arm64` automatically. The matching profile validates the host, runs the native script tests, fetches the pinned platform package from the corresponding `github/copilot-cli` release during `generate-resources`, packages the classifier JAR during `package`, and verifies its native contents. Before opting in, validate that Node.js reports glibc for the build host: ```bash node copilot-native/scripts/validate-native-host.mjs linux-x64 mvn -pl copilot-native clean verify -Dcopilot.native.libc=glibc ``` The `inprocess` test profile performs the same validation and native packaging automatically, so the full in-process test command remains: ```bash mvn -Pinprocess clean verify ``` On Windows x64 or ARM64 PowerShell, initialize Java and run the same profile: ```powershell mvn -Pinprocess clean verify ``` The same command validates in-process mode on Apple Silicon macOS: ```bash node copilot-native/scripts/validate-native-host.mjs darwin-arm64 mvn -Pinprocess clean verify ``` The same command validates in-process mode on Linux ARM64: ```bash node copilot-native/scripts/validate-native-host.mjs linux-arm64 mvn -Pinprocess clean verify -Dcopilot.native.libc=glibc ``` On Intel macOS, Linux musl, and other unsupported hosts, do not set `copilot.native.libc=glibc`. A normal build produces only the OS-neutral primary, sources, and Javadoc JARs; it does not run native script tests, download or stage native files, or produce a platform classifier JAR. To build only the OS-neutral artifacts on any host, or override the glibc opt-in, disable native download and packaging: ```bash mvn -pl copilot-native clean package -DskipTests -Dcopilot.native.libc=glibc -Dcopilot.native.skip.download=true ``` The verified Linux x64 checks are: ```bash node --test copilot-native/scripts/fetch-native.test.mjs copilot-native/scripts/validate-native-host.test.mjs mvn -pl copilot-native help:active-profiles -Dcopilot.native.libc=glibc -Dcopilot.native.skip.download=false mvn -pl copilot-native test -Dcopilot.native.libc=glibc mvn clean verify -Dcopilot.native.libc=glibc mvn clean package -pl copilot-native -DskipTests -Dcopilot.native.libc=glibc -Dcopilot.native.skip.download=true ``` Each classifier JAR includes `runtime.node`, `platform.properties`, and `copilot-runtime` (or `copilot-runtime.exe`) under its `native/` directory. It does not contain the legacy `copilot` SEA. The placeholder JAR remains OS-neutral and contains no native binaries. Unsupported hosts retain the placeholder-only behavior. ## License MIT — see [LICENSE](../LICENSE) for details.