diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml
new file mode 100644
index 00000000..741e54f3
--- /dev/null
+++ b/.github/workflows/publish-maven.yml
@@ -0,0 +1,153 @@
+name: Publish Java SDK to Maven Central
+
+env:
+ HUSKY: 0
+
+on:
+ workflow_dispatch:
+ inputs:
+ dist-tag:
+ description: "Tag to publish under"
+ type: choice
+ required: true
+ default: "latest"
+ options:
+ - latest
+ - prerelease
+ version:
+ description: "Version override (optional, e.g., 1.0.0). If empty, auto-increments."
+ type: string
+ required: false
+
+permissions:
+ contents: write
+ id-token: write
+
+concurrency:
+ group: publish-maven
+ cancel-in-progress: false
+
+jobs:
+ # Calculate version using the nodejs version script (shared across all SDKs)
+ version:
+ name: Calculate Version
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.version.outputs.VERSION }}
+ current: ${{ steps.version.outputs.CURRENT }}
+ current-prerelease: ${{ steps.version.outputs.CURRENT_PRERELEASE }}
+ defaults:
+ run:
+ working-directory: ./nodejs
+ steps:
+ - uses: actions/checkout@v6
+ - uses: actions/setup-node@v6
+ with:
+ node-version: "22.x"
+ - run: npm ci --ignore-scripts
+ - name: Get version
+ id: version
+ run: |
+ CURRENT="$(node scripts/get-version.js current)"
+ echo "CURRENT=$CURRENT" >> $GITHUB_OUTPUT
+ echo "Current latest version: $CURRENT" >> $GITHUB_STEP_SUMMARY
+ CURRENT_PRERELEASE="$(node scripts/get-version.js current-prerelease)"
+ echo "CURRENT_PRERELEASE=$CURRENT_PRERELEASE" >> $GITHUB_OUTPUT
+ echo "Current prerelease version: $CURRENT_PRERELEASE" >> $GITHUB_STEP_SUMMARY
+ if [ -n "${{ github.event.inputs.version }}" ]; then
+ VERSION="${{ github.event.inputs.version }}"
+ # Validate version format matches dist-tag
+ if [ "${{ github.event.inputs.dist-tag }}" = "latest" ]; then
+ if [[ "$VERSION" == *-* ]]; then
+ echo "❌ Error: Version '$VERSION' has a prerelease suffix but dist-tag is 'latest'" >> $GITHUB_STEP_SUMMARY
+ echo "Use a version without suffix (e.g., '1.0.0') for latest releases"
+ exit 1
+ fi
+ else
+ if [[ "$VERSION" != *-* ]]; then
+ echo "❌ Error: Version '$VERSION' has no prerelease suffix but dist-tag is 'prerelease'" >> $GITHUB_STEP_SUMMARY
+ echo "Use a version with suffix (e.g., '1.0.0-preview.0') for prerelease"
+ exit 1
+ fi
+ fi
+ echo "Using manual version override: $VERSION" >> $GITHUB_STEP_SUMMARY
+ else
+ VERSION="$(node scripts/get-version.js ${{ github.event.inputs.dist-tag }})"
+ echo "Auto-incremented version: $VERSION" >> $GITHUB_STEP_SUMMARY
+ fi
+ echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
+
+ publish-maven:
+ name: Publish Java SDK to Maven Central
+ needs: version
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./java
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@v5
+ with:
+ java-version: "21"
+ distribution: "temurin"
+ server-id: central
+ server-username: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
+ server-password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
+ gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
+ gpg-passphrase: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
+
+ - name: Set version in pom.xml
+ run: mvn versions:set -DnewVersion=${{ needs.version.outputs.version }} -DgenerateBackupPoms=false
+
+ - name: Build and verify
+ run: mvn clean verify -DskipTests
+
+ - name: Deploy to Maven Central
+ run: mvn deploy -DskipTests -Prelease
+ env:
+ MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
+ MAVEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
+ MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v6
+ with:
+ name: java-package
+ path: java/target/*.jar
+
+ github-release:
+ name: Create GitHub Release
+ needs: [version, publish-maven]
+ if: github.ref == 'refs/heads/main'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - name: Create GitHub Release
+ if: github.event.inputs.dist-tag == 'latest'
+ run: |
+ NOTES_FLAG=""
+ if git rev-parse "v${{ needs.version.outputs.current }}" >/dev/null 2>&1; then
+ NOTES_FLAG="--notes-start-tag v${{ needs.version.outputs.current }}"
+ fi
+ gh release create "java/v${{ needs.version.outputs.version }}" \
+ --title "Java SDK v${{ needs.version.outputs.version }}" \
+ --generate-notes $NOTES_FLAG \
+ --target ${{ github.sha }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Create GitHub Pre-Release
+ if: github.event.inputs.dist-tag == 'prerelease'
+ run: |
+ NOTES_FLAG=""
+ if git rev-parse "v${{ needs.version.outputs.current-prerelease }}" >/dev/null 2>&1; then
+ NOTES_FLAG="--notes-start-tag v${{ needs.version.outputs.current-prerelease }}"
+ fi
+ gh release create "java/v${{ needs.version.outputs.version }}" \
+ --prerelease \
+ --title "Java SDK v${{ needs.version.outputs.version }}" \
+ --generate-notes $NOTES_FLAG \
+ --target ${{ github.sha }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/sdk-e2e-tests.yml b/.github/workflows/sdk-e2e-tests.yml
index 3665f051..02928de4 100644
--- a/.github/workflows/sdk-e2e-tests.yml
+++ b/.github/workflows/sdk-e2e-tests.yml
@@ -216,3 +216,40 @@ jobs:
env:
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
run: dotnet test --no-build -v n
+
+ java-sdk:
+ name: "Java SDK Tests"
+
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ working-directory: ./java
+ steps:
+ - uses: actions/checkout@v6
+ - uses: ./.github/actions/setup-copilot
+ - uses: actions/setup-java@v5
+ with:
+ java-version: "21"
+ distribution: "temurin"
+
+ - name: Run spotless check
+ run: |
+ mvn spotless:check
+ if [ $? -ne 0 ]; then
+ echo "❌ spotless:check failed. Please run 'mvn spotless:apply' in java"
+ exit 1
+ fi
+ echo "✅ spotless:check passed"
+
+ - name: Build SDK
+ run: mvn compile
+
+ - name: Install test harness dependencies
+ working-directory: ./test/harness
+ run: npm ci --ignore-scripts
+
+ - name: Run Java SDK tests
+ env:
+ COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
+ run: mvn verify
diff --git a/README.md b/README.md
index cf437522..9d0b8ee5 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ All SDKs are in technical preview and may change in breaking ways as we move tow
| **Python** | [`./python/`](./python/README.md) | `pip install github-copilot-sdk` |
| **Go** | [`./go/`](./go/README.md) | `go get github.com/github/copilot-sdk/go` |
| **.NET** | [`./dotnet/`](./dotnet/README.md) | `dotnet add package GitHub.Copilot.SDK` |
+| **Java** | [`./java/`](./java/README.md) | Add dependency (see [README](./java/README.md)) |
See the individual SDK READMEs for installation, usage examples, and API reference.
diff --git a/java/.gitignore b/java/.gitignore
new file mode 100644
index 00000000..d4fee620
--- /dev/null
+++ b/java/.gitignore
@@ -0,0 +1,36 @@
+# Maven
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+
+# IDE
+.idea/
+*.iml
+*.ipr
+*.iws
+.vscode/
+.settings/
+.project
+.classpath
+*.swp
+*.swo
+*~
+
+# Build
+build/
+out/
+bin/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
diff --git a/java/.mvn/wrapper/maven-wrapper.properties b/java/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..8dea6c22
--- /dev/null
+++ b/java/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,3 @@
+wrapperVersion=3.3.4
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
diff --git a/java/README.md b/java/README.md
new file mode 100644
index 00000000..d2c591b7
--- /dev/null
+++ b/java/README.md
@@ -0,0 +1,436 @@
+# Copilot SDK for Java
+
+Java SDK for programmatic control of GitHub Copilot CLI.
+
+> **Note:** This SDK is in technical preview and may change in breaking ways.
+
+## Requirements
+
+- Java 21 or later
+- GitHub Copilot CLI installed and in PATH (or provide custom `cliPath`)
+
+## Installation
+
+### Maven
+
+```xml
+
+ com.github.copilot
+ copilot-sdk
+ 0.1.0
+
+```
+
+### Gradle
+
+```groovy
+implementation 'com.github.copilot:copilot-sdk:0.1.0'
+```
+
+## Quick Start
+
+```java
+import com.github.copilot.sdk.*;
+import com.github.copilot.sdk.events.*;
+import com.github.copilot.sdk.json.*;
+
+import java.util.concurrent.CompletableFuture;
+
+public class Example {
+ public static void main(String[] args) throws Exception {
+ // Create and start client
+ try (var client = new CopilotClient()) {
+ client.start().get();
+
+ // Create a session
+ var session = client.createSession(
+ new SessionConfig().setModel(CopilotModel.CLAUDE_SONNET_4_5.toString())
+ ).get();
+
+ // Wait for response using session.idle event
+ var done = new CompletableFuture();
+
+ session.on(evt -> {
+ if (evt instanceof AssistantMessageEvent msg) {
+ System.out.println(msg.getData().getContent());
+ } else if (evt instanceof SessionIdleEvent) {
+ done.complete(null);
+ }
+ });
+
+ // Send a message and wait for completion
+ session.send(new MessageOptions().setPrompt("What is 2+2?")).get();
+ done.get();
+ }
+ }
+}
+```
+
+## Try it with JBang
+
+You can quickly try the SDK without setting up a full project using [JBang](https://www.jbang.dev/):
+
+```bash
+# Assuming you are in the `java/` directory of this repository
+# Install the SDK locally first (not yet on Maven Central)
+mvn install
+
+# Install JBang (if not already installed)
+# macOS: brew install jbang
+# Linux/Windows: curl -Ls https://sh.jbang.dev | bash -s - app setup
+
+# Run the example
+jbang jbang-example.java
+```
+
+The `jbang-example.java` file includes the dependency declaration and can be run directly:
+
+```java
+//DEPS com.github.copilot:copilot-sdk:0.1.0
+```
+
+## API Reference
+
+### CopilotClient
+
+#### Constructor
+
+```java
+new CopilotClient()
+new CopilotClient(CopilotClientOptions options)
+```
+
+**Options:**
+
+- `cliPath` - Path to CLI executable (default: "copilot" from PATH)
+- `cliArgs` - Extra arguments prepended before SDK-managed flags
+- `cliUrl` - URL of existing CLI server to connect to (e.g., `"localhost:8080"`). When provided, the client will not spawn a CLI process.
+- `port` - Server port (default: 0 for random)
+- `useStdio` - Use stdio transport instead of TCP (default: true)
+- `logLevel` - Log level (default: "info")
+- `autoStart` - Auto-start server (default: true)
+- `autoRestart` - Auto-restart on crash (default: true)
+- `cwd` - Working directory for the CLI process
+- `environment` - Environment variables to pass to the CLI process
+
+#### Methods
+
+##### `start(): CompletableFuture`
+
+Start the CLI server and establish connection.
+
+##### `stop(): CompletableFuture`
+
+Stop the server and close all sessions.
+
+##### `forceStop(): CompletableFuture`
+
+Force stop the CLI server without graceful cleanup.
+
+##### `createSession(SessionConfig config): CompletableFuture`
+
+Create a new conversation session.
+
+**Config:**
+
+- `sessionId` - Custom session ID
+- `model` - Model to use ("gpt-5", "claude-sonnet-4.5", etc.)
+- `tools` - Custom tools exposed to the CLI
+- `systemMessage` - System message customization
+- `availableTools` - List of tool names to allow
+- `excludedTools` - List of tool names to disable
+- `provider` - Custom API provider configuration (BYOK)
+- `streaming` - Enable streaming of response chunks (default: false)
+- `mcpServers` - MCP server configurations
+- `customAgents` - Custom agent configurations
+- `onPermissionRequest` - Handler for permission requests
+
+##### `resumeSession(String sessionId, ResumeSessionConfig config): CompletableFuture`
+
+Resume an existing session.
+
+##### `ping(String message): CompletableFuture`
+
+Ping the server to check connectivity.
+
+##### `getState(): ConnectionState`
+
+Get current connection state. Returns one of: `DISCONNECTED`, `CONNECTING`, `CONNECTED`, `ERROR`.
+
+##### `listSessions(): CompletableFuture>`
+
+List all available sessions.
+
+##### `deleteSession(String sessionId): CompletableFuture`
+
+Delete a session and its data from disk.
+
+##### `getLastSessionId(): CompletableFuture`
+
+Get the ID of the most recently used session.
+
+---
+
+### CopilotSession
+
+Represents a single conversation session.
+
+#### Properties
+
+- `getSessionId()` - The unique identifier for this session
+
+#### Methods
+
+##### `send(MessageOptions options): CompletableFuture`
+
+Send a message to the session.
+
+**Options:**
+
+- `prompt` - The message/prompt to send
+- `attachments` - File attachments
+- `mode` - Delivery mode ("enqueue" or "immediate")
+
+Returns the message ID.
+
+##### `sendAndWait(MessageOptions options, long timeoutMs): CompletableFuture`
+
+Send a message and wait for the session to become idle. Default timeout is 60 seconds.
+
+##### `on(Consumer handler): Closeable`
+
+Subscribe to session events. Returns a `Closeable` to unsubscribe.
+
+```java
+var subscription = session.on(evt -> {
+ System.out.println("Event: " + evt.getType());
+});
+
+// Later...
+subscription.close();
+```
+
+##### `abort(): CompletableFuture`
+
+Abort the currently processing message in this session.
+
+##### `getMessages(): CompletableFuture>`
+
+Get all events/messages from this session.
+
+##### `close()`
+
+Dispose the session and free resources.
+
+---
+
+## Event Types
+
+Sessions emit various events during processing. Each event type extends `AbstractSessionEvent`:
+
+- `UserMessageEvent` - User message added
+- `AssistantMessageEvent` - Assistant response
+- `AssistantMessageDeltaEvent` - Streaming response chunk
+- `ToolExecutionStartEvent` - Tool execution started
+- `ToolExecutionCompleteEvent` - Tool execution completed
+- `SessionStartEvent` - Session started
+- `SessionIdleEvent` - Session is idle
+- `SessionErrorEvent` - Session error occurred
+- `SessionResumeEvent` - Session was resumed
+- And more...
+
+Use pattern matching (Java 21+) to handle specific event types:
+
+```java
+session.on(evt -> {
+ if (evt instanceof AssistantMessageEvent msg) {
+ System.out.println(msg.getData().getContent());
+ } else if (evt instanceof SessionErrorEvent err) {
+ System.out.println("Error: " + err.getData().getMessage());
+ }
+});
+```
+
+## Streaming
+
+Enable streaming to receive assistant response chunks as they're generated:
+
+```java
+var session = client.createSession(
+ new SessionConfig()
+ .setModel("gpt-5")
+ .setStreaming(true)
+).get();
+
+var done = new CompletableFuture();
+
+session.on(evt -> {
+ if (evt instanceof AssistantMessageDeltaEvent delta) {
+ // Streaming message chunk - print incrementally
+ System.out.print(delta.getData().getDeltaContent());
+ } else if (evt instanceof AssistantMessageEvent msg) {
+ // Final message - complete content
+ System.out.println("\n--- Final message ---");
+ System.out.println(msg.getData().getContent());
+ } else if (evt instanceof SessionIdleEvent) {
+ done.complete(null);
+ }
+});
+
+session.send(new MessageOptions().setPrompt("Tell me a short story")).get();
+done.get();
+```
+
+## Advanced Usage
+
+### Manual Server Control
+
+```java
+var client = new CopilotClient(
+ new CopilotClientOptions().setAutoStart(false)
+);
+
+// Start manually
+client.start().get();
+
+// Use client...
+
+// Stop manually
+client.stop().get();
+```
+
+### Tools
+
+You can let the CLI call back into your process when the model needs capabilities you own:
+
+```java
+var lookupTool = ToolDefinition.create(
+ "lookup_issue",
+ "Fetch issue details from our tracker",
+ Map.of(
+ "type", "object",
+ "properties", Map.of(
+ "id", Map.of("type", "string", "description", "Issue identifier")
+ ),
+ "required", List.of("id")
+ ),
+ invocation -> {
+ String id = ((Map) invocation.getArguments()).get("id").toString();
+ return CompletableFuture.completedFuture(fetchIssue(id));
+ }
+);
+
+var session = client.createSession(
+ new SessionConfig()
+ .setModel("gpt-5")
+ .setTools(List.of(lookupTool))
+).get();
+```
+
+### System Message Customization
+
+Control the system prompt using `SystemMessageConfig` in session config:
+
+```java
+var session = client.createSession(
+ new SessionConfig()
+ .setModel("gpt-5")
+ .setSystemMessage(new SystemMessageConfig()
+ .setMode(SystemMessageMode.APPEND)
+ .setContent("""
+
+ - Always check for security vulnerabilities
+ - Suggest performance improvements when applicable
+
+ """))
+).get();
+```
+
+For full control (removes all guardrails), use `REPLACE` mode:
+
+```java
+var session = client.createSession(
+ new SessionConfig()
+ .setModel("gpt-5")
+ .setSystemMessage(new SystemMessageConfig()
+ .setMode(SystemMessageMode.REPLACE)
+ .setContent("You are a helpful assistant."))
+).get();
+```
+
+### Multiple Sessions
+
+```java
+var session1 = client.createSession(
+ new SessionConfig().setModel("gpt-5")
+).get();
+
+var session2 = client.createSession(
+ new SessionConfig().setModel("claude-sonnet-4.5")
+).get();
+
+// Both sessions are independent
+session1.send(new MessageOptions().setPrompt("Hello from session 1")).get();
+session2.send(new MessageOptions().setPrompt("Hello from session 2")).get();
+```
+
+### File Attachments
+
+```java
+session.send(new MessageOptions()
+ .setPrompt("Analyze this file")
+ .setAttachments(List.of(
+ new Attachment()
+ .setType("file")
+ .setPath("/path/to/file.java")
+ .setDisplayName("My File")
+ ))
+).get();
+```
+
+### Bring Your Own Key (BYOK)
+
+Use a custom API provider:
+
+```java
+var session = client.createSession(
+ new SessionConfig()
+ .setProvider(new ProviderConfig()
+ .setType("openai")
+ .setBaseUrl("https://api.openai.com/v1")
+ .setApiKey("your-api-key"))
+).get();
+```
+
+### Permission Handling
+
+Handle permission requests from the CLI:
+
+```java
+var session = client.createSession(
+ new SessionConfig()
+ .setModel("gpt-5")
+ .setOnPermissionRequest((request, invocation) -> {
+ // Approve or deny the permission request
+ var result = new PermissionRequestResult();
+ result.setKind("user-approved");
+ return CompletableFuture.completedFuture(result);
+ })
+).get();
+```
+
+## Error Handling
+
+```java
+try {
+ var session = client.createSession().get();
+ session.send(new MessageOptions().setPrompt("Hello")).get();
+} catch (ExecutionException ex) {
+ Throwable cause = ex.getCause();
+ System.err.println("Error: " + cause.getMessage());
+}
+```
+
+## License
+
+MIT
diff --git a/java/jbang-example.java b/java/jbang-example.java
new file mode 100644
index 00000000..2531d495
--- /dev/null
+++ b/java/jbang-example.java
@@ -0,0 +1,34 @@
+
+//DEPS com.github.copilot:copilot-sdk:0.1.0
+import com.github.copilot.sdk.*;
+import com.github.copilot.sdk.events.*;
+import com.github.copilot.sdk.json.*;
+import java.util.concurrent.CompletableFuture;
+
+class CopilotSDK {
+ public static void main(String[] args) throws Exception {
+ // Create and start client
+ try (var client = new CopilotClient()) {
+ client.start().get();
+
+ // Create a session
+ var session = client.createSession(
+ new SessionConfig().setModel(CopilotModel.CLAUDE_SONNET_4_5.toString())).get();
+
+ // Wait for response using session.idle event
+ var done = new CompletableFuture();
+
+ session.on(evt -> {
+ if (evt instanceof AssistantMessageEvent msg) {
+ System.out.println(msg.getData().getContent());
+ } else if (evt instanceof SessionIdleEvent) {
+ done.complete(null);
+ }
+ });
+
+ // Send a message and wait for completion
+ session.send(new MessageOptions().setPrompt("What is 2+2?")).get();
+ done.get();
+ }
+ }
+}
diff --git a/java/mvnw b/java/mvnw
new file mode 100755
index 00000000..bd8896bf
--- /dev/null
+++ b/java/mvnw
@@ -0,0 +1,295 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.3.4
+#
+# Optional ENV vars
+# -----------------
+# JAVA_HOME - location of a JDK home dir, required when download maven via java source
+# MVNW_REPOURL - repo url base for downloading maven distribution
+# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
+# ----------------------------------------------------------------------------
+
+set -euf
+[ "${MVNW_VERBOSE-}" != debug ] || set -x
+
+# OS specific support.
+native_path() { printf %s\\n "$1"; }
+case "$(uname)" in
+CYGWIN* | MINGW*)
+ [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+ native_path() { cygpath --path --windows "$1"; }
+ ;;
+esac
+
+# set JAVACMD and JAVACCMD
+set_java_home() {
+ # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+ if [ -n "${JAVA_HOME-}" ]; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ]; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACCMD="$JAVA_HOME/jre/sh/javac"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ JAVACCMD="$JAVA_HOME/bin/javac"
+
+ if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+ echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+ echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+ return 1
+ fi
+ fi
+ else
+ JAVACMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v java
+ )" || :
+ JAVACCMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v javac
+ )" || :
+
+ if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+ echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+ return 1
+ fi
+ fi
+}
+
+# hash string like Java String::hashCode
+hash_string() {
+ str="${1:-}" h=0
+ while [ -n "$str" ]; do
+ char="${str%"${str#?}"}"
+ h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+ str="${str#?}"
+ done
+ printf %x\\n $h
+}
+
+verbose() { :; }
+[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
+
+die() {
+ printf %s\\n "$1" >&2
+ exit 1
+}
+
+trim() {
+ # MWRAPPER-139:
+ # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+ # Needed for removing poorly interpreted newline sequences when running in more
+ # exotic environments such as mingw bash on Windows.
+ printf "%s" "${1}" | tr -d '[:space:]'
+}
+
+scriptDir="$(dirname "$0")"
+scriptName="$(basename "$0")"
+
+# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
+while IFS="=" read -r key value; do
+ case "${key-}" in
+ distributionUrl) distributionUrl=$(trim "${value-}") ;;
+ distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+ esac
+done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+
+case "${distributionUrl##*/}" in
+maven-mvnd-*bin.*)
+ MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+ *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+ :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+ :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+ :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+ *)
+ echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+ distributionPlatform=linux-amd64
+ ;;
+ esac
+ distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+ ;;
+maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
+*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+esac
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
+distributionUrlName="${distributionUrl##*/}"
+distributionUrlNameMain="${distributionUrlName%.*}"
+distributionUrlNameMain="${distributionUrlNameMain%-bin}"
+MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
+MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
+
+exec_maven() {
+ unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+ exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+}
+
+if [ -d "$MAVEN_HOME" ]; then
+ verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ exec_maven "$@"
+fi
+
+case "${distributionUrl-}" in
+*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
+*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
+esac
+
+# prepare tmp dir
+if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
+ clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+ trap clean HUP INT TERM EXIT
+else
+ die "cannot create temp dir"
+fi
+
+mkdir -p -- "${MAVEN_HOME%/*}"
+
+# Download and Install Apache Maven
+verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+verbose "Downloading from: $distributionUrl"
+verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+# select .zip or .tar.gz
+if ! command -v unzip >/dev/null; then
+ distributionUrl="${distributionUrl%.zip}.tar.gz"
+ distributionUrlName="${distributionUrl##*/}"
+fi
+
+# verbose opt
+__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
+[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
+
+# normalize http auth
+case "${MVNW_PASSWORD:+has-password}" in
+'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+esac
+
+if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
+ verbose "Found wget ... using wget"
+ wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
+ verbose "Found curl ... using curl"
+ curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+elif set_java_home; then
+ verbose "Falling back to use Java to download"
+ javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+ targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+ cat >"$javaSource" <<-END
+ public class Downloader extends java.net.Authenticator
+ {
+ protected java.net.PasswordAuthentication getPasswordAuthentication()
+ {
+ return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+ }
+ public static void main( String[] args ) throws Exception
+ {
+ setDefault( new Downloader() );
+ java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+ }
+ }
+ END
+ # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+ verbose " - Compiling Downloader.java ..."
+ "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+ verbose " - Running Downloader.java ..."
+ "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+fi
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+if [ -n "${distributionSha256Sum-}" ]; then
+ distributionSha256Result=false
+ if [ "$MVN_CMD" = mvnd.sh ]; then
+ echo "Checksum validation is not supported for maven-mvnd." >&2
+ echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ elif command -v sha256sum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ elif command -v shasum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+ echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ fi
+ if [ $distributionSha256Result = false ]; then
+ echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+ echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+# unzip and move
+if command -v unzip >/dev/null; then
+ unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+else
+ tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+fi
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+actualDistributionDir=""
+
+# First try the expected directory name (for regular distributions)
+if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
+ if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$distributionUrlNameMain"
+ fi
+fi
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if [ -z "$actualDistributionDir" ]; then
+ # enable globbing to iterate over items
+ set +f
+ for dir in "$TMP_DOWNLOAD_DIR"/*; do
+ if [ -d "$dir" ]; then
+ if [ -f "$dir/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$(basename "$dir")"
+ break
+ fi
+ fi
+ done
+ set -f
+fi
+
+if [ -z "$actualDistributionDir" ]; then
+ verbose "Contents of $TMP_DOWNLOAD_DIR:"
+ verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
+ die "Could not find Maven distribution directory in extracted archive"
+fi
+
+verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"
diff --git a/java/mvnw.cmd b/java/mvnw.cmd
new file mode 100644
index 00000000..5761d948
--- /dev/null
+++ b/java/mvnw.cmd
@@ -0,0 +1,189 @@
+<# : batch portion
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.3.4
+@REM
+@REM Optional ENV vars
+@REM MVNW_REPOURL - repo url base for downloading maven distribution
+@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+ IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+ $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+ Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+ "maven-mvnd-*" {
+ $USE_MVND = $true
+ $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+ $MVN_CMD = "mvnd.cmd"
+ break
+ }
+ default {
+ $USE_MVND = $false
+ $MVN_CMD = $script -replace '^mvnw','mvn'
+ break
+ }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+if ($env:MVNW_REPOURL) {
+ $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+ $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+
+$MAVEN_M2_PATH = "$HOME/.m2"
+if ($env:MAVEN_USER_HOME) {
+ $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
+}
+
+if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
+ New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
+}
+
+$MAVEN_WRAPPER_DISTS = $null
+if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
+ $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
+} else {
+ $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
+}
+
+$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+ Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+ exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+ Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+ if ($TMP_DOWNLOAD_DIR.Exists) {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+ }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+ $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+ if ($USE_MVND) {
+ Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+ }
+ Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+ if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+ Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+ }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+$actualDistributionDir = ""
+
+# First try the expected directory name (for regular distributions)
+$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
+$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
+if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
+ $actualDistributionDir = $distributionUrlNameMain
+}
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if (!$actualDistributionDir) {
+ Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
+ $testPath = Join-Path $_.FullName "bin/$MVN_CMD"
+ if (Test-Path -Path $testPath -PathType Leaf) {
+ $actualDistributionDir = $_.Name
+ }
+ }
+}
+
+if (!$actualDistributionDir) {
+ Write-Error "Could not find Maven distribution directory in extracted archive"
+}
+
+Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+ Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+ if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+ Write-Error "fail to move MAVEN_HOME"
+ }
+} finally {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/java/pom.xml b/java/pom.xml
new file mode 100644
index 00000000..e62be68e
--- /dev/null
+++ b/java/pom.xml
@@ -0,0 +1,165 @@
+
+
+
+ 4.0.0
+
+ com.github.copilot
+ copilot-sdk
+ 0.1.0
+ jar
+
+ GitHub Copilot SDK
+ SDK for programmatic control of GitHub Copilot CLI
+ https://github.com/github/copilot-sdk
+
+
+
+ MIT License
+ https://opensource.org/licenses/MIT
+
+
+
+
+
+ GitHub
+ GitHub
+ https://github.com
+
+
+
+
+ scm:git:git://github.com/github/copilot-sdk.git
+ scm:git:ssh://github.com:github/copilot-sdk.git
+ https://github.com/github/copilot-sdk
+
+
+
+ 21
+ UTF-8
+
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.20.1
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ 2.20
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ 2.20.1
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.14.1
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.14.1
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.4
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+ 2.44.5
+
+
+
+ 4.33
+
+
+
+
+
+ true
+ 4
+
+
+
+
+
+
+
+
+
+ release
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.4.0
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.12.0
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.8
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.10.0
+ true
+
+ central
+ true
+
+
+
+
+
+
+
diff --git a/java/src/main/java/com/github/copilot/sdk/ConnectionState.java b/java/src/main/java/com/github/copilot/sdk/ConnectionState.java
new file mode 100644
index 00000000..6d208d6c
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/ConnectionState.java
@@ -0,0 +1,35 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk;
+
+/**
+ * Represents the connection state of a {@link CopilotClient}.
+ *
+ * The connection state indicates the current status of the client's connection
+ * to the Copilot CLI server.
+ *
+ * @see CopilotClient#getState()
+ */
+public enum ConnectionState {
+ /**
+ * The client is not connected to the server.
+ */
+ DISCONNECTED,
+
+ /**
+ * The client is in the process of connecting to the server.
+ */
+ CONNECTING,
+
+ /**
+ * The client is connected and ready to accept requests.
+ */
+ CONNECTED,
+
+ /**
+ * The client encountered an error during connection or operation.
+ */
+ ERROR
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/CopilotClient.java b/java/src/main/java/com/github/copilot/sdk/CopilotClient.java
new file mode 100644
index 00000000..831f5306
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/CopilotClient.java
@@ -0,0 +1,763 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.Socket;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.copilot.sdk.events.AbstractSessionEvent;
+import com.github.copilot.sdk.events.SessionEventParser;
+import com.github.copilot.sdk.json.CopilotClientOptions;
+import com.github.copilot.sdk.json.CreateSessionRequest;
+import com.github.copilot.sdk.json.CreateSessionResponse;
+import com.github.copilot.sdk.json.DeleteSessionResponse;
+import com.github.copilot.sdk.json.GetLastSessionIdResponse;
+import com.github.copilot.sdk.json.ListSessionsResponse;
+import com.github.copilot.sdk.json.PermissionRequestResult;
+import com.github.copilot.sdk.json.PingResponse;
+import com.github.copilot.sdk.json.ResumeSessionConfig;
+import com.github.copilot.sdk.json.ResumeSessionRequest;
+import com.github.copilot.sdk.json.ResumeSessionResponse;
+import com.github.copilot.sdk.json.SessionConfig;
+import com.github.copilot.sdk.json.SessionMetadata;
+import com.github.copilot.sdk.json.ToolDef;
+import com.github.copilot.sdk.json.ToolDefinition;
+import com.github.copilot.sdk.json.ToolInvocation;
+import com.github.copilot.sdk.json.ToolResultObject;
+
+/**
+ * Provides a client for interacting with the Copilot CLI server.
+ *
+ * The CopilotClient manages the connection to the Copilot CLI server and
+ * provides methods to create and manage conversation sessions. It can either
+ * spawn a CLI server process or connect to an existing server.
+ *
+ */
+public class CopilotClient implements AutoCloseable {
+
+ private static final Logger LOG = Logger.getLogger(CopilotClient.class.getName());
+ private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper();
+
+ private final CopilotClientOptions options;
+ private final Map sessions = new ConcurrentHashMap<>();
+ private volatile CompletableFuture connectionFuture;
+ private volatile boolean disposed = false;
+ private final String optionsHost;
+ private final Integer optionsPort;
+
+ /**
+ * Creates a new CopilotClient with default options.
+ */
+ public CopilotClient() {
+ this(new CopilotClientOptions());
+ }
+
+ /**
+ * Creates a new CopilotClient with the specified options.
+ *
+ * @param options
+ * Options for creating the client
+ * @throws IllegalArgumentException
+ * if mutually exclusive options are provided
+ */
+ public CopilotClient(CopilotClientOptions options) {
+ this.options = options != null ? options : new CopilotClientOptions();
+
+ // Validate mutually exclusive options
+ if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty()
+ && (this.options.isUseStdio() || this.options.getCliPath() != null)) {
+ throw new IllegalArgumentException("CliUrl is mutually exclusive with UseStdio and CliPath");
+ }
+
+ // Parse CliUrl if provided
+ if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty()) {
+ URI uri = parseCliUrl(this.options.getCliUrl());
+ this.optionsHost = uri.getHost();
+ this.optionsPort = uri.getPort();
+ } else {
+ this.optionsHost = null;
+ this.optionsPort = null;
+ }
+ }
+
+ private static URI parseCliUrl(String url) {
+ // If it's just a port number, treat as localhost
+ try {
+ int port = Integer.parseInt(url);
+ return URI.create("http://localhost:" + port);
+ } catch (NumberFormatException e) {
+ // Not a port number, continue
+ }
+
+ // Add scheme if missing
+ if (!url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://")) {
+ url = "https://" + url;
+ }
+
+ return URI.create(url);
+ }
+
+ /**
+ * Starts the Copilot client and connects to the server.
+ *
+ * @return A future that completes when the connection is established
+ */
+ public CompletableFuture start() {
+ if (connectionFuture == null) {
+ synchronized (this) {
+ if (connectionFuture == null) {
+ connectionFuture = startCore();
+ }
+ }
+ }
+ return connectionFuture.thenApply(c -> null);
+ }
+
+ private CompletableFuture startCore() {
+ LOG.fine("Starting Copilot client");
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ Connection connection;
+
+ if (optionsHost != null && optionsPort != null) {
+ // External server (TCP)
+ connection = connectToServer(null, optionsHost, optionsPort);
+ } else {
+ // Child process (stdio or TCP)
+ ProcessInfo processInfo = startCliServer();
+ connection = connectToServer(processInfo.process, processInfo.port != null ? "localhost" : null,
+ processInfo.port);
+ }
+
+ // Register handlers for server-to-client calls
+ registerRpcHandlers(connection.rpc);
+
+ // Verify protocol version
+ verifyProtocolVersion(connection);
+
+ LOG.info("Copilot client connected");
+ return connection;
+ } catch (Exception e) {
+ throw new CompletionException(e);
+ }
+ });
+ }
+
+ private void registerRpcHandlers(JsonRpcClient rpc) {
+ // Handle session events
+ rpc.registerMethodHandler("session.event", (requestId, params) -> {
+ try {
+ String sessionId = params.get("sessionId").asText();
+ JsonNode eventNode = params.get("event");
+
+ CopilotSession session = sessions.get(sessionId);
+ if (session != null && eventNode != null) {
+ AbstractSessionEvent event = SessionEventParser.parse(eventNode.toString());
+ if (event != null) {
+ session.dispatchEvent(event);
+ }
+ }
+ } catch (Exception e) {
+ LOG.log(Level.SEVERE, "Error handling session event", e);
+ }
+ });
+
+ // Handle tool calls
+ rpc.registerMethodHandler("tool.call", (requestId, params) -> {
+ handleToolCall(rpc, requestId, params);
+ });
+
+ // Handle permission requests
+ rpc.registerMethodHandler("permission.request", (requestId, params) -> {
+ handlePermissionRequest(rpc, requestId, params);
+ });
+ }
+
+ private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params) {
+ CompletableFuture.runAsync(() -> {
+ try {
+ String sessionId = params.get("sessionId").asText();
+ String toolCallId = params.get("toolCallId").asText();
+ String toolName = params.get("toolName").asText();
+ JsonNode arguments = params.get("arguments");
+
+ CopilotSession session = sessions.get(sessionId);
+ if (session == null) {
+ rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session " + sessionId);
+ return;
+ }
+
+ ToolDefinition tool = session.getTool(toolName);
+ if (tool == null || tool.getHandler() == null) {
+ ToolResultObject result = new ToolResultObject()
+ .setTextResultForLlm("Tool '" + toolName + "' is not supported.").setResultType("failure")
+ .setError("tool '" + toolName + "' not supported");
+ rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result));
+ return;
+ }
+
+ ToolInvocation invocation = new ToolInvocation().setSessionId(sessionId).setToolCallId(toolCallId)
+ .setToolName(toolName).setArguments(arguments);
+
+ tool.getHandler().invoke(invocation).thenAccept(result -> {
+ try {
+ ToolResultObject toolResult;
+ if (result instanceof ToolResultObject tr) {
+ toolResult = tr;
+ } else {
+ toolResult = new ToolResultObject().setResultType("success").setTextResultForLlm(
+ result instanceof String s ? s : MAPPER.writeValueAsString(result));
+ }
+ rpc.sendResponse(Long.parseLong(requestId), Map.of("result", toolResult));
+ } catch (Exception e) {
+ LOG.log(Level.SEVERE, "Error sending tool result", e);
+ }
+ }).exceptionally(ex -> {
+ try {
+ ToolResultObject result = new ToolResultObject()
+ .setTextResultForLlm(
+ "Invoking this tool produced an error. Detailed information is not available.")
+ .setResultType("failure").setError(ex.getMessage());
+ rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result));
+ } catch (Exception e) {
+ LOG.log(Level.SEVERE, "Error sending tool error", e);
+ }
+ return null;
+ });
+ } catch (Exception e) {
+ LOG.log(Level.SEVERE, "Error handling tool call", e);
+ try {
+ rpc.sendErrorResponse(Long.parseLong(requestId), -32603, e.getMessage());
+ } catch (IOException ioe) {
+ LOG.log(Level.SEVERE, "Failed to send error response", ioe);
+ }
+ }
+ });
+ }
+
+ private void handlePermissionRequest(JsonRpcClient rpc, String requestId, JsonNode params) {
+ CompletableFuture.runAsync(() -> {
+ try {
+ String sessionId = params.get("sessionId").asText();
+ JsonNode permissionRequest = params.get("permissionRequest");
+
+ CopilotSession session = sessions.get(sessionId);
+ if (session == null) {
+ PermissionRequestResult result = new PermissionRequestResult()
+ .setKind("denied-no-approval-rule-and-could-not-request-from-user");
+ rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result));
+ return;
+ }
+
+ session.handlePermissionRequest(permissionRequest).thenAccept(result -> {
+ try {
+ rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result));
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Error sending permission result", e);
+ }
+ }).exceptionally(ex -> {
+ try {
+ PermissionRequestResult result = new PermissionRequestResult()
+ .setKind("denied-no-approval-rule-and-could-not-request-from-user");
+ rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result));
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Error sending permission denied", e);
+ }
+ return null;
+ });
+ } catch (Exception e) {
+ LOG.log(Level.SEVERE, "Error handling permission request", e);
+ }
+ });
+ }
+
+ private void verifyProtocolVersion(Connection connection) throws Exception {
+ int expectedVersion = SdkProtocolVersion.get();
+ Map params = new HashMap<>();
+ params.put("message", null);
+ PingResponse pingResponse = connection.rpc.invoke("ping", params, PingResponse.class).get(30, TimeUnit.SECONDS);
+
+ if (pingResponse.getProtocolVersion() == null) {
+ throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion
+ + ", but server does not report a protocol version. "
+ + "Please update your server to ensure compatibility.");
+ }
+
+ if (pingResponse.getProtocolVersion() != expectedVersion) {
+ throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion
+ + ", but server reports version " + pingResponse.getProtocolVersion() + ". "
+ + "Please update your SDK or server to ensure compatibility.");
+ }
+ }
+
+ /**
+ * Stops the client and closes all sessions.
+ *
+ * @return A future that completes when the client is stopped
+ */
+ public CompletableFuture stop() {
+ List> closeFutures = new ArrayList<>();
+
+ for (CopilotSession session : new ArrayList<>(sessions.values())) {
+ closeFutures.add(CompletableFuture.runAsync(() -> {
+ try {
+ session.close();
+ } catch (Exception e) {
+ LOG.log(Level.WARNING, "Error closing session " + session.getSessionId(), e);
+ }
+ }));
+ }
+ sessions.clear();
+
+ return CompletableFuture.allOf(closeFutures.toArray(new CompletableFuture[0]))
+ .thenCompose(v -> cleanupConnection());
+ }
+
+ /**
+ * Forces an immediate stop of the client without graceful cleanup.
+ *
+ * @return A future that completes when the client is stopped
+ */
+ public CompletableFuture forceStop() {
+ sessions.clear();
+ return cleanupConnection();
+ }
+
+ private CompletableFuture cleanupConnection() {
+ CompletableFuture future = connectionFuture;
+ connectionFuture = null;
+
+ if (future == null) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ return future.thenAccept(connection -> {
+ try {
+ connection.rpc.close();
+ } catch (Exception e) {
+ LOG.log(Level.FINE, "Error closing RPC", e);
+ }
+
+ if (connection.process != null) {
+ try {
+ if (connection.process.isAlive()) {
+ connection.process.destroyForcibly();
+ }
+ } catch (Exception e) {
+ LOG.log(Level.FINE, "Error killing process", e);
+ }
+ }
+ }).exceptionally(ex -> null);
+ }
+
+ /**
+ * Creates a new Copilot session with the specified configuration.
+ *
+ * The session maintains conversation state and can be used to send messages and
+ * receive responses. Remember to close the session when done.
+ *
+ * @param config
+ * configuration for the session (model, tools, etc.)
+ * @return a future that resolves with the created CopilotSession
+ * @see #createSession()
+ * @see SessionConfig
+ */
+ public CompletableFuture createSession(SessionConfig config) {
+ return ensureConnected().thenCompose(connection -> {
+ CreateSessionRequest request = new CreateSessionRequest();
+ if (config != null) {
+ request.setModel(config.getModel());
+ request.setSessionId(config.getSessionId());
+ request.setTools(config.getTools() != null
+ ? config.getTools().stream()
+ .map(t -> new ToolDef(t.getName(), t.getDescription(), t.getParameters()))
+ .collect(Collectors.toList())
+ : null);
+ request.setSystemMessage(config.getSystemMessage());
+ request.setAvailableTools(config.getAvailableTools());
+ request.setExcludedTools(config.getExcludedTools());
+ request.setProvider(config.getProvider());
+ request.setRequestPermission(config.getOnPermissionRequest() != null ? true : null);
+ request.setStreaming(config.isStreaming() ? true : null);
+ request.setMcpServers(config.getMcpServers());
+ request.setCustomAgents(config.getCustomAgents());
+ }
+
+ return connection.rpc.invoke("session.create", request, CreateSessionResponse.class).thenApply(response -> {
+ CopilotSession session = new CopilotSession(response.getSessionId(), connection.rpc);
+ if (config != null && config.getTools() != null) {
+ session.registerTools(config.getTools());
+ }
+ if (config != null && config.getOnPermissionRequest() != null) {
+ session.registerPermissionHandler(config.getOnPermissionRequest());
+ }
+ sessions.put(response.getSessionId(), session);
+ return session;
+ });
+ });
+ }
+
+ /**
+ * Creates a new Copilot session with default configuration.
+ *
+ * @return a future that resolves with the created CopilotSession
+ * @see #createSession(SessionConfig)
+ */
+ public CompletableFuture createSession() {
+ return createSession(null);
+ }
+
+ /**
+ * Resumes an existing Copilot session.
+ *
+ * This restores a previously saved session, allowing you to continue a
+ * conversation. The session's history is preserved.
+ *
+ * @param sessionId
+ * the ID of the session to resume
+ * @param config
+ * configuration for the resumed session
+ * @return a future that resolves with the resumed CopilotSession
+ * @see #resumeSession(String)
+ * @see #listSessions()
+ * @see #getLastSessionId()
+ */
+ public CompletableFuture resumeSession(String sessionId, ResumeSessionConfig config) {
+ return ensureConnected().thenCompose(connection -> {
+ ResumeSessionRequest request = new ResumeSessionRequest();
+ request.setSessionId(sessionId);
+ if (config != null) {
+ request.setTools(config.getTools() != null
+ ? config.getTools().stream()
+ .map(t -> new ToolDef(t.getName(), t.getDescription(), t.getParameters()))
+ .collect(Collectors.toList())
+ : null);
+ request.setProvider(config.getProvider());
+ request.setRequestPermission(config.getOnPermissionRequest() != null ? true : null);
+ request.setStreaming(config.isStreaming() ? true : null);
+ request.setMcpServers(config.getMcpServers());
+ request.setCustomAgents(config.getCustomAgents());
+ }
+
+ return connection.rpc.invoke("session.resume", request, ResumeSessionResponse.class).thenApply(response -> {
+ CopilotSession session = new CopilotSession(response.getSessionId(), connection.rpc);
+ if (config != null && config.getTools() != null) {
+ session.registerTools(config.getTools());
+ }
+ if (config != null && config.getOnPermissionRequest() != null) {
+ session.registerPermissionHandler(config.getOnPermissionRequest());
+ }
+ sessions.put(response.getSessionId(), session);
+ return session;
+ });
+ });
+ }
+
+ /**
+ * Resumes an existing session with default configuration.
+ *
+ * @param sessionId
+ * the ID of the session to resume
+ * @return a future that resolves with the resumed CopilotSession
+ * @see #resumeSession(String, ResumeSessionConfig)
+ */
+ public CompletableFuture resumeSession(String sessionId) {
+ return resumeSession(sessionId, null);
+ }
+
+ /**
+ * Gets the current connection state.
+ *
+ * @return the current connection state
+ * @see ConnectionState
+ */
+ public ConnectionState getState() {
+ if (connectionFuture == null)
+ return ConnectionState.DISCONNECTED;
+ if (connectionFuture.isCompletedExceptionally())
+ return ConnectionState.ERROR;
+ if (!connectionFuture.isDone())
+ return ConnectionState.CONNECTING;
+ return ConnectionState.CONNECTED;
+ }
+
+ /**
+ * Pings the server to check connectivity.
+ *
+ * This can be used to verify that the server is responsive and to check the
+ * protocol version.
+ *
+ * @param message
+ * an optional message to echo back
+ * @return a future that resolves with the ping response
+ * @see PingResponse
+ */
+ public CompletableFuture ping(String message) {
+ return ensureConnected().thenCompose(connection -> connection.rpc.invoke("ping",
+ Map.of("message", message != null ? message : ""), PingResponse.class));
+ }
+
+ /**
+ * Gets the ID of the most recently used session.
+ *
+ * This is useful for resuming the last conversation without needing to list all
+ * sessions.
+ *
+ * @return a future that resolves with the last session ID, or {@code null} if
+ * no sessions exist
+ * @see #resumeSession(String)
+ */
+ public CompletableFuture getLastSessionId() {
+ return ensureConnected().thenCompose(
+ connection -> connection.rpc.invoke("session.getLastId", Map.of(), GetLastSessionIdResponse.class)
+ .thenApply(GetLastSessionIdResponse::getSessionId));
+ }
+
+ /**
+ * Deletes a session by ID.
+ *
+ * This permanently removes the session and its conversation history.
+ *
+ * @param sessionId
+ * the ID of the session to delete
+ * @return a future that completes when the session is deleted
+ * @throws RuntimeException
+ * if the deletion fails
+ */
+ public CompletableFuture deleteSession(String sessionId) {
+ return ensureConnected().thenCompose(connection -> connection.rpc
+ .invoke("session.delete", Map.of("sessionId", sessionId), DeleteSessionResponse.class)
+ .thenAccept(response -> {
+ if (!response.isSuccess()) {
+ throw new RuntimeException(
+ "Failed to delete session " + sessionId + ": " + response.getError());
+ }
+ sessions.remove(sessionId);
+ }));
+ }
+
+ /**
+ * Lists all available sessions.
+ *
+ * Returns metadata about all sessions that can be resumed, including their IDs,
+ * start times, and summaries.
+ *
+ * @return a future that resolves with a list of session metadata
+ * @see SessionMetadata
+ * @see #resumeSession(String)
+ */
+ public CompletableFuture> listSessions() {
+ return ensureConnected()
+ .thenCompose(connection -> connection.rpc.invoke("session.list", Map.of(), ListSessionsResponse.class)
+ .thenApply(ListSessionsResponse::getSessions));
+ }
+
+ private CompletableFuture ensureConnected() {
+ if (connectionFuture == null && !options.isAutoStart()) {
+ throw new IllegalStateException("Client not connected. Call start() first.");
+ }
+
+ start();
+ return connectionFuture;
+ }
+
+ private ProcessInfo startCliServer() throws IOException, InterruptedException {
+ String cliPath = options.getCliPath() != null ? options.getCliPath() : "copilot";
+ List args = new ArrayList<>();
+
+ if (options.getCliArgs() != null) {
+ args.addAll(Arrays.asList(options.getCliArgs()));
+ }
+
+ args.add("--server");
+ args.add("--log-level");
+ args.add(options.getLogLevel());
+
+ if (options.isUseStdio()) {
+ args.add("--stdio");
+ } else if (options.getPort() > 0) {
+ args.add("--port");
+ args.add(String.valueOf(options.getPort()));
+ }
+
+ List command = resolveCliCommand(cliPath, args);
+
+ ProcessBuilder pb = new ProcessBuilder(command);
+ pb.redirectErrorStream(false);
+
+ if (options.getCwd() != null) {
+ pb.directory(new File(options.getCwd()));
+ }
+
+ if (options.getEnvironment() != null) {
+ pb.environment().clear();
+ pb.environment().putAll(options.getEnvironment());
+ }
+ pb.environment().remove("NODE_DEBUG");
+
+ Process process = pb.start();
+
+ // Forward stderr to logger in background
+ Thread stderrThread = new Thread(() -> {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ LOG.fine("[CLI] " + line);
+ }
+ } catch (IOException e) {
+ LOG.log(Level.FINE, "Error reading stderr", e);
+ }
+ }, "cli-stderr-reader");
+ stderrThread.setDaemon(true);
+ stderrThread.start();
+
+ Integer detectedPort = null;
+ if (!options.isUseStdio()) {
+ // Wait for port announcement
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+ Pattern portPattern = Pattern.compile("listening on port (\\d+)", Pattern.CASE_INSENSITIVE);
+ long deadline = System.currentTimeMillis() + 30000;
+
+ while (System.currentTimeMillis() < deadline) {
+ String line = reader.readLine();
+ if (line == null) {
+ throw new IOException("CLI process exited unexpectedly");
+ }
+
+ Matcher matcher = portPattern.matcher(line);
+ if (matcher.find()) {
+ detectedPort = Integer.parseInt(matcher.group(1));
+ break;
+ }
+ }
+
+ if (detectedPort == null) {
+ process.destroyForcibly();
+ throw new IOException("Timeout waiting for CLI to announce port");
+ }
+ }
+ }
+
+ return new ProcessInfo(process, detectedPort);
+ }
+
+ private List resolveCliCommand(String cliPath, List args) {
+ boolean isJsFile = cliPath.toLowerCase().endsWith(".js");
+
+ if (isJsFile) {
+ List result = new ArrayList<>();
+ result.add("node");
+ result.add(cliPath);
+ result.addAll(args);
+ return result;
+ }
+
+ // On Windows, use cmd /c to resolve the executable
+ String os = System.getProperty("os.name").toLowerCase();
+ if (os.contains("win") && !new File(cliPath).isAbsolute()) {
+ List result = new ArrayList<>();
+ result.add("cmd");
+ result.add("/c");
+ result.add(cliPath);
+ result.addAll(args);
+ return result;
+ }
+
+ List result = new ArrayList<>();
+ result.add(cliPath);
+ result.addAll(args);
+ return result;
+ }
+
+ private Connection connectToServer(Process process, String tcpHost, Integer tcpPort) throws IOException {
+ JsonRpcClient rpc;
+
+ if (options.isUseStdio()) {
+ if (process == null) {
+ throw new IllegalStateException("CLI process not started");
+ }
+ rpc = JsonRpcClient.fromProcess(process);
+ } else {
+ if (tcpHost == null || tcpPort == null) {
+ throw new IllegalStateException("Cannot connect because TCP host or port are not available");
+ }
+ Socket socket = new Socket(tcpHost, tcpPort);
+ rpc = JsonRpcClient.fromSocket(socket);
+ }
+
+ return new Connection(rpc, process);
+ }
+
+ @Override
+ public void close() {
+ if (disposed)
+ return;
+ disposed = true;
+ try {
+ forceStop().get(5, TimeUnit.SECONDS);
+ } catch (Exception e) {
+ LOG.log(Level.FINE, "Error during close", e);
+ }
+ }
+
+ private static class ProcessInfo {
+ final Process process;
+ final Integer port;
+
+ ProcessInfo(Process process, Integer port) {
+ this.process = process;
+ this.port = port;
+ }
+ }
+
+ private static class Connection {
+ final JsonRpcClient rpc;
+
+ final Process process;
+
+ Connection(JsonRpcClient rpc, Process process) {
+ this.rpc = rpc;
+ this.process = process;
+ }
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/CopilotModel.java b/java/src/main/java/com/github/copilot/sdk/CopilotModel.java
new file mode 100644
index 00000000..7f6b5bd4
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/CopilotModel.java
@@ -0,0 +1,64 @@
+package com.github.copilot.sdk;
+
+/**
+ * Available Copilot models.
+ *
+ *
+ * The actual availability of models depends on your GitHub Copilot
+ * subscription.
+ */
+public enum CopilotModel {
+ /** Claude Sonnet 4.5 */
+ CLAUDE_SONNET_4_5("claude-sonnet-4.5"),
+ /** Claude Haiku 4.5 */
+ CLAUDE_HAIKU_4_5("claude-haiku-4.5"),
+ /** Claude Opus 4.5 */
+ CLAUDE_OPUS_4_5("claude-opus-4.5"),
+ /** Claude Sonnet 4 */
+ CLAUDE_SONNET_4("claude-sonnet-4"),
+ /** GPT-5.2 Codex */
+ GPT_5_2_CODEX("gpt-5.2-codex"),
+ /** GPT-5.1 Codex Max */
+ GPT_5_1_CODEX_MAX("gpt-5.1-codex-max"),
+ /** GPT-5.1 Codex */
+ GPT_5_1_CODEX("gpt-5.1-codex"),
+ /** GPT-5.2 */
+ GPT_5_2("gpt-5.2"),
+ /** GPT-5.1 */
+ GPT_5_1("gpt-5.1"),
+ /** GPT-5 */
+ GPT_5("gpt-5"),
+ /** GPT-5.1 Codex Mini */
+ GPT_5_1_CODEX_MINI("gpt-5.1-codex-mini"),
+ /** GPT-5 Mini */
+ GPT_5_MINI("gpt-5-mini"),
+ /** GPT-4.1 */
+ GPT_4_1("gpt-4.1"),
+ /** Gemini 3 Pro Preview */
+ GEMINI_3_PRO_PREVIEW("gemini-3-pro-preview");
+
+ private final String value;
+
+ CopilotModel(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the model identifier string to use with the API.
+ *
+ * @return the model identifier
+ */
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Returns the string representation of the model.
+ *
+ * @return the model identifier string
+ */
+ @Override
+ public String toString() {
+ return value;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/CopilotSession.java b/java/src/main/java/com/github/copilot/sdk/CopilotSession.java
new file mode 100644
index 00000000..d97c12dd
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/CopilotSession.java
@@ -0,0 +1,422 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.copilot.sdk.events.AbstractSessionEvent;
+import com.github.copilot.sdk.events.AssistantMessageEvent;
+import com.github.copilot.sdk.events.SessionErrorEvent;
+import com.github.copilot.sdk.events.SessionEventParser;
+import com.github.copilot.sdk.events.SessionIdleEvent;
+import com.github.copilot.sdk.json.GetMessagesResponse;
+import com.github.copilot.sdk.json.MessageOptions;
+import com.github.copilot.sdk.json.PermissionHandler;
+import com.github.copilot.sdk.json.PermissionInvocation;
+import com.github.copilot.sdk.json.PermissionRequest;
+import com.github.copilot.sdk.json.PermissionRequestResult;
+import com.github.copilot.sdk.json.SendMessageRequest;
+import com.github.copilot.sdk.json.SendMessageResponse;
+import com.github.copilot.sdk.json.ToolDefinition;
+
+/**
+ * Represents a single conversation session with the Copilot CLI.
+ *
+ * A session maintains conversation state, handles events, and manages tool
+ * execution. Sessions are created via {@link CopilotClient#createSession} or
+ * resumed via {@link CopilotClient#resumeSession}.
+ *
+ *
+ *
+ * @see CopilotClient#createSession(com.github.copilot.sdk.json.SessionConfig)
+ * @see CopilotClient#resumeSession(String,
+ * com.github.copilot.sdk.json.ResumeSessionConfig)
+ * @see AbstractSessionEvent
+ */
+public final class CopilotSession implements AutoCloseable {
+
+ private static final Logger LOG = Logger.getLogger(CopilotSession.class.getName());
+ private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper();
+
+ private final String sessionId;
+ private final JsonRpcClient rpc;
+ private final Set> eventHandlers = ConcurrentHashMap.newKeySet();
+ private final Map toolHandlers = new ConcurrentHashMap<>();
+ private final AtomicReference permissionHandler = new AtomicReference<>();
+
+ /**
+ * Creates a new session with the given ID and RPC client.
+ *
+ * This constructor is package-private. Sessions should be created via
+ * {@link CopilotClient#createSession} or {@link CopilotClient#resumeSession}.
+ *
+ * @param sessionId
+ * the unique session identifier
+ * @param rpc
+ * the JSON-RPC client for communication
+ */
+ CopilotSession(String sessionId, JsonRpcClient rpc) {
+ this.sessionId = sessionId;
+ this.rpc = rpc;
+ }
+
+ /**
+ * Gets the unique identifier for this session.
+ *
+ * @return the session ID
+ */
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ /**
+ * Sends a simple text message to the Copilot session.
+ *
+ * This is a convenience method equivalent to
+ * {@code send(new MessageOptions().setPrompt(prompt))}.
+ *
+ * @param prompt
+ * the message text to send
+ * @return a future that resolves with the message ID assigned by the server
+ * @see #send(MessageOptions)
+ */
+ public CompletableFuture send(String prompt) {
+ return send(new MessageOptions().setPrompt(prompt));
+ }
+
+ /**
+ * Sends a simple text message and waits until the session becomes idle.
+ *
+ * This is a convenience method equivalent to
+ * {@code sendAndWait(new MessageOptions().setPrompt(prompt))}.
+ *
+ * @param prompt
+ * the message text to send
+ * @return a future that resolves with the final assistant message event, or
+ * {@code null} if no assistant message was received
+ * @see #sendAndWait(MessageOptions)
+ */
+ public CompletableFuture sendAndWait(String prompt) {
+ return sendAndWait(new MessageOptions().setPrompt(prompt));
+ }
+
+ /**
+ * Sends a message to the Copilot session.
+ *
+ * This method sends a message asynchronously and returns immediately. Use
+ * {@link #sendAndWait(MessageOptions)} to wait for the response.
+ *
+ * @param options
+ * the message options containing the prompt and attachments
+ * @return a future that resolves with the message ID assigned by the server
+ * @see #sendAndWait(MessageOptions)
+ * @see #send(String)
+ */
+ public CompletableFuture send(MessageOptions options) {
+ SendMessageRequest request = new SendMessageRequest();
+ request.setSessionId(sessionId);
+ request.setPrompt(options.getPrompt());
+ request.setAttachments(options.getAttachments());
+ request.setMode(options.getMode());
+
+ return rpc.invoke("session.send", request, SendMessageResponse.class)
+ .thenApply(SendMessageResponse::getMessageId);
+ }
+
+ /**
+ * Sends a message and waits until the session becomes idle.
+ *
+ * This method blocks until the assistant finishes processing the message or
+ * until the timeout expires. It's suitable for simple request/response
+ * interactions where you don't need to process streaming events.
+ *
+ * @param options
+ * the message options containing the prompt and attachments
+ * @param timeoutMs
+ * timeout in milliseconds (0 or negative for no timeout)
+ * @return a future that resolves with the final assistant message event, or
+ * {@code null} if no assistant message was received. The future
+ * completes exceptionally with a TimeoutException if the timeout
+ * expires.
+ * @see #sendAndWait(MessageOptions)
+ * @see #send(MessageOptions)
+ */
+ public CompletableFuture sendAndWait(MessageOptions options, long timeoutMs) {
+ CompletableFuture future = new CompletableFuture<>();
+ AtomicReference lastAssistantMessage = new AtomicReference<>();
+
+ Consumer handler = evt -> {
+ if (evt instanceof AssistantMessageEvent msg) {
+ lastAssistantMessage.set(msg);
+ } else if (evt instanceof SessionIdleEvent) {
+ future.complete(lastAssistantMessage.get());
+ } else if (evt instanceof SessionErrorEvent errorEvent) {
+ String message = errorEvent.getData() != null ? errorEvent.getData().getMessage() : "session error";
+ future.completeExceptionally(new RuntimeException("Session error: " + message));
+ }
+ };
+
+ Closeable subscription = on(handler);
+
+ send(options).exceptionally(ex -> {
+ try {
+ subscription.close();
+ } catch (Exception e) {
+ LOG.log(Level.SEVERE, "Error closing subscription", e);
+ }
+ future.completeExceptionally(ex);
+ return null;
+ });
+
+ // Set up timeout
+ ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
+ scheduler.schedule(() -> {
+ if (!future.isDone()) {
+ future.completeExceptionally(new TimeoutException("sendAndWait timed out after " + timeoutMs + "ms"));
+ }
+ scheduler.shutdown();
+ }, timeoutMs, TimeUnit.MILLISECONDS);
+
+ return future.whenComplete((result, ex) -> {
+ try {
+ subscription.close();
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Error closing subscription", e);
+ }
+ scheduler.shutdown();
+ });
+ }
+
+ /**
+ * Sends a message and waits until the session becomes idle with default 60
+ * second timeout.
+ *
+ * @param options
+ * the message options containing the prompt and attachments
+ * @return a future that resolves with the final assistant message event, or
+ * {@code null} if no assistant message was received
+ * @see #sendAndWait(MessageOptions, long)
+ */
+ public CompletableFuture sendAndWait(MessageOptions options) {
+ return sendAndWait(options, 60000);
+ }
+
+ /**
+ * Registers a callback for session events.
+ *
+ * The handler will be invoked for all events in this session, including
+ * assistant messages, tool calls, and session state changes.
+ *
+ *
+ *
+ * @param handler
+ * a callback to be invoked when a session event occurs
+ * @return a Closeable that, when closed, unsubscribes the handler
+ * @see AbstractSessionEvent
+ */
+ public Closeable on(Consumer handler) {
+ eventHandlers.add(handler);
+ return () -> eventHandlers.remove(handler);
+ }
+
+ /**
+ * Dispatches an event to all registered handlers.
+ *
+ * This is called internally when events are received from the server.
+ *
+ * @param event
+ * the event to dispatch
+ */
+ void dispatchEvent(AbstractSessionEvent event) {
+ for (Consumer handler : eventHandlers) {
+ try {
+ handler.accept(event);
+ } catch (Exception e) {
+ LOG.log(Level.SEVERE, "Error in event handler", e);
+ }
+ }
+ }
+
+ /**
+ * Registers custom tool handlers for this session.
+ *
+ * Called internally when creating or resuming a session with tools.
+ *
+ * @param tools
+ * the list of tool definitions with handlers
+ */
+ void registerTools(List tools) {
+ toolHandlers.clear();
+ if (tools != null) {
+ for (ToolDefinition tool : tools) {
+ toolHandlers.put(tool.getName(), tool);
+ }
+ }
+ }
+
+ /**
+ * Retrieves a registered tool by name.
+ *
+ * @param name
+ * the tool name
+ * @return the tool definition, or {@code null} if not found
+ */
+ ToolDefinition getTool(String name) {
+ return toolHandlers.get(name);
+ }
+
+ /**
+ * Registers a handler for permission requests.
+ *
+ * Called internally when creating or resuming a session with permission
+ * handling.
+ *
+ * @param handler
+ * the permission handler
+ */
+ void registerPermissionHandler(PermissionHandler handler) {
+ permissionHandler.set(handler);
+ }
+
+ /**
+ * Handles a permission request from the Copilot CLI.
+ *
+ * Called internally when the server requests permission for an operation.
+ *
+ * @param permissionRequestData
+ * the JSON data for the permission request
+ * @return a future that resolves with the permission result
+ */
+ CompletableFuture handlePermissionRequest(JsonNode permissionRequestData) {
+ PermissionHandler handler = permissionHandler.get();
+ if (handler == null) {
+ PermissionRequestResult result = new PermissionRequestResult();
+ result.setKind("denied-no-approval-rule-and-could-not-request-from-user");
+ return CompletableFuture.completedFuture(result);
+ }
+
+ try {
+ PermissionRequest request = MAPPER.treeToValue(permissionRequestData, PermissionRequest.class);
+ PermissionInvocation invocation = new PermissionInvocation();
+ invocation.setSessionId(sessionId);
+ return handler.handle(request, invocation);
+ } catch (JsonProcessingException e) {
+ LOG.log(Level.SEVERE, "Failed to parse permission request", e);
+ PermissionRequestResult result = new PermissionRequestResult();
+ result.setKind("denied-no-approval-rule-and-could-not-request-from-user");
+ return CompletableFuture.completedFuture(result);
+ }
+ }
+
+ /**
+ * Gets the complete list of messages and events in the session.
+ *
+ * This retrieves the full conversation history, including all user messages,
+ * assistant responses, tool invocations, and other session events.
+ *
+ * @return a future that resolves with a list of all session events
+ * @see AbstractSessionEvent
+ */
+ public CompletableFuture> getMessages() {
+ return rpc.invoke("session.getMessages", Map.of("sessionId", sessionId), GetMessagesResponse.class)
+ .thenApply(response -> {
+ List events = new ArrayList<>();
+ if (response.getEvents() != null) {
+ for (JsonNode eventNode : response.getEvents()) {
+ try {
+ AbstractSessionEvent event = SessionEventParser.parse(eventNode.toString());
+ if (event != null) {
+ events.add(event);
+ }
+ } catch (Exception e) {
+ LOG.log(Level.WARNING, "Failed to parse event", e);
+ }
+ }
+ }
+ return events;
+ });
+ }
+
+ /**
+ * Aborts the currently processing message in this session.
+ *
+ * Use this to cancel a long-running operation or stop the assistant from
+ * continuing to generate a response.
+ *
+ * @return a future that completes when the abort is acknowledged
+ */
+ public CompletableFuture abort() {
+ return rpc.invoke("session.abort", Map.of("sessionId", sessionId), Void.class);
+ }
+
+ /**
+ * Disposes the session and releases all associated resources.
+ *
+ * This destroys the session on the server, clears all event handlers, and
+ * releases tool and permission handlers. After calling this method, the session
+ * cannot be used again.
+ */
+ @Override
+ public void close() {
+ try {
+ rpc.invoke("session.destroy", Map.of("sessionId", sessionId), Void.class).get(5, TimeUnit.SECONDS);
+ } catch (Exception e) {
+ LOG.log(Level.FINE, "Error destroying session", e);
+ }
+
+ eventHandlers.clear();
+ toolHandlers.clear();
+ permissionHandler.set(null);
+ }
+
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/JsonRpcClient.java b/java/src/main/java/com/github/copilot/sdk/JsonRpcClient.java
new file mode 100644
index 00000000..bf527548
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/JsonRpcClient.java
@@ -0,0 +1,327 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+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;
+import java.util.logging.Logger;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.github.copilot.sdk.json.JsonRpcError;
+import com.github.copilot.sdk.json.JsonRpcRequest;
+import com.github.copilot.sdk.json.JsonRpcResponse;
+
+/**
+ * JSON-RPC 2.0 client implementation for communicating with the Copilot CLI.
+ */
+class JsonRpcClient implements AutoCloseable {
+
+ private static final Logger LOG = Logger.getLogger(JsonRpcClient.class.getName());
+ private static final ObjectMapper MAPPER = createObjectMapper();
+
+ private final InputStream inputStream;
+ private final OutputStream outputStream;
+ private final Socket socket;
+ private final Process process;
+ private final AtomicLong requestIdCounter = new AtomicLong(0);
+ private final Map> pendingRequests = new ConcurrentHashMap<>();
+ private final Map> notificationHandlers = new ConcurrentHashMap<>();
+ private final ExecutorService readerExecutor;
+ private volatile boolean running = true;
+
+ private JsonRpcClient(InputStream inputStream, OutputStream outputStream, Socket socket, Process process) {
+ this.inputStream = inputStream;
+ 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;
+ });
+ startReader();
+ }
+
+ static ObjectMapper createObjectMapper() {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.registerModule(new JavaTimeModule());
+ mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+ mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
+ return mapper;
+ }
+
+ public static ObjectMapper getObjectMapper() {
+ return MAPPER;
+ }
+
+ /**
+ * Creates a JSON-RPC client using stdio with a process.
+ */
+ public static JsonRpcClient fromProcess(Process process) {
+ return new JsonRpcClient(process.getInputStream(), process.getOutputStream(), null, process);
+ }
+
+ /**
+ * Creates a JSON-RPC client using TCP socket.
+ */
+ public static JsonRpcClient fromSocket(Socket socket) throws IOException {
+ return new JsonRpcClient(socket.getInputStream(), socket.getOutputStream(), socket, null);
+ }
+
+ /**
+ * Registers a handler for JSON-RPC method calls (requests/notifications from
+ * server).
+ */
+ public void registerMethodHandler(String method, BiConsumer handler) {
+ notificationHandlers.put(method, handler);
+ }
+
+ /**
+ * Sends a JSON-RPC request and waits for the response.
+ */
+ public CompletableFuture invoke(String method, Object params, Class responseType) {
+ long id = requestIdCounter.incrementAndGet();
+ CompletableFuture future = new CompletableFuture<>();
+ pendingRequests.put(id, future);
+
+ JsonRpcRequest request = new JsonRpcRequest();
+ request.setJsonrpc("2.0");
+ request.setId(id);
+ request.setMethod(method);
+ request.setParams(params);
+
+ try {
+ sendMessage(request);
+ } catch (IOException e) {
+ pendingRequests.remove(id);
+ future.completeExceptionally(e);
+ }
+
+ return future.thenApply(result -> {
+ try {
+ if (responseType == Void.class || responseType == void.class) {
+ return null;
+ }
+ return MAPPER.treeToValue(result, responseType);
+ } catch (JsonProcessingException e) {
+ throw new CompletionException(e);
+ }
+ });
+ }
+
+ /**
+ * Sends a JSON-RPC notification (no response expected).
+ */
+ public void notify(String method, Object params) throws IOException {
+ JsonRpcRequest notification = new JsonRpcRequest();
+ notification.setJsonrpc("2.0");
+ notification.setMethod(method);
+ notification.setParams(params);
+ sendMessage(notification);
+ }
+
+ /**
+ * Sends a JSON-RPC response to a server request.
+ */
+ public void sendResponse(Object id, Object result) throws IOException {
+ JsonRpcResponse response = new JsonRpcResponse();
+ response.setJsonrpc("2.0");
+ response.setId(id);
+ response.setResult(result);
+ sendMessage(response);
+ }
+
+ /**
+ * Sends a JSON-RPC error response to a server request.
+ */
+ public void sendErrorResponse(Object id, int code, String message) throws IOException {
+ JsonRpcResponse response = new JsonRpcResponse();
+ response.setJsonrpc("2.0");
+ response.setId(id);
+ JsonRpcError error = new JsonRpcError();
+ error.setCode(code);
+ error.setMessage(message);
+ response.setError(error);
+ sendMessage(response);
+ }
+
+ private synchronized void sendMessage(Object message) throws IOException {
+ String json = MAPPER.writeValueAsString(message);
+ byte[] content = json.getBytes(StandardCharsets.UTF_8);
+ String header = "Content-Length: " + content.length + "\r\n\r\n";
+
+ outputStream.write(header.getBytes(StandardCharsets.UTF_8));
+ outputStream.write(content);
+ outputStream.flush();
+
+ LOG.fine("Sent: " + json);
+ }
+
+ private void startReader() {
+ readerExecutor.submit(() -> {
+ try {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+
+ while (running) {
+ String line = reader.readLine();
+ if (line == null) {
+ break;
+ }
+
+ // Parse headers
+ int contentLength = -1;
+ while (!line.isEmpty()) {
+ if (line.toLowerCase().startsWith("content-length:")) {
+ contentLength = Integer.parseInt(line.substring(15).trim());
+ }
+ line = reader.readLine();
+ if (line == null) {
+ return;
+ }
+ }
+
+ if (contentLength <= 0) {
+ continue;
+ }
+
+ // Read content
+ char[] buffer = new char[contentLength];
+ int read = 0;
+ while (read < contentLength) {
+ int result = reader.read(buffer, read, contentLength - read);
+ if (result == -1) {
+ return;
+ }
+ read += result;
+ }
+
+ String content = new String(buffer);
+ LOG.fine("Received: " + content);
+
+ handleMessage(content);
+ }
+ } catch (Exception e) {
+ if (running) {
+ LOG.log(Level.SEVERE, "Error in JSON-RPC reader", e);
+ }
+ }
+ });
+ }
+
+ private void handleMessage(String content) {
+ try {
+ JsonNode node = MAPPER.readTree(content);
+
+ // Check if this is a response to our request
+ if (node.has("id") && !node.get("id").isNull() && (node.has("result") || node.has("error"))) {
+ long id = node.get("id").asLong();
+ CompletableFuture future = pendingRequests.remove(id);
+ if (future != null) {
+ if (node.has("error")) {
+ JsonNode errorNode = node.get("error");
+ String errorMessage = errorNode.has("message")
+ ? errorNode.get("message").asText()
+ : "Unknown error";
+ int errorCode = errorNode.has("code") ? errorNode.get("code").asInt() : -1;
+ future.completeExceptionally(new JsonRpcException(errorCode, errorMessage));
+ } else {
+ future.complete(node.get("result"));
+ }
+ }
+ }
+ // Check if this is a request from server (has method and id)
+ else if (node.has("method")) {
+ String method = node.get("method").asText();
+ JsonNode params = node.get("params");
+ Object id = node.has("id") && !node.get("id").isNull() ? node.get("id") : null;
+
+ BiConsumer handler = notificationHandlers.get(method);
+ if (handler != null) {
+ try {
+ // Create a context that includes the request ID for responses
+ handler.accept(id != null ? id.toString() : null, params);
+ } catch (Exception e) {
+ LOG.log(Level.SEVERE, "Error handling method " + method, e);
+ if (id != null) {
+ try {
+ sendErrorResponse(id, -32603, e.getMessage());
+ } catch (IOException ioe) {
+ LOG.log(Level.SEVERE, "Failed to send error response", ioe);
+ }
+ }
+ }
+ } else {
+ LOG.fine("No handler for method: " + method);
+ if (id != null) {
+ try {
+ sendErrorResponse(id, -32601, "Method not found: " + method);
+ } catch (IOException ioe) {
+ LOG.log(Level.SEVERE, "Failed to send error response", ioe);
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ LOG.log(Level.SEVERE, "Error parsing JSON-RPC message", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ running = false;
+ readerExecutor.shutdownNow();
+
+ // Cancel all pending requests
+ pendingRequests.forEach((id, future) -> future.completeExceptionally(new IOException("Client closed")));
+ pendingRequests.clear();
+
+ try {
+ if (socket != null) {
+ socket.close();
+ }
+ } catch (IOException e) {
+ LOG.log(Level.FINE, "Error closing socket", e);
+ }
+
+ if (process != null) {
+ process.destroy();
+ }
+ }
+
+ public boolean isConnected() {
+ if (socket != null) {
+ return socket.isConnected() && !socket.isClosed();
+ }
+ if (process != null) {
+ return process.isAlive();
+ }
+ return false;
+ }
+
+ public Process getProcess() {
+ return process;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/JsonRpcException.java b/java/src/main/java/com/github/copilot/sdk/JsonRpcException.java
new file mode 100644
index 00000000..2afb7a9f
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/JsonRpcException.java
@@ -0,0 +1,48 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk;
+
+/**
+ * Exception thrown when a JSON-RPC error occurs during communication with the
+ * Copilot CLI server.
+ *
+ * This exception wraps error responses from the JSON-RPC protocol, including
+ * the error code and message returned by the server.
+ */
+final class JsonRpcException extends RuntimeException {
+
+ private final int code;
+
+ /**
+ * Creates a new JSON-RPC exception.
+ *
+ * @param code
+ * the JSON-RPC error code
+ * @param message
+ * the error message from the server
+ */
+ public JsonRpcException(int code, String message) {
+ super(message);
+ this.code = code;
+ }
+
+ /**
+ * Returns the JSON-RPC error code.
+ *
+ * Standard JSON-RPC error codes include:
+ *
+ *
-32700: Parse error
+ *
-32600: Invalid request
+ *
-32601: Method not found
+ *
-32602: Invalid params
+ *
-32603: Internal error
+ *
+ *
+ * @return the error code
+ */
+ public int getCode() {
+ return code;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java b/java/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java
new file mode 100644
index 00000000..3539a595
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java
@@ -0,0 +1,35 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+// Code generated by update-protocol-version.ts. DO NOT EDIT.
+
+package com.github.copilot.sdk;
+
+/**
+ * Provides the SDK protocol version. This must match the version expected by
+ * the copilot-agent-runtime server.
+ */
+public enum SdkProtocolVersion {
+
+ LATEST(1);
+
+ private int versionNumber;
+
+ private SdkProtocolVersion(int versionNumber) {
+ this.versionNumber = versionNumber;
+ }
+
+ public int getVersionNumber() {
+ return this.versionNumber;
+ }
+
+ /**
+ * Gets the SDK protocol version.
+ *
+ * @return the protocol version
+ */
+ public static int get() {
+ return LATEST.getVersionNumber();
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/SystemMessageMode.java b/java/src/main/java/com/github/copilot/sdk/SystemMessageMode.java
new file mode 100644
index 00000000..355eb84e
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/SystemMessageMode.java
@@ -0,0 +1,50 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * Specifies how the system message should be applied to a session.
+ *
+ * The system message controls the behavior and personality of the AI assistant.
+ * This enum determines whether to append custom instructions to the default
+ * system message or replace it entirely.
+ *
+ * @see com.github.copilot.sdk.json.SystemMessageConfig
+ */
+public enum SystemMessageMode {
+ /**
+ * Append the custom content to the default system message.
+ *
+ * This mode preserves the default guardrails and behaviors while adding
+ * additional instructions or context.
+ */
+ APPEND("append"),
+
+ /**
+ * Replace the default system message entirely with the custom content.
+ *
+ * Warning: This mode removes all default guardrails and
+ * behaviors. Use with caution.
+ */
+ REPLACE("replace");
+
+ private final String value;
+
+ SystemMessageMode(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the JSON value for this mode.
+ *
+ * @return the string value used in JSON serialization
+ */
+ @JsonValue
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/events/AbortEvent.java b/java/src/main/java/com/github/copilot/sdk/events/AbortEvent.java
new file mode 100644
index 00000000..f05177fd
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/events/AbortEvent.java
@@ -0,0 +1,46 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.events;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Event: abort
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public final class AbortEvent extends AbstractSessionEvent {
+
+ @JsonProperty("data")
+ private AbortData data;
+
+ @Override
+ public String getType() {
+ return "abort";
+ }
+
+ public AbortData getData() {
+ return data;
+ }
+
+ public void setData(AbortData data) {
+ this.data = data;
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class AbortData {
+
+ @JsonProperty("reason")
+ private String reason;
+
+ public String getReason() {
+ return reason;
+ }
+
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java b/java/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java
new file mode 100644
index 00000000..39cd8e4f
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java
@@ -0,0 +1,166 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.events;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.time.OffsetDateTime;
+import java.util.UUID;
+
+/**
+ * Base class for all session events in the Copilot SDK.
+ *
+ * Session events represent all activities that occur during a Copilot
+ * conversation, including messages from the user and assistant, tool
+ * executions, and session state changes. Events are delivered to handlers
+ * registered via
+ * {@link com.github.copilot.sdk.CopilotSession#on(java.util.function.Consumer)}.
+ *
+ *
+ * This corresponds to the event type in the JSON protocol (e.g.,
+ * "assistant.message", "session.idle").
+ *
+ * @return the event type string
+ */
+ public abstract String getType();
+
+ /**
+ * Gets the unique identifier for this event.
+ *
+ * @return the event UUID
+ */
+ public UUID getId() {
+ return id;
+ }
+
+ /**
+ * Sets the event identifier.
+ *
+ * @param id
+ * the event UUID
+ */
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ /**
+ * Gets the timestamp when this event occurred.
+ *
+ * @return the event timestamp
+ */
+ public OffsetDateTime getTimestamp() {
+ return timestamp;
+ }
+
+ /**
+ * Sets the event timestamp.
+ *
+ * @param timestamp
+ * the event timestamp
+ */
+ public void setTimestamp(OffsetDateTime timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ /**
+ * Gets the parent event ID, if this event is a child of another.
+ *
+ * @return the parent event UUID, or {@code null}
+ */
+ public UUID getParentId() {
+ return parentId;
+ }
+
+ /**
+ * Sets the parent event ID.
+ *
+ * @param parentId
+ * the parent event UUID
+ */
+ public void setParentId(UUID parentId) {
+ this.parentId = parentId;
+ }
+
+ /**
+ * Returns whether this is an ephemeral event.
+ *
+ * Ephemeral events are not persisted in session history.
+ *
+ * @return {@code true} if ephemeral, {@code false} otherwise
+ */
+ public Boolean getEphemeral() {
+ return ephemeral;
+ }
+
+ /**
+ * Sets whether this is an ephemeral event.
+ *
+ * @param ephemeral
+ * {@code true} if ephemeral
+ */
+ public void setEphemeral(Boolean ephemeral) {
+ this.ephemeral = ephemeral;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java b/java/src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java
new file mode 100644
index 00000000..b1ee77b6
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java
@@ -0,0 +1,46 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.events;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Event: assistant.intent
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public final class AssistantIntentEvent extends AbstractSessionEvent {
+
+ @JsonProperty("data")
+ private AssistantIntentData data;
+
+ @Override
+ public String getType() {
+ return "assistant.intent";
+ }
+
+ public AssistantIntentData getData() {
+ return data;
+ }
+
+ public void setData(AssistantIntentData data) {
+ this.data = data;
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class AssistantIntentData {
+
+ @JsonProperty("intent")
+ private String intent;
+
+ public String getIntent() {
+ return intent;
+ }
+
+ public void setIntent(String intent) {
+ this.intent = intent;
+ }
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java b/java/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java
new file mode 100644
index 00000000..fe25edc7
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java
@@ -0,0 +1,79 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.events;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Event: assistant.message_delta
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public final class AssistantMessageDeltaEvent extends AbstractSessionEvent {
+
+ @JsonProperty("data")
+ private AssistantMessageDeltaData data;
+
+ @Override
+ public String getType() {
+ return "assistant.message_delta";
+ }
+
+ public AssistantMessageDeltaData getData() {
+ return data;
+ }
+
+ public void setData(AssistantMessageDeltaData data) {
+ this.data = data;
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class AssistantMessageDeltaData {
+
+ @JsonProperty("messageId")
+ private String messageId;
+
+ @JsonProperty("deltaContent")
+ private String deltaContent;
+
+ @JsonProperty("totalResponseSizeBytes")
+ private Double totalResponseSizeBytes;
+
+ @JsonProperty("parentToolCallId")
+ private String parentToolCallId;
+
+ public String getMessageId() {
+ return messageId;
+ }
+
+ public void setMessageId(String messageId) {
+ this.messageId = messageId;
+ }
+
+ public String getDeltaContent() {
+ return deltaContent;
+ }
+
+ public void setDeltaContent(String deltaContent) {
+ this.deltaContent = deltaContent;
+ }
+
+ public Double getTotalResponseSizeBytes() {
+ return totalResponseSizeBytes;
+ }
+
+ public void setTotalResponseSizeBytes(Double totalResponseSizeBytes) {
+ this.totalResponseSizeBytes = totalResponseSizeBytes;
+ }
+
+ public String getParentToolCallId() {
+ return parentToolCallId;
+ }
+
+ public void setParentToolCallId(String parentToolCallId) {
+ this.parentToolCallId = parentToolCallId;
+ }
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java b/java/src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java
new file mode 100644
index 00000000..6dc73798
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java
@@ -0,0 +1,234 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.events;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/**
+ * Event representing a complete message from the assistant.
+ *
+ * This event is fired when the assistant has finished generating a response.
+ * For streaming responses, use {@link AssistantMessageDeltaEvent} instead.
+ *
+ *
+ * This class deserializes JSON event data into the appropriate
+ * {@link AbstractSessionEvent} subclass based on the "type" field. It is used
+ * internally by the SDK to convert server events to Java objects.
+ *
+ *
Supported Event Types
+ *
+ *
Session: session.start, session.resume, session.error,
+ * session.idle, session.info, etc.
+ *
Assistant: assistant.message, assistant.message_delta,
+ * assistant.turn_start, assistant.turn_end, etc.
+ *
Tool: tool.execution_start, tool.execution_complete,
+ * etc.
+ *
User: user.message, pending_messages.modified
+ *
Subagent: subagent.started, subagent.completed,
+ * etc.
+ * Attachments provide additional context to the AI assistant, such as source
+ * code files, documents, or other relevant content. All setter methods return
+ * {@code this} for method chaining.
+ *
+ *
Example Usage
+ *
+ *
{@code
+ * var attachment = new Attachment().setType("file").setPath("/path/to/source.java").setDisplayName("Main Source File");
+ * }
+ *
+ * @param type
+ * the attachment type
+ * @return this attachment for method chaining
+ */
+ public Attachment setType(String type) {
+ this.type = type;
+ return this;
+ }
+
+ /**
+ * Gets the file path.
+ *
+ * @return the absolute path to the file
+ */
+ public String getPath() {
+ return path;
+ }
+
+ /**
+ * Sets the file path.
+ *
+ * This should be an absolute path to the file on the filesystem.
+ *
+ * @param path
+ * the absolute file path
+ * @return this attachment for method chaining
+ */
+ public Attachment setPath(String path) {
+ this.path = path;
+ return this;
+ }
+
+ /**
+ * Gets the display name.
+ *
+ * @return the display name for the attachment
+ */
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ /**
+ * Sets a human-readable display name for the attachment.
+ *
+ * This name is shown to the assistant and may be used when referring to the
+ * file in responses.
+ *
+ * @param displayName
+ * the display name
+ * @return this attachment for method chaining
+ */
+ public Attachment setDisplayName(String displayName) {
+ this.displayName = displayName;
+ return this;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/AzureOptions.java b/java/src/main/java/com/github/copilot/sdk/json/AzureOptions.java
new file mode 100644
index 00000000..41f98ba3
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/AzureOptions.java
@@ -0,0 +1,53 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Azure OpenAI-specific configuration options.
+ *
+ * When using a BYOK (Bring Your Own Key) setup with Azure OpenAI, this class
+ * allows you to specify Azure-specific settings such as the API version to use.
+ *
+ *
Example Usage
+ *
+ *
{@code
+ * var provider = new ProviderConfig().setType("azure-openai").setHost("your-resource.openai.azure.com")
+ * .setApiKey("your-api-key").setAzure(new AzureOptions().setApiVersion("2024-02-01"));
+ * }
+ *
+ * @see ProviderConfig#setAzure(AzureOptions)
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class AzureOptions {
+
+ @JsonProperty("apiVersion")
+ private String apiVersion;
+
+ /**
+ * Gets the Azure OpenAI API version.
+ *
+ * @return the API version string
+ */
+ public String getApiVersion() {
+ return apiVersion;
+ }
+
+ /**
+ * Sets the Azure OpenAI API version to use.
+ *
+ * Examples: {@code "2024-02-01"}, {@code "2023-12-01-preview"}
+ *
+ * @param apiVersion
+ * the API version string
+ * @return this options object for method chaining
+ */
+ public AzureOptions setApiVersion(String apiVersion) {
+ this.apiVersion = apiVersion;
+ return this;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/java/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java
new file mode 100644
index 00000000..368ab891
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java
@@ -0,0 +1,296 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import java.util.Map;
+import java.util.logging.Logger;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+/**
+ * Configuration options for creating a
+ * {@link com.github.copilot.sdk.CopilotClient}.
+ *
+ * This class provides a fluent API for configuring how the client connects to
+ * and manages the Copilot CLI server. All setter methods return {@code this}
+ * for method chaining.
+ *
+ *
Example Usage
+ *
+ *
{@code
+ * var options = new CopilotClientOptions().setCliPath("/usr/local/bin/copilot").setLogLevel("debug")
+ * .setAutoStart(true);
+ *
+ * var client = new CopilotClient(options);
+ * }
+ *
+ * @see com.github.copilot.sdk.CopilotClient
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class CopilotClientOptions {
+
+ private String cliPath;
+ private String[] cliArgs;
+ private String cwd;
+ private int port;
+ private boolean useStdio = true;
+ private String cliUrl;
+ private String logLevel = "info";
+ private boolean autoStart = true;
+ private boolean autoRestart = true;
+ private Map environment;
+ private Logger logger;
+
+ /**
+ * Gets the path to the Copilot CLI executable.
+ *
+ * @return the CLI path, or {@code null} to use "copilot" from PATH
+ */
+ public String getCliPath() {
+ return cliPath;
+ }
+
+ /**
+ * Sets the path to the Copilot CLI executable.
+ *
+ * @param cliPath
+ * the path to the CLI executable, or {@code null} to use "copilot"
+ * from PATH
+ * @return this options instance for method chaining
+ */
+ public CopilotClientOptions setCliPath(String cliPath) {
+ this.cliPath = cliPath;
+ return this;
+ }
+
+ /**
+ * Gets the extra CLI arguments.
+ *
+ * @return the extra arguments to pass to the CLI
+ */
+ public String[] getCliArgs() {
+ return cliArgs;
+ }
+
+ /**
+ * Sets extra arguments to pass to the CLI process.
+ *
+ * These arguments are prepended before SDK-managed flags.
+ *
+ * @param cliArgs
+ * the extra arguments to pass
+ * @return this options instance for method chaining
+ */
+ public CopilotClientOptions setCliArgs(String[] cliArgs) {
+ this.cliArgs = cliArgs;
+ return this;
+ }
+
+ /**
+ * Gets the working directory for the CLI process.
+ *
+ * @return the working directory path
+ */
+ public String getCwd() {
+ return cwd;
+ }
+
+ /**
+ * Sets the working directory for the CLI process.
+ *
+ * @param cwd
+ * the working directory path
+ * @return this options instance for method chaining
+ */
+ public CopilotClientOptions setCwd(String cwd) {
+ this.cwd = cwd;
+ return this;
+ }
+
+ /**
+ * Gets the TCP port for the CLI server.
+ *
+ * @return the port number, or 0 for a random port
+ */
+ public int getPort() {
+ return port;
+ }
+
+ /**
+ * Sets the TCP port for the CLI server to listen on.
+ *
+ * This is only used when {@link #isUseStdio()} is {@code false}.
+ *
+ * @param port
+ * the port number, or 0 for a random port
+ * @return this options instance for method chaining
+ */
+ public CopilotClientOptions setPort(int port) {
+ this.port = port;
+ return this;
+ }
+
+ /**
+ * Returns whether to use stdio transport instead of TCP.
+ *
+ * @return {@code true} to use stdio (default), {@code false} to use TCP
+ */
+ public boolean isUseStdio() {
+ return useStdio;
+ }
+
+ /**
+ * Sets whether to use stdio transport instead of TCP.
+ *
+ * Stdio transport is more efficient and is the default. TCP transport can be
+ * useful for debugging or connecting to remote servers.
+ *
+ * @param useStdio
+ * {@code true} to use stdio, {@code false} to use TCP
+ * @return this options instance for method chaining
+ */
+ public CopilotClientOptions setUseStdio(boolean useStdio) {
+ this.useStdio = useStdio;
+ return this;
+ }
+
+ /**
+ * Gets the URL of an existing CLI server to connect to.
+ *
+ * @return the CLI server URL, or {@code null} to spawn a new process
+ */
+ public String getCliUrl() {
+ return cliUrl;
+ }
+
+ /**
+ * Sets the URL of an existing CLI server to connect to.
+ *
+ * When provided, the client will not spawn a CLI process but will connect to
+ * the specified URL instead. Format: "host:port" or "http://host:port".
+ *
+ * Note: This is mutually exclusive with
+ * {@link #setUseStdio(boolean)} and {@link #setCliPath(String)}.
+ *
+ * @param cliUrl
+ * the CLI server URL to connect to
+ * @return this options instance for method chaining
+ */
+ public CopilotClientOptions setCliUrl(String cliUrl) {
+ this.cliUrl = cliUrl;
+ return this;
+ }
+
+ /**
+ * Gets the log level for the CLI process.
+ *
+ * @return the log level (default: "info")
+ */
+ public String getLogLevel() {
+ return logLevel;
+ }
+
+ /**
+ * Sets the log level for the CLI process.
+ *
+ * Valid levels include: "error", "warn", "info", "debug", "trace".
+ *
+ * @param logLevel
+ * the log level
+ * @return this options instance for method chaining
+ */
+ public CopilotClientOptions setLogLevel(String logLevel) {
+ this.logLevel = logLevel;
+ return this;
+ }
+
+ /**
+ * Returns whether the client should automatically start the server.
+ *
+ * @return {@code true} to auto-start (default), {@code false} for manual start
+ */
+ public boolean isAutoStart() {
+ return autoStart;
+ }
+
+ /**
+ * Sets whether the client should automatically start the CLI server when the
+ * first request is made.
+ *
+ * @param autoStart
+ * {@code true} to auto-start, {@code false} for manual start
+ * @return this options instance for method chaining
+ */
+ public CopilotClientOptions setAutoStart(boolean autoStart) {
+ this.autoStart = autoStart;
+ return this;
+ }
+
+ /**
+ * Returns whether the client should automatically restart the server on crash.
+ *
+ * @return {@code true} to auto-restart (default), {@code false} otherwise
+ */
+ public boolean isAutoRestart() {
+ return autoRestart;
+ }
+
+ /**
+ * Sets whether the client should automatically restart the CLI server if it
+ * crashes unexpectedly.
+ *
+ * @param autoRestart
+ * {@code true} to auto-restart, {@code false} otherwise
+ * @return this options instance for method chaining
+ */
+ public CopilotClientOptions setAutoRestart(boolean autoRestart) {
+ this.autoRestart = autoRestart;
+ return this;
+ }
+
+ /**
+ * Gets the environment variables for the CLI process.
+ *
+ * @return the environment variables map
+ */
+ public Map getEnvironment() {
+ return environment;
+ }
+
+ /**
+ * Sets environment variables to pass to the CLI process.
+ *
+ * When set, these environment variables replace the inherited environment.
+ *
+ * @param environment
+ * the environment variables map
+ * @return this options instance for method chaining
+ */
+ public CopilotClientOptions setEnvironment(Map environment) {
+ this.environment = environment;
+ return this;
+ }
+
+ /**
+ * Gets the custom logger for the client.
+ *
+ * @return the logger instance
+ */
+ public Logger getLogger() {
+ return logger;
+ }
+
+ /**
+ * Sets a custom logger for the client.
+ *
+ * @param logger
+ * the logger instance to use
+ * @return this options instance for method chaining
+ */
+ public CopilotClientOptions setLogger(Logger logger) {
+ this.logger = logger;
+ return this;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java b/java/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java
new file mode 100644
index 00000000..c725a234
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java
@@ -0,0 +1,168 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Internal request object for creating a new session.
+ *
+ * This is a low-level class for JSON-RPC communication. For creating sessions,
+ * use
+ * {@link com.github.copilot.sdk.CopilotClient#createSession(SessionConfig)}.
+ *
+ * @see com.github.copilot.sdk.CopilotClient#createSession(SessionConfig)
+ * @see SessionConfig
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class CreateSessionRequest {
+
+ @JsonProperty("model")
+ private String model;
+
+ @JsonProperty("sessionId")
+ private String sessionId;
+
+ @JsonProperty("tools")
+ private List tools;
+
+ @JsonProperty("systemMessage")
+ private SystemMessageConfig systemMessage;
+
+ @JsonProperty("availableTools")
+ private List availableTools;
+
+ @JsonProperty("excludedTools")
+ private List excludedTools;
+
+ @JsonProperty("provider")
+ private ProviderConfig provider;
+
+ @JsonProperty("requestPermission")
+ private Boolean requestPermission;
+
+ @JsonProperty("streaming")
+ private Boolean streaming;
+
+ @JsonProperty("mcpServers")
+ private Map mcpServers;
+
+ @JsonProperty("customAgents")
+ private List customAgents;
+
+ /** Gets the model name. @return the model */
+ public String getModel() {
+ return model;
+ }
+
+ /** Sets the model name. @param model the model */
+ public void setModel(String model) {
+ this.model = model;
+ }
+
+ /** Gets the session ID. @return the session ID */
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ /** Sets the session ID. @param sessionId the session ID */
+ public void setSessionId(String sessionId) {
+ this.sessionId = sessionId;
+ }
+
+ /** Gets the tools. @return the tool definitions */
+ public List getTools() {
+ return tools;
+ }
+
+ /** Sets the tools. @param tools the tool definitions */
+ public void setTools(List tools) {
+ this.tools = tools;
+ }
+
+ /** Gets the system message config. @return the config */
+ public SystemMessageConfig getSystemMessage() {
+ return systemMessage;
+ }
+
+ /** Sets the system message config. @param systemMessage the config */
+ public void setSystemMessage(SystemMessageConfig systemMessage) {
+ this.systemMessage = systemMessage;
+ }
+
+ /** Gets available tools. @return the tool names */
+ public List getAvailableTools() {
+ return availableTools;
+ }
+
+ /** Sets available tools. @param availableTools the tool names */
+ public void setAvailableTools(List availableTools) {
+ this.availableTools = availableTools;
+ }
+
+ /** Gets excluded tools. @return the tool names */
+ public List getExcludedTools() {
+ return excludedTools;
+ }
+
+ /** Sets excluded tools. @param excludedTools the tool names */
+ public void setExcludedTools(List excludedTools) {
+ this.excludedTools = excludedTools;
+ }
+
+ /** Gets the provider config. @return the provider */
+ public ProviderConfig getProvider() {
+ return provider;
+ }
+
+ /** Sets the provider config. @param provider the provider */
+ public void setProvider(ProviderConfig provider) {
+ this.provider = provider;
+ }
+
+ /** Gets request permission flag. @return the flag */
+ public Boolean getRequestPermission() {
+ return requestPermission;
+ }
+
+ /** Sets request permission flag. @param requestPermission the flag */
+ public void setRequestPermission(Boolean requestPermission) {
+ this.requestPermission = requestPermission;
+ }
+
+ /** Gets streaming flag. @return the flag */
+ public Boolean getStreaming() {
+ return streaming;
+ }
+
+ /** Sets streaming flag. @param streaming the flag */
+ public void setStreaming(Boolean streaming) {
+ this.streaming = streaming;
+ }
+
+ /** Gets MCP servers. @return the servers map */
+ public Map getMcpServers() {
+ return mcpServers;
+ }
+
+ /** Sets MCP servers. @param mcpServers the servers map */
+ public void setMcpServers(Map mcpServers) {
+ this.mcpServers = mcpServers;
+ }
+
+ /** Gets custom agents. @return the agents */
+ public List getCustomAgents() {
+ return customAgents;
+ }
+
+ /** Sets custom agents. @param customAgents the agents */
+ public void setCustomAgents(List customAgents) {
+ this.customAgents = customAgents;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java b/java/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java
new file mode 100644
index 00000000..93041787
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java
@@ -0,0 +1,17 @@
+package com.github.copilot.sdk.json;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class CreateSessionResponse {
+ @JsonProperty("sessionId")
+ private String sessionId;
+
+ public String getSessionId() {
+ return sessionId;
+ }
+ public void setSessionId(String sessionId) {
+ this.sessionId = sessionId;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java b/java/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java
new file mode 100644
index 00000000..ff4a20c4
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java
@@ -0,0 +1,212 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Configuration for a custom agent in a Copilot session.
+ *
+ * Custom agents extend the capabilities of the base Copilot assistant with
+ * specialized behavior, tools, and prompts. Each agent can be referenced in
+ * messages using the {@code @agent-name} mention syntax.
+ *
+ *
Example Usage
+ *
+ *
{@code
+ * var agent = new CustomAgentConfig().setName("code-reviewer").setDisplayName("Code Reviewer")
+ * .setDescription("Reviews code for best practices").setPrompt("You are a code review expert...")
+ * .setTools(List.of("read_file", "search_code"));
+ *
+ * var config = new SessionConfig().setCustomAgents(List.of(agent));
+ * }
+ *
+ * @see SessionConfig#setCustomAgents(List)
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class CustomAgentConfig {
+
+ @JsonProperty("name")
+ private String name;
+
+ @JsonProperty("displayName")
+ private String displayName;
+
+ @JsonProperty("description")
+ private String description;
+
+ @JsonProperty("tools")
+ private List tools;
+
+ @JsonProperty("prompt")
+ private String prompt;
+
+ @JsonProperty("mcpServers")
+ private Map mcpServers;
+
+ @JsonProperty("infer")
+ private Boolean infer;
+
+ /**
+ * Gets the unique identifier name for this agent.
+ *
+ * @return the agent name used for {@code @mentions}
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the unique identifier name for this agent.
+ *
+ * This name is used to mention the agent in messages (e.g.,
+ * {@code @code-reviewer}).
+ *
+ * @param name
+ * the agent identifier (alphanumeric and hyphens)
+ * @return this config for method chaining
+ */
+ public CustomAgentConfig setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * Gets the human-readable display name.
+ *
+ * @return the display name shown to users
+ */
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ /**
+ * Sets the human-readable display name.
+ *
+ * @param displayName
+ * the friendly name for the agent
+ * @return this config for method chaining
+ */
+ public CustomAgentConfig setDisplayName(String displayName) {
+ this.displayName = displayName;
+ return this;
+ }
+
+ /**
+ * Gets the agent description.
+ *
+ * @return the description of what this agent does
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Sets a description of the agent's capabilities.
+ *
+ * This helps users understand when to use this agent.
+ *
+ * @param description
+ * the agent description
+ * @return this config for method chaining
+ */
+ public CustomAgentConfig setDescription(String description) {
+ this.description = description;
+ return this;
+ }
+
+ /**
+ * Gets the list of tool names available to this agent.
+ *
+ * @return the list of tool identifiers
+ */
+ public List getTools() {
+ return tools;
+ }
+
+ /**
+ * Sets the tools available to this agent.
+ *
+ * These can reference both built-in tools and custom tools registered in the
+ * session.
+ *
+ * @param tools
+ * the list of tool names
+ * @return this config for method chaining
+ */
+ public CustomAgentConfig setTools(List tools) {
+ this.tools = tools;
+ return this;
+ }
+
+ /**
+ * Gets the system prompt for this agent.
+ *
+ * @return the agent's system prompt
+ */
+ public String getPrompt() {
+ return prompt;
+ }
+
+ /**
+ * Sets the system prompt that defines this agent's behavior.
+ *
+ * This prompt is used to customize the agent's responses and capabilities.
+ *
+ * @param prompt
+ * the system prompt
+ * @return this config for method chaining
+ */
+ public CustomAgentConfig setPrompt(String prompt) {
+ this.prompt = prompt;
+ return this;
+ }
+
+ /**
+ * Gets the MCP server configurations for this agent.
+ *
+ * @return the MCP servers map
+ */
+ public Map getMcpServers() {
+ return mcpServers;
+ }
+
+ /**
+ * Sets MCP (Model Context Protocol) servers available to this agent.
+ *
+ * @param mcpServers
+ * the MCP server configurations
+ * @return this config for method chaining
+ */
+ public CustomAgentConfig setMcpServers(Map mcpServers) {
+ this.mcpServers = mcpServers;
+ return this;
+ }
+
+ /**
+ * Gets whether inference mode is enabled.
+ *
+ * @return the infer flag, or {@code null} if not set
+ */
+ public Boolean getInfer() {
+ return infer;
+ }
+
+ /**
+ * Sets whether to enable inference mode for this agent.
+ *
+ * @param infer
+ * {@code true} to enable inference mode
+ * @return this config for method chaining
+ */
+ public CustomAgentConfig setInfer(Boolean infer) {
+ this.infer = infer;
+ return this;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/DeleteSessionResponse.java b/java/src/main/java/com/github/copilot/sdk/json/DeleteSessionResponse.java
new file mode 100644
index 00000000..0ef2c2f0
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/DeleteSessionResponse.java
@@ -0,0 +1,64 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Internal response object from deleting a session.
+ *
+ * This is a low-level class for JSON-RPC communication containing the result of
+ * a session deletion operation.
+ *
+ * @see com.github.copilot.sdk.CopilotClient#deleteSession(String)
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class DeleteSessionResponse {
+
+ @JsonProperty("success")
+ private boolean success;
+
+ @JsonProperty("error")
+ private String error;
+
+ /**
+ * Returns whether the deletion was successful.
+ *
+ * @return {@code true} if the session was deleted successfully
+ */
+ public boolean isSuccess() {
+ return success;
+ }
+
+ /**
+ * Sets whether the deletion was successful.
+ *
+ * @param success
+ * {@code true} if successful
+ */
+ public void setSuccess(boolean success) {
+ this.success = success;
+ }
+
+ /**
+ * Gets the error message if the deletion failed.
+ *
+ * @return the error message, or {@code null} if successful
+ */
+ public String getError() {
+ return error;
+ }
+
+ /**
+ * Sets the error message.
+ *
+ * @param error
+ * the error message
+ */
+ public void setError(String error) {
+ this.error = error;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/GetLastSessionIdResponse.java b/java/src/main/java/com/github/copilot/sdk/json/GetLastSessionIdResponse.java
new file mode 100644
index 00000000..b8618202
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/GetLastSessionIdResponse.java
@@ -0,0 +1,17 @@
+package com.github.copilot.sdk.json;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class GetLastSessionIdResponse {
+ @JsonProperty("sessionId")
+ private String sessionId;
+
+ public String getSessionId() {
+ return sessionId;
+ }
+ public void setSessionId(String sessionId) {
+ this.sessionId = sessionId;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/GetMessagesResponse.java b/java/src/main/java/com/github/copilot/sdk/json/GetMessagesResponse.java
new file mode 100644
index 00000000..30470f24
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/GetMessagesResponse.java
@@ -0,0 +1,22 @@
+package com.github.copilot.sdk.json;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.JsonNode;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class GetMessagesResponse {
+
+ @JsonProperty("events")
+ private List events;
+
+ public List getEvents() {
+ return events;
+ }
+
+ public void setEvents(List events) {
+ this.events = events;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/JsonRpcError.java b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcError.java
new file mode 100644
index 00000000..f599079e
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcError.java
@@ -0,0 +1,97 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * JSON-RPC 2.0 error structure.
+ *
+ * This is an internal class representing an error in a JSON-RPC response. It
+ * contains an error code, message, and optional additional data.
+ *
+ *
Standard Error Codes
+ *
+ *
-32700: Parse error
+ *
-32600: Invalid Request
+ *
-32601: Method not found
+ *
-32602: Invalid params
+ *
-32603: Internal error
+ *
+ *
+ * @see JsonRpcResponse
+ * @see JSON-RPC
+ * Error Object
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class JsonRpcError {
+
+ @JsonProperty("code")
+ private int code;
+
+ @JsonProperty("message")
+ private String message;
+
+ @JsonProperty("data")
+ private Object data;
+
+ /**
+ * Gets the error code.
+ *
+ * @return the integer error code
+ */
+ public int getCode() {
+ return code;
+ }
+
+ /**
+ * Sets the error code.
+ *
+ * @param code
+ * the integer error code
+ */
+ public void setCode(int code) {
+ this.code = code;
+ }
+
+ /**
+ * Gets the error message.
+ *
+ * @return the human-readable error message
+ */
+ public String getMessage() {
+ return message;
+ }
+
+ /**
+ * Sets the error message.
+ *
+ * @param message
+ * the error message
+ */
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ /**
+ * Gets the additional error data.
+ *
+ * @return the additional data, or {@code null} if none
+ */
+ public Object getData() {
+ return data;
+ }
+
+ /**
+ * Sets the additional error data.
+ *
+ * @param data
+ * the additional data
+ */
+ public void setData(Object data) {
+ this.data = data;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/JsonRpcRequest.java b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcRequest.java
new file mode 100644
index 00000000..e4f4692d
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcRequest.java
@@ -0,0 +1,110 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * JSON-RPC 2.0 request structure.
+ *
+ * This is an internal class representing the wire format of a JSON-RPC request.
+ * It follows the JSON-RPC 2.0 specification.
+ *
+ * @see JsonRpcResponse
+ * @see JSON-RPC 2.0
+ * Specification
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class JsonRpcRequest {
+
+ @JsonProperty("jsonrpc")
+ private String jsonrpc;
+
+ @JsonProperty("id")
+ private Long id;
+
+ @JsonProperty("method")
+ private String method;
+
+ @JsonProperty("params")
+ private Object params;
+
+ /**
+ * Gets the JSON-RPC version.
+ *
+ * @return the version string (should be "2.0")
+ */
+ public String getJsonrpc() {
+ return jsonrpc;
+ }
+
+ /**
+ * Sets the JSON-RPC version.
+ *
+ * @param jsonrpc
+ * the version string
+ */
+ public void setJsonrpc(String jsonrpc) {
+ this.jsonrpc = jsonrpc;
+ }
+
+ /**
+ * Gets the request ID.
+ *
+ * @return the request identifier
+ */
+ public Long getId() {
+ return id;
+ }
+
+ /**
+ * Sets the request ID.
+ *
+ * @param id
+ * the request identifier
+ */
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ /**
+ * Gets the method name.
+ *
+ * @return the RPC method to invoke
+ */
+ public String getMethod() {
+ return method;
+ }
+
+ /**
+ * Sets the method name.
+ *
+ * @param method
+ * the RPC method to invoke
+ */
+ public void setMethod(String method) {
+ this.method = method;
+ }
+
+ /**
+ * Gets the method parameters.
+ *
+ * @return the parameters object
+ */
+ public Object getParams() {
+ return params;
+ }
+
+ /**
+ * Sets the method parameters.
+ *
+ * @param params
+ * the parameters object
+ */
+ public void setParams(Object params) {
+ this.params = params;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/JsonRpcResponse.java b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcResponse.java
new file mode 100644
index 00000000..9eb3dd91
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcResponse.java
@@ -0,0 +1,112 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * JSON-RPC 2.0 response structure.
+ *
+ * This is an internal class representing the wire format of a JSON-RPC
+ * response. It follows the JSON-RPC 2.0 specification. A response contains
+ * either a result or an error, but not both.
+ *
+ * @see JsonRpcRequest
+ * @see JsonRpcError
+ * @see JSON-RPC 2.0
+ * Specification
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class JsonRpcResponse {
+
+ @JsonProperty("jsonrpc")
+ private String jsonrpc;
+
+ @JsonProperty("id")
+ private Object id;
+
+ @JsonProperty("result")
+ private Object result;
+
+ @JsonProperty("error")
+ private JsonRpcError error;
+
+ /**
+ * Gets the JSON-RPC version.
+ *
+ * @return the version string (should be "2.0")
+ */
+ public String getJsonrpc() {
+ return jsonrpc;
+ }
+
+ /**
+ * Sets the JSON-RPC version.
+ *
+ * @param jsonrpc
+ * the version string
+ */
+ public void setJsonrpc(String jsonrpc) {
+ this.jsonrpc = jsonrpc;
+ }
+
+ /**
+ * Gets the response ID.
+ *
+ * @return the request identifier this response corresponds to
+ */
+ public Object getId() {
+ return id;
+ }
+
+ /**
+ * Sets the response ID.
+ *
+ * @param id
+ * the response identifier
+ */
+ public void setId(Object id) {
+ this.id = id;
+ }
+
+ /**
+ * Gets the result of the RPC call.
+ *
+ * @return the result object, or {@code null} if there was an error
+ */
+ public Object getResult() {
+ return result;
+ }
+
+ /**
+ * Sets the result of the RPC call.
+ *
+ * @param result
+ * the result object
+ */
+ public void setResult(Object result) {
+ this.result = result;
+ }
+
+ /**
+ * Gets the error if the RPC call failed.
+ *
+ * @return the error object, or {@code null} if successful
+ */
+ public JsonRpcError getError() {
+ return error;
+ }
+
+ /**
+ * Sets the error for a failed RPC call.
+ *
+ * @param error
+ * the error object
+ */
+ public void setError(JsonRpcError error) {
+ this.error = error;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/ListSessionsResponse.java b/java/src/main/java/com/github/copilot/sdk/json/ListSessionsResponse.java
new file mode 100644
index 00000000..1c4490e2
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/ListSessionsResponse.java
@@ -0,0 +1,45 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Internal response object from listing sessions.
+ *
+ * This is a low-level class for JSON-RPC communication containing the list of
+ * available sessions.
+ *
+ * @see com.github.copilot.sdk.CopilotClient#listSessions()
+ * @see SessionMetadata
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class ListSessionsResponse {
+
+ @JsonProperty("sessions")
+ private List sessions;
+
+ /**
+ * Gets the list of sessions.
+ *
+ * @return the list of session metadata
+ */
+ public List getSessions() {
+ return sessions;
+ }
+
+ /**
+ * Sets the list of sessions.
+ *
+ * @param sessions
+ * the list of session metadata
+ */
+ public void setSessions(List sessions) {
+ this.sessions = sessions;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/MessageOptions.java b/java/src/main/java/com/github/copilot/sdk/json/MessageOptions.java
new file mode 100644
index 00000000..b553e90d
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/MessageOptions.java
@@ -0,0 +1,108 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+/**
+ * Options for sending a message to a Copilot session.
+ *
+ * This class specifies the message content and optional attachments to send to
+ * the assistant. All setter methods return {@code this} for method chaining.
+ *
+ *
Example Usage
+ *
+ *
{@code
+ * var options = new MessageOptions().setPrompt("Explain this code")
+ * .setAttachments(List.of(new Attachment().setType("file").setPath("/path/to/file.java")));
+ *
+ * session.send(options).get();
+ * }
+ *
+ * @see com.github.copilot.sdk.CopilotSession#send(MessageOptions)
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class MessageOptions {
+
+ private String prompt;
+ private List attachments;
+ private String mode;
+
+ /**
+ * Gets the message prompt.
+ *
+ * @return the prompt text
+ */
+ public String getPrompt() {
+ return prompt;
+ }
+
+ /**
+ * Sets the message prompt to send to the assistant.
+ *
+ * @param prompt
+ * the message text
+ * @return this options instance for method chaining
+ */
+ public MessageOptions setPrompt(String prompt) {
+ this.prompt = prompt;
+ return this;
+ }
+
+ /**
+ * Gets the file attachments.
+ *
+ * @return the list of attachments
+ */
+ public List getAttachments() {
+ return attachments;
+ }
+
+ /**
+ * Sets file attachments to include with the message.
+ *
+ * Attachments provide additional context to the assistant, such as source code
+ * files, documents, or other relevant files.
+ *
+ * @param attachments
+ * the list of file attachments
+ * @return this options instance for method chaining
+ * @see Attachment
+ */
+ public MessageOptions setAttachments(List attachments) {
+ this.attachments = attachments;
+ return this;
+ }
+
+ /**
+ * Sets the message delivery mode.
+ *
+ * Valid modes:
+ *
+ *
"enqueue" - Queue the message for processing (default)
+ *
"immediate" - Process the message immediately
+ *
+ *
+ * @param mode
+ * the delivery mode
+ * @return this options instance for method chaining
+ */
+ public MessageOptions setMode(String mode) {
+ this.mode = mode;
+ return this;
+ }
+
+ /**
+ * Gets the delivery mode.
+ *
+ * @return the delivery mode
+ */
+ public String getMode() {
+ return mode;
+ }
+
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java b/java/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java
new file mode 100644
index 00000000..5facbd0e
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java
@@ -0,0 +1,51 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Functional interface for handling permission requests from the AI assistant.
+ *
+ * When the assistant needs permission to perform certain actions (such as
+ * executing tools or accessing resources), this handler is invoked to approve
+ * or deny the request.
+ *
+ *
+ * The handler should evaluate the request and return a result indicating
+ * whether the permission is granted or denied.
+ *
+ * @param request
+ * the permission request details
+ * @param invocation
+ * the invocation context with session information
+ * @return a future that completes with the permission decision
+ */
+ CompletableFuture handle(PermissionRequest request, PermissionInvocation invocation);
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/PermissionInvocation.java b/java/src/main/java/com/github/copilot/sdk/json/PermissionInvocation.java
new file mode 100644
index 00000000..baec1c6c
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/PermissionInvocation.java
@@ -0,0 +1,39 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+/**
+ * Context information for a permission request invocation.
+ *
+ * This object provides context about the session where the permission request
+ * originated.
+ *
+ * @see PermissionHandler
+ */
+public final class PermissionInvocation {
+
+ private String sessionId;
+
+ /**
+ * Gets the session ID where the permission was requested.
+ *
+ * @return the session ID
+ */
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ /**
+ * Sets the session ID.
+ *
+ * @param sessionId
+ * the session ID
+ * @return this invocation for method chaining
+ */
+ public PermissionInvocation setSessionId(String sessionId) {
+ this.sessionId = sessionId;
+ return this;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/PermissionRequest.java b/java/src/main/java/com/github/copilot/sdk/json/PermissionRequest.java
new file mode 100644
index 00000000..bcdcecfb
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/PermissionRequest.java
@@ -0,0 +1,88 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Represents a permission request from the AI assistant.
+ *
+ * When the assistant needs permission to perform certain actions, this object
+ * contains the details of the request, including the kind of permission and any
+ * associated tool call.
+ *
+ * @see PermissionHandler
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class PermissionRequest {
+
+ @JsonProperty("kind")
+ private String kind;
+
+ @JsonProperty("toolCallId")
+ private String toolCallId;
+
+ private Map extensionData;
+
+ /**
+ * Gets the kind of permission being requested.
+ *
+ * @return the permission kind
+ */
+ public String getKind() {
+ return kind;
+ }
+
+ /**
+ * Sets the permission kind.
+ *
+ * @param kind
+ * the permission kind
+ */
+ public void setKind(String kind) {
+ this.kind = kind;
+ }
+
+ /**
+ * Gets the associated tool call ID, if applicable.
+ *
+ * @return the tool call ID, or {@code null} if not a tool-related request
+ */
+ public String getToolCallId() {
+ return toolCallId;
+ }
+
+ /**
+ * Sets the tool call ID.
+ *
+ * @param toolCallId
+ * the tool call ID
+ */
+ public void setToolCallId(String toolCallId) {
+ this.toolCallId = toolCallId;
+ }
+
+ /**
+ * Gets additional extension data for the request.
+ *
+ * @return the extension data map
+ */
+ public Map getExtensionData() {
+ return extensionData;
+ }
+
+ /**
+ * Sets additional extension data for the request.
+ *
+ * @param extensionData
+ * the extension data map
+ */
+ public void setExtensionData(Map extensionData) {
+ this.extensionData = extensionData;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/sdk/json/PermissionRequestResult.java b/java/src/main/java/com/github/copilot/sdk/json/PermissionRequestResult.java
new file mode 100644
index 00000000..6a99bc6a
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/sdk/json/PermissionRequestResult.java
@@ -0,0 +1,78 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Result of a permission request decision.
+ *
+ * This object indicates whether a permission request was approved or denied,
+ * and may include additional rules for future similar requests.
+ *
+ *
Common Result Kinds
+ *
+ *
"user-approved" - User approved the permission request
+ *
"user-denied" - User denied the permission request
+ *
"denied-no-approval-rule-and-could-not-request-from-user" - No handler
+ * and couldn't ask user
+ *
+ *
+ * @see PermissionHandler
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class PermissionRequestResult {
+
+ @JsonProperty("kind")
+ private String kind;
+
+ @JsonProperty("rules")
+ private List