diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg
index ddc73f3a8..f1a7c5eb3 100644
--- a/.github/badges/jacoco.svg
+++ b/.github/badges/jacoco.svg
@@ -12,7 +12,7 @@
coveragecoverage
- 84.8%
- 84.8%
+ 84.4%
+ 84.4%
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index d7dafb081..284e2b800 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -104,7 +104,7 @@ When porting from .NET:
- 4-space indentation (enforced by Spotless with Eclipse formatter)
- Fluent setter pattern for configuration classes (e.g., `new SessionConfig().setModel("gpt-5").setTools(tools)`)
- Public APIs require Javadoc (enforced by Checkstyle, except `json` and `events` packages)
-- Pre-commit hook runs `mvn spotless:check` - enable with: `git config core.hooksPath .githooks`
+- Pre-commit hook runs `mvn spotless:check` - Must be manually enabled with: `git config core.hooksPath .githooks`, except in the Copilot coding agent environment. This hook is explicitly enabled in the Copilot coding agent environment. See [copilot-setup-steps.yml](workflows/copilot-setup-steps.yml).
### Handler Pattern
@@ -244,6 +244,18 @@ This SDK is designed to be **lightweight with minimal dependencies**:
5. Check for security vulnerabilities
6. Get team approval for non-trivial additions
+## Pre-commit Hooks and Formatting (Coding Agent)
+
+The repository has a pre-commit hook (`.githooks/pre-commit`) that is **automatically enabled** in the Copilot coding agent environment via `copilot-setup-steps.yml`. The hook runs `mvn spotless:check` on any commit that includes changes under `src/`.
+
+**If a commit fails due to the pre-commit hook:**
+
+1. Run `mvn spotless:apply` to auto-fix formatting issues.
+2. Re-stage the changed files with `git add -u`.
+3. Retry the commit.
+
+**Best practice:** Always run `mvn spotless:apply` before committing Java source changes to avoid hook failures in the first place. If you forget and the hook rejects the commit, follow the three steps above and continue.
+
## Commit and PR Guidelines
### Commit Messages
diff --git a/.github/templates/index.html b/.github/templates/index.html
index 9af01dded..d273ad074 100644
--- a/.github/templates/index.html
+++ b/.github/templates/index.html
@@ -65,8 +65,8 @@
Available Versions
-
- ⚠️ Disclaimer: This is the official Java SDK for GitHub Copilot. This repository treats the official .NET and nodejs SDKs for GitHub Copilot as reference implementations. These SDKS are all officially supported as GitHub open source projects. The Java implementation follows the backward compatibility guarantees offered by the reference implementations. As such this implementation may introduce breaking changes, according to the policy declared by the reference implementations. Use at your own risk.
+
+ ℹ️ Public Preview: This is the official Java SDK for GitHub Copilot. This repository treats the official .NET and Node.js SDKs for GitHub Copilot as reference implementations. These SDKs are all officially supported as GitHub open source projects. The Java implementation follows the backward compatibility guarantees offered by the reference implementations. While in public preview, minor breaking changes may still occur between releases.
diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml
index 6a0cdec5b..145629457 100644
--- a/.github/workflows/copilot-setup-steps.yml
+++ b/.github/workflows/copilot-setup-steps.yml
@@ -41,6 +41,10 @@ jobs:
distribution: 'temurin'
cache: 'maven'
+ # Enable repository pre-commit hooks (including Spotless checks for relevant source changes)
+ - name: Enable pre-commit hooks
+ run: git config core.hooksPath .githooks
+
# Verify installations
- name: Verify tool installations
run: |
@@ -50,4 +54,6 @@ jobs:
java -version
gh --version
gh aw version
+ echo "--- Git hooks path ---"
+ git config core.hooksPath
echo "✅ All tools installed successfully"
diff --git a/.github/workflows/notes.template b/.github/workflows/notes.template
index 0fd7af642..9c148cdf1 100644
--- a/.github/workflows/notes.template
+++ b/.github/workflows/notes.template
@@ -1,6 +1,6 @@
# Installation
-⚠️ **Disclaimer:** This is the official Java SDK for GitHub Copilot. This repository treats the official .NET and nodejs SDKs for GitHub Copilot as reference implementations. These SDKS are all officially supported as GitHub open source projects. The Java implementation follows the backward compatibility guarantees offered by the reference implementations. As such this implementation may introduce breaking changes, according to the policy declared by the reference implementations. Use at your own risk.
+ℹ️ **Public Preview:** This is the official Java SDK for GitHub Copilot. This repository treats the official .NET and Node.js SDKs for GitHub Copilot as reference implementations. These SDKs are all officially supported as GitHub open source projects. The Java implementation follows the backward compatibility guarantees offered by the reference implementations. While in public preview, minor breaking changes may still occur between releases.
⚠️ **Artifact versioning plan:** Releases of this implementation track releases of the reference implementation. For each release of the reference implementation, there may follow a corresponding relase of this implementation with the same number as the reference implementation. Release identifiers of the reference implementation are in the form `vMaj.Min.Micro`. For example v0.1.32. The corresponding maven version for the release will be `Maj.Min.Micro-java.N`, where `Maj`, `Min` and `Micro` are the corresponding numbers for the reference impementation release, and `N` is a monotonically increasing sequence number starting with 0 for each release. See the corrseponding architectural decision record for more information in the `docs/adr` directory of the source code.
diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml
index a7bb58f01..242853209 100644
--- a/.github/workflows/publish-maven.yml
+++ b/.github/workflows/publish-maven.yml
@@ -121,8 +121,13 @@ jobs:
sed -i "s|[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-java\.[0-9][0-9]*\)\{0,1\}|${VERSION}|g" README.md
sed -i "s|copilot-sdk-java:[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-java\.[0-9][0-9]*\)\{0,1\}|copilot-sdk-java:${VERSION}|g" README.md
+ # Update snapshot version in README.md
+ DEV_VERSION="${{ steps.versions.outputs.dev_version }}"
+ sed -i "s|[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-java\.[0-9][0-9]*\)\{0,1\}-SNAPSHOT|${DEV_VERSION}|g" README.md
+
# Update version in jbang-example.java
sed -i "s|copilot-sdk-java:[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-java\.[0-9][0-9]*\)\{0,1\}|copilot-sdk-java:${VERSION}|g" jbang-example.java
+ sed -i 's|copilot-sdk-java:${project\.version}|copilot-sdk-java:'"${VERSION}"'|g' jbang-example.java
# Update version in cookbook files (hardcoded for direct GitHub browsing and JBang usage)
find src/site/markdown/cookbook -name "*.md" -type f -exec \
@@ -206,7 +211,7 @@ jobs:
# Build the gh release command
GH_ARGS=("${CURRENT_TAG}")
- GH_ARGS+=("--title" "Copilot Java SDK ${VERSION}")
+ GH_ARGS+=("--title" "GitHub Copilot SDK for Java ${VERSION}")
GH_ARGS+=("--notes" "${RELEASE_NOTES}")
GH_ARGS+=("--generate-notes")
diff --git a/.github/workflows/run-smoke-test.yml b/.github/workflows/run-smoke-test.yml
index d5181e038..8d0feac0c 100644
--- a/.github/workflows/run-smoke-test.yml
+++ b/.github/workflows/run-smoke-test.yml
@@ -11,8 +11,8 @@ permissions:
contents: read
jobs:
- smoke-test:
- name: Build SDK and run smoke test
+ smoke-test-jdk17:
+ name: Build SDK and run smoke test (JDK 17)
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
@@ -59,3 +59,55 @@ jobs:
cd smoke-test
java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar
echo "Smoke test passed (exit code 0)"
+
+ smoke-test-java25:
+ name: Build SDK and run smoke test (JDK 25)
+ runs-on: ubuntu-latest
+ if: github.ref == 'refs/heads/main'
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Set up JDK 25
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
+ with:
+ java-version: "25"
+ distribution: "microsoft"
+ cache: "maven"
+
+ - uses: ./.github/actions/setup-copilot
+
+ - name: Build SDK and install to local repo
+ run: mvn -DskipTests -Pskip-test-harness clean install
+
+ - name: Create and run smoke test via Copilot CLI
+ env:
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ run: |
+ cat > /tmp/smoke-test-prompt.txt << 'PROMPT_EOF'
+ You are running inside the copilot-sdk-java repository.
+ The SDK has already been built and installed into the local Maven repository.
+ JDK 25 and Maven are already installed and on PATH.
+
+ Execute the prompt at `src/test/prompts/PROMPT-smoke-test.md` with the following critical overrides:
+
+ **Critical override — disable SNAPSHOT updates (but allow downloads):** The goal of this workflow is to validate the SDK SNAPSHOT that was just built and installed locally, not any newer SNAPSHOT that might exist in a remote repository. To ensure Maven does not download a newer timestamped SNAPSHOT of the SDK while still allowing it to download any missing plugins or dependencies, you must run the smoke-test Maven build without `-U` and with `--no-snapshot-updates`, so that it uses the locally installed SDK artifact. Use `mvn --no-snapshot-updates clean package` instead of `mvn -U clean package` or `mvn -o clean package`.
+
+ **Critical override — do NOT run the jar:** Stop after the `mvn --no-snapshot-updates clean package` build succeeds. Do NOT execute Step 4 (java -jar) or Step 5 (verify exit code) from the prompt. The workflow will run the jar in a separate deterministic step to guarantee the exit code propagates correctly.
+
+ **Critical override — enable Virtual Threads for JDK 25:** After creating the Java source file from the README "Quick Start" section but BEFORE building, you must modify the source file to enable virtual thread support. The Quick Start code contains inline comments that start with `// JDK 25+:` — these are instructions. Find every such comment and follow what it says (comment out lines it says to comment out, uncomment lines it says to uncomment). Add any imports required by the newly uncommented code (e.g. `java.util.concurrent.Executors`).
+ Also set `maven.compiler.source` and `maven.compiler.target` to `25` in the `pom.xml`.
+
+ Follow steps 1-3 only: create the `smoke-test/` directory, create `pom.xml` and the Java source file exactly as specified, apply the JDK 25 virtual thread modifications described above, and build with `mvn --no-snapshot-updates clean package` (no SNAPSHOT updates and without `-U`).
+
+ If any step fails, exit with a non-zero exit code. Do not silently fix errors.
+ PROMPT_EOF
+
+ copilot --yolo --prompt "$(cat /tmp/smoke-test-prompt.txt)"
+
+ - name: Run smoke test jar
+ env:
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ run: |
+ cd smoke-test
+ java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar
+ echo "Smoke test passed (exit code 0)"
diff --git a/.github/workflows/weekly-upstream-sync.md b/.github/workflows/weekly-upstream-sync.md
index 641f29301..0aaff8d1e 100644
--- a/.github/workflows/weekly-upstream-sync.md
+++ b/.github/workflows/weekly-upstream-sync.md
@@ -41,111 +41,77 @@ safe-outputs:
noop:
report-as-issue: false
---
-# Weekly Upstream Sync Agentic Workflow
-This document describes the `weekly-upstream-sync.yml` GitHub Actions workflow, which automates the detection of new changes in the official [Copilot SDK](https://github.com/github/copilot-sdk) and delegates the merge work to the Copilot coding agent.
+# Weekly Upstream Sync
-## Overview
+You are an automation agent that detects new upstream changes and creates GitHub issues. You do **NOT** perform any code merges, edits, or pushes. Do **NOT** invoke any skills (especially `agentic-merge-upstream`). Your only job is to check for changes and use safe-output tools to create or close issues.
-The workflow runs on a **weekly schedule** (every Monday at 10:00 UTC) and can also be triggered manually. It does **not** perform the actual merge — instead, it detects upstream changes and creates a GitHub issue assigned to `copilot`, instructing the agent to follow the [agentic-merge-upstream](../prompts/agentic-merge-upstream.prompt.md) prompt to port the changes.
+## Instructions
-The agent must also create the Pull Request with the label `upstream-sync`. This allows the workflow to track the merge progress and avoid creating duplicate issues if the agent is still working on a previous sync.
+Follow these steps exactly:
-## Trigger
+### Step 1: Read `.lastmerge`
-| Trigger | Schedule |
-|---|---|
-| `schedule` | Every Monday at 10:00 UTC (`0 10 * * 1`) |
-| `workflow_dispatch` | Manual trigger from the Actions tab |
+Read the file `.lastmerge` in the repository root. It contains the SHA of the last upstream commit that was merged into this Java SDK.
-## Workflow Steps
+### Step 2: Check for upstream changes
-### 1. Checkout repository
+Clone the upstream repository and compare commits:
-Checks out the repo to read the `.lastmerge` file, which contains the SHA of the last upstream commit that was merged into the Java SDK.
-
-### 2. Check for upstream changes
-
-- Reads the last merged commit hash from `.lastmerge`
-- Clones the upstream `github/copilot-sdk` repository
-- Compares `.lastmerge` against upstream `HEAD`
-- If they match: sets `has_changes=false`
-- If they differ: counts new commits, generates a summary (up to 20 most recent), and sets outputs (`commit_count`, `upstream_head`, `last_merge`, `summary`)
-
-### 3. Close previous upstream-sync issues (when changes found)
-
-**Condition:** `has_changes == true`
+```bash
+LAST_MERGE=$(cat .lastmerge)
+git clone --quiet https://github.com/github/copilot-sdk.git /tmp/gh-aw/agent/upstream
+cd /tmp/gh-aw/agent/upstream
+UPSTREAM_HEAD=$(git rev-parse HEAD)
+```
-Before creating a new issue, closes any existing open issues with the `upstream-sync` label. This prevents stale issues from accumulating when previous sync attempts were incomplete or superseded. Each closed issue receives a comment explaining it was superseded.
+If `LAST_MERGE` equals `UPSTREAM_HEAD`, there are **no new changes**. Go to Step 3a.
-### 4. Close stale upstream-sync issues (when no changes found)
+If they differ, count the new commits and generate a summary:
-**Condition:** `has_changes == false`
+```bash
+COMMIT_COUNT=$(git rev-list --count "$LAST_MERGE".."$UPSTREAM_HEAD")
+SUMMARY=$(git log --oneline "$LAST_MERGE".."$UPSTREAM_HEAD" | head -20)
+```
-If the upstream is already up to date, closes any lingering open `upstream-sync` issues with a comment noting that no changes were detected. This handles the case where a previous issue was created but the changes were merged manually (updating `.lastmerge`) before the agent completed.
+Go to Step 3b.
-### 5. Create issue and assign to Copilot
+### Step 3a: No changes detected
-**Condition:** `has_changes == true`
+1. Search for any open issues with the `upstream-sync` label using the GitHub MCP tools.
+2. If there are open `upstream-sync` issues, close each one using the `close_issue` safe-output tool with a comment: "No new upstream changes detected. The Java SDK is up to date. Closing."
+3. Call the `noop` safe-output tool with message: "No new upstream changes since last merge ()."
+4. **Stop here.** Do not proceed further.
-Creates a new GitHub issue with:
+### Step 3b: Changes detected
-- **Title:** `Upstream sync: N new commits (YYYY-MM-DD)`
-- **Label:** `upstream-sync`
-- **Assignee:** `copilot`
-- **Body:** Contains commit count, commit range links, a summary of recent commits, and a link to the merge prompt
+1. Search for any open issues with the `upstream-sync` label using the GitHub MCP tools.
+2. Close each existing open `upstream-sync` issue using the `close_issue` safe-output tool with a comment: "Superseded by a newer upstream sync check."
+3. Create a new issue using the `create_issue` safe-output tool with:
+ - **Title:** `Upstream sync: new commits ()`
+ - **Body:** Include the following information:
+ ```
+ ## Automated Upstream Sync
-The Copilot coding agent picks up the issue, creates a branch and PR, then follows the merge prompt to port the changes.
+ There are **** new commits in the [official Copilot SDK](https://github.com/github/copilot-sdk) since the last merge.
-### 6. Summary
+ - **Last merged commit:** [``](https://github.com/github/copilot-sdk/commit/)
+ - **Upstream HEAD:** [``](https://github.com/github/copilot-sdk/commit/)
-Writes a GitHub Actions step summary with:
+ ### Recent upstream commits
-- Whether changes were detected
-- Commit count and range
-- Recent upstream commits
-- Link to the created issue (if any)
+ ```
+
+ ```
-## Flow Diagram
+ ### Instructions
-```
-┌─────────────────────┐
-│ Schedule / Manual │
-└──────────┬──────────┘
- │
- ▼
-┌─────────────────────┐
-│ Read .lastmerge │
-│ Clone upstream SDK │
-│ Compare commits │
-└──────────┬──────────┘
- │
- ┌─────┴─────┐
- │ │
- changes? no changes
- │ │
- ▼ ▼
-┌──────────┐ ┌──────────────────┐
-│ Close old│ │ Close stale │
-│ issues │ │ issues │
-└────┬─────┘ └──────────────────┘
- │
- ▼
-┌──────────────────────────┐
-│ Create issue assigned to │
-│ copilot │
-└──────────────────────────┘
- │
- ▼
-┌──────────────────────────┐
-│ Agent follows prompt to │
-│ port changes → PR │
-└──────────────────────────┘
-```
+ Follow the [agentic-merge-upstream](.github/prompts/agentic-merge-upstream.prompt.md) prompt to port these changes to the Java SDK.
+ ```
+4. After creating the issue, use the `assign_to_agent` safe-output tool to assign Copilot to the newly created issue.
-## Related Files
+## Important constraints
-| File | Purpose |
-|---|---|
-| `.lastmerge` | Stores the SHA of the last merged upstream commit |
-| [agentic-merge-upstream.prompt.md](../prompts/agentic-merge-upstream.prompt.md) | Detailed instructions the Copilot agent follows to port changes |
-| `.github/scripts/upstream-sync/` | Helper scripts used by the merge prompt |
+- **Do NOT edit any files**, create branches, or push code.
+- **Do NOT invoke any skills** such as `agentic-merge-upstream` or `commit-as-pull-request`.
+- **Do NOT attempt to merge or port upstream changes.** That is done by a separate agent that picks up the issue you create.
+- You **MUST** call at least one safe-output tool (`create_issue`, `close_issue`, `noop`, etc.) before finishing.
diff --git a/.gitignore b/.gitignore
index 8d7f42429..ddb2508ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,6 @@ smoke-test
*job-logs.txt
temporary-prompts/
changebundle.txt*
+.classpath
+.project
+.settings
diff --git a/.lastmerge b/.lastmerge
index a0cf76b72..83feb636c 100644
--- a/.lastmerge
+++ b/.lastmerge
@@ -1 +1 @@
-40887393a9e687dacc141a645799441b0313ff15
+c3fa6cbfb83d4a20b7912b1a17013d48f5a277a1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c54038b9c..38f6e6589 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,8 +8,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
-> **Upstream sync:** [`github/copilot-sdk@4088739`](https://github.com/github/copilot-sdk/commit/40887393a9e687dacc141a645799441b0313ff15)
+> **Upstream sync:** [`github/copilot-sdk@c3fa6cb`](https://github.com/github/copilot-sdk/commit/c3fa6cbfb83d4a20b7912b1a17013d48f5a277a1)
+
+## [0.2.2-java.1] - 2026-04-07
+
+> **Upstream sync:** [`github/copilot-sdk@c3fa6cb`](https://github.com/github/copilot-sdk/commit/c3fa6cbfb83d4a20b7912b1a17013d48f5a277a1)
+### Added
+
+- Slash commands — register `/command` handlers invoked from the CLI TUI via `SessionConfig.setCommands()` (upstream: [`f7fd757`](https://github.com/github/copilot-sdk/commit/f7fd757))
+- `CommandDefinition`, `CommandContext`, `CommandHandler`, `CommandWireDefinition` — types for defining and handling slash commands
+- `CommandExecuteEvent` — event dispatched when a registered slash command is executed
+- Elicitation (UI dialogs) — incoming handler via `SessionConfig.setOnElicitationRequest()` and outgoing convenience methods via `session.getUi()` (upstream: [`f7fd757`](https://github.com/github/copilot-sdk/commit/f7fd757))
+- `ElicitationContext`, `ElicitationHandler`, `ElicitationParams`, `ElicitationResult`, `ElicitationResultAction`, `ElicitationSchema`, `InputOptions` — types for elicitation
+- `ElicitationRequestedEvent` — event dispatched when an elicitation request is received
+- `SessionUiApi` — convenience API on `session.getUi()` for `confirm()`, `select()`, `input()`, and `elicitation()` calls
+- `SessionCapabilities` and `SessionUiCapabilities` — session capability reporting populated from create/resume response
+- `CapabilitiesChangedEvent` — event dispatched when session capabilities are updated
+- `CopilotClient.getSessionMetadata(String)` — O(1) session lookup by ID
+- `GetSessionMetadataResponse` — response type for `getSessionMetadata`
+### Fixed
+
+- Permission events already resolved by a pre-hook now short-circuit before invoking the client-side handler
+- `SessionUiApi` Javadoc now uses valid Java null-check syntax instead of `?.`
+- README updated to say "GitHub Copilot CLI 1.0.17" instead of "GitHub Copilot 1.0.17"
+
+## [0.2.1-java.1] - 2026-04-02
+
+> **Upstream sync:** [`github/copilot-sdk@4088739`](https://github.com/github/copilot-sdk/commit/40887393a9e687dacc141a645799441b0313ff15)
## [0.2.1-java.0] - 2026-03-26
> **Upstream sync:** [`github/copilot-sdk@4088739`](https://github.com/github/copilot-sdk/commit/40887393a9e687dacc141a645799441b0313ff15)
@@ -462,12 +488,23 @@ New types: `GetForegroundSessionResponse`, `SetForegroundSessionResponse`
- Pre-commit hook for Spotless code formatting
- Comprehensive API documentation
-[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...HEAD
+[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...HEAD
+[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1
+[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...HEAD
+[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1
+[0.2.1-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...v0.2.1-java.1
+[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...HEAD
+[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1
+[0.2.1-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...v0.2.1-java.1
[0.2.1-java.0]: https://github.com/github/copilot-sdk-java/compare/v0.1.32-java.0...v0.2.1-java.0
-[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...HEAD
+[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...HEAD
+[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1
+[0.2.1-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...v0.2.1-java.1
[0.2.1-java.0]: https://github.com/github/copilot-sdk-java/compare/v0.1.32-java.0...v0.2.1-java.0
[0.1.32-java.0]: https://github.com/github/copilot-sdk-java/compare/v1.0.11...v0.1.32-java.0
-[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...HEAD
+[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...HEAD
+[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1
+[0.2.1-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...v0.2.1-java.1
[0.2.1-java.0]: https://github.com/github/copilot-sdk-java/compare/v0.1.32-java.0...v0.2.1-java.0
[0.1.32-java.0]: https://github.com/github/copilot-sdk-java/compare/v1.0.11...v0.1.32-java.0
[1.0.11]: https://github.com/github/copilot-sdk-java/compare/v1.0.10...v1.0.11
diff --git a/README.md b/README.md
index 0084eb417..a33aaedda 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
## Background
-> ⚠️ **Disclaimer:** This SDK tracks the pre-GA [GitHub Copilot SDKs](https://github.com/github/copilot-sdk) for [.NET](https://github.com/github/copilot-sdk/tree/main/dotnet) and [nodejs](https://github.com/github/copilot-sdk/tree/main/nodejs). This SDK may change in breaking ways. Use at your own risk.
+> ℹ️ **Public Preview:** This SDK tracks the [GitHub Copilot SDKs](https://github.com/github/copilot-sdk) for [.NET](https://github.com/github/copilot-sdk/tree/main/dotnet) and [Node.js](https://github.com/github/copilot-sdk/tree/main/nodejs). While in public preview, minor breaking changes may still occur between releases.
Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build AI-powered applications and agentic workflows.
@@ -24,8 +24,8 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A
### Requirements
-- Java 17 or later
-- GitHub Copilot CLI 0.0.411-1 or later installed and in PATH (or provide custom `cliPath`)
+- Java 17 or later. **JDK 25 recommended**. Selecting JDK 25 enables the use of virtual threads, as shown in the [Quick Start](#quick-start).
+- GitHub Copilot CLI 1.0.17 or later installed and in `PATH` (or provide custom `cliPath`)
### Maven
@@ -33,7 +33,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A
com.githubcopilot-sdk-java
- 0.2.1-java.0
+ 0.2.2-java.1
```
@@ -53,14 +53,14 @@ Snapshot builds of the next development version are published to Maven Central S
com.githubcopilot-sdk-java
- 0.2.1-java.0-SNAPSHOT
+ 0.2.3-java.1-SNAPSHOT
```
### Gradle
```groovy
-implementation 'com.github:copilot-sdk-java:0.2.1-java.0'
+implementation 'com.github:copilot-sdk-java:0.2.2-java.1'
```
## Quick Start
@@ -69,16 +69,23 @@ implementation 'com.github:copilot-sdk-java:0.2.1-java.0'
import com.github.copilot.sdk.CopilotClient;
import com.github.copilot.sdk.events.AssistantMessageEvent;
import com.github.copilot.sdk.events.SessionUsageInfoEvent;
+import com.github.copilot.sdk.json.CopilotClientOptions;
import com.github.copilot.sdk.json.MessageOptions;
import com.github.copilot.sdk.json.PermissionHandler;
import com.github.copilot.sdk.json.SessionConfig;
+import java.util.concurrent.Executors;
+
public class CopilotSDK {
public static void main(String[] args) throws Exception {
var lastMessage = new String[]{null};
// Create and start client
- try (var client = new CopilotClient()) {
+ try (var client = new CopilotClient()) { // JDK 25+: comment out this line
+ // JDK 25+: uncomment the following 3 lines for virtual thread support
+ // var options = new CopilotClientOptions()
+ // .setExecutor(Executors.newVirtualThreadPerTaskExecutor());
+ // try (var client = new CopilotClient(options)) {
client.start().get();
// Create a session
diff --git a/instructions/copilot-sdk-java.instructions.md b/instructions/copilot-sdk-java.instructions.md
index bf18a3c5a..7881322fd 100644
--- a/instructions/copilot-sdk-java.instructions.md
+++ b/instructions/copilot-sdk-java.instructions.md
@@ -6,7 +6,7 @@ name: 'GitHub Copilot SDK Java Instructions'
## Core Principles
-- The SDK is in technical preview and may have breaking changes
+- The SDK is in public preview and may have breaking changes
- Requires Java 17 or later
- Requires GitHub Copilot CLI installed and in PATH
- Uses `CompletableFuture` for all async operations
diff --git a/jbang-example.java b/jbang-example.java
index 3d02653c1..dd1f80762 100644
--- a/jbang-example.java
+++ b/jbang-example.java
@@ -1,5 +1,5 @@
!
-//DEPS com.github:copilot-sdk-java:${project.version}
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import com.github.copilot.sdk.CopilotClient;
import com.github.copilot.sdk.events.AssistantMessageEvent;
import com.github.copilot.sdk.events.SessionUsageInfoEvent;
diff --git a/pom.xml b/pom.xml
index d8417fdeb..b61c36166 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
com.githubcopilot-sdk-java
- 0.2.1-java.0
+ 0.2.3-java.1-SNAPSHOTjarGitHub Copilot SDK :: Java
@@ -33,7 +33,7 @@
scm:git:https://github.com/github/copilot-sdk-java.gitscm:git:https://github.com/github/copilot-sdk-java.githttps://github.com/github/copilot-sdk-java
- v0.2.1-java.0
+ HEAD
@@ -51,6 +51,8 @@
${copilot.sdk.clone.dir}/testfalse
+
+
@@ -86,6 +88,12 @@
5.14.1test
+
+ org.mockito
+ mockito-core
+ 5.23.0
+ test
+
@@ -239,8 +247,8 @@
maven-surefire-plugin3.5.4
-
- ${testExecutionAgentArgs}
+
+ ${testExecutionAgentArgs} ${surefire.jvm.args}${copilot.tests.dir}${copilot.sdk.clone.dir}
@@ -537,6 +545,18 @@
+
+
+ jdk21+
+
+ [21,)
+
+
+ -XX:+EnableDynamicAgentLoading
+
+ skip-test-harness
diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java
index 707469428..f00e2fd11 100644
--- a/src/main/java/com/github/copilot/sdk/CopilotClient.java
+++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java
@@ -13,6 +13,8 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -22,6 +24,7 @@
import com.github.copilot.sdk.json.DeleteSessionResponse;
import com.github.copilot.sdk.json.GetAuthStatusResponse;
import com.github.copilot.sdk.json.GetLastSessionIdResponse;
+import com.github.copilot.sdk.json.GetSessionMetadataResponse;
import com.github.copilot.sdk.json.GetModelsResponse;
import com.github.copilot.sdk.json.GetStatusResponse;
import com.github.copilot.sdk.json.ListSessionsResponse;
@@ -150,42 +153,51 @@ public CompletableFuture start() {
private CompletableFuture startCore() {
LOG.fine("Starting Copilot client");
- return CompletableFuture.supplyAsync(() -> {
- try {
- JsonRpcClient rpc;
- Process process = null;
-
- if (optionsHost != null && optionsPort != null) {
- // External server (TCP)
- rpc = serverManager.connectToServer(null, optionsHost, optionsPort);
- } else {
- // Child process (stdio or TCP)
- CliServerManager.ProcessInfo processInfo = serverManager.startCliServer();
- process = processInfo.process();
- rpc = serverManager.connectToServer(process, processInfo.port() != null ? "localhost" : null,
- processInfo.port());
- }
+ Executor exec = options.getExecutor();
+ try {
+ return exec != null
+ ? CompletableFuture.supplyAsync(this::startCoreBody, exec)
+ : CompletableFuture.supplyAsync(this::startCoreBody);
+ } catch (RejectedExecutionException e) {
+ return CompletableFuture.failedFuture(e);
+ }
+ }
- Connection connection = new Connection(rpc, process);
+ private Connection startCoreBody() {
+ try {
+ JsonRpcClient rpc;
+ Process process = null;
+
+ if (optionsHost != null && optionsPort != null) {
+ // External server (TCP)
+ rpc = serverManager.connectToServer(null, optionsHost, optionsPort);
+ } else {
+ // Child process (stdio or TCP)
+ CliServerManager.ProcessInfo processInfo = serverManager.startCliServer();
+ process = processInfo.process();
+ rpc = serverManager.connectToServer(process, processInfo.port() != null ? "localhost" : null,
+ processInfo.port());
+ }
- // Register handlers for server-to-client calls
- RpcHandlerDispatcher dispatcher = new RpcHandlerDispatcher(sessions, lifecycleManager::dispatch);
- dispatcher.registerHandlers(rpc);
+ Connection connection = new Connection(rpc, process);
- // Verify protocol version
- verifyProtocolVersion(connection);
+ // Register handlers for server-to-client calls
+ RpcHandlerDispatcher dispatcher = new RpcHandlerDispatcher(sessions, lifecycleManager::dispatch,
+ options.getExecutor());
+ dispatcher.registerHandlers(rpc);
- LOG.info("Copilot client connected");
- return connection;
- } catch (Exception e) {
- String stderr = serverManager.getStderrOutput();
- if (!stderr.isEmpty()) {
- throw new CompletionException(
- new IOException("CLI process exited unexpectedly. stderr: " + stderr, e));
- }
- throw new CompletionException(e);
+ // Verify protocol version
+ verifyProtocolVersion(connection);
+
+ LOG.info("Copilot client connected");
+ return connection;
+ } catch (Exception e) {
+ String stderr = serverManager.getStderrOutput();
+ if (!stderr.isEmpty()) {
+ throw new CompletionException(new IOException("CLI process exited unexpectedly. stderr: " + stderr, e));
}
- });
+ throw new CompletionException(e);
+ }
}
private static final int MIN_PROTOCOL_VERSION = 2;
@@ -228,15 +240,27 @@ private void verifyProtocolVersion(Connection connection) throws Exception {
*/
public CompletableFuture stop() {
var closeFutures = new ArrayList>();
+ Executor exec = options.getExecutor();
for (CopilotSession session : new ArrayList<>(sessions.values())) {
- closeFutures.add(CompletableFuture.runAsync(() -> {
+ Runnable closeTask = () -> {
try {
session.close();
} catch (Exception e) {
LOG.log(Level.WARNING, "Error closing session " + session.getSessionId(), e);
}
- }));
+ };
+ CompletableFuture future;
+ try {
+ future = exec != null
+ ? CompletableFuture.runAsync(closeTask, exec)
+ : CompletableFuture.runAsync(closeTask);
+ } catch (RejectedExecutionException e) {
+ LOG.log(Level.WARNING, "Executor rejected session close task; closing inline", e);
+ closeTask.run();
+ future = CompletableFuture.completedFuture(null);
+ }
+ closeFutures.add(future);
}
sessions.clear();
@@ -329,6 +353,9 @@ public CompletableFuture createSession(SessionConfig config) {
: java.util.UUID.randomUUID().toString();
var session = new CopilotSession(sessionId, connection.rpc);
+ if (options.getExecutor() != null) {
+ session.setExecutor(options.getExecutor());
+ }
SessionRequestBuilder.configureSession(session, config);
sessions.put(sessionId, session);
@@ -348,6 +375,7 @@ public CompletableFuture createSession(SessionConfig config) {
return connection.rpc.invoke("session.create", request, CreateSessionResponse.class).thenApply(response -> {
session.setWorkspacePath(response.workspacePath());
+ session.setCapabilities(response.capabilities());
// If the server returned a different sessionId (e.g. a v2 CLI that ignores
// the client-supplied ID), re-key the sessions map.
String returnedId = response.sessionId();
@@ -399,6 +427,9 @@ public CompletableFuture resumeSession(String sessionId, ResumeS
return ensureConnected().thenCompose(connection -> {
// Register the session before the RPC call to avoid missing early events.
var session = new CopilotSession(sessionId, connection.rpc);
+ if (options.getExecutor() != null) {
+ session.setExecutor(options.getExecutor());
+ }
SessionRequestBuilder.configureSession(session, config);
sessions.put(sessionId, session);
@@ -415,6 +446,7 @@ public CompletableFuture resumeSession(String sessionId, ResumeS
return connection.rpc.invoke("session.resume", request, ResumeSessionResponse.class).thenApply(response -> {
session.setWorkspacePath(response.workspacePath());
+ session.setCapabilities(response.capabilities());
// If the server returned a different sessionId than what was requested, re-key.
String returnedId = response.sessionId();
if (returnedId != null && !returnedId.equals(sessionId)) {
@@ -628,6 +660,34 @@ public CompletableFuture> listSessions(SessionListFilter f
});
}
+ /**
+ * Gets metadata for a specific session by ID.
+ *
+ * This provides an efficient O(1) lookup of a single session's metadata instead
+ * of listing all sessions.
+ *
+ *
Example Usage
+ *
+ *
{@code
+ * var metadata = client.getSessionMetadata("session-123").get();
+ * if (metadata != null) {
+ * System.out.println("Session started at: " + metadata.getStartTime());
+ * }
+ * }
+ *
+ * @param sessionId
+ * the ID of the session to look up
+ * @return a future that resolves with the {@link SessionMetadata}, or
+ * {@code null} if the session was not found
+ * @see SessionMetadata
+ * @since 1.0.0
+ */
+ public CompletableFuture getSessionMetadata(String sessionId) {
+ return ensureConnected().thenCompose(connection -> connection.rpc
+ .invoke("session.getMetadata", Map.of("sessionId", sessionId), GetSessionMetadataResponse.class)
+ .thenApply(GetSessionMetadataResponse::session));
+ }
+
/**
* Gets the ID of the session currently displayed in the TUI.
*
diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java
index 8c68e1e3e..23b1b5368 100644
--- a/src/main/java/com/github/copilot/sdk/CopilotSession.java
+++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java
@@ -13,7 +13,11 @@
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Executors;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
@@ -27,14 +31,27 @@
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.CapabilitiesChangedEvent;
+import com.github.copilot.sdk.events.CommandExecuteEvent;
+import com.github.copilot.sdk.events.ElicitationRequestedEvent;
import com.github.copilot.sdk.events.ExternalToolRequestedEvent;
import com.github.copilot.sdk.events.PermissionRequestedEvent;
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.AgentInfo;
+import com.github.copilot.sdk.json.CommandContext;
+import com.github.copilot.sdk.json.CommandDefinition;
+import com.github.copilot.sdk.json.CommandHandler;
+import com.github.copilot.sdk.json.ElicitationContext;
+import com.github.copilot.sdk.json.ElicitationHandler;
+import com.github.copilot.sdk.json.ElicitationParams;
+import com.github.copilot.sdk.json.ElicitationResult;
+import com.github.copilot.sdk.json.ElicitationResultAction;
+import com.github.copilot.sdk.json.ElicitationSchema;
import com.github.copilot.sdk.json.GetMessagesResponse;
import com.github.copilot.sdk.json.HookInvocation;
+import com.github.copilot.sdk.json.InputOptions;
import com.github.copilot.sdk.json.MessageOptions;
import com.github.copilot.sdk.json.PermissionHandler;
import com.github.copilot.sdk.json.PermissionInvocation;
@@ -45,9 +62,12 @@
import com.github.copilot.sdk.json.PreToolUseHookInput;
import com.github.copilot.sdk.json.SendMessageRequest;
import com.github.copilot.sdk.json.SendMessageResponse;
+import com.github.copilot.sdk.json.SessionCapabilities;
import com.github.copilot.sdk.json.SessionEndHookInput;
import com.github.copilot.sdk.json.SessionHooks;
import com.github.copilot.sdk.json.SessionStartHookInput;
+import com.github.copilot.sdk.json.SessionUiApi;
+import com.github.copilot.sdk.json.SessionUiCapabilities;
import com.github.copilot.sdk.json.ToolDefinition;
import com.github.copilot.sdk.json.ToolResultObject;
import com.github.copilot.sdk.json.UserInputHandler;
@@ -112,15 +132,21 @@ public final class CopilotSession implements AutoCloseable {
*/
private volatile String sessionId;
private volatile String workspacePath;
+ private volatile SessionCapabilities capabilities = new SessionCapabilities();
+ private final SessionUiApi ui;
private final JsonRpcClient rpc;
private final Set> eventHandlers = ConcurrentHashMap.newKeySet();
private final Map toolHandlers = new ConcurrentHashMap<>();
+ private final Map commandHandlers = new ConcurrentHashMap<>();
private final AtomicReference permissionHandler = new AtomicReference<>();
private final AtomicReference userInputHandler = new AtomicReference<>();
+ private final AtomicReference elicitationHandler = new AtomicReference<>();
private final AtomicReference hooksHandler = new AtomicReference<>();
private volatile EventErrorHandler eventErrorHandler;
private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS;
private volatile Map>> transformCallbacks;
+ private final ScheduledExecutorService timeoutScheduler;
+ private volatile Executor executor;
/** Tracks whether this session instance has been terminated via close(). */
private volatile boolean isTerminated = false;
@@ -157,6 +183,22 @@ public final class CopilotSession implements AutoCloseable {
this.sessionId = sessionId;
this.rpc = rpc;
this.workspacePath = workspacePath;
+ this.ui = new SessionUiApiImpl();
+ var executor = new ScheduledThreadPoolExecutor(1, r -> {
+ var t = new Thread(r, "sendAndWait-timeout");
+ t.setDaemon(true);
+ return t;
+ });
+ executor.setRemoveOnCancelPolicy(true);
+ this.timeoutScheduler = executor;
+ }
+
+ /**
+ * Sets the executor for internal async operations. Package-private; called by
+ * CopilotClient after construction.
+ */
+ void setExecutor(Executor executor) {
+ this.executor = executor;
}
/**
@@ -204,6 +246,30 @@ void setWorkspacePath(String workspacePath) {
this.workspacePath = workspacePath;
}
+ /**
+ * Gets the capabilities reported by the host for this session.
+ *
+ * Capabilities are populated from the session create/resume response and
+ * updated in real time via {@code capabilities.changed} events.
+ *
+ * @return the session capabilities (never {@code null})
+ */
+ public SessionCapabilities getCapabilities() {
+ return capabilities;
+ }
+
+ /**
+ * Gets the UI API for eliciting information from the user during this session.
+ *
+ * All methods on this API throw {@link IllegalStateException} if the host does
+ * not report elicitation support via {@link #getCapabilities()}.
+ *
+ * @return the UI API
+ */
+ public SessionUiApi getUi() {
+ return ui;
+ }
+
/**
* Sets a custom error handler for exceptions thrown by event handlers.
*
+ * Called internally when creating or resuming a session with commands.
+ *
+ * @param commands
+ * the command definitions to register
+ */
+ void registerCommands(java.util.List commands) {
+ commandHandlers.clear();
+ if (commands != null) {
+ for (CommandDefinition cmd : commands) {
+ if (cmd.getName() != null && cmd.getHandler() != null) {
+ commandHandlers.put(cmd.getName(), cmd.getHandler());
+ }
+ }
+ }
+ }
+
+ /**
+ * Registers an elicitation handler for this session.
+ *
+ * Called internally when creating or resuming a session with an elicitation
+ * handler.
+ *
+ * @param handler
+ * the handler to invoke when an elicitation request is received
+ */
+ void registerElicitationHandler(ElicitationHandler handler) {
+ elicitationHandler.set(handler);
+ }
+
+ /**
+ * Sets the capabilities reported by the host for this session.
+ *
+ * Called internally after session create/resume response.
+ *
+ * @param sessionCapabilities
+ * the capabilities to set, or {@code null} for empty capabilities
+ */
+ void setCapabilities(SessionCapabilities sessionCapabilities) {
+ this.capabilities = sessionCapabilities != null ? sessionCapabilities : new SessionCapabilities();
+ }
+
/**
* Handles a user input request from the Copilot CLI.
*
+ * Broadcast when the host's session capabilities change. The SDK updates
+ * {@link com.github.copilot.sdk.CopilotSession#getCapabilities()} accordingly.
+ *
+ * @since 1.0.0
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public final class CapabilitiesChangedEvent extends AbstractSessionEvent {
+
+ @JsonProperty("data")
+ private CapabilitiesChangedData data;
+
+ @Override
+ public String getType() {
+ return "capabilities.changed";
+ }
+
+ public CapabilitiesChangedData getData() {
+ return data;
+ }
+
+ public void setData(CapabilitiesChangedData data) {
+ this.data = data;
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record CapabilitiesChangedData(@JsonProperty("ui") CapabilitiesChangedUi ui) {
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record CapabilitiesChangedUi(@JsonProperty("elicitation") Boolean elicitation) {
+ }
+}
diff --git a/src/main/java/com/github/copilot/sdk/events/CommandExecuteEvent.java b/src/main/java/com/github/copilot/sdk/events/CommandExecuteEvent.java
new file mode 100644
index 000000000..c08c4a88d
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/events/CommandExecuteEvent.java
@@ -0,0 +1,43 @@
+/*---------------------------------------------------------------------------------------------
+ * 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: command.execute
+ *
+ * Broadcast when the user executes a slash command registered by this client.
+ * Clients that have a matching command handler should respond via
+ * {@code session.commands.handlePendingCommand}.
+ *
+ * @since 1.0.0
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public final class CommandExecuteEvent extends AbstractSessionEvent {
+
+ @JsonProperty("data")
+ private CommandExecuteData data;
+
+ @Override
+ public String getType() {
+ return "command.execute";
+ }
+
+ public CommandExecuteData getData() {
+ return data;
+ }
+
+ public void setData(CommandExecuteData data) {
+ this.data = data;
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record CommandExecuteData(@JsonProperty("requestId") String requestId,
+ @JsonProperty("command") String command, @JsonProperty("commandName") String commandName,
+ @JsonProperty("args") String args) {
+ }
+}
diff --git a/src/main/java/com/github/copilot/sdk/events/ElicitationRequestedEvent.java b/src/main/java/com/github/copilot/sdk/events/ElicitationRequestedEvent.java
new file mode 100644
index 000000000..e459dfb77
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/events/ElicitationRequestedEvent.java
@@ -0,0 +1,54 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.events;
+
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Event: elicitation.requested
+ *
+ * Broadcast when the server or an MCP tool requests structured input from the
+ * user. Clients that have an elicitation handler should respond via
+ * {@code session.ui.handlePendingElicitation}.
+ *
+ * @since 1.0.0
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public final class ElicitationRequestedEvent extends AbstractSessionEvent {
+
+ @JsonProperty("data")
+ private ElicitationRequestedData data;
+
+ @Override
+ public String getType() {
+ return "elicitation.requested";
+ }
+
+ public ElicitationRequestedData getData() {
+ return data;
+ }
+
+ public void setData(ElicitationRequestedData data) {
+ this.data = data;
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record ElicitationRequestedData(@JsonProperty("requestId") String requestId,
+ @JsonProperty("toolCallId") String toolCallId, @JsonProperty("elicitationSource") String elicitationSource,
+ @JsonProperty("message") String message, @JsonProperty("mode") String mode,
+ @JsonProperty("requestedSchema") ElicitationRequestedSchema requestedSchema,
+ @JsonProperty("url") String url) {
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record ElicitationRequestedSchema(@JsonProperty("type") String type,
+ @JsonProperty("properties") Map properties,
+ @JsonProperty("required") List required) {
+ }
+}
diff --git a/src/main/java/com/github/copilot/sdk/events/PermissionRequestedEvent.java b/src/main/java/com/github/copilot/sdk/events/PermissionRequestedEvent.java
index d8f9ec147..7ebce5ac7 100644
--- a/src/main/java/com/github/copilot/sdk/events/PermissionRequestedEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/PermissionRequestedEvent.java
@@ -38,6 +38,7 @@ public void setData(PermissionRequestedData data) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record PermissionRequestedData(@JsonProperty("requestId") String requestId,
- @JsonProperty("permissionRequest") PermissionRequest permissionRequest) {
+ @JsonProperty("permissionRequest") PermissionRequest permissionRequest,
+ @JsonProperty("resolvedByHook") Boolean resolvedByHook) {
}
}
diff --git a/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java b/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java
index 308317e6b..dda971769 100644
--- a/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java
+++ b/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java
@@ -99,6 +99,9 @@ public class SessionEventParser {
TYPE_MAP.put("permission.completed", PermissionCompletedEvent.class);
TYPE_MAP.put("command.queued", CommandQueuedEvent.class);
TYPE_MAP.put("command.completed", CommandCompletedEvent.class);
+ TYPE_MAP.put("command.execute", CommandExecuteEvent.class);
+ TYPE_MAP.put("elicitation.requested", ElicitationRequestedEvent.class);
+ TYPE_MAP.put("capabilities.changed", CapabilitiesChangedEvent.class);
TYPE_MAP.put("exit_plan_mode.requested", ExitPlanModeRequestedEvent.class);
TYPE_MAP.put("exit_plan_mode.completed", ExitPlanModeCompletedEvent.class);
TYPE_MAP.put("system.notification", SystemNotificationEvent.class);
diff --git a/src/main/java/com/github/copilot/sdk/json/CommandContext.java b/src/main/java/com/github/copilot/sdk/json/CommandContext.java
new file mode 100644
index 000000000..4657699bb
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/CommandContext.java
@@ -0,0 +1,74 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+/**
+ * Context passed to a {@link CommandHandler} when a slash command is executed.
+ *
+ * @since 1.0.0
+ */
+public class CommandContext {
+
+ private String sessionId;
+ private String command;
+ private String commandName;
+ private String args;
+
+ /** Gets the session ID where the command was invoked. @return the session ID */
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ /** Sets the session ID. @param sessionId the session ID @return this */
+ public CommandContext setSessionId(String sessionId) {
+ this.sessionId = sessionId;
+ return this;
+ }
+
+ /**
+ * Gets the full command text (e.g., {@code /deploy production}).
+ *
+ * @return the full command text
+ */
+ public String getCommand() {
+ return command;
+ }
+
+ /** Sets the full command text. @param command the command text @return this */
+ public CommandContext setCommand(String command) {
+ this.command = command;
+ return this;
+ }
+
+ /**
+ * Gets the command name without the leading {@code /}.
+ *
+ * @return the command name
+ */
+ public String getCommandName() {
+ return commandName;
+ }
+
+ /** Sets the command name. @param commandName the command name @return this */
+ public CommandContext setCommandName(String commandName) {
+ this.commandName = commandName;
+ return this;
+ }
+
+ /**
+ * Gets the raw argument string after the command name.
+ *
+ * @return the argument string
+ */
+ public String getArgs() {
+ return args;
+ }
+
+ /** Sets the argument string. @param args the argument string @return this */
+ public CommandContext setArgs(String args) {
+ this.args = args;
+ return this;
+ }
+}
diff --git a/src/main/java/com/github/copilot/sdk/json/CommandDefinition.java b/src/main/java/com/github/copilot/sdk/json/CommandDefinition.java
new file mode 100644
index 000000000..33a6cbada
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/CommandDefinition.java
@@ -0,0 +1,98 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+/**
+ * Defines a slash command that users can invoke from the CLI TUI.
+ *
+ * Register commands via {@link SessionConfig#setCommands(java.util.List)} or
+ * {@link ResumeSessionConfig#setCommands(java.util.List)}. Each command appears
+ * as {@code /name} in the CLI TUI.
+ *
+ *
Example Usage
+ *
+ *
{@code
+ * var config = new SessionConfig().setCommands(List.of(
+ * new CommandDefinition().setName("deploy").setDescription("Deploy the application").setHandler(context -> {
+ * System.out.println("Deploying: " + context.getArgs());
+ * return CompletableFuture.completedFuture(null);
+ * })));
+ * }
+ *
+ * @see CommandHandler
+ * @see CommandContext
+ * @since 1.0.0
+ */
+public class CommandDefinition {
+
+ private String name;
+ private String description;
+ private CommandHandler handler;
+
+ /**
+ * Gets the command name (without leading {@code /}).
+ *
+ * @return the command name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the command name (without leading {@code /}).
+ *
+ * For example, {@code "deploy"} registers the {@code /deploy} command.
+ *
+ * @param name
+ * the command name
+ * @return this instance for method chaining
+ */
+ public CommandDefinition setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * Gets the human-readable description shown in the command completion UI.
+ *
+ * @return the description, or {@code null} if not set
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Sets the human-readable description shown in the command completion UI.
+ *
+ * @param description
+ * the description
+ * @return this instance for method chaining
+ */
+ public CommandDefinition setDescription(String description) {
+ this.description = description;
+ return this;
+ }
+
+ /**
+ * Gets the handler invoked when the command is executed.
+ *
+ * @return the command handler
+ */
+ public CommandHandler getHandler() {
+ return handler;
+ }
+
+ /**
+ * Sets the handler invoked when the command is executed.
+ *
+ * @param handler
+ * the command handler
+ * @return this instance for method chaining
+ */
+ public CommandDefinition setHandler(CommandHandler handler) {
+ this.handler = handler;
+ return this;
+ }
+}
diff --git a/src/main/java/com/github/copilot/sdk/json/CommandHandler.java b/src/main/java/com/github/copilot/sdk/json/CommandHandler.java
new file mode 100644
index 000000000..d63955638
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/CommandHandler.java
@@ -0,0 +1,41 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Functional interface for handling slash-command executions.
+ *
+ * Implement this interface to define the behavior of a registered slash
+ * command. The handler is invoked when the user executes the command in the CLI
+ * TUI.
+ *
+ *
+ *
+ * @see CommandDefinition
+ * @since 1.0.0
+ */
+@FunctionalInterface
+public interface CommandHandler {
+
+ /**
+ * Handles a slash-command execution.
+ *
+ * @param context
+ * the command context containing session ID, command text, and
+ * arguments
+ * @return a future that completes when the command handling is done
+ */
+ CompletableFuture handle(CommandContext context);
+}
diff --git a/src/main/java/com/github/copilot/sdk/json/CommandWireDefinition.java b/src/main/java/com/github/copilot/sdk/json/CommandWireDefinition.java
new file mode 100644
index 000000000..2ee65c58e
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/CommandWireDefinition.java
@@ -0,0 +1,58 @@
+/*---------------------------------------------------------------------------------------------
+ * 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;
+
+/**
+ * Wire-format representation of a command definition for RPC serialization.
+ *
+ * This is a low-level class used internally. Use {@link CommandDefinition} to
+ * define commands for a session.
+ *
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class CommandWireDefinition {
+
+ @JsonProperty("name")
+ private String name;
+
+ @JsonProperty("description")
+ private String description;
+
+ /** Creates an empty definition. */
+ public CommandWireDefinition() {
+ }
+
+ /** Creates a definition with name and description. */
+ public CommandWireDefinition(String name, String description) {
+ this.name = name;
+ this.description = description;
+ }
+
+ /** Gets the command name. @return the name */
+ public String getName() {
+ return name;
+ }
+
+ /** Sets the command name. @param name the name @return this */
+ public CommandWireDefinition setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /** Gets the description. @return the description */
+ public String getDescription() {
+ return description;
+ }
+
+ /** Sets the description. @param description the description @return this */
+ public CommandWireDefinition setDescription(String description) {
+ this.description = description;
+ return this;
+ }
+}
diff --git a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java
index 4cdee912c..2e9a80456 100644
--- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java
+++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java
@@ -4,9 +4,13 @@
package com.github.copilot.sdk.json;
+import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
import java.util.function.Supplier;
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -34,132 +38,125 @@
@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;
@Deprecated
private boolean autoRestart;
+ private boolean autoStart = true;
+ private String[] cliArgs;
+ private String cliPath;
+ private String cliUrl;
+ private String cwd;
private Map environment;
+ private Executor executor;
private String gitHubToken;
- private Boolean useLoggedInUser;
+ private String logLevel = "info";
private Supplier>> onListModels;
+ private int port;
private TelemetryConfig telemetry;
+ private Boolean useLoggedInUser;
+ private boolean useStdio = true;
/**
- * 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.
+ * Returns whether the client should automatically restart the server on crash.
*
- * @return the extra arguments to pass to the CLI
+ * @return the auto-restart flag value (no longer has any effect)
+ * @deprecated This option has no effect and will be removed in a future
+ * release.
*/
- public String[] getCliArgs() {
- return cliArgs;
+ @Deprecated
+ public boolean isAutoRestart() {
+ return autoRestart;
}
/**
- * Sets extra arguments to pass to the CLI process.
- *
- * These arguments are prepended before SDK-managed flags.
+ * Sets whether the client should automatically restart the CLI server if it
+ * crashes unexpectedly.
*
- * @param cliArgs
- * the extra arguments to pass
+ * @param autoRestart
+ * ignored — this option no longer has any effect
* @return this options instance for method chaining
+ * @deprecated This option has no effect and will be removed in a future
+ * release.
*/
- public CopilotClientOptions setCliArgs(String[] cliArgs) {
- this.cliArgs = cliArgs;
+ @Deprecated
+ public CopilotClientOptions setAutoRestart(boolean autoRestart) {
+ this.autoRestart = autoRestart;
return this;
}
/**
- * Gets the working directory for the CLI process.
+ * Returns whether the client should automatically start the server.
*
- * @return the working directory path
+ * @return {@code true} to auto-start (default), {@code false} for manual start
*/
- public String getCwd() {
- return cwd;
+ public boolean isAutoStart() {
+ return autoStart;
}
/**
- * Sets the working directory for the CLI process.
+ * Sets whether the client should automatically start the CLI server when the
+ * first request is made.
*
- * @param cwd
- * the working directory path
+ * @param autoStart
+ * {@code true} to auto-start, {@code false} for manual start
* @return this options instance for method chaining
*/
- public CopilotClientOptions setCwd(String cwd) {
- this.cwd = cwd;
+ public CopilotClientOptions setAutoStart(boolean autoStart) {
+ this.autoStart = autoStart;
return this;
}
/**
- * Gets the TCP port for the CLI server.
+ * Gets the extra CLI arguments.
+ *
+ * Returns a shallow copy of the internal array, or {@code null} if no arguments
+ * have been set.
*
- * @return the port number, or 0 for a random port
+ * @return a copy of the extra arguments, or {@code null}
*/
- public int getPort() {
- return port;
+ public String[] getCliArgs() {
+ return cliArgs != null ? Arrays.copyOf(cliArgs, cliArgs.length) : null;
}
/**
- * Sets the TCP port for the CLI server to listen on.
+ * Sets extra arguments to pass to the CLI process.
*
- * This is only used when {@link #isUseStdio()} is {@code false}.
+ * These arguments are prepended before SDK-managed flags. A shallow copy of the
+ * provided array is stored. If {@code null} or empty, the existing arguments
+ * are cleared.
*
- * @param port
- * the port number, or 0 for a random port
+ * @param cliArgs
+ * the extra arguments to pass, or {@code null}/empty to clear
* @return this options instance for method chaining
*/
- public CopilotClientOptions setPort(int port) {
- this.port = port;
+ public CopilotClientOptions setCliArgs(String[] cliArgs) {
+ if (cliArgs == null || cliArgs.length == 0) {
+ if (this.cliArgs != null) {
+ this.cliArgs = new String[0];
+ }
+ } else {
+ this.cliArgs = Arrays.copyOf(cliArgs, cliArgs.length);
+ }
return this;
}
/**
- * Returns whether to use stdio transport instead of TCP.
+ * Gets the path to the Copilot CLI executable.
*
- * @return {@code true} to use stdio (default), {@code false} to use TCP
+ * @return the CLI path, or {@code null} to use "copilot" from PATH
*/
- public boolean isUseStdio() {
- return useStdio;
+ public String getCliPath() {
+ return cliPath;
}
/**
- * 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.
+ * Sets the path to the Copilot CLI executable.
*
- * @param useStdio
- * {@code true} to use stdio, {@code false} to use TCP
+ * @param cliPath
+ * the path to the CLI executable
* @return this options instance for method chaining
*/
- public CopilotClientOptions setUseStdio(boolean useStdio) {
- this.useStdio = useStdio;
+ public CopilotClientOptions setCliPath(String cliPath) {
+ this.cliPath = Objects.requireNonNull(cliPath, "cliPath must not be null");
return this;
}
@@ -182,107 +179,101 @@ public String getCliUrl() {
* {@link #setUseStdio(boolean)} and {@link #setCliPath(String)}.
*
* @param cliUrl
- * the CLI server URL to connect to
+ * the CLI server URL to connect to (must not be {@code null} or
+ * empty)
* @return this options instance for method chaining
+ * @throws IllegalArgumentException
+ * if {@code cliUrl} is {@code null} or empty
*/
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;
+ this.cliUrl = Objects.requireNonNull(cliUrl, "cliUrl must not be null");
return this;
}
/**
- * Returns whether the client should automatically start the server.
+ * Gets the working directory for the CLI process.
*
- * @return {@code true} to auto-start (default), {@code false} for manual start
+ * @return the working directory path
*/
- public boolean isAutoStart() {
- return autoStart;
+ public String getCwd() {
+ return cwd;
}
/**
- * Sets whether the client should automatically start the CLI server when the
- * first request is made.
+ * Sets the working directory for the CLI process.
*
- * @param autoStart
- * {@code true} to auto-start, {@code false} for manual start
+ * @param cwd
+ * the working directory path (must not be {@code null} or empty)
* @return this options instance for method chaining
+ * @throws IllegalArgumentException
+ * if {@code cwd} is {@code null} or empty
*/
- public CopilotClientOptions setAutoStart(boolean autoStart) {
- this.autoStart = autoStart;
+ public CopilotClientOptions setCwd(String cwd) {
+ this.cwd = Objects.requireNonNull(cwd, "cwd must not be null");
return this;
}
/**
- * Returns whether the client should automatically restart the server on crash.
+ * Gets the environment variables for the CLI process.
+ *
+ * Returns a shallow copy of the internal map, or {@code null} if no environment
+ * has been set.
*
- * @return the auto-restart flag value (no longer has any effect)
- * @deprecated This option has no effect and will be removed in a future
- * release.
+ * @return a copy of the environment variables map, or {@code null}
*/
- @Deprecated
- public boolean isAutoRestart() {
- return autoRestart;
+ public Map getEnvironment() {
+ return environment != null ? new HashMap<>(environment) : null;
}
/**
- * Sets whether the client should automatically restart the CLI server if it
- * crashes unexpectedly.
+ * Sets environment variables to pass to the CLI process.
+ *
+ * When set, these environment variables replace the inherited environment. A
+ * shallow copy of the provided map is stored. If {@code null} or empty, the
+ * existing environment is cleared.
*
- * @param autoRestart
- * ignored — this option no longer has any effect
+ * @param environment
+ * the environment variables map, or {@code null}/empty to clear
* @return this options instance for method chaining
- * @deprecated This option has no effect and will be removed in a future
- * release.
*/
- @Deprecated
- public CopilotClientOptions setAutoRestart(boolean autoRestart) {
- this.autoRestart = autoRestart;
+ public CopilotClientOptions setEnvironment(Map environment) {
+ if (environment == null || environment.isEmpty()) {
+ if (this.environment != null) {
+ this.environment.clear();
+ }
+ } else {
+ this.environment = new HashMap<>(environment);
+ }
return this;
}
/**
- * Gets the environment variables for the CLI process.
+ * Gets the executor used for internal asynchronous operations.
*
- * @return the environment variables map
+ * @return the executor, or {@code null} to use the default
+ * {@code ForkJoinPool.commonPool()}
*/
- public Map getEnvironment() {
- return environment;
+ public Executor getExecutor() {
+ return executor;
}
/**
- * Sets environment variables to pass to the CLI process.
+ * Sets the executor used for internal asynchronous operations.
+ *
+ * When provided, the SDK uses this executor for all internal
+ * {@code CompletableFuture} combinators instead of the default
+ * {@code ForkJoinPool.commonPool()}. This allows callers to isolate SDK work
+ * onto a dedicated thread pool or integrate with container-managed threading.
*
- * When set, these environment variables replace the inherited environment.
+ * Passing {@code null} reverts to the default {@code ForkJoinPool.commonPool()}
+ * behavior.
*
- * @param environment
- * the environment variables map
- * @return this options instance for method chaining
+ * @param executor
+ * the executor to use, or {@code null} for the default
+ * @return this options instance for fluent chaining
*/
- public CopilotClientOptions setEnvironment(Map environment) {
- this.environment = environment;
+ public CopilotClientOptions setExecutor(Executor executor) {
+ this.executor = executor;
return this;
}
@@ -302,11 +293,13 @@ public String getGitHubToken() {
* variable. This takes priority over other authentication methods.
*
* @param gitHubToken
- * the GitHub token
+ * the GitHub token (must not be {@code null} or empty)
* @return this options instance for method chaining
+ * @throws IllegalArgumentException
+ * if {@code gitHubToken} is {@code null} or empty
*/
public CopilotClientOptions setGitHubToken(String gitHubToken) {
- this.gitHubToken = gitHubToken;
+ this.gitHubToken = Objects.requireNonNull(gitHubToken, "gitHubToken must not be null");
return this;
}
@@ -331,33 +324,32 @@ public String getGithubToken() {
*/
@Deprecated
public CopilotClientOptions setGithubToken(String githubToken) {
- this.gitHubToken = githubToken;
+ this.gitHubToken = Objects.requireNonNull(githubToken, "githubToken must not be null");
return this;
}
/**
- * Returns whether to use the logged-in user for authentication.
+ * Gets the log level for the CLI process.
*
- * @return {@code true} to use logged-in user auth, {@code false} to use only
- * explicit tokens, or {@code null} to use default behavior
+ * @return the log level (default: "info")
*/
- public Boolean getUseLoggedInUser() {
- return useLoggedInUser;
+ public String getLogLevel() {
+ return logLevel;
}
/**
- * Sets whether to use the logged-in user for authentication.
+ * Sets the log level for the CLI process.
*
- * When true, the CLI server will attempt to use stored OAuth tokens or gh CLI
- * auth. When false, only explicit tokens (gitHubToken or environment variables)
- * are used. Default: true (but defaults to false when gitHubToken is provided).
+ * Valid levels include: "error", "warn", "info", "debug", "trace".
*
- * @param useLoggedInUser
- * {@code true} to use logged-in user auth, {@code false} otherwise
+ * @param logLevel
+ * the log level (must not be {@code null} or empty)
* @return this options instance for method chaining
+ * @throws IllegalArgumentException
+ * if {@code logLevel} is {@code null} or empty
*/
- public CopilotClientOptions setUseLoggedInUser(Boolean useLoggedInUser) {
- this.useLoggedInUser = useLoggedInUser;
+ public CopilotClientOptions setLogLevel(String logLevel) {
+ this.logLevel = Objects.requireNonNull(logLevel, "logLevel must not be null");
return this;
}
@@ -378,11 +370,37 @@ public Supplier>> getOnListModels() {
* available from your custom provider.
*
* @param onListModels
- * the handler that returns the list of available models
+ * the handler that returns the list of available models (must not be
+ * {@code null})
* @return this options instance for method chaining
+ * @throws IllegalArgumentException
+ * if {@code onListModels} is {@code null}
*/
public CopilotClientOptions setOnListModels(Supplier>> onListModels) {
- this.onListModels = onListModels;
+ this.onListModels = Objects.requireNonNull(onListModels, "onListModels must not be null");
+ 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;
}
@@ -399,8 +417,8 @@ public TelemetryConfig getTelemetry() {
/**
* Sets the OpenTelemetry configuration for the CLI server.
*
- * When set to a non-{@code null} value, the CLI server is started with
- * OpenTelemetry instrumentation enabled using the provided settings.
+ * When set, the CLI server is started with OpenTelemetry instrumentation
+ * enabled using the provided settings.
*
* @param telemetry
* the telemetry configuration
@@ -408,7 +426,60 @@ public TelemetryConfig getTelemetry() {
* @since 1.2.0
*/
public CopilotClientOptions setTelemetry(TelemetryConfig telemetry) {
- this.telemetry = telemetry;
+ this.telemetry = Objects.requireNonNull(telemetry, "telemetry must not be null");
+ return this;
+ }
+
+ /**
+ * Returns whether to use the logged-in user for authentication.
+ *
+ * @return {@code true} to use logged-in user auth, {@code false} to use only
+ * explicit tokens, or {@code null} to use default behavior
+ */
+ public Boolean getUseLoggedInUser() {
+ return useLoggedInUser;
+ }
+
+ /**
+ * Sets whether to use the logged-in user for authentication.
+ *
+ * When true, the CLI server will attempt to use stored OAuth tokens or gh CLI
+ * auth. When false, only explicit tokens (gitHubToken or environment variables)
+ * are used. Default: true (but defaults to false when gitHubToken is provided).
+ *
+ * Passing {@code null} is equivalent to passing {@link Boolean#FALSE}.
+ *
+ * @param useLoggedInUser
+ * {@code true} to use logged-in user auth, {@code false} or
+ * {@code null} otherwise
+ * @return this options instance for method chaining
+ */
+ public CopilotClientOptions setUseLoggedInUser(Boolean useLoggedInUser) {
+ this.useLoggedInUser = useLoggedInUser != null ? useLoggedInUser : Boolean.FALSE;
+ 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;
}
@@ -425,20 +496,21 @@ public CopilotClientOptions setTelemetry(TelemetryConfig telemetry) {
@Override
public CopilotClientOptions clone() {
CopilotClientOptions copy = new CopilotClientOptions();
- copy.cliPath = this.cliPath;
+ copy.autoRestart = this.autoRestart;
+ copy.autoStart = this.autoStart;
copy.cliArgs = this.cliArgs != null ? this.cliArgs.clone() : null;
- copy.cwd = this.cwd;
- copy.port = this.port;
- copy.useStdio = this.useStdio;
+ copy.cliPath = this.cliPath;
copy.cliUrl = this.cliUrl;
- copy.logLevel = this.logLevel;
- copy.autoStart = this.autoStart;
- copy.autoRestart = this.autoRestart;
+ copy.cwd = this.cwd;
copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null;
+ copy.executor = this.executor;
copy.gitHubToken = this.gitHubToken;
- copy.useLoggedInUser = this.useLoggedInUser;
+ copy.logLevel = this.logLevel;
copy.onListModels = this.onListModels;
+ copy.port = this.port;
copy.telemetry = this.telemetry;
+ copy.useLoggedInUser = this.useLoggedInUser;
+ copy.useStdio = this.useStdio;
return copy;
}
}
diff --git a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java
index c0243f14b..d030631de 100644
--- a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java
+++ b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java
@@ -91,6 +91,12 @@ public final class CreateSessionRequest {
@JsonProperty("configDir")
private String configDir;
+ @JsonProperty("commands")
+ private List commands;
+
+ @JsonProperty("requestElicitation")
+ private Boolean requestElicitation;
+
/** Gets the model name. @return the model */
public String getModel() {
return model;
@@ -312,4 +318,24 @@ public String getConfigDir() {
public void setConfigDir(String configDir) {
this.configDir = configDir;
}
+
+ /** Gets the commands wire definitions. @return the commands */
+ public List getCommands() {
+ return commands == null ? null : Collections.unmodifiableList(commands);
+ }
+
+ /** Sets the commands wire definitions. @param commands the commands */
+ public void setCommands(List commands) {
+ this.commands = commands;
+ }
+
+ /** Gets the requestElicitation flag. @return the flag */
+ public Boolean getRequestElicitation() {
+ return requestElicitation;
+ }
+
+ /** Sets the requestElicitation flag. @param requestElicitation the flag */
+ public void setRequestElicitation(Boolean requestElicitation) {
+ this.requestElicitation = requestElicitation;
+ }
}
diff --git a/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java b/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java
index 5b1a177f0..b47af050b 100644
--- a/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java
+++ b/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java
@@ -11,9 +11,12 @@
* @param workspacePath
* the workspace path, or {@code null} if infinite sessions are
* disabled
+ * @param capabilities
+ * the capabilities reported by the host, or {@code null}
* @since 1.0.0
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record CreateSessionResponse(@JsonProperty("sessionId") String sessionId,
- @JsonProperty("workspacePath") String workspacePath) {
+ @JsonProperty("workspacePath") String workspacePath,
+ @JsonProperty("capabilities") SessionCapabilities capabilities) {
}
diff --git a/src/main/java/com/github/copilot/sdk/json/ElicitationContext.java b/src/main/java/com/github/copilot/sdk/json/ElicitationContext.java
new file mode 100644
index 000000000..87687b194
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/ElicitationContext.java
@@ -0,0 +1,112 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+/**
+ * Context for an elicitation request received from the server or MCP tools.
+ *
+ * @since 1.0.0
+ */
+public class ElicitationContext {
+
+ private String sessionId;
+ private String message;
+ private ElicitationSchema requestedSchema;
+ private String mode;
+ private String elicitationSource;
+ private String url;
+
+ /**
+ * Gets the session ID that triggered the elicitation request. @return the
+ * session ID
+ */
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ /** Sets the session ID. @param sessionId the session ID @return this */
+ public ElicitationContext setSessionId(String sessionId) {
+ this.sessionId = sessionId;
+ return this;
+ }
+
+ /**
+ * Gets the message describing what information is needed from the user.
+ *
+ * @return the message
+ */
+ public String getMessage() {
+ return message;
+ }
+
+ /** Sets the message. @param message the message @return this */
+ public ElicitationContext setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+ /**
+ * Gets the JSON Schema describing the form fields to present (form mode only).
+ *
+ * @return the schema, or {@code null}
+ */
+ public ElicitationSchema getRequestedSchema() {
+ return requestedSchema;
+ }
+
+ /** Sets the schema. @param requestedSchema the schema @return this */
+ public ElicitationContext setRequestedSchema(ElicitationSchema requestedSchema) {
+ this.requestedSchema = requestedSchema;
+ return this;
+ }
+
+ /**
+ * Gets the elicitation mode: {@code "form"} for structured input, {@code "url"}
+ * for browser redirect.
+ *
+ * @return the mode, or {@code null} (defaults to {@code "form"})
+ */
+ public String getMode() {
+ return mode;
+ }
+
+ /** Sets the mode. @param mode the mode @return this */
+ public ElicitationContext setMode(String mode) {
+ this.mode = mode;
+ return this;
+ }
+
+ /**
+ * Gets the source that initiated the request (e.g., MCP server name).
+ *
+ * @return the elicitation source, or {@code null}
+ */
+ public String getElicitationSource() {
+ return elicitationSource;
+ }
+
+ /**
+ * Sets the elicitation source. @param elicitationSource the source @return this
+ */
+ public ElicitationContext setElicitationSource(String elicitationSource) {
+ this.elicitationSource = elicitationSource;
+ return this;
+ }
+
+ /**
+ * Gets the URL to open in the user's browser (url mode only).
+ *
+ * @return the URL, or {@code null}
+ */
+ public String getUrl() {
+ return url;
+ }
+
+ /** Sets the URL. @param url the URL @return this */
+ public ElicitationContext setUrl(String url) {
+ this.url = url;
+ return this;
+ }
+}
diff --git a/src/main/java/com/github/copilot/sdk/json/ElicitationHandler.java b/src/main/java/com/github/copilot/sdk/json/ElicitationHandler.java
new file mode 100644
index 000000000..d0a0d0616
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/ElicitationHandler.java
@@ -0,0 +1,44 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Functional interface for handling elicitation requests from the server.
+ *
+ * Register an elicitation handler via
+ * {@link SessionConfig#setOnElicitationRequest(ElicitationHandler)} or
+ * {@link ResumeSessionConfig#setOnElicitationRequest(ElicitationHandler)}. When
+ * provided, the server routes elicitation requests to this handler and reports
+ * elicitation as a supported capability.
+ *
+ *
Example Usage
+ *
+ *
{@code
+ * ElicitationHandler handler = context -> {
+ * // Show the form to the user and collect responses
+ * Map formValues = showForm(context.getMessage(), context.getRequestedSchema());
+ * return CompletableFuture.completedFuture(
+ * new ElicitationResult().setAction(ElicitationResultAction.ACCEPT).setContent(formValues));
+ * };
+ * }
+ *
+ * @see ElicitationContext
+ * @see ElicitationResult
+ * @since 1.0.0
+ */
+@FunctionalInterface
+public interface ElicitationHandler {
+
+ /**
+ * Handles an elicitation request from the server.
+ *
+ * @param context
+ * the elicitation context containing the message, schema, and mode
+ * @return a future that resolves with the elicitation result
+ */
+ CompletableFuture handle(ElicitationContext context);
+}
diff --git a/src/main/java/com/github/copilot/sdk/json/ElicitationParams.java b/src/main/java/com/github/copilot/sdk/json/ElicitationParams.java
new file mode 100644
index 000000000..8bd81022e
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/ElicitationParams.java
@@ -0,0 +1,58 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+/**
+ * Parameters for an elicitation request sent from the SDK to the host.
+ *
+ * @since 1.0.0
+ */
+public class ElicitationParams {
+
+ private String message;
+ private ElicitationSchema requestedSchema;
+
+ /**
+ * Gets the message describing what information is needed from the user.
+ *
+ * @return the message
+ */
+ public String getMessage() {
+ return message;
+ }
+
+ /**
+ * Sets the message describing what information is needed from the user.
+ *
+ * @param message
+ * the message
+ * @return this instance for method chaining
+ */
+ public ElicitationParams setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+ /**
+ * Gets the JSON Schema describing the form fields to present.
+ *
+ * @return the requested schema
+ */
+ public ElicitationSchema getRequestedSchema() {
+ return requestedSchema;
+ }
+
+ /**
+ * Sets the JSON Schema describing the form fields to present.
+ *
+ * @param requestedSchema
+ * the schema
+ * @return this instance for method chaining
+ */
+ public ElicitationParams setRequestedSchema(ElicitationSchema requestedSchema) {
+ this.requestedSchema = requestedSchema;
+ return this;
+ }
+}
diff --git a/src/main/java/com/github/copilot/sdk/json/ElicitationResult.java b/src/main/java/com/github/copilot/sdk/json/ElicitationResult.java
new file mode 100644
index 000000000..3ba30b83d
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/ElicitationResult.java
@@ -0,0 +1,68 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import java.util.Map;
+
+/**
+ * Result returned from an elicitation dialog.
+ *
+ * @since 1.0.0
+ */
+public class ElicitationResult {
+
+ private ElicitationResultAction action;
+ private Map content;
+
+ /**
+ * Gets the user action taken on the elicitation dialog.
+ *
+ * {@link ElicitationResultAction#ACCEPT} means the user submitted the form,
+ * {@link ElicitationResultAction#DECLINE} means the user rejected the request,
+ * and {@link ElicitationResultAction#CANCEL} means the user dismissed the
+ * dialog.
+ *
+ * @return the user action
+ */
+ public ElicitationResultAction getAction() {
+ return action;
+ }
+
+ /**
+ * Sets the user action taken on the elicitation dialog.
+ *
+ * @param action
+ * the user action
+ * @return this instance for method chaining
+ */
+ public ElicitationResult setAction(ElicitationResultAction action) {
+ this.action = action;
+ return this;
+ }
+
+ /**
+ * Gets the form values submitted by the user.
+ *
+ * Only present when {@link #getAction()} is
+ * {@link ElicitationResultAction#ACCEPT}.
+ *
+ * @return the submitted form values, or {@code null} if the user did not accept
+ */
+ public Map getContent() {
+ return content;
+ }
+
+ /**
+ * Sets the form values submitted by the user.
+ *
+ * @param content
+ * the submitted form values
+ * @return this instance for method chaining
+ */
+ public ElicitationResult setContent(Map content) {
+ this.content = content;
+ return this;
+ }
+}
diff --git a/src/main/java/com/github/copilot/sdk/json/ElicitationResultAction.java b/src/main/java/com/github/copilot/sdk/json/ElicitationResultAction.java
new file mode 100644
index 000000000..fd280cdeb
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/ElicitationResultAction.java
@@ -0,0 +1,33 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+/**
+ * Action value for an {@link ElicitationResult}.
+ *
+ * @since 1.0.0
+ */
+public enum ElicitationResultAction {
+
+ /** The user submitted the form (accepted). */
+ ACCEPT("accept"),
+
+ /** The user explicitly rejected the request. */
+ DECLINE("decline"),
+
+ /** The user dismissed the dialog without responding. */
+ CANCEL("cancel");
+
+ private final String value;
+
+ ElicitationResultAction(String value) {
+ this.value = value;
+ }
+
+ /** Returns the wire-format string value. @return the string value */
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/src/main/java/com/github/copilot/sdk/json/ElicitationSchema.java b/src/main/java/com/github/copilot/sdk/json/ElicitationSchema.java
new file mode 100644
index 000000000..c3d548775
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/ElicitationSchema.java
@@ -0,0 +1,92 @@
+/*---------------------------------------------------------------------------------------------
+ * 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;
+
+/**
+ * JSON Schema describing the form fields to present for an elicitation dialog.
+ *
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ElicitationSchema {
+
+ @JsonProperty("type")
+ private String type = "object";
+
+ @JsonProperty("properties")
+ private Map properties;
+
+ @JsonProperty("required")
+ private List required;
+
+ /**
+ * Gets the schema type indicator (always {@code "object"}).
+ *
+ * @return the type
+ */
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * Sets the schema type indicator.
+ *
+ * @param type
+ * the type (typically {@code "object"})
+ * @return this instance for method chaining
+ */
+ public ElicitationSchema setType(String type) {
+ this.type = type;
+ return this;
+ }
+
+ /**
+ * Gets the form field definitions, keyed by field name.
+ *
+ * @return the properties map
+ */
+ public Map getProperties() {
+ return properties;
+ }
+
+ /**
+ * Sets the form field definitions, keyed by field name.
+ *
+ * @param properties
+ * the properties map
+ * @return this instance for method chaining
+ */
+ public ElicitationSchema setProperties(Map properties) {
+ this.properties = properties;
+ return this;
+ }
+
+ /**
+ * Gets the list of required field names.
+ *
+ * @return the required field names, or {@code null}
+ */
+ public List getRequired() {
+ return required;
+ }
+
+ /**
+ * Sets the list of required field names.
+ *
+ * @param required
+ * the required field names
+ * @return this instance for method chaining
+ */
+ public ElicitationSchema setRequired(List required) {
+ this.required = required;
+ return this;
+ }
+}
diff --git a/src/main/java/com/github/copilot/sdk/json/GetSessionMetadataResponse.java b/src/main/java/com/github/copilot/sdk/json/GetSessionMetadataResponse.java
new file mode 100644
index 000000000..eeceb4177
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/GetSessionMetadataResponse.java
@@ -0,0 +1,19 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 getting session metadata by ID.
+ *
+ * @param session
+ * the session metadata, or {@code null} if not found
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record GetSessionMetadataResponse(@JsonProperty("session") SessionMetadata session) {
+}
diff --git a/src/main/java/com/github/copilot/sdk/json/InputOptions.java b/src/main/java/com/github/copilot/sdk/json/InputOptions.java
new file mode 100644
index 000000000..9b0b6c8dd
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/InputOptions.java
@@ -0,0 +1,108 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+/**
+ * Options for the {@link SessionUiApi#input(String, InputOptions)} convenience
+ * method.
+ *
+ * @since 1.0.0
+ */
+public class InputOptions {
+
+ private String title;
+ private String description;
+ private Integer minLength;
+ private Integer maxLength;
+ private String format;
+ private String defaultValue;
+
+ /** Gets the title label for the input field. @return the title */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Sets the title label for the input field. @param title the title @return this
+ */
+ public InputOptions setTitle(String title) {
+ this.title = title;
+ return this;
+ }
+
+ /** Gets the descriptive text shown below the field. @return the description */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Sets the descriptive text shown below the field. @param description the
+ * description @return this
+ */
+ public InputOptions setDescription(String description) {
+ this.description = description;
+ return this;
+ }
+
+ /** Gets the minimum character length. @return the min length */
+ public Integer getMinLength() {
+ return minLength;
+ }
+
+ /**
+ * Sets the minimum character length. @param minLength the min length @return
+ * this
+ */
+ public InputOptions setMinLength(Integer minLength) {
+ this.minLength = minLength;
+ return this;
+ }
+
+ /** Gets the maximum character length. @return the max length */
+ public Integer getMaxLength() {
+ return maxLength;
+ }
+
+ /**
+ * Sets the maximum character length. @param maxLength the max length @return
+ * this
+ */
+ public InputOptions setMaxLength(Integer maxLength) {
+ this.maxLength = maxLength;
+ return this;
+ }
+
+ /**
+ * Gets the semantic format hint (e.g., {@code "email"}, {@code "uri"},
+ * {@code "date"}, {@code "date-time"}).
+ *
+ * @return the format hint
+ */
+ public String getFormat() {
+ return format;
+ }
+
+ /** Sets the semantic format hint. @param format the format @return this */
+ public InputOptions setFormat(String format) {
+ this.format = format;
+ return this;
+ }
+
+ /**
+ * Gets the default value pre-populated in the field. @return the default value
+ */
+ public String getDefaultValue() {
+ return defaultValue;
+ }
+
+ /**
+ * Sets the default value pre-populated in the field. @param defaultValue the
+ * default value @return this
+ */
+ public InputOptions setDefaultValue(String defaultValue) {
+ this.defaultValue = defaultValue;
+ return this;
+ }
+}
diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java
index eab3c789c..139f5238b 100644
--- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java
+++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java
@@ -58,6 +58,8 @@ public class ResumeSessionConfig {
private List disabledSkills;
private InfiniteSessionConfig infiniteSessions;
private Consumer onEvent;
+ private List commands;
+ private ElicitationHandler onElicitationRequest;
/**
* Gets the AI model to use.
@@ -555,6 +557,56 @@ public ResumeSessionConfig setOnEvent(Consumer onEvent) {
return this;
}
+ /**
+ * Gets the slash commands registered for this session.
+ *
+ * @return the list of command definitions, or {@code null}
+ */
+ public List getCommands() {
+ return commands == null ? null : Collections.unmodifiableList(commands);
+ }
+
+ /**
+ * Sets slash commands registered for this session.
+ *
+ * When the CLI has a TUI, each command appears as {@code /name} for the user to
+ * invoke. The handler is called when the user executes the command.
+ *
+ * @param commands
+ * the list of command definitions
+ * @return this config for method chaining
+ * @see CommandDefinition
+ */
+ public ResumeSessionConfig setCommands(List commands) {
+ this.commands = commands;
+ return this;
+ }
+
+ /**
+ * Gets the elicitation request handler.
+ *
+ * @return the elicitation handler, or {@code null}
+ */
+ public ElicitationHandler getOnElicitationRequest() {
+ return onElicitationRequest;
+ }
+
+ /**
+ * Sets a handler for elicitation requests from the server or MCP tools.
+ *
+ * When provided, the server will route elicitation requests to this handler and
+ * report elicitation as a supported capability.
+ *
+ * @param onElicitationRequest
+ * the elicitation handler
+ * @return this config for method chaining
+ * @see ElicitationHandler
+ */
+ public ResumeSessionConfig setOnElicitationRequest(ElicitationHandler onElicitationRequest) {
+ this.onElicitationRequest = onElicitationRequest;
+ return this;
+ }
+
/**
* Creates a shallow clone of this {@code ResumeSessionConfig} instance.
*
@@ -591,6 +643,8 @@ public ResumeSessionConfig clone() {
copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null;
copy.infiniteSessions = this.infiniteSessions;
copy.onEvent = this.onEvent;
+ copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null;
+ copy.onElicitationRequest = this.onElicitationRequest;
return copy;
}
}
diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java
index 31d88399a..7be9a6281 100644
--- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java
+++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java
@@ -95,6 +95,12 @@ public final class ResumeSessionRequest {
@JsonProperty("infiniteSessions")
private InfiniteSessionConfig infiniteSessions;
+ @JsonProperty("commands")
+ private List commands;
+
+ @JsonProperty("requestElicitation")
+ private Boolean requestElicitation;
+
/** Gets the session ID. @return the session ID */
public String getSessionId() {
return sessionId;
@@ -332,4 +338,24 @@ public InfiniteSessionConfig getInfiniteSessions() {
public void setInfiniteSessions(InfiniteSessionConfig infiniteSessions) {
this.infiniteSessions = infiniteSessions;
}
+
+ /** Gets the commands wire definitions. @return the commands */
+ public List getCommands() {
+ return commands == null ? null : Collections.unmodifiableList(commands);
+ }
+
+ /** Sets the commands wire definitions. @param commands the commands */
+ public void setCommands(List commands) {
+ this.commands = commands;
+ }
+
+ /** Gets the requestElicitation flag. @return the flag */
+ public Boolean getRequestElicitation() {
+ return requestElicitation;
+ }
+
+ /** Sets the requestElicitation flag. @param requestElicitation the flag */
+ public void setRequestElicitation(Boolean requestElicitation) {
+ this.requestElicitation = requestElicitation;
+ }
}
diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java
index 654c1486c..8349c5d30 100644
--- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java
+++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java
@@ -11,9 +11,12 @@
* @param workspacePath
* the workspace path, or {@code null} if infinite sessions are
* disabled
+ * @param capabilities
+ * the capabilities reported by the host, or {@code null}
* @since 1.0.0
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ResumeSessionResponse(@JsonProperty("sessionId") String sessionId,
- @JsonProperty("workspacePath") String workspacePath) {
+ @JsonProperty("workspacePath") String workspacePath,
+ @JsonProperty("capabilities") SessionCapabilities capabilities) {
}
diff --git a/src/main/java/com/github/copilot/sdk/json/SessionCapabilities.java b/src/main/java/com/github/copilot/sdk/json/SessionCapabilities.java
new file mode 100644
index 000000000..4eb4fc025
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/SessionCapabilities.java
@@ -0,0 +1,39 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+/**
+ * Represents the capabilities reported by the host for a session.
+ *
+ * Capabilities are populated from the session create/resume response and
+ * updated in real time via {@code capabilities.changed} events.
+ *
+ * @since 1.0.0
+ */
+public class SessionCapabilities {
+
+ private SessionUiCapabilities ui;
+
+ /**
+ * Gets the UI-related capabilities.
+ *
+ * @return the UI capabilities, or {@code null} if not reported
+ */
+ public SessionUiCapabilities getUi() {
+ return ui;
+ }
+
+ /**
+ * Sets the UI-related capabilities.
+ *
+ * @param ui
+ * the UI capabilities
+ * @return this instance for method chaining
+ */
+ public SessionCapabilities setUi(SessionUiCapabilities ui) {
+ this.ui = ui;
+ return this;
+ }
+}
diff --git a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java
index 76c15660d..5dcd39788 100644
--- a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java
+++ b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java
@@ -58,6 +58,8 @@ public class SessionConfig {
private List disabledSkills;
private String configDir;
private Consumer onEvent;
+ private List commands;
+ private ElicitationHandler onElicitationRequest;
/**
* Gets the custom session ID.
@@ -595,6 +597,56 @@ public SessionConfig setOnEvent(Consumer onEvent) {
return this;
}
+ /**
+ * Gets the slash commands registered for this session.
+ *
+ * @return the list of command definitions, or {@code null}
+ */
+ public List getCommands() {
+ return commands == null ? null : Collections.unmodifiableList(commands);
+ }
+
+ /**
+ * Sets slash commands registered for this session.
+ *
+ * When the CLI has a TUI, each command appears as {@code /name} for the user to
+ * invoke. The handler is called when the user executes the command.
+ *
+ * @param commands
+ * the list of command definitions
+ * @return this config instance for method chaining
+ * @see CommandDefinition
+ */
+ public SessionConfig setCommands(List commands) {
+ this.commands = commands;
+ return this;
+ }
+
+ /**
+ * Gets the elicitation request handler.
+ *
+ * @return the elicitation handler, or {@code null}
+ */
+ public ElicitationHandler getOnElicitationRequest() {
+ return onElicitationRequest;
+ }
+
+ /**
+ * Sets a handler for elicitation requests from the server or MCP tools.
+ *
+ * When provided, the server will route elicitation requests to this handler and
+ * report elicitation as a supported capability.
+ *
+ * @param onElicitationRequest
+ * the elicitation handler
+ * @return this config instance for method chaining
+ * @see ElicitationHandler
+ */
+ public SessionConfig setOnElicitationRequest(ElicitationHandler onElicitationRequest) {
+ this.onElicitationRequest = onElicitationRequest;
+ return this;
+ }
+
/**
* Creates a shallow clone of this {@code SessionConfig} instance.
*
@@ -631,6 +683,8 @@ public SessionConfig clone() {
copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null;
copy.configDir = this.configDir;
copy.onEvent = this.onEvent;
+ copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null;
+ copy.onElicitationRequest = this.onElicitationRequest;
return copy;
}
}
diff --git a/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java b/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java
new file mode 100644
index 000000000..f0a43f261
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java
@@ -0,0 +1,86 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Provides UI methods for eliciting information from the user during a session.
+ *
+ * All methods on this interface throw {@link IllegalStateException} if the host
+ * does not report elicitation support via
+ * {@link com.github.copilot.sdk.CopilotSession#getCapabilities()}. Check
+ * {@code session.getCapabilities().getUi() != null &&
+ * Boolean.TRUE.equals(session.getCapabilities().getUi().getElicitation())}
+ * before calling.
+ *
+ *
Example Usage
+ *
+ *
{@code
+ * var caps = session.getCapabilities();
+ * if (caps.getUi() != null && Boolean.TRUE.equals(caps.getUi().getElicitation())) {
+ * boolean confirmed = session.getUi().confirm("Are you sure?").get();
+ * }
+ * }
+ *
+ * @see com.github.copilot.sdk.CopilotSession#getUi()
+ * @since 1.0.0
+ */
+public interface SessionUiApi {
+
+ /**
+ * Shows a generic elicitation dialog with a custom schema.
+ *
+ * @param params
+ * the elicitation parameters including message and schema
+ * @return a future that resolves with the {@link ElicitationResult}
+ * @throws IllegalStateException
+ * if the host does not support elicitation
+ */
+ CompletableFuture elicitation(ElicitationParams params);
+
+ /**
+ * Shows a confirmation dialog and returns the user's boolean answer.
+ *
+ * Returns {@code false} if the user declines or cancels.
+ *
+ * @param message
+ * the message to display
+ * @return a future that resolves to {@code true} if the user confirmed
+ * @throws IllegalStateException
+ * if the host does not support elicitation
+ */
+ CompletableFuture confirm(String message);
+
+ /**
+ * Shows a selection dialog with the given options.
+ *
+ * Returns the selected value, or {@code null} if the user declines/cancels.
+ *
+ * @param message
+ * the message to display
+ * @param options
+ * the options to present
+ * @return a future that resolves to the selected string, or {@code null}
+ * @throws IllegalStateException
+ * if the host does not support elicitation
+ */
+ CompletableFuture select(String message, String[] options);
+
+ /**
+ * Shows a text input dialog.
+ *
+ * Returns the entered text, or {@code null} if the user declines/cancels.
+ *
+ * @param message
+ * the message to display
+ * @param options
+ * optional input field options, or {@code null}
+ * @return a future that resolves to the entered string, or {@code null}
+ * @throws IllegalStateException
+ * if the host does not support elicitation
+ */
+ CompletableFuture input(String message, InputOptions options);
+}
diff --git a/src/main/java/com/github/copilot/sdk/json/SessionUiCapabilities.java b/src/main/java/com/github/copilot/sdk/json/SessionUiCapabilities.java
new file mode 100644
index 000000000..9b8e0b587
--- /dev/null
+++ b/src/main/java/com/github/copilot/sdk/json/SessionUiCapabilities.java
@@ -0,0 +1,37 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk.json;
+
+/**
+ * UI-specific capability flags for a session.
+ *
+ * @since 1.0.0
+ */
+public class SessionUiCapabilities {
+
+ private Boolean elicitation;
+
+ /**
+ * Returns whether the host supports interactive elicitation dialogs.
+ *
+ * @return {@code true} if elicitation is supported, {@code false} or
+ * {@code null} otherwise
+ */
+ public Boolean getElicitation() {
+ return elicitation;
+ }
+
+ /**
+ * Sets whether the host supports interactive elicitation dialogs.
+ *
+ * @param elicitation
+ * {@code true} if elicitation is supported
+ * @return this instance for method chaining
+ */
+ public SessionUiCapabilities setElicitation(Boolean elicitation) {
+ this.elicitation = elicitation;
+ return this;
+ }
+}
diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md
index bc9302840..5ae5c8f94 100644
--- a/src/site/markdown/advanced.md
+++ b/src/site/markdown/advanced.md
@@ -47,6 +47,13 @@ This guide covers advanced scenarios for extending and customizing your Copilot
- [Custom Event Error Handler](#Custom_Event_Error_Handler)
- [Event Error Policy](#Event_Error_Policy)
- [OpenTelemetry](#OpenTelemetry)
+- [Slash Commands](#Slash_Commands)
+ - [Registering Commands](#Registering_Commands)
+- [Elicitation (UI Dialogs)](#Elicitation_UI_Dialogs)
+ - [Incoming Elicitation Handler](#Incoming_Elicitation_Handler)
+ - [Session Capabilities](#Session_Capabilities)
+ - [Outgoing Elicitation via session.getUi()](#Outgoing_Elicitation_via_session.getUi)
+- [Getting Session Metadata by ID](#Getting_Session_Metadata_by_ID)
---
@@ -1093,6 +1100,143 @@ See [TelemetryConfig](apidocs/com/github/copilot/sdk/json/TelemetryConfig.html)
---
+## Slash Commands
+
+Register custom slash commands that users can invoke from the CLI TUI with `/commandname`.
+
+### Registering Commands
+
+```java
+var config = new SessionConfig()
+ .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
+ .setCommands(List.of(
+ new CommandDefinition()
+ .setName("deploy")
+ .setDescription("Deploy the current branch")
+ .setHandler(context -> {
+ System.out.println("Deploying with args: " + context.getArgs());
+ // perform deployment ...
+ return CompletableFuture.completedFuture(null);
+ }),
+ new CommandDefinition()
+ .setName("rollback")
+ .setDescription("Roll back the last deployment")
+ .setHandler(context -> {
+ // perform rollback ...
+ return CompletableFuture.completedFuture(null);
+ })
+ ));
+
+try (CopilotClient client = new CopilotClient()) {
+ client.start().get();
+ var session = client.createSession(config).get();
+ // Users can now type /deploy or /rollback in the TUI
+}
+```
+
+Each `CommandDefinition` requires a `name` (without the leading `/`), an optional `description` shown in the TUI's command completion UI, and a `CommandHandler` that is invoked when the user executes the command.
+
+The `CommandContext` passed to the handler provides:
+- `getSessionId()` — the ID of the session where the command was invoked
+- `getCommand()` — the full command text (e.g., `/deploy production`)
+- `getCommandName()` — command name without the leading `/` (e.g., `deploy`)
+- `getArgs()` — the argument string after the command name (e.g., `production`)
+
+---
+
+## Elicitation (UI Dialogs)
+
+Elicitation allows your application to present structured UI dialogs to the user. There are two directions:
+
+1. **Incoming** — The server or an MCP tool requests input from the user via your `onElicitationRequest` handler.
+2. **Outgoing** — Your session-side code proactively requests input via `session.getUi()`.
+
+### Incoming Elicitation Handler
+
+Register a handler to receive elicitation requests from the server:
+
+```java
+var config = new SessionConfig()
+ .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
+ .setOnElicitationRequest(context -> {
+ System.out.println("Elicitation request: " + context.getMessage());
+ // Show the form to the user ...
+ var content = Map.of("confirmed", true);
+ return CompletableFuture.completedFuture(
+ new ElicitationResult()
+ .setAction(ElicitationResultAction.ACCEPT)
+ .setContent(content)
+ );
+ });
+```
+
+When `onElicitationRequest` is set, the SDK reports elicitation as a supported capability and the server will route elicitation requests to your handler.
+
+### Session Capabilities
+
+After `createSession` or `resumeSession`, check `session.getCapabilities()` to see what the host supports:
+
+```java
+var session = client.createSession(config).get();
+
+var caps = session.getCapabilities();
+if (caps.getUi() != null && Boolean.TRUE.equals(caps.getUi().getElicitation())) {
+ System.out.println("Elicitation is supported");
+}
+```
+
+Capabilities are updated in real time when a `capabilities.changed` event is received.
+
+### Outgoing Elicitation via `session.getUi()`
+
+If the host reports elicitation support, you can call the convenience methods on `session.getUi()`:
+
+```java
+var ui = session.getUi();
+
+// Boolean confirmation
+boolean confirmed = ui.confirm("Are you sure you want to proceed?").get();
+
+// Selection from options
+String choice = ui.select("Choose an environment", new String[]{"dev", "staging", "prod"}).get();
+
+// Text input
+String value = ui.input("Enter your name", null).get();
+
+// Custom schema
+var result = ui.elicitation(new ElicitationParams()
+ .setMessage("Enter deployment details")
+ .setRequestedSchema(new ElicitationSchema()
+ .setProperties(Map.of(
+ "branch", Map.of("type", "string"),
+ "environment", Map.of("type", "string", "enum", List.of("dev", "staging", "prod"))
+ ))
+ .setRequired(List.of("branch", "environment"))
+ )).get();
+```
+
+All `getUi()` methods throw `IllegalStateException` if the host does not support elicitation. Always check capabilities first.
+
+---
+
+## Getting Session Metadata by ID
+
+Retrieve metadata for a specific session without listing all sessions:
+
+```java
+SessionMetadata metadata = client.getSessionMetadata("session-123").get();
+if (metadata != null) {
+ System.out.println("Session: " + metadata.getSessionId());
+ System.out.println("Started: " + metadata.getStartTime());
+} else {
+ System.out.println("Session not found");
+}
+```
+
+This is more efficient than `listSessions()` when you already know the session ID, as it performs a direct O(1) lookup instead of scanning all sessions.
+
+---
+
## Next Steps
- 📖 **[Documentation](documentation.html)** - Core concepts, events, streaming, models, tool filtering, reasoning effort
diff --git a/src/site/markdown/cookbook/error-handling.md b/src/site/markdown/cookbook/error-handling.md
index d085ecd91..4240dc1ff 100644
--- a/src/site/markdown/cookbook/error-handling.md
+++ b/src/site/markdown/cookbook/error-handling.md
@@ -30,7 +30,7 @@ jbang BasicErrorHandling.java
**Code:**
```java
-//DEPS com.github:copilot-sdk-java:0.2.1-java.0
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import com.github.copilot.sdk.CopilotClient;
import com.github.copilot.sdk.events.AssistantMessageEvent;
import com.github.copilot.sdk.json.MessageOptions;
@@ -64,7 +64,7 @@ public class BasicErrorHandling {
## Handling specific error types
```java
-//DEPS com.github:copilot-sdk-java:0.2.1-java.0
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import com.github.copilot.sdk.CopilotClient;
import java.util.concurrent.ExecutionException;
@@ -99,7 +99,7 @@ public class SpecificErrorHandling {
## Timeout handling
```java
-//DEPS com.github:copilot-sdk-java:0.2.1-java.0
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import com.github.copilot.sdk.CopilotSession;
import com.github.copilot.sdk.events.AssistantMessageEvent;
import com.github.copilot.sdk.json.MessageOptions;
@@ -130,7 +130,7 @@ public class TimeoutHandling {
## Aborting a request
```java
-//DEPS com.github:copilot-sdk-java:0.2.1-java.0
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import com.github.copilot.sdk.CopilotSession;
import com.github.copilot.sdk.json.MessageOptions;
import java.util.concurrent.Executors;
@@ -162,7 +162,7 @@ public class AbortRequest {
## Graceful shutdown
```java
-//DEPS com.github:copilot-sdk-java:0.2.1-java.0
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import com.github.copilot.sdk.CopilotClient;
public class GracefulShutdown {
@@ -192,7 +192,7 @@ public class GracefulShutdown {
## Try-with-resources pattern
```java
-//DEPS com.github:copilot-sdk-java:0.2.1-java.0
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import com.github.copilot.sdk.CopilotClient;
import com.github.copilot.sdk.events.AssistantMessageEvent;
import com.github.copilot.sdk.json.MessageOptions;
@@ -224,7 +224,7 @@ public class TryWithResources {
## Handling tool errors
```java
-//DEPS com.github:copilot-sdk-java:0.2.1-java.0
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import com.github.copilot.sdk.CopilotClient;
import com.github.copilot.sdk.events.AssistantMessageEvent;
import com.github.copilot.sdk.json.MessageOptions;
diff --git a/src/site/markdown/cookbook/managing-local-files.md b/src/site/markdown/cookbook/managing-local-files.md
index 4c0622928..9535772b2 100644
--- a/src/site/markdown/cookbook/managing-local-files.md
+++ b/src/site/markdown/cookbook/managing-local-files.md
@@ -34,7 +34,7 @@ jbang ManagingLocalFiles.java
**Code:**
```java
-//DEPS com.github:copilot-sdk-java:0.2.1-java.0
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import com.github.copilot.sdk.CopilotClient;
import com.github.copilot.sdk.events.AssistantMessageEvent;
import com.github.copilot.sdk.events.SessionIdleEvent;
@@ -161,7 +161,7 @@ session.send(new MessageOptions().setPrompt(prompt));
## Interactive file organization
```java
-//DEPS com.github:copilot-sdk-java:0.2.1-java.0
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import java.io.BufferedReader;
import java.io.InputStreamReader;
diff --git a/src/site/markdown/cookbook/multiple-sessions.md b/src/site/markdown/cookbook/multiple-sessions.md
index 94a83acae..776b6db6d 100644
--- a/src/site/markdown/cookbook/multiple-sessions.md
+++ b/src/site/markdown/cookbook/multiple-sessions.md
@@ -30,7 +30,7 @@ jbang MultipleSessions.java
**Code:**
```java
-//DEPS com.github:copilot-sdk-java:0.2.1-java.0
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import com.github.copilot.sdk.CopilotClient;
import com.github.copilot.sdk.events.AssistantMessageEvent;
import com.github.copilot.sdk.json.MessageOptions;
@@ -123,7 +123,7 @@ try {
## Managing session lifecycle with CompletableFuture
```java
-//DEPS com.github:copilot-sdk-java:0.2.1-java.0
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import java.util.concurrent.CompletableFuture;
import java.util.List;
@@ -164,6 +164,69 @@ public class ParallelSessions {
}
```
+## Providing a custom Executor for parallel sessions
+
+By default, `CompletableFuture` operations run on `ForkJoinPool.commonPool()`,
+which has limited parallelism (typically `Runtime.availableProcessors() - 1`
+threads). When multiple sessions block waiting for CLI responses, those threads
+are unavailable for other work—a condition known as *pool starvation*.
+
+Use `CopilotClientOptions.setExecutor(Executor)` to supply a dedicated thread
+pool so that SDK work does not compete with the rest of your application for
+common-pool threads:
+
+```java
+//DEPS com.github:copilot-sdk-java:${project.version}
+import com.github.copilot.sdk.CopilotClient;
+import com.github.copilot.sdk.json.CopilotClientOptions;
+import com.github.copilot.sdk.json.SessionConfig;
+import com.github.copilot.sdk.json.MessageOptions;
+import com.github.copilot.sdk.json.PermissionHandler;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class ParallelSessionsWithExecutor {
+ public static void main(String[] args) throws Exception {
+ ExecutorService pool = Executors.newFixedThreadPool(4);
+ try {
+ var options = new CopilotClientOptions().setExecutor(pool);
+ try (CopilotClient client = new CopilotClient(options)) {
+ client.start().get();
+
+ var s1 = client.createSession(new SessionConfig()
+ .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
+ .setModel("gpt-5")).get();
+ var s2 = client.createSession(new SessionConfig()
+ .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
+ .setModel("gpt-5")).get();
+ var s3 = client.createSession(new SessionConfig()
+ .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
+ .setModel("claude-sonnet-4.5")).get();
+
+ CompletableFuture.allOf(
+ s1.sendAndWait(new MessageOptions().setPrompt("Question 1")),
+ s2.sendAndWait(new MessageOptions().setPrompt("Question 2")),
+ s3.sendAndWait(new MessageOptions().setPrompt("Question 3"))
+ ).get();
+
+ s1.close();
+ s2.close();
+ s3.close();
+ }
+ } finally {
+ pool.shutdown();
+ }
+ }
+}
+```
+
+Passing `null` (or omitting `setExecutor` entirely) keeps the default
+`ForkJoinPool.commonPool()` behaviour. The executor is used for all internal
+`CompletableFuture.runAsync` / `supplyAsync` calls—including client start/stop,
+tool-call dispatch, permission dispatch, user-input dispatch, and hooks.
+
## Use cases
- **Multi-user applications**: One session per user
diff --git a/src/site/markdown/cookbook/persisting-sessions.md b/src/site/markdown/cookbook/persisting-sessions.md
index 213959ce6..e653b8a6a 100644
--- a/src/site/markdown/cookbook/persisting-sessions.md
+++ b/src/site/markdown/cookbook/persisting-sessions.md
@@ -30,7 +30,7 @@ jbang PersistingSessions.java
**Code:**
```java
-//DEPS com.github:copilot-sdk-java:0.2.1-java.0
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import com.github.copilot.sdk.CopilotClient;
import com.github.copilot.sdk.events.AssistantMessageEvent;
import com.github.copilot.sdk.json.MessageOptions;
@@ -127,7 +127,7 @@ public class DeleteSession {
## Getting session history
```java
-//DEPS com.github:copilot-sdk-java:0.2.1-java.0
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import com.github.copilot.sdk.CopilotClient;
import com.github.copilot.sdk.events.AssistantMessageEvent;
import com.github.copilot.sdk.events.UserMessageEvent;
@@ -162,7 +162,7 @@ public class SessionHistory {
## Complete example with session management
```java
-//DEPS com.github:copilot-sdk-java:0.2.1-java.0
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import java.util.Scanner;
public class SessionManager {
diff --git a/src/site/markdown/cookbook/pr-visualization.md b/src/site/markdown/cookbook/pr-visualization.md
index 77b6631b8..ad2939842 100644
--- a/src/site/markdown/cookbook/pr-visualization.md
+++ b/src/site/markdown/cookbook/pr-visualization.md
@@ -34,7 +34,7 @@ jbang PRVisualization.java github/copilot-sdk
## Full example: PRVisualization.java
```java
-//DEPS com.github:copilot-sdk-java:0.2.1-java.0
+//DEPS com.github:copilot-sdk-java:0.2.2-java.1
import com.github.copilot.sdk.CopilotClient;
import com.github.copilot.sdk.events.AssistantMessageEvent;
import com.github.copilot.sdk.events.ToolExecutionStartEvent;
diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md
index 8a9f919ac..a96f66698 100644
--- a/src/site/markdown/documentation.md
+++ b/src/site/markdown/documentation.md
@@ -245,6 +245,19 @@ The SDK supports event types organized by category. All events extend `AbstractS
|-------|-------------|-------------|
| `CommandQueuedEvent` | `command.queued` | A command was queued for execution |
| `CommandCompletedEvent` | `command.completed` | A queued command completed |
+| `CommandExecuteEvent` | `command.execute` | A registered slash command was dispatched for execution |
+
+### Elicitation Events
+
+| Event | Type String | Description |
+|-------|-------------|-------------|
+| `ElicitationRequestedEvent` | `elicitation.requested` | An elicitation (UI dialog) request was received |
+
+### Capability Events
+
+| Event | Type String | Description |
+|-------|-------------|-------------|
+| `CapabilitiesChangedEvent` | `capabilities.changed` | Session capabilities were updated |
### Plan Mode Events
@@ -633,6 +646,8 @@ When resuming a session, you can optionally reconfigure many settings. This is u
| `skillDirectories` | Directories to load skills from |
| `disabledSkills` | Skills to disable |
| `infiniteSessions` | Configure infinite session behavior |
+| `commands` | Slash command definitions for the resumed session |
+| `onElicitationRequest` | Handler for incoming elicitation requests |
| `disableResume` | When `true`, resumes without emitting a `session.resume` event |
| `onEvent` | Event handler registered before session resumption |
@@ -691,6 +706,8 @@ Complete list of all `SessionConfig` options for `createSession()`:
| `skillDirectories` | List<String> | Directories to load skills from | [Skills](advanced.html#Skills_Configuration) |
| `disabledSkills` | List<String> | Skills to disable by name | [Skills](advanced.html#Skills_Configuration) |
| `configDir` | String | Custom configuration directory | [Config Dir](advanced.html#Custom_Configuration_Directory) |
+| `commands` | List<CommandDefinition> | Slash command definitions | [Slash Commands](advanced.html#Slash_Commands) |
+| `onElicitationRequest` | ElicitationHandler | Handler for incoming elicitation requests | [Elicitation](advanced.html#Elicitation_UI_Dialogs) |
| `onEvent` | Consumer<AbstractSessionEvent> | Event handler registered before session creation | [Early Event Registration](advanced.html#Early_Event_Registration) |
### Cloning SessionConfig
diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md
index 2f93c4ce9..60b96ce9d 100644
--- a/src/site/markdown/index.md
+++ b/src/site/markdown/index.md
@@ -1,6 +1,6 @@
# GitHub Copilot SDK for Java
-> ⚠️ **Disclaimer:** This is the official Java SDK for GitHub Copilot. This repository treats the official .NET and nodejs SDKs for GitHub Copilot as reference implementations. These SDKS are all officially supported as GitHub open source projects. The Java implementation follows the backward compatibility guarantees offered by the reference implementations. As such this implementation may introduce breaking changes, according to the policy declared by the reference implementations. Use at your own risk.
+> ℹ️ **Public Preview:** This is the official Java SDK for GitHub Copilot. This repository treats the official .NET and Node.js SDKs for GitHub Copilot as reference implementations. These SDKs are all officially supported as GitHub open source projects. The Java implementation follows the backward compatibility guarantees offered by the reference implementations. While in public preview, minor breaking changes may still occur between releases.
Welcome to the documentation for the **GitHub Copilot SDK for Java** — a Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build AI-powered applications and agentic workflows.
@@ -9,7 +9,7 @@ Welcome to the documentation for the **GitHub Copilot SDK for Java** — a Java
### Requirements
- Java 17 or later
-- GitHub Copilot CLI 0.0.411-1 or later installed and in PATH (or provide custom `cliPath`)
+- GitHub Copilot CLI 1.0.17 or later installed and in PATH (or provide custom `cliPath`)
### Installation
diff --git a/src/site/site.xml b/src/site/site.xml
index f89ebe076..d012c0335 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -59,6 +59,9 @@
+
+
+
diff --git a/src/test/java/com/github/copilot/sdk/AgentInfoTest.java b/src/test/java/com/github/copilot/sdk/AgentInfoTest.java
new file mode 100644
index 000000000..0893773e7
--- /dev/null
+++ b/src/test/java/com/github/copilot/sdk/AgentInfoTest.java
@@ -0,0 +1,64 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+import com.github.copilot.sdk.json.AgentInfo;
+
+/**
+ * Unit tests for {@link AgentInfo} getters, setters, and fluent chaining.
+ */
+class AgentInfoTest {
+
+ @Test
+ void defaultValuesAreNull() {
+ var agent = new AgentInfo();
+ assertNull(agent.getName());
+ assertNull(agent.getDisplayName());
+ assertNull(agent.getDescription());
+ }
+
+ @Test
+ void nameGetterSetter() {
+ var agent = new AgentInfo();
+ agent.setName("coder");
+ assertEquals("coder", agent.getName());
+ }
+
+ @Test
+ void displayNameGetterSetter() {
+ var agent = new AgentInfo();
+ agent.setDisplayName("Code Assistant");
+ assertEquals("Code Assistant", agent.getDisplayName());
+ }
+
+ @Test
+ void descriptionGetterSetter() {
+ var agent = new AgentInfo();
+ agent.setDescription("Helps with coding tasks");
+ assertEquals("Helps with coding tasks", agent.getDescription());
+ }
+
+ @Test
+ void fluentChainingReturnsThis() {
+ var agent = new AgentInfo().setName("coder").setDisplayName("Code Assistant")
+ .setDescription("Helps with coding tasks");
+
+ assertEquals("coder", agent.getName());
+ assertEquals("Code Assistant", agent.getDisplayName());
+ assertEquals("Helps with coding tasks", agent.getDescription());
+ }
+
+ @Test
+ void fluentChainingReturnsSameInstance() {
+ var agent = new AgentInfo();
+ assertSame(agent, agent.setName("test"));
+ assertSame(agent, agent.setDisplayName("Test"));
+ assertSame(agent, agent.setDescription("A test agent"));
+ }
+}
diff --git a/src/test/java/com/github/copilot/sdk/CapiProxy.java b/src/test/java/com/github/copilot/sdk/CapiProxy.java
index 1a7df2d6c..bcd064d94 100644
--- a/src/test/java/com/github/copilot/sdk/CapiProxy.java
+++ b/src/test/java/com/github/copilot/sdk/CapiProxy.java
@@ -89,7 +89,11 @@ public String start() throws IOException, InterruptedException {
}
// Start the harness server using npx tsx
- var pb = new ProcessBuilder("npx", "tsx", "server.ts");
+ // On Windows, npx is installed as npx.cmd which requires cmd /c to launch
+ boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
+ var pb = isWindows
+ ? new ProcessBuilder("cmd", "/c", "npx", "tsx", "server.ts")
+ : new ProcessBuilder("npx", "tsx", "server.ts");
pb.directory(harnessDir.toFile());
pb.redirectErrorStream(false);
diff --git a/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java b/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java
index 86d6be875..e556839cc 100644
--- a/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java
+++ b/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java
@@ -13,6 +13,7 @@
import org.junit.jupiter.api.Test;
import com.github.copilot.sdk.json.CopilotClientOptions;
+import com.github.copilot.sdk.json.TelemetryConfig;
/**
* Unit tests for {@link CliServerManager} covering parseCliUrl,
@@ -69,13 +70,18 @@ void connectToServerTcpMode() throws Exception {
}
}
+ private static Process startBlockingProcess() throws IOException {
+ boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows");
+ return (isWindows ? new ProcessBuilder("cmd", "/c", "more") : new ProcessBuilder("cat")).start();
+ }
+
@Test
void connectToServerStdioMode() throws Exception {
var options = new CopilotClientOptions();
var manager = new CliServerManager(options);
// Create a dummy process for stdio mode
- Process process = new ProcessBuilder("cat").start();
+ Process process = startBlockingProcess();
try {
JsonRpcClient client = manager.connectToServer(process, null, null);
assertNotNull(client);
@@ -125,6 +131,14 @@ void processInfoWithNullPort() {
// resolveCliCommand is private, so we test indirectly through startCliServer
// with specific cliPath values.
+ // On Windows, "/nonexistent/copilot" is not an absolute path (no drive letter),
+ // so resolveCliCommand wraps it with "cmd /c" and ProcessBuilder.start()
+ // succeeds
+ // (launching cmd.exe). Use a Windows-absolute path to ensure IOException.
+ private static final String NONEXISTENT_CLI = System.getProperty("os.name").toLowerCase().contains("win")
+ ? "C:\\nonexistent\\copilot"
+ : "/nonexistent/copilot";
+
@Test
void startCliServerWithJsFile() throws Exception {
// Using a .js file path causes resolveCliCommand to prepend "node"
@@ -146,8 +160,8 @@ void startCliServerWithJsFile() throws Exception {
@Test
void startCliServerWithCliArgs() throws Exception {
// Test that cliArgs are included in the command
- var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot")
- .setCliArgs(new String[]{"--extra-flag"}).setUseStdio(true);
+ var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setCliArgs(new String[]{"--extra-flag"})
+ .setUseStdio(true);
var manager = new CliServerManager(options);
var ex = assertThrows(IOException.class, () -> manager.startCliServer());
@@ -157,7 +171,7 @@ void startCliServerWithCliArgs() throws Exception {
@Test
void startCliServerWithExplicitPort() throws Exception {
// Test the explicit port branch (useStdio=false, port > 0)
- var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setUseStdio(false).setPort(9999);
+ var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setUseStdio(false).setPort(9999);
var manager = new CliServerManager(options);
var ex = assertThrows(IOException.class, () -> manager.startCliServer());
@@ -167,7 +181,7 @@ void startCliServerWithExplicitPort() throws Exception {
@Test
void startCliServerWithGitHubToken() throws Exception {
// Test the github token branch
- var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setGitHubToken("ghp_test123")
+ var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setGitHubToken("ghp_test123")
.setUseStdio(true);
var manager = new CliServerManager(options);
@@ -178,7 +192,7 @@ void startCliServerWithGitHubToken() throws Exception {
@Test
void startCliServerWithUseLoggedInUserExplicit() throws Exception {
// Test the explicit useLoggedInUser=false branch (adds --no-auto-login)
- var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setUseLoggedInUser(false)
+ var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setUseLoggedInUser(false)
.setUseStdio(true);
var manager = new CliServerManager(options);
@@ -189,7 +203,7 @@ void startCliServerWithUseLoggedInUserExplicit() throws Exception {
@Test
void startCliServerWithGitHubTokenAndNoExplicitUseLoggedInUser() throws Exception {
// When gitHubToken is set and useLoggedInUser is null, defaults to false
- var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setGitHubToken("ghp_test123")
+ var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setGitHubToken("ghp_test123")
.setUseStdio(true);
var manager = new CliServerManager(options);
@@ -199,8 +213,8 @@ void startCliServerWithGitHubTokenAndNoExplicitUseLoggedInUser() throws Exceptio
@Test
void startCliServerWithNullCliPath() throws Exception {
- // Test the null cliPath branch (defaults to "copilot")
- var options = new CopilotClientOptions().setCliPath(null).setUseStdio(true);
+ // Test the default cliPath branch (defaults to "copilot" when not set)
+ var options = new CopilotClientOptions().setUseStdio(true);
var manager = new CliServerManager(options);
// "copilot" likely doesn't exist in the test env — that's fine
@@ -212,4 +226,28 @@ void startCliServerWithNullCliPath() throws Exception {
assertNotNull(e);
}
}
+
+ @Test
+ void startCliServerWithTelemetryAllOptions() throws Exception {
+ // The telemetry env vars are applied before ProcessBuilder.start()
+ // so even with a nonexistent CLI path, the telemetry code path is exercised
+ var telemetry = new TelemetryConfig().setOtlpEndpoint("http://localhost:4318").setFilePath("/tmp/telemetry.log")
+ .setExporterType("otlp-http").setSourceName("test-app").setCaptureContent(true);
+ var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setTelemetry(telemetry).setUseStdio(true);
+ var manager = new CliServerManager(options);
+
+ var ex = assertThrows(IOException.class, () -> manager.startCliServer());
+ assertNotNull(ex);
+ }
+
+ @Test
+ void startCliServerWithTelemetryCaptureContentFalse() throws Exception {
+ // Test the false branch of getCaptureContent()
+ var telemetry = new TelemetryConfig().setCaptureContent(false);
+ var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setTelemetry(telemetry).setUseStdio(true);
+ var manager = new CliServerManager(options);
+
+ var ex = assertThrows(IOException.class, () -> manager.startCliServer());
+ assertNotNull(ex);
+ }
}
diff --git a/src/test/java/com/github/copilot/sdk/CommandsTest.java b/src/test/java/com/github/copilot/sdk/CommandsTest.java
new file mode 100644
index 000000000..baf26b39b
--- /dev/null
+++ b/src/test/java/com/github/copilot/sdk/CommandsTest.java
@@ -0,0 +1,156 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import org.junit.jupiter.api.Test;
+
+import com.github.copilot.sdk.json.CommandContext;
+import com.github.copilot.sdk.json.CommandDefinition;
+import com.github.copilot.sdk.json.CommandHandler;
+import com.github.copilot.sdk.json.CommandWireDefinition;
+import com.github.copilot.sdk.json.PermissionHandler;
+import com.github.copilot.sdk.json.ResumeSessionConfig;
+import com.github.copilot.sdk.json.SessionConfig;
+
+/**
+ * Unit tests for the Commands feature (CommandDefinition, CommandContext,
+ * SessionConfig.commands, ResumeSessionConfig.commands, and the wire
+ * representation).
+ *
+ *
+ * Ported from {@code CommandsTests.cs} in the upstream dotnet SDK.
+ *
+ */
+class CommandsTest {
+
+ @Test
+ void commandDefinitionHasRequiredProperties() {
+ CommandHandler handler = context -> CompletableFuture.completedFuture(null);
+ var cmd = new CommandDefinition().setName("deploy").setDescription("Deploy the app").setHandler(handler);
+
+ assertEquals("deploy", cmd.getName());
+ assertEquals("Deploy the app", cmd.getDescription());
+ assertNotNull(cmd.getHandler());
+ }
+
+ @Test
+ void commandContextHasAllProperties() {
+ var ctx = new CommandContext().setSessionId("session-1").setCommand("/deploy production")
+ .setCommandName("deploy").setArgs("production");
+
+ assertEquals("session-1", ctx.getSessionId());
+ assertEquals("/deploy production", ctx.getCommand());
+ assertEquals("deploy", ctx.getCommandName());
+ assertEquals("production", ctx.getArgs());
+ }
+
+ @Test
+ void sessionConfigCommandsAreCloned() {
+ CommandHandler handler = ctx -> CompletableFuture.completedFuture(null);
+ var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
+ .setCommands(List.of(new CommandDefinition().setName("deploy").setHandler(handler)));
+
+ var clone = config.clone();
+
+ assertNotNull(clone.getCommands());
+ assertEquals(1, clone.getCommands().size());
+ assertEquals("deploy", clone.getCommands().get(0).getName());
+
+ // Collections should be independent — clone list is a copy
+ assertNotSame(config.getCommands(), clone.getCommands());
+ }
+
+ @Test
+ void resumeConfigCommandsAreCloned() {
+ CommandHandler handler = ctx -> CompletableFuture.completedFuture(null);
+ var config = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
+ .setCommands(List.of(new CommandDefinition().setName("deploy").setHandler(handler)));
+
+ var clone = config.clone();
+
+ assertNotNull(clone.getCommands());
+ assertEquals(1, clone.getCommands().size());
+ assertEquals("deploy", clone.getCommands().get(0).getName());
+ }
+
+ @Test
+ void buildCreateRequestIncludesCommandWireDefinitions() {
+ CommandHandler handler = ctx -> CompletableFuture.completedFuture(null);
+ var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setCommands(
+ List.of(new CommandDefinition().setName("deploy").setDescription("Deploy").setHandler(handler),
+ new CommandDefinition().setName("rollback").setHandler(handler)));
+
+ var request = SessionRequestBuilder.buildCreateRequest(config);
+
+ assertNotNull(request.getCommands());
+ assertEquals(2, request.getCommands().size());
+ assertEquals("deploy", request.getCommands().get(0).getName());
+ assertEquals("Deploy", request.getCommands().get(0).getDescription());
+ assertEquals("rollback", request.getCommands().get(1).getName());
+ assertNull(request.getCommands().get(1).getDescription());
+ }
+
+ @Test
+ void buildResumeRequestIncludesCommandWireDefinitions() {
+ CommandHandler handler = ctx -> CompletableFuture.completedFuture(null);
+ var config = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setCommands(
+ List.of(new CommandDefinition().setName("deploy").setDescription("Deploy").setHandler(handler)));
+
+ var request = SessionRequestBuilder.buildResumeRequest("session-1", config);
+
+ assertNotNull(request.getCommands());
+ assertEquals(1, request.getCommands().size());
+ assertEquals("deploy", request.getCommands().get(0).getName());
+ assertEquals("Deploy", request.getCommands().get(0).getDescription());
+ }
+
+ @Test
+ void buildCreateRequestWithNoCommandsHasNullCommandsList() {
+ var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL);
+
+ var request = SessionRequestBuilder.buildCreateRequest(config);
+
+ assertNull(request.getCommands());
+ }
+
+ @Test
+ void commandWireDefinitionHasNameAndDescription() {
+ var wire = new CommandWireDefinition("deploy", "Deploy the app");
+
+ assertEquals("deploy", wire.getName());
+ assertEquals("Deploy the app", wire.getDescription());
+ }
+
+ @Test
+ void commandWireDefinitionNullDescriptionAllowed() {
+ var wire = new CommandWireDefinition("rollback", null);
+
+ assertEquals("rollback", wire.getName());
+ assertNull(wire.getDescription());
+ }
+
+ @Test
+ void commandWireDefinitionFluentSetters() {
+ var wire = new CommandWireDefinition();
+ wire.setName("status");
+ wire.setDescription("Show deployment status");
+
+ assertEquals("status", wire.getName());
+ assertEquals("Show deployment status", wire.getDescription());
+ }
+
+ @Test
+ void commandWireDefinitionFluentSettersChaining() {
+ var wire = new CommandWireDefinition().setName("logs").setDescription("View application logs");
+
+ assertEquals("logs", wire.getName());
+ assertEquals("View application logs", wire.getDescription());
+ }
+}
diff --git a/src/test/java/com/github/copilot/sdk/CompactionTest.java b/src/test/java/com/github/copilot/sdk/CompactionTest.java
index 49640ac00..ae8f8b1ea 100644
--- a/src/test/java/com/github/copilot/sdk/CompactionTest.java
+++ b/src/test/java/com/github/copilot/sdk/CompactionTest.java
@@ -56,7 +56,7 @@ static void teardown() throws Exception {
* compaction/should_trigger_compaction_with_low_threshold_and_emit_events
*/
@Test
- @Timeout(value = 120, unit = TimeUnit.SECONDS)
+ @Timeout(value = 300, unit = TimeUnit.SECONDS)
void testShouldTriggerCompactionWithLowThresholdAndEmitEvents() throws Exception {
ctx.configureForTest("compaction", "should_trigger_compaction_with_low_threshold_and_emit_events");
@@ -96,8 +96,8 @@ void testShouldTriggerCompactionWithLowThresholdAndEmitEvents() throws Exception
// Wait for compaction to complete - it may arrive slightly after sendAndWait
// returns due to async event delivery from the CLI
- assertTrue(compactionCompleteLatch.await(10, TimeUnit.SECONDS),
- "Should have received a compaction complete event within 10 seconds");
+ assertTrue(compactionCompleteLatch.await(30, TimeUnit.SECONDS),
+ "Should have received a compaction complete event within 30 seconds");
long compactionStartCount = events.stream().filter(e -> e instanceof SessionCompactionStartEvent).count();
long compactionCompleteCount = events.stream().filter(e -> e instanceof SessionCompactionCompleteEvent)
.count();
diff --git a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java
index f3eceb4c2..f3787705f 100644
--- a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java
+++ b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java
@@ -17,10 +17,13 @@
import com.github.copilot.sdk.events.AbstractSessionEvent;
import com.github.copilot.sdk.json.CopilotClientOptions;
+import com.github.copilot.sdk.json.InfiniteSessionConfig;
import com.github.copilot.sdk.json.MessageOptions;
import com.github.copilot.sdk.json.ModelInfo;
import com.github.copilot.sdk.json.ResumeSessionConfig;
import com.github.copilot.sdk.json.SessionConfig;
+import com.github.copilot.sdk.json.SystemMessageConfig;
+import com.github.copilot.sdk.json.TelemetryConfig;
class ConfigCloneTest {
@@ -49,10 +52,16 @@ void copilotClientOptionsArrayIndependence() {
original.setCliArgs(args);
CopilotClientOptions cloned = original.clone();
- cloned.getCliArgs()[0] = "--changed";
+ // Mutate the source array after set — should not affect original or clone
+ args[0] = "--changed";
+
+ assertEquals("--flag1", original.getCliArgs()[0]);
+ assertEquals("--flag1", cloned.getCliArgs()[0]);
+
+ // getCliArgs() returns a copy, so mutating it should not affect internals
+ original.getCliArgs()[0] = "--mutated";
assertEquals("--flag1", original.getCliArgs()[0]);
- assertEquals("--changed", cloned.getCliArgs()[0]);
}
@Test
@@ -64,12 +73,15 @@ void copilotClientOptionsEnvironmentIndependence() {
CopilotClientOptions cloned = original.clone();
- // Mutate the original environment map to test independence
+ // Mutate the source map after set — should not affect original or clone
env.put("KEY2", "value2");
- // The cloned config should be unaffected by mutations to the original map
+ assertEquals(1, original.getEnvironment().size());
assertEquals(1, cloned.getEnvironment().size());
- assertEquals(2, original.getEnvironment().size());
+
+ // getEnvironment() returns a copy, so mutating it should not affect internals
+ original.getEnvironment().put("KEY3", "value3");
+ assertEquals(1, original.getEnvironment().size());
}
@Test
@@ -184,4 +196,97 @@ void clonePreservesNullFields() {
MessageOptions msgClone = msg.clone();
assertNull(msgClone.getMode());
}
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void copilotClientOptionsDeprecatedAutoRestart() {
+ CopilotClientOptions opts = new CopilotClientOptions();
+ assertFalse(opts.isAutoRestart());
+ opts.setAutoRestart(true);
+ assertTrue(opts.isAutoRestart());
+ }
+
+ @Test
+ void copilotClientOptionsSetCliArgsNullClearsExisting() {
+ CopilotClientOptions opts = new CopilotClientOptions();
+ opts.setCliArgs(new String[]{"--flag1"});
+ assertNotNull(opts.getCliArgs());
+
+ // Setting null should clear the existing array
+ opts.setCliArgs(null);
+ assertNotNull(opts.getCliArgs());
+ assertEquals(0, opts.getCliArgs().length);
+ }
+
+ @Test
+ void copilotClientOptionsSetEnvironmentNullClearsExisting() {
+ CopilotClientOptions opts = new CopilotClientOptions();
+ opts.setEnvironment(Map.of("KEY", "VALUE"));
+ assertNotNull(opts.getEnvironment());
+
+ // Setting null should clear the existing map (clears in-place → returns empty
+ // map)
+ opts.setEnvironment(null);
+ var env = opts.getEnvironment();
+ assertTrue(env == null || env.isEmpty());
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void copilotClientOptionsDeprecatedGithubToken() {
+ CopilotClientOptions opts = new CopilotClientOptions();
+ opts.setGithubToken("ghp_deprecated_token");
+ assertEquals("ghp_deprecated_token", opts.getGithubToken());
+ assertEquals("ghp_deprecated_token", opts.getGitHubToken());
+ }
+
+ @Test
+ void copilotClientOptionsSetTelemetry() {
+ var telemetry = new TelemetryConfig().setOtlpEndpoint("http://localhost:4318");
+ var opts = new CopilotClientOptions();
+ opts.setTelemetry(telemetry);
+ assertSame(telemetry, opts.getTelemetry());
+ }
+
+ @Test
+ void copilotClientOptionsSetUseLoggedInUserNull() {
+ var opts = new CopilotClientOptions();
+ opts.setUseLoggedInUser(null);
+ // null → Boolean.FALSE
+ assertEquals(Boolean.FALSE, opts.getUseLoggedInUser());
+ }
+
+ @Test
+ void resumeSessionConfigAllSetters() {
+ var config = new ResumeSessionConfig();
+
+ var sysMsg = new SystemMessageConfig();
+ config.setSystemMessage(sysMsg);
+ assertSame(sysMsg, config.getSystemMessage());
+
+ config.setAvailableTools(List.of("bash", "read_file"));
+ assertEquals(List.of("bash", "read_file"), config.getAvailableTools());
+
+ config.setExcludedTools(List.of("write_file"));
+ assertEquals(List.of("write_file"), config.getExcludedTools());
+
+ config.setReasoningEffort("high");
+ assertEquals("high", config.getReasoningEffort());
+
+ config.setWorkingDirectory("/project/src");
+ assertEquals("/project/src", config.getWorkingDirectory());
+
+ config.setConfigDir("/home/user/.config/copilot");
+ assertEquals("/home/user/.config/copilot", config.getConfigDir());
+
+ config.setSkillDirectories(List.of("/skills/custom"));
+ assertEquals(List.of("/skills/custom"), config.getSkillDirectories());
+
+ config.setDisabledSkills(List.of("some-skill"));
+ assertEquals(List.of("some-skill"), config.getDisabledSkills());
+
+ var infiniteConfig = new InfiniteSessionConfig().setEnabled(true);
+ config.setInfiniteSessions(infiniteConfig);
+ assertSame(infiniteConfig, config.getInfiniteSessions());
+ }
}
diff --git a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java
index 787312cef..39406d260 100644
--- a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java
+++ b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java
@@ -7,6 +7,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -821,4 +822,31 @@ void testSessionListFilterFluentAPI() throws Exception {
session.close();
}
}
+
+ /**
+ * Verifies that getSessionMetadata returns metadata for a known session ID.
+ *
+ * @see Snapshot: session/should_get_session_metadata_by_id
+ */
+ @Test
+ void testShouldGetSessionMetadataById() throws Exception {
+ ctx.configureForTest("session", "should_get_session_metadata_by_id");
+
+ try (CopilotClient client = ctx.createClient()) {
+ var session = client
+ .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
+
+ session.sendAndWait(new MessageOptions().setPrompt("Say hello")).get(60, TimeUnit.SECONDS);
+
+ var metadata = client.getSessionMetadata(session.getSessionId()).get(30, TimeUnit.SECONDS);
+ assertNotNull(metadata, "Metadata should not be null for known session");
+ assertEquals(session.getSessionId(), metadata.getSessionId(), "Metadata session ID should match");
+
+ // A non-existent session should return null
+ var notFound = client.getSessionMetadata("non-existent-session-id").get(30, TimeUnit.SECONDS);
+ assertNull(notFound, "Non-existent session should return null");
+
+ session.close();
+ }
+ }
}
diff --git a/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java b/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java
new file mode 100644
index 000000000..203f5faed
--- /dev/null
+++ b/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java
@@ -0,0 +1,172 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.copilot.sdk.json.GetForegroundSessionResponse;
+import com.github.copilot.sdk.json.PermissionRequest;
+import com.github.copilot.sdk.json.PermissionRequestResult;
+import com.github.copilot.sdk.json.PostToolUseHookInput;
+import com.github.copilot.sdk.json.PostToolUseHookOutput;
+import com.github.copilot.sdk.json.PreToolUseHookInput;
+import com.github.copilot.sdk.json.PreToolUseHookOutput;
+import com.github.copilot.sdk.json.SectionOverride;
+import com.github.copilot.sdk.json.SetForegroundSessionResponse;
+import com.github.copilot.sdk.json.ToolBinaryResult;
+import com.github.copilot.sdk.json.ToolResultObject;
+
+/**
+ * Unit tests for various data transfer objects and record types that were
+ * missing coverage, including hook output factory methods, record constructors,
+ * and getters for hook inputs.
+ */
+class DataObjectCoverageTest {
+
+ // ===== PreToolUseHookOutput factory methods =====
+
+ @Test
+ void preToolUseHookOutputDenyWithReason() {
+ var output = PreToolUseHookOutput.deny("Security policy violation");
+ assertEquals("deny", output.permissionDecision());
+ assertEquals("Security policy violation", output.permissionDecisionReason());
+ assertNull(output.modifiedArgs());
+ }
+
+ @Test
+ void preToolUseHookOutputAsk() {
+ var output = PreToolUseHookOutput.ask();
+ assertEquals("ask", output.permissionDecision());
+ assertNull(output.permissionDecisionReason());
+ }
+
+ @Test
+ void preToolUseHookOutputWithModifiedArgs() {
+ ObjectNode args = JsonNodeFactory.instance.objectNode();
+ args.put("path", "/safe/path");
+
+ var output = PreToolUseHookOutput.withModifiedArgs("allow", args);
+ assertEquals("allow", output.permissionDecision());
+ assertEquals(args, output.modifiedArgs());
+ }
+
+ // ===== PostToolUseHookOutput record =====
+
+ @Test
+ void postToolUseHookOutputRecord() {
+ var output = new PostToolUseHookOutput(null, "Extra context", false);
+ assertNull(output.modifiedResult());
+ assertEquals("Extra context", output.additionalContext());
+ assertFalse(output.suppressOutput());
+ }
+
+ // ===== ToolBinaryResult record =====
+
+ @Test
+ void toolBinaryResultRecord() {
+ var result = new ToolBinaryResult("base64data==", "image/png", "image", "A chart");
+ assertEquals("base64data==", result.data());
+ assertEquals("image/png", result.mimeType());
+ assertEquals("image", result.type());
+ assertEquals("A chart", result.description());
+ }
+
+ // ===== GetForegroundSessionResponse record =====
+
+ @Test
+ void getForegroundSessionResponseRecord() {
+ var response = new GetForegroundSessionResponse("session-123", "/home/user/project");
+ assertEquals("session-123", response.sessionId());
+ assertEquals("/home/user/project", response.workspacePath());
+ }
+
+ // ===== SetForegroundSessionResponse record =====
+
+ @Test
+ void setForegroundSessionResponseRecord() {
+ var successResponse = new SetForegroundSessionResponse(true, null);
+ assertTrue(successResponse.success());
+ assertNull(successResponse.error());
+
+ var errorResponse = new SetForegroundSessionResponse(false, "Session not found");
+ assertFalse(errorResponse.success());
+ assertEquals("Session not found", errorResponse.error());
+ }
+
+ // ===== ToolResultObject factory methods =====
+
+ @Test
+ void toolResultObjectErrorWithTextAndError() {
+ var result = ToolResultObject.error("partial output", "File not found");
+ assertEquals("error", result.resultType());
+ assertEquals("partial output", result.textResultForLlm());
+ assertEquals("File not found", result.error());
+ }
+
+ @Test
+ void toolResultObjectFailure() {
+ var result = ToolResultObject.failure("Tool unavailable", "Unknown tool");
+ assertEquals("failure", result.resultType());
+ assertEquals("Tool unavailable", result.textResultForLlm());
+ assertEquals("Unknown tool", result.error());
+ }
+
+ // ===== PermissionRequest additional setters =====
+
+ @Test
+ void permissionRequestSetExtensionData() {
+ var req = new PermissionRequest();
+ req.setExtensionData(java.util.Map.of("key", "value"));
+ assertEquals("value", req.getExtensionData().get("key"));
+ }
+
+ // ===== SectionOverride setContent =====
+
+ @Test
+ void sectionOverrideSetContent() {
+ var override = new SectionOverride();
+ override.setContent("Custom content");
+ assertEquals("Custom content", override.getContent());
+ }
+
+ // ===== PreToolUseHookInput getters =====
+
+ @Test
+ void preToolUseHookInputGetters() {
+ var input = new PreToolUseHookInput();
+ // Default values
+ assertEquals(0L, input.getTimestamp());
+ assertNull(input.getCwd());
+ assertNull(input.getToolArgs());
+ }
+
+ // ===== PostToolUseHookInput getters =====
+
+ @Test
+ void postToolUseHookInputGetters() {
+ var input = new PostToolUseHookInput();
+ // Default values
+ assertEquals(0L, input.getTimestamp());
+ assertNull(input.getCwd());
+ assertNull(input.getToolArgs());
+ }
+
+ // ===== PermissionRequestResult setRules =====
+
+ @Test
+ void permissionRequestResultSetRules() {
+ var result = new PermissionRequestResult().setKind("allow");
+ var rules = new java.util.ArrayList