From 6283d8a581edf846b599f90fa50da9d9f91b4782 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 19 Jan 2026 16:33:01 -0500 Subject: [PATCH 1/2] Add Java SDK for Copilot CLI --- .github/workflows/publish-maven.yml | 153 ++++ .github/workflows/sdk-e2e-tests.yml | 37 + README.md | 1 + java/.gitignore | 36 + java/.mvn/wrapper/maven-wrapper.properties | 3 + java/README.md | 436 ++++++++++ java/jbang-example.java | 34 + java/mvnw | 295 +++++++ java/mvnw.cmd | 189 +++++ java/pom.xml | 165 ++++ .../github/copilot/sdk/ConnectionState.java | 35 + .../com/github/copilot/sdk/CopilotClient.java | 762 ++++++++++++++++++ .../com/github/copilot/sdk/CopilotModel.java | 64 ++ .../github/copilot/sdk/CopilotSession.java | 422 ++++++++++ .../com/github/copilot/sdk/JsonRpcClient.java | 327 ++++++++ .../github/copilot/sdk/JsonRpcException.java | 48 ++ .../copilot/sdk/SdkProtocolVersion.java | 35 + .../github/copilot/sdk/SystemMessageMode.java | 50 ++ .../github/copilot/sdk/events/AbortEvent.java | 46 ++ .../sdk/events/AbstractSessionEvent.java | 166 ++++ .../sdk/events/AssistantIntentEvent.java | 46 ++ .../events/AssistantMessageDeltaEvent.java | 79 ++ .../sdk/events/AssistantMessageEvent.java | 234 ++++++ .../events/AssistantReasoningDeltaEvent.java | 57 ++ .../sdk/events/AssistantReasoningEvent.java | 57 ++ .../sdk/events/AssistantTurnEndEvent.java | 46 ++ .../sdk/events/AssistantTurnStartEvent.java | 46 ++ .../sdk/events/AssistantUsageEvent.java | 158 ++++ .../copilot/sdk/events/HookEndEvent.java | 116 +++ .../copilot/sdk/events/HookStartEvent.java | 68 ++ .../events/PendingMessagesModifiedEvent.java | 36 + .../SessionCompactionCompleteEvent.java | 123 +++ .../events/SessionCompactionStartEvent.java | 36 + .../copilot/sdk/events/SessionErrorEvent.java | 68 ++ .../sdk/events/SessionEventParser.java | 144 ++++ .../sdk/events/SessionHandoffEvent.java | 140 ++++ .../copilot/sdk/events/SessionIdleEvent.java | 36 + .../copilot/sdk/events/SessionInfoEvent.java | 57 ++ .../sdk/events/SessionModelChangeEvent.java | 57 ++ .../sdk/events/SessionResumeEvent.java | 59 ++ .../copilot/sdk/events/SessionStartEvent.java | 103 +++ .../sdk/events/SessionTruncationEvent.java | 123 +++ .../sdk/events/SessionUsageInfoEvent.java | 68 ++ .../sdk/events/SubagentCompletedEvent.java | 57 ++ .../sdk/events/SubagentFailedEvent.java | 68 ++ .../sdk/events/SubagentSelectedEvent.java | 68 ++ .../sdk/events/SubagentStartedEvent.java | 79 ++ .../sdk/events/SystemMessageEvent.java | 70 ++ .../events/ToolExecutionCompleteEvent.java | 155 ++++ .../ToolExecutionPartialResultEvent.java | 57 ++ .../sdk/events/ToolExecutionStartEvent.java | 79 ++ .../sdk/events/ToolUserRequestedEvent.java | 68 ++ .../copilot/sdk/events/UserMessageEvent.java | 118 +++ .../github/copilot/sdk/json/Attachment.java | 109 +++ .../github/copilot/sdk/json/AzureOptions.java | 53 ++ .../sdk/json/CopilotClientOptions.java | 296 +++++++ .../sdk/json/CreateSessionRequest.java | 168 ++++ .../sdk/json/CreateSessionResponse.java | 17 + .../copilot/sdk/json/CustomAgentConfig.java | 212 +++++ .../sdk/json/DeleteSessionResponse.java | 64 ++ .../sdk/json/GetLastSessionIdResponse.java | 17 + .../copilot/sdk/json/GetMessagesResponse.java | 22 + .../github/copilot/sdk/json/JsonRpcError.java | 97 +++ .../copilot/sdk/json/JsonRpcRequest.java | 110 +++ .../copilot/sdk/json/JsonRpcResponse.java | 112 +++ .../sdk/json/ListSessionsResponse.java | 45 ++ .../copilot/sdk/json/MessageOptions.java | 108 +++ .../copilot/sdk/json/PermissionHandler.java | 51 ++ .../sdk/json/PermissionInvocation.java | 39 + .../copilot/sdk/json/PermissionRequest.java | 88 ++ .../sdk/json/PermissionRequestResult.java | 78 ++ .../github/copilot/sdk/json/PingResponse.java | 89 ++ .../copilot/sdk/json/ProviderConfig.java | 192 +++++ .../copilot/sdk/json/ResumeSessionConfig.java | 169 ++++ .../sdk/json/ResumeSessionRequest.java | 117 +++ .../sdk/json/ResumeSessionResponse.java | 17 + .../copilot/sdk/json/SendMessageRequest.java | 76 ++ .../copilot/sdk/json/SendMessageResponse.java | 42 + .../copilot/sdk/json/SessionConfig.java | 312 +++++++ .../copilot/sdk/json/SessionMetadata.java | 148 ++++ .../copilot/sdk/json/SystemMessageConfig.java | 88 ++ .../copilot/sdk/json/ToolBinaryResult.java | 125 +++ .../com/github/copilot/sdk/json/ToolDef.java | 108 +++ .../copilot/sdk/json/ToolDefinition.java | 196 +++++ .../github/copilot/sdk/json/ToolHandler.java | 48 ++ .../copilot/sdk/json/ToolInvocation.java | 115 +++ .../copilot/sdk/json/ToolResultObject.java | 173 ++++ .../com/github/copilot/sdk/CapiProxy.java | 319 ++++++++ .../github/copilot/sdk/CopilotClientTest.java | 166 ++++ .../copilot/sdk/CopilotSessionTest.java | 423 ++++++++++ .../github/copilot/sdk/E2ETestContext.java | 258 ++++++ .../github/copilot/sdk/McpAndAgentsTest.java | 276 +++++++ .../github/copilot/sdk/PermissionsTest.java | 263 ++++++ .../com/github/copilot/sdk/ToolsTest.java | 201 +++++ justfile | 22 +- 95 files changed, 11706 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/publish-maven.yml create mode 100644 java/.gitignore create mode 100644 java/.mvn/wrapper/maven-wrapper.properties create mode 100644 java/README.md create mode 100644 java/jbang-example.java create mode 100755 java/mvnw create mode 100644 java/mvnw.cmd create mode 100644 java/pom.xml create mode 100644 java/src/main/java/com/github/copilot/sdk/ConnectionState.java create mode 100644 java/src/main/java/com/github/copilot/sdk/CopilotClient.java create mode 100644 java/src/main/java/com/github/copilot/sdk/CopilotModel.java create mode 100644 java/src/main/java/com/github/copilot/sdk/CopilotSession.java create mode 100644 java/src/main/java/com/github/copilot/sdk/JsonRpcClient.java create mode 100644 java/src/main/java/com/github/copilot/sdk/JsonRpcException.java create mode 100644 java/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java create mode 100644 java/src/main/java/com/github/copilot/sdk/SystemMessageMode.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/AbortEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/AssistantReasoningDeltaEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/AssistantReasoningEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/AssistantTurnEndEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/HookEndEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/HookStartEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/PendingMessagesModifiedEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SessionCompactionCompleteEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SessionCompactionStartEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SessionErrorEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SessionHandoffEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SessionIdleEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SessionInfoEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SessionModelChangeEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SessionResumeEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SessionStartEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SessionTruncationEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SessionUsageInfoEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SubagentCompletedEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SubagentFailedEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SubagentSelectedEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SubagentStartedEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/SystemMessageEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/ToolExecutionCompleteEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/ToolExecutionPartialResultEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/ToolExecutionStartEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/ToolUserRequestedEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/events/UserMessageEvent.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/Attachment.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/AzureOptions.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/DeleteSessionResponse.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/GetLastSessionIdResponse.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/GetMessagesResponse.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/JsonRpcError.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/JsonRpcRequest.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/JsonRpcResponse.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/ListSessionsResponse.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/MessageOptions.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/PermissionInvocation.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/PermissionRequest.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/PermissionRequestResult.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/PingResponse.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/SendMessageRequest.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/SendMessageResponse.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/SessionConfig.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/SessionMetadata.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/SystemMessageConfig.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/ToolBinaryResult.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/ToolDef.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/ToolDefinition.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/ToolHandler.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/ToolInvocation.java create mode 100644 java/src/main/java/com/github/copilot/sdk/json/ToolResultObject.java create mode 100644 java/src/test/java/com/github/copilot/sdk/CapiProxy.java create mode 100644 java/src/test/java/com/github/copilot/sdk/CopilotClientTest.java create mode 100644 java/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java create mode 100644 java/src/test/java/com/github/copilot/sdk/E2ETestContext.java create mode 100644 java/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java create mode 100644 java/src/test/java/com/github/copilot/sdk/PermissionsTest.java create mode 100644 java/src/test/java/com/github/copilot/sdk/ToolsTest.java diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml new file mode 100644 index 0000000..741e54f --- /dev/null +++ b/.github/workflows/publish-maven.yml @@ -0,0 +1,153 @@ +name: Publish Java SDK to Maven Central + +env: + HUSKY: 0 + +on: + workflow_dispatch: + inputs: + dist-tag: + description: "Tag to publish under" + type: choice + required: true + default: "latest" + options: + - latest + - prerelease + version: + description: "Version override (optional, e.g., 1.0.0). If empty, auto-increments." + type: string + required: false + +permissions: + contents: write + id-token: write + +concurrency: + group: publish-maven + cancel-in-progress: false + +jobs: + # Calculate version using the nodejs version script (shared across all SDKs) + version: + name: Calculate Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.VERSION }} + current: ${{ steps.version.outputs.CURRENT }} + current-prerelease: ${{ steps.version.outputs.CURRENT_PRERELEASE }} + defaults: + run: + working-directory: ./nodejs + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: "22.x" + - run: npm ci --ignore-scripts + - name: Get version + id: version + run: | + CURRENT="$(node scripts/get-version.js current)" + echo "CURRENT=$CURRENT" >> $GITHUB_OUTPUT + echo "Current latest version: $CURRENT" >> $GITHUB_STEP_SUMMARY + CURRENT_PRERELEASE="$(node scripts/get-version.js current-prerelease)" + echo "CURRENT_PRERELEASE=$CURRENT_PRERELEASE" >> $GITHUB_OUTPUT + echo "Current prerelease version: $CURRENT_PRERELEASE" >> $GITHUB_STEP_SUMMARY + if [ -n "${{ github.event.inputs.version }}" ]; then + VERSION="${{ github.event.inputs.version }}" + # Validate version format matches dist-tag + if [ "${{ github.event.inputs.dist-tag }}" = "latest" ]; then + if [[ "$VERSION" == *-* ]]; then + echo "❌ Error: Version '$VERSION' has a prerelease suffix but dist-tag is 'latest'" >> $GITHUB_STEP_SUMMARY + echo "Use a version without suffix (e.g., '1.0.0') for latest releases" + exit 1 + fi + else + if [[ "$VERSION" != *-* ]]; then + echo "❌ Error: Version '$VERSION' has no prerelease suffix but dist-tag is 'prerelease'" >> $GITHUB_STEP_SUMMARY + echo "Use a version with suffix (e.g., '1.0.0-preview.0') for prerelease" + exit 1 + fi + fi + echo "Using manual version override: $VERSION" >> $GITHUB_STEP_SUMMARY + else + VERSION="$(node scripts/get-version.js ${{ github.event.inputs.dist-tag }})" + echo "Auto-incremented version: $VERSION" >> $GITHUB_STEP_SUMMARY + fi + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + + publish-maven: + name: Publish Java SDK to Maven Central + needs: version + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./java + steps: + - uses: actions/checkout@v6 + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: "21" + distribution: "temurin" + server-id: central + server-username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + server-password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} + gpg-passphrase: ${{ secrets.MAVEN_GPG_PASSPHRASE }} + + - name: Set version in pom.xml + run: mvn versions:set -DnewVersion=${{ needs.version.outputs.version }} -DgenerateBackupPoms=false + + - name: Build and verify + run: mvn clean verify -DskipTests + + - name: Deploy to Maven Central + run: mvn deploy -DskipTests -Prelease + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} + + - name: Upload artifact + uses: actions/upload-artifact@v6 + with: + name: java-package + path: java/target/*.jar + + github-release: + name: Create GitHub Release + needs: [version, publish-maven] + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Create GitHub Release + if: github.event.inputs.dist-tag == 'latest' + run: | + NOTES_FLAG="" + if git rev-parse "v${{ needs.version.outputs.current }}" >/dev/null 2>&1; then + NOTES_FLAG="--notes-start-tag v${{ needs.version.outputs.current }}" + fi + gh release create "java/v${{ needs.version.outputs.version }}" \ + --title "Java SDK v${{ needs.version.outputs.version }}" \ + --generate-notes $NOTES_FLAG \ + --target ${{ github.sha }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Create GitHub Pre-Release + if: github.event.inputs.dist-tag == 'prerelease' + run: | + NOTES_FLAG="" + if git rev-parse "v${{ needs.version.outputs.current-prerelease }}" >/dev/null 2>&1; then + NOTES_FLAG="--notes-start-tag v${{ needs.version.outputs.current-prerelease }}" + fi + gh release create "java/v${{ needs.version.outputs.version }}" \ + --prerelease \ + --title "Java SDK v${{ needs.version.outputs.version }}" \ + --generate-notes $NOTES_FLAG \ + --target ${{ github.sha }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sdk-e2e-tests.yml b/.github/workflows/sdk-e2e-tests.yml index 3665f05..02928de 100644 --- a/.github/workflows/sdk-e2e-tests.yml +++ b/.github/workflows/sdk-e2e-tests.yml @@ -216,3 +216,40 @@ jobs: env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} run: dotnet test --no-build -v n + + java-sdk: + name: "Java SDK Tests" + + runs-on: ubuntu-latest + defaults: + run: + shell: bash + working-directory: ./java + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/setup-copilot + - uses: actions/setup-java@v5 + with: + java-version: "21" + distribution: "temurin" + + - name: Run spotless check + run: | + mvn spotless:check + if [ $? -ne 0 ]; then + echo "❌ spotless:check failed. Please run 'mvn spotless:apply' in java" + exit 1 + fi + echo "✅ spotless:check passed" + + - name: Build SDK + run: mvn compile + + - name: Install test harness dependencies + working-directory: ./test/harness + run: npm ci --ignore-scripts + + - name: Run Java SDK tests + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + run: mvn verify diff --git a/README.md b/README.md index cf43752..9d0b8ee 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ All SDKs are in technical preview and may change in breaking ways as we move tow | **Python** | [`./python/`](./python/README.md) | `pip install github-copilot-sdk` | | **Go** | [`./go/`](./go/README.md) | `go get github.com/github/copilot-sdk/go` | | **.NET** | [`./dotnet/`](./dotnet/README.md) | `dotnet add package GitHub.Copilot.SDK` | +| **Java** | [`./java/`](./java/README.md) | Add dependency (see [README](./java/README.md)) | See the individual SDK READMEs for installation, usage examples, and API reference. diff --git a/java/.gitignore b/java/.gitignore new file mode 100644 index 0000000..d4fee62 --- /dev/null +++ b/java/.gitignore @@ -0,0 +1,36 @@ +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# IDE +.idea/ +*.iml +*.ipr +*.iws +.vscode/ +.settings/ +.project +.classpath +*.swp +*.swo +*~ + +# Build +build/ +out/ +bin/ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log diff --git a/java/.mvn/wrapper/maven-wrapper.properties b/java/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8dea6c2 --- /dev/null +++ b/java/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/java/README.md b/java/README.md new file mode 100644 index 0000000..d2c591b --- /dev/null +++ b/java/README.md @@ -0,0 +1,436 @@ +# Copilot SDK for Java + +Java SDK for programmatic control of GitHub Copilot CLI. + +> **Note:** This SDK is in technical preview and may change in breaking ways. + +## Requirements + +- Java 21 or later +- GitHub Copilot CLI installed and in PATH (or provide custom `cliPath`) + +## Installation + +### Maven + +```xml + + com.github.copilot + copilot-sdk + 0.1.0 + +``` + +### Gradle + +```groovy +implementation 'com.github.copilot:copilot-sdk:0.1.0' +``` + +## Quick Start + +```java +import com.github.copilot.sdk.*; +import com.github.copilot.sdk.events.*; +import com.github.copilot.sdk.json.*; + +import java.util.concurrent.CompletableFuture; + +public class Example { + public static void main(String[] args) throws Exception { + // Create and start client + try (var client = new CopilotClient()) { + client.start().get(); + + // Create a session + var session = client.createSession( + new SessionConfig().setModel(CopilotModel.CLAUDE_SONNET_4_5.toString()) + ).get(); + + // Wait for response using session.idle event + var done = new CompletableFuture(); + + session.on(evt -> { + if (evt instanceof AssistantMessageEvent msg) { + System.out.println(msg.getData().getContent()); + } else if (evt instanceof SessionIdleEvent) { + done.complete(null); + } + }); + + // Send a message and wait for completion + session.send(new MessageOptions().setPrompt("What is 2+2?")).get(); + done.get(); + } + } +} +``` + +## Try it with JBang + +You can quickly try the SDK without setting up a full project using [JBang](https://www.jbang.dev/): + +```bash +# Assuming you are in the `java/` directory of this repository +# Install the SDK locally first (not yet on Maven Central) +mvn install + +# Install JBang (if not already installed) +# macOS: brew install jbang +# Linux/Windows: curl -Ls https://sh.jbang.dev | bash -s - app setup + +# Run the example +jbang jbang-example.java +``` + +The `jbang-example.java` file includes the dependency declaration and can be run directly: + +```java +//DEPS com.github.copilot:copilot-sdk:0.1.0 +``` + +## API Reference + +### CopilotClient + +#### Constructor + +```java +new CopilotClient() +new CopilotClient(CopilotClientOptions options) +``` + +**Options:** + +- `cliPath` - Path to CLI executable (default: "copilot" from PATH) +- `cliArgs` - Extra arguments prepended before SDK-managed flags +- `cliUrl` - URL of existing CLI server to connect to (e.g., `"localhost:8080"`). When provided, the client will not spawn a CLI process. +- `port` - Server port (default: 0 for random) +- `useStdio` - Use stdio transport instead of TCP (default: true) +- `logLevel` - Log level (default: "info") +- `autoStart` - Auto-start server (default: true) +- `autoRestart` - Auto-restart on crash (default: true) +- `cwd` - Working directory for the CLI process +- `environment` - Environment variables to pass to the CLI process + +#### Methods + +##### `start(): CompletableFuture` + +Start the CLI server and establish connection. + +##### `stop(): CompletableFuture` + +Stop the server and close all sessions. + +##### `forceStop(): CompletableFuture` + +Force stop the CLI server without graceful cleanup. + +##### `createSession(SessionConfig config): CompletableFuture` + +Create a new conversation session. + +**Config:** + +- `sessionId` - Custom session ID +- `model` - Model to use ("gpt-5", "claude-sonnet-4.5", etc.) +- `tools` - Custom tools exposed to the CLI +- `systemMessage` - System message customization +- `availableTools` - List of tool names to allow +- `excludedTools` - List of tool names to disable +- `provider` - Custom API provider configuration (BYOK) +- `streaming` - Enable streaming of response chunks (default: false) +- `mcpServers` - MCP server configurations +- `customAgents` - Custom agent configurations +- `onPermissionRequest` - Handler for permission requests + +##### `resumeSession(String sessionId, ResumeSessionConfig config): CompletableFuture` + +Resume an existing session. + +##### `ping(String message): CompletableFuture` + +Ping the server to check connectivity. + +##### `getState(): ConnectionState` + +Get current connection state. Returns one of: `DISCONNECTED`, `CONNECTING`, `CONNECTED`, `ERROR`. + +##### `listSessions(): CompletableFuture>` + +List all available sessions. + +##### `deleteSession(String sessionId): CompletableFuture` + +Delete a session and its data from disk. + +##### `getLastSessionId(): CompletableFuture` + +Get the ID of the most recently used session. + +--- + +### CopilotSession + +Represents a single conversation session. + +#### Properties + +- `getSessionId()` - The unique identifier for this session + +#### Methods + +##### `send(MessageOptions options): CompletableFuture` + +Send a message to the session. + +**Options:** + +- `prompt` - The message/prompt to send +- `attachments` - File attachments +- `mode` - Delivery mode ("enqueue" or "immediate") + +Returns the message ID. + +##### `sendAndWait(MessageOptions options, long timeoutMs): CompletableFuture` + +Send a message and wait for the session to become idle. Default timeout is 60 seconds. + +##### `on(Consumer handler): Closeable` + +Subscribe to session events. Returns a `Closeable` to unsubscribe. + +```java +var subscription = session.on(evt -> { + System.out.println("Event: " + evt.getType()); +}); + +// Later... +subscription.close(); +``` + +##### `abort(): CompletableFuture` + +Abort the currently processing message in this session. + +##### `getMessages(): CompletableFuture>` + +Get all events/messages from this session. + +##### `close()` + +Dispose the session and free resources. + +--- + +## Event Types + +Sessions emit various events during processing. Each event type extends `AbstractSessionEvent`: + +- `UserMessageEvent` - User message added +- `AssistantMessageEvent` - Assistant response +- `AssistantMessageDeltaEvent` - Streaming response chunk +- `ToolExecutionStartEvent` - Tool execution started +- `ToolExecutionCompleteEvent` - Tool execution completed +- `SessionStartEvent` - Session started +- `SessionIdleEvent` - Session is idle +- `SessionErrorEvent` - Session error occurred +- `SessionResumeEvent` - Session was resumed +- And more... + +Use pattern matching (Java 21+) to handle specific event types: + +```java +session.on(evt -> { + if (evt instanceof AssistantMessageEvent msg) { + System.out.println(msg.getData().getContent()); + } else if (evt instanceof SessionErrorEvent err) { + System.out.println("Error: " + err.getData().getMessage()); + } +}); +``` + +## Streaming + +Enable streaming to receive assistant response chunks as they're generated: + +```java +var session = client.createSession( + new SessionConfig() + .setModel("gpt-5") + .setStreaming(true) +).get(); + +var done = new CompletableFuture(); + +session.on(evt -> { + if (evt instanceof AssistantMessageDeltaEvent delta) { + // Streaming message chunk - print incrementally + System.out.print(delta.getData().getDeltaContent()); + } else if (evt instanceof AssistantMessageEvent msg) { + // Final message - complete content + System.out.println("\n--- Final message ---"); + System.out.println(msg.getData().getContent()); + } else if (evt instanceof SessionIdleEvent) { + done.complete(null); + } +}); + +session.send(new MessageOptions().setPrompt("Tell me a short story")).get(); +done.get(); +``` + +## Advanced Usage + +### Manual Server Control + +```java +var client = new CopilotClient( + new CopilotClientOptions().setAutoStart(false) +); + +// Start manually +client.start().get(); + +// Use client... + +// Stop manually +client.stop().get(); +``` + +### Tools + +You can let the CLI call back into your process when the model needs capabilities you own: + +```java +var lookupTool = ToolDefinition.create( + "lookup_issue", + "Fetch issue details from our tracker", + Map.of( + "type", "object", + "properties", Map.of( + "id", Map.of("type", "string", "description", "Issue identifier") + ), + "required", List.of("id") + ), + invocation -> { + String id = ((Map) invocation.getArguments()).get("id").toString(); + return CompletableFuture.completedFuture(fetchIssue(id)); + } +); + +var session = client.createSession( + new SessionConfig() + .setModel("gpt-5") + .setTools(List.of(lookupTool)) +).get(); +``` + +### System Message Customization + +Control the system prompt using `SystemMessageConfig` in session config: + +```java +var session = client.createSession( + new SessionConfig() + .setModel("gpt-5") + .setSystemMessage(new SystemMessageConfig() + .setMode(SystemMessageMode.APPEND) + .setContent(""" + + - Always check for security vulnerabilities + - Suggest performance improvements when applicable + + """)) +).get(); +``` + +For full control (removes all guardrails), use `REPLACE` mode: + +```java +var session = client.createSession( + new SessionConfig() + .setModel("gpt-5") + .setSystemMessage(new SystemMessageConfig() + .setMode(SystemMessageMode.REPLACE) + .setContent("You are a helpful assistant.")) +).get(); +``` + +### Multiple Sessions + +```java +var session1 = client.createSession( + new SessionConfig().setModel("gpt-5") +).get(); + +var session2 = client.createSession( + new SessionConfig().setModel("claude-sonnet-4.5") +).get(); + +// Both sessions are independent +session1.send(new MessageOptions().setPrompt("Hello from session 1")).get(); +session2.send(new MessageOptions().setPrompt("Hello from session 2")).get(); +``` + +### File Attachments + +```java +session.send(new MessageOptions() + .setPrompt("Analyze this file") + .setAttachments(List.of( + new Attachment() + .setType("file") + .setPath("/path/to/file.java") + .setDisplayName("My File") + )) +).get(); +``` + +### Bring Your Own Key (BYOK) + +Use a custom API provider: + +```java +var session = client.createSession( + new SessionConfig() + .setProvider(new ProviderConfig() + .setType("openai") + .setBaseUrl("https://api.openai.com/v1") + .setApiKey("your-api-key")) +).get(); +``` + +### Permission Handling + +Handle permission requests from the CLI: + +```java +var session = client.createSession( + new SessionConfig() + .setModel("gpt-5") + .setOnPermissionRequest((request, invocation) -> { + // Approve or deny the permission request + var result = new PermissionRequestResult(); + result.setKind("user-approved"); + return CompletableFuture.completedFuture(result); + }) +).get(); +``` + +## Error Handling + +```java +try { + var session = client.createSession().get(); + session.send(new MessageOptions().setPrompt("Hello")).get(); +} catch (ExecutionException ex) { + Throwable cause = ex.getCause(); + System.err.println("Error: " + cause.getMessage()); +} +``` + +## License + +MIT diff --git a/java/jbang-example.java b/java/jbang-example.java new file mode 100644 index 0000000..2531d49 --- /dev/null +++ b/java/jbang-example.java @@ -0,0 +1,34 @@ + +//DEPS com.github.copilot:copilot-sdk:0.1.0 +import com.github.copilot.sdk.*; +import com.github.copilot.sdk.events.*; +import com.github.copilot.sdk.json.*; +import java.util.concurrent.CompletableFuture; + +class CopilotSDK { + public static void main(String[] args) throws Exception { + // Create and start client + try (var client = new CopilotClient()) { + client.start().get(); + + // Create a session + var session = client.createSession( + new SessionConfig().setModel(CopilotModel.CLAUDE_SONNET_4_5.toString())).get(); + + // Wait for response using session.idle event + var done = new CompletableFuture(); + + session.on(evt -> { + if (evt instanceof AssistantMessageEvent msg) { + System.out.println(msg.getData().getContent()); + } else if (evt instanceof SessionIdleEvent) { + done.complete(null); + } + }); + + // Send a message and wait for completion + session.send(new MessageOptions().setPrompt("What is 2+2?")).get(); + done.get(); + } + } +} diff --git a/java/mvnw b/java/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/java/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/java/mvnw.cmd b/java/mvnw.cmd new file mode 100644 index 0000000..5761d94 --- /dev/null +++ b/java/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/java/pom.xml b/java/pom.xml new file mode 100644 index 0000000..e62be68 --- /dev/null +++ b/java/pom.xml @@ -0,0 +1,165 @@ + + + + 4.0.0 + + com.github.copilot + copilot-sdk + 0.1.0 + jar + + GitHub Copilot SDK + SDK for programmatic control of GitHub Copilot CLI + https://github.com/github/copilot-sdk + + + + MIT License + https://opensource.org/licenses/MIT + + + + + + GitHub + GitHub + https://github.com + + + + + scm:git:git://github.com/github/copilot-sdk.git + scm:git:ssh://github.com:github/copilot-sdk.git + https://github.com/github/copilot-sdk + + + + 21 + UTF-8 + + + + + + com.fasterxml.jackson.core + jackson-databind + 2.20.1 + + + com.fasterxml.jackson.core + jackson-annotations + 2.20 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.20.1 + + + + + org.junit.jupiter + junit-jupiter + 5.14.1 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.1 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + + com.diffplug.spotless + spotless-maven-plugin + 2.44.5 + + + + 4.33 + + + + + + true + 4 + + + + + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 3.4.0 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.12.0 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.8 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.10.0 + true + + central + true + + + + + + + diff --git a/java/src/main/java/com/github/copilot/sdk/ConnectionState.java b/java/src/main/java/com/github/copilot/sdk/ConnectionState.java new file mode 100644 index 0000000..6d208d6 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/ConnectionState.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +/** + * Represents the connection state of a {@link CopilotClient}. + *

+ * The connection state indicates the current status of the client's connection + * to the Copilot CLI server. + * + * @see CopilotClient#getState() + */ +public enum ConnectionState { + /** + * The client is not connected to the server. + */ + DISCONNECTED, + + /** + * The client is in the process of connecting to the server. + */ + CONNECTING, + + /** + * The client is connected and ready to accept requests. + */ + CONNECTED, + + /** + * The client encountered an error during connection or operation. + */ + ERROR +} diff --git a/java/src/main/java/com/github/copilot/sdk/CopilotClient.java b/java/src/main/java/com/github/copilot/sdk/CopilotClient.java new file mode 100644 index 0000000..7d775ea --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -0,0 +1,762 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.Socket; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.events.AbstractSessionEvent; +import com.github.copilot.sdk.events.SessionEventParser; +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.CreateSessionRequest; +import com.github.copilot.sdk.json.CreateSessionResponse; +import com.github.copilot.sdk.json.DeleteSessionResponse; +import com.github.copilot.sdk.json.GetLastSessionIdResponse; +import com.github.copilot.sdk.json.ListSessionsResponse; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.PingResponse; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.ResumeSessionRequest; +import com.github.copilot.sdk.json.ResumeSessionResponse; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SessionMetadata; +import com.github.copilot.sdk.json.ToolDef; +import com.github.copilot.sdk.json.ToolDefinition; +import com.github.copilot.sdk.json.ToolInvocation; +import com.github.copilot.sdk.json.ToolResultObject; + +/** + * Provides a client for interacting with the Copilot CLI server. + *

+ * The CopilotClient manages the connection to the Copilot CLI server and + * provides methods to create and manage conversation sessions. It can either + * spawn a CLI server process or connect to an existing server. + *

+ * Example usage: + * + *

{@code
+ * try (CopilotClient client = new CopilotClient()) {
+ * 	client.start().get();
+ *
+ * 	CopilotSession session = client.createSession(new SessionConfig().setModel("gpt-5")).get();
+ *
+ * 	session.on(evt -> {
+ * 		if (evt instanceof AssistantMessageEvent msg) {
+ * 			System.out.println(msg.getData().getContent());
+ * 		}
+ * 	});
+ *
+ * 	session.send(new MessageOptions().setPrompt("Hello!")).get();
+ * }
+ * }
+ */ +public class CopilotClient implements AutoCloseable { + + private static final Logger LOG = Logger.getLogger(CopilotClient.class.getName()); + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + private final CopilotClientOptions options; + private final Map sessions = new ConcurrentHashMap<>(); + private volatile CompletableFuture connectionFuture; + private volatile boolean disposed = false; + private final String optionsHost; + private final Integer optionsPort; + + /** + * Creates a new CopilotClient with default options. + */ + public CopilotClient() { + this(new CopilotClientOptions()); + } + + /** + * Creates a new CopilotClient with the specified options. + * + * @param options + * Options for creating the client + * @throws IllegalArgumentException + * if mutually exclusive options are provided + */ + public CopilotClient(CopilotClientOptions options) { + this.options = options != null ? options : new CopilotClientOptions(); + + // Validate mutually exclusive options + if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty() + && (this.options.isUseStdio() || this.options.getCliPath() != null)) { + throw new IllegalArgumentException("CliUrl is mutually exclusive with UseStdio and CliPath"); + } + + // Parse CliUrl if provided + if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty()) { + URI uri = parseCliUrl(this.options.getCliUrl()); + this.optionsHost = uri.getHost(); + this.optionsPort = uri.getPort(); + } else { + this.optionsHost = null; + this.optionsPort = null; + } + } + + private static URI parseCliUrl(String url) { + // If it's just a port number, treat as localhost + try { + int port = Integer.parseInt(url); + return URI.create("http://localhost:" + port); + } catch (NumberFormatException e) { + // Not a port number, continue + } + + // Add scheme if missing + if (!url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://")) { + url = "https://" + url; + } + + return URI.create(url); + } + + /** + * Starts the Copilot client and connects to the server. + * + * @return A future that completes when the connection is established + */ + public CompletableFuture start() { + if (connectionFuture == null) { + synchronized (this) { + if (connectionFuture == null) { + connectionFuture = startCore(); + } + } + } + return connectionFuture.thenApply(c -> null); + } + + private CompletableFuture startCore() { + LOG.fine("Starting Copilot client"); + + return CompletableFuture.supplyAsync(() -> { + try { + Connection connection; + + if (optionsHost != null && optionsPort != null) { + // External server (TCP) + connection = connectToServer(null, optionsHost, optionsPort); + } else { + // Child process (stdio or TCP) + ProcessInfo processInfo = startCliServer(); + connection = connectToServer(processInfo.process, processInfo.port != null ? "localhost" : null, + processInfo.port); + } + + // Register handlers for server-to-client calls + registerRpcHandlers(connection.rpc); + + // Verify protocol version + verifyProtocolVersion(connection); + + LOG.info("Copilot client connected"); + return connection; + } catch (Exception e) { + throw new CompletionException(e); + } + }); + } + + private void registerRpcHandlers(JsonRpcClient rpc) { + // Handle session events + rpc.registerMethodHandler("session.event", (requestId, params) -> { + try { + String sessionId = params.get("sessionId").asText(); + JsonNode eventNode = params.get("event"); + + CopilotSession session = sessions.get(sessionId); + if (session != null && eventNode != null) { + AbstractSessionEvent event = SessionEventParser.parse(eventNode.toString()); + if (event != null) { + session.dispatchEvent(event); + } + } + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling session event", e); + } + }); + + // Handle tool calls + rpc.registerMethodHandler("tool.call", (requestId, params) -> { + handleToolCall(rpc, requestId, params); + }); + + // Handle permission requests + rpc.registerMethodHandler("permission.request", (requestId, params) -> { + handlePermissionRequest(rpc, requestId, params); + }); + } + + private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params) { + CompletableFuture.runAsync(() -> { + try { + String sessionId = params.get("sessionId").asText(); + String toolCallId = params.get("toolCallId").asText(); + String toolName = params.get("toolName").asText(); + JsonNode arguments = params.get("arguments"); + + CopilotSession session = sessions.get(sessionId); + if (session == null) { + rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session " + sessionId); + return; + } + + ToolDefinition tool = session.getTool(toolName); + if (tool == null || tool.getHandler() == null) { + ToolResultObject result = new ToolResultObject() + .setTextResultForLlm("Tool '" + toolName + "' is not supported.").setResultType("failure") + .setError("tool '" + toolName + "' not supported"); + rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result)); + return; + } + + ToolInvocation invocation = new ToolInvocation().setSessionId(sessionId).setToolCallId(toolCallId) + .setToolName(toolName).setArguments(arguments); + + tool.getHandler().invoke(invocation).thenAccept(result -> { + try { + ToolResultObject toolResult; + if (result instanceof ToolResultObject tr) { + toolResult = tr; + } else { + toolResult = new ToolResultObject().setResultType("success").setTextResultForLlm( + result instanceof String s ? s : MAPPER.writeValueAsString(result)); + } + rpc.sendResponse(Long.parseLong(requestId), Map.of("result", toolResult)); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error sending tool result", e); + } + }).exceptionally(ex -> { + try { + ToolResultObject result = new ToolResultObject() + .setTextResultForLlm( + "Invoking this tool produced an error. Detailed information is not available.") + .setResultType("failure").setError(ex.getMessage()); + rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result)); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error sending tool error", e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling tool call", e); + try { + rpc.sendErrorResponse(Long.parseLong(requestId), -32603, e.getMessage()); + } catch (IOException ioe) { + LOG.log(Level.SEVERE, "Failed to send error response", ioe); + } + } + }); + } + + private void handlePermissionRequest(JsonRpcClient rpc, String requestId, JsonNode params) { + CompletableFuture.runAsync(() -> { + try { + String sessionId = params.get("sessionId").asText(); + JsonNode permissionRequest = params.get("permissionRequest"); + + CopilotSession session = sessions.get(sessionId); + if (session == null) { + PermissionRequestResult result = new PermissionRequestResult() + .setKind("denied-no-approval-rule-and-could-not-request-from-user"); + rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result)); + return; + } + + session.handlePermissionRequest(permissionRequest).thenAccept(result -> { + try { + rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result)); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending permission result", e); + } + }).exceptionally(ex -> { + try { + PermissionRequestResult result = new PermissionRequestResult() + .setKind("denied-no-approval-rule-and-could-not-request-from-user"); + rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result)); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending permission denied", e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling permission request", e); + } + }); + } + + private void verifyProtocolVersion(Connection connection) throws Exception { + int expectedVersion = SdkProtocolVersion.get(); + Map params = new HashMap<>(); + params.put("message", null); + PingResponse pingResponse = connection.rpc.invoke("ping", params, PingResponse.class).get(30, TimeUnit.SECONDS); + + if (pingResponse.getProtocolVersion() == null) { + throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion + + ", but server does not report a protocol version. " + + "Please update your server to ensure compatibility."); + } + + if (pingResponse.getProtocolVersion() != expectedVersion) { + throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion + + ", but server reports version " + pingResponse.getProtocolVersion() + ". " + + "Please update your SDK or server to ensure compatibility."); + } + } + + /** + * Stops the client and closes all sessions. + * + * @return A future that completes when the client is stopped + */ + public CompletableFuture stop() { + List> closeFutures = new ArrayList<>(); + + for (CopilotSession session : new ArrayList<>(sessions.values())) { + closeFutures.add(CompletableFuture.runAsync(() -> { + try { + session.close(); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error closing session " + session.getSessionId(), e); + } + })); + } + sessions.clear(); + + return CompletableFuture.allOf(closeFutures.toArray(new CompletableFuture[0])) + .thenCompose(v -> cleanupConnection()); + } + + /** + * Forces an immediate stop of the client without graceful cleanup. + * + * @return A future that completes when the client is stopped + */ + public CompletableFuture forceStop() { + sessions.clear(); + return cleanupConnection(); + } + + private CompletableFuture cleanupConnection() { + CompletableFuture future = connectionFuture; + connectionFuture = null; + + if (future == null) { + return CompletableFuture.completedFuture(null); + } + + return future.thenAccept(connection -> { + try { + connection.rpc.close(); + } catch (Exception e) { + LOG.log(Level.FINE, "Error closing RPC", e); + } + + if (connection.process != null) { + try { + if (connection.process.isAlive()) { + connection.process.destroyForcibly(); + } + } catch (Exception e) { + LOG.log(Level.FINE, "Error killing process", e); + } + } + }).exceptionally(ex -> null); + } + + /** + * Creates a new Copilot session with the specified configuration. + *

+ * The session maintains conversation state and can be used to send messages and + * receive responses. Remember to close the session when done. + * + * @param config + * configuration for the session (model, tools, etc.) + * @return a future that resolves with the created CopilotSession + * @see #createSession() + * @see SessionConfig + */ + public CompletableFuture createSession(SessionConfig config) { + return ensureConnected().thenCompose(connection -> { + CreateSessionRequest request = new CreateSessionRequest(); + if (config != null) { + request.setModel(config.getModel()); + request.setSessionId(config.getSessionId()); + request.setTools(config.getTools() != null + ? config.getTools().stream() + .map(t -> new ToolDef(t.getName(), t.getDescription(), t.getParameters())) + .collect(Collectors.toList()) + : null); + request.setSystemMessage(config.getSystemMessage()); + request.setAvailableTools(config.getAvailableTools()); + request.setExcludedTools(config.getExcludedTools()); + request.setProvider(config.getProvider()); + request.setRequestPermission(config.getOnPermissionRequest() != null ? true : null); + request.setStreaming(config.isStreaming() ? true : null); + request.setMcpServers(config.getMcpServers()); + request.setCustomAgents(config.getCustomAgents()); + } + + return connection.rpc.invoke("session.create", request, CreateSessionResponse.class).thenApply(response -> { + CopilotSession session = new CopilotSession(response.getSessionId(), connection.rpc); + if (config != null && config.getTools() != null) { + session.registerTools(config.getTools()); + } + if (config != null && config.getOnPermissionRequest() != null) { + session.registerPermissionHandler(config.getOnPermissionRequest()); + } + sessions.put(response.getSessionId(), session); + return session; + }); + }); + } + + /** + * Creates a new Copilot session with default configuration. + * + * @return a future that resolves with the created CopilotSession + * @see #createSession(SessionConfig) + */ + public CompletableFuture createSession() { + return createSession(null); + } + + /** + * Resumes an existing Copilot session. + *

+ * This restores a previously saved session, allowing you to continue a + * conversation. The session's history is preserved. + * + * @param sessionId + * the ID of the session to resume + * @param config + * configuration for the resumed session + * @return a future that resolves with the resumed CopilotSession + * @see #resumeSession(String) + * @see #listSessions() + * @see #getLastSessionId() + */ + public CompletableFuture resumeSession(String sessionId, ResumeSessionConfig config) { + return ensureConnected().thenCompose(connection -> { + ResumeSessionRequest request = new ResumeSessionRequest(); + request.setSessionId(sessionId); + if (config != null) { + request.setTools(config.getTools() != null + ? config.getTools().stream() + .map(t -> new ToolDef(t.getName(), t.getDescription(), t.getParameters())) + .collect(Collectors.toList()) + : null); + request.setProvider(config.getProvider()); + request.setRequestPermission(config.getOnPermissionRequest() != null ? true : null); + request.setStreaming(config.isStreaming() ? true : null); + request.setMcpServers(config.getMcpServers()); + request.setCustomAgents(config.getCustomAgents()); + } + + return connection.rpc.invoke("session.resume", request, ResumeSessionResponse.class).thenApply(response -> { + CopilotSession session = new CopilotSession(response.getSessionId(), connection.rpc); + if (config != null && config.getTools() != null) { + session.registerTools(config.getTools()); + } + if (config != null && config.getOnPermissionRequest() != null) { + session.registerPermissionHandler(config.getOnPermissionRequest()); + } + sessions.put(response.getSessionId(), session); + return session; + }); + }); + } + + /** + * Resumes an existing session with default configuration. + * + * @param sessionId + * the ID of the session to resume + * @return a future that resolves with the resumed CopilotSession + * @see #resumeSession(String, ResumeSessionConfig) + */ + public CompletableFuture resumeSession(String sessionId) { + return resumeSession(sessionId, null); + } + + /** + * Gets the current connection state. + * + * @return the current connection state + * @see ConnectionState + */ + public ConnectionState getState() { + if (connectionFuture == null) + return ConnectionState.DISCONNECTED; + if (connectionFuture.isCompletedExceptionally()) + return ConnectionState.ERROR; + if (!connectionFuture.isDone()) + return ConnectionState.CONNECTING; + return ConnectionState.CONNECTED; + } + + /** + * Pings the server to check connectivity. + *

+ * This can be used to verify that the server is responsive and to check the + * protocol version. + * + * @param message + * an optional message to echo back + * @return a future that resolves with the ping response + * @see PingResponse + */ + public CompletableFuture ping(String message) { + return ensureConnected().thenCompose(connection -> connection.rpc.invoke("ping", + Map.of("message", message != null ? message : ""), PingResponse.class)); + } + + /** + * Gets the ID of the most recently used session. + *

+ * This is useful for resuming the last conversation without needing to list all + * sessions. + * + * @return a future that resolves with the last session ID, or {@code null} if + * no sessions exist + * @see #resumeSession(String) + */ + public CompletableFuture getLastSessionId() { + return ensureConnected().thenCompose( + connection -> connection.rpc.invoke("session.getLastId", Map.of(), GetLastSessionIdResponse.class) + .thenApply(GetLastSessionIdResponse::getSessionId)); + } + + /** + * Deletes a session by ID. + *

+ * This permanently removes the session and its conversation history. + * + * @param sessionId + * the ID of the session to delete + * @return a future that completes when the session is deleted + * @throws RuntimeException + * if the deletion fails + */ + public CompletableFuture deleteSession(String sessionId) { + return ensureConnected().thenCompose(connection -> connection.rpc + .invoke("session.delete", Map.of("sessionId", sessionId), DeleteSessionResponse.class) + .thenAccept(response -> { + if (!response.isSuccess()) { + throw new RuntimeException( + "Failed to delete session " + sessionId + ": " + response.getError()); + } + sessions.remove(sessionId); + })); + } + + /** + * Lists all available sessions. + *

+ * Returns metadata about all sessions that can be resumed, including their IDs, + * start times, and summaries. + * + * @return a future that resolves with a list of session metadata + * @see SessionMetadata + * @see #resumeSession(String) + */ + public CompletableFuture> listSessions() { + return ensureConnected() + .thenCompose(connection -> connection.rpc.invoke("session.list", Map.of(), ListSessionsResponse.class) + .thenApply(ListSessionsResponse::getSessions)); + } + + private CompletableFuture ensureConnected() { + if (connectionFuture == null && !options.isAutoStart()) { + throw new IllegalStateException("Client not connected. Call start() first."); + } + + start(); + return connectionFuture; + } + + private ProcessInfo startCliServer() throws IOException, InterruptedException { + String cliPath = options.getCliPath() != null ? options.getCliPath() : "copilot"; + List args = new ArrayList<>(); + + if (options.getCliArgs() != null) { + args.addAll(Arrays.asList(options.getCliArgs())); + } + + args.add("--server"); + args.add("--log-level"); + args.add(options.getLogLevel()); + + if (options.isUseStdio()) { + args.add("--stdio"); + } else if (options.getPort() > 0) { + args.add("--port"); + args.add(String.valueOf(options.getPort())); + } + + List command = resolveCliCommand(cliPath, args); + + ProcessBuilder pb = new ProcessBuilder(command); + pb.redirectErrorStream(false); + + if (options.getCwd() != null) { + pb.directory(new File(options.getCwd())); + } + + if (options.getEnvironment() != null) { + pb.environment().clear(); + pb.environment().putAll(options.getEnvironment()); + } + pb.environment().remove("NODE_DEBUG"); + + Process process = pb.start(); + + // Forward stderr to logger in background + Thread stderrThread = new Thread(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String line; + while ((line = reader.readLine()) != null) { + LOG.fine("[CLI] " + line); + } + } catch (IOException e) { + LOG.log(Level.FINE, "Error reading stderr", e); + } + }, "cli-stderr-reader"); + stderrThread.setDaemon(true); + stderrThread.start(); + + Integer detectedPort = null; + if (!options.isUseStdio()) { + // Wait for port announcement + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + Pattern portPattern = Pattern.compile("listening on port (\\d+)", Pattern.CASE_INSENSITIVE); + long deadline = System.currentTimeMillis() + 30000; + + while (System.currentTimeMillis() < deadline) { + String line = reader.readLine(); + if (line == null) { + throw new IOException("CLI process exited unexpectedly"); + } + + Matcher matcher = portPattern.matcher(line); + if (matcher.find()) { + detectedPort = Integer.parseInt(matcher.group(1)); + break; + } + } + + if (detectedPort == null) { + process.destroyForcibly(); + throw new IOException("Timeout waiting for CLI to announce port"); + } + } + + return new ProcessInfo(process, detectedPort); + } + + private List resolveCliCommand(String cliPath, List args) { + boolean isJsFile = cliPath.toLowerCase().endsWith(".js"); + + if (isJsFile) { + List result = new ArrayList<>(); + result.add("node"); + result.add(cliPath); + result.addAll(args); + return result; + } + + // On Windows, use cmd /c to resolve the executable + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("win") && !new File(cliPath).isAbsolute()) { + List result = new ArrayList<>(); + result.add("cmd"); + result.add("/c"); + result.add(cliPath); + result.addAll(args); + return result; + } + + List result = new ArrayList<>(); + result.add(cliPath); + result.addAll(args); + return result; + } + + private Connection connectToServer(Process process, String tcpHost, Integer tcpPort) throws IOException { + JsonRpcClient rpc; + + if (options.isUseStdio()) { + if (process == null) { + throw new IllegalStateException("CLI process not started"); + } + rpc = JsonRpcClient.fromProcess(process); + } else { + if (tcpHost == null || tcpPort == null) { + throw new IllegalStateException("Cannot connect because TCP host or port are not available"); + } + Socket socket = new Socket(tcpHost, tcpPort); + rpc = JsonRpcClient.fromSocket(socket); + } + + return new Connection(rpc, process); + } + + @Override + public void close() { + if (disposed) + return; + disposed = true; + try { + forceStop().get(5, TimeUnit.SECONDS); + } catch (Exception e) { + LOG.log(Level.FINE, "Error during close", e); + } + } + + private static class ProcessInfo { + final Process process; + final Integer port; + + ProcessInfo(Process process, Integer port) { + this.process = process; + this.port = port; + } + } + + private static class Connection { + final JsonRpcClient rpc; + + final Process process; + + Connection(JsonRpcClient rpc, Process process) { + this.rpc = rpc; + this.process = process; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/CopilotModel.java b/java/src/main/java/com/github/copilot/sdk/CopilotModel.java new file mode 100644 index 0000000..7f6b5bd --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/CopilotModel.java @@ -0,0 +1,64 @@ +package com.github.copilot.sdk; + +/** + * Available Copilot models. + * + *

+ * The actual availability of models depends on your GitHub Copilot + * subscription. + */ +public enum CopilotModel { + /** Claude Sonnet 4.5 */ + CLAUDE_SONNET_4_5("claude-sonnet-4.5"), + /** Claude Haiku 4.5 */ + CLAUDE_HAIKU_4_5("claude-haiku-4.5"), + /** Claude Opus 4.5 */ + CLAUDE_OPUS_4_5("claude-opus-4.5"), + /** Claude Sonnet 4 */ + CLAUDE_SONNET_4("claude-sonnet-4"), + /** GPT-5.2 Codex */ + GPT_5_2_CODEX("gpt-5.2-codex"), + /** GPT-5.1 Codex Max */ + GPT_5_1_CODEX_MAX("gpt-5.1-codex-max"), + /** GPT-5.1 Codex */ + GPT_5_1_CODEX("gpt-5.1-codex"), + /** GPT-5.2 */ + GPT_5_2("gpt-5.2"), + /** GPT-5.1 */ + GPT_5_1("gpt-5.1"), + /** GPT-5 */ + GPT_5("gpt-5"), + /** GPT-5.1 Codex Mini */ + GPT_5_1_CODEX_MINI("gpt-5.1-codex-mini"), + /** GPT-5 Mini */ + GPT_5_MINI("gpt-5-mini"), + /** GPT-4.1 */ + GPT_4_1("gpt-4.1"), + /** Gemini 3 Pro Preview */ + GEMINI_3_PRO_PREVIEW("gemini-3-pro-preview"); + + private final String value; + + CopilotModel(String value) { + this.value = value; + } + + /** + * Returns the model identifier string to use with the API. + * + * @return the model identifier + */ + public String getValue() { + return value; + } + + /** + * Returns the string representation of the model. + * + * @return the model identifier string + */ + @Override + public String toString() { + return value; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/CopilotSession.java b/java/src/main/java/com/github/copilot/sdk/CopilotSession.java new file mode 100644 index 0000000..d97c12d --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -0,0 +1,422 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.events.AbstractSessionEvent; +import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.events.SessionErrorEvent; +import com.github.copilot.sdk.events.SessionEventParser; +import com.github.copilot.sdk.events.SessionIdleEvent; +import com.github.copilot.sdk.json.GetMessagesResponse; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.PermissionInvocation; +import com.github.copilot.sdk.json.PermissionRequest; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.SendMessageRequest; +import com.github.copilot.sdk.json.SendMessageResponse; +import com.github.copilot.sdk.json.ToolDefinition; + +/** + * Represents a single conversation session with the Copilot CLI. + *

+ * A session maintains conversation state, handles events, and manages tool + * execution. Sessions are created via {@link CopilotClient#createSession} or + * resumed via {@link CopilotClient#resumeSession}. + * + *

Example Usage

+ * + *
{@code
+ * // Create a session
+ * CopilotSession session = client.createSession(new SessionConfig().setModel("gpt-5")).get();
+ *
+ * // Register event handlers
+ * session.on(evt -> {
+ * 	if (evt instanceof AssistantMessageEvent msg) {
+ * 		System.out.println(msg.getData().getContent());
+ * 	}
+ * });
+ *
+ * // Send messages
+ * session.sendAndWait(new MessageOptions().setPrompt("Hello!")).get();
+ *
+ * // Clean up
+ * session.close();
+ * }
+ * + * @see CopilotClient#createSession(com.github.copilot.sdk.json.SessionConfig) + * @see CopilotClient#resumeSession(String, + * com.github.copilot.sdk.json.ResumeSessionConfig) + * @see AbstractSessionEvent + */ +public final class CopilotSession implements AutoCloseable { + + private static final Logger LOG = Logger.getLogger(CopilotSession.class.getName()); + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + private final String sessionId; + private final JsonRpcClient rpc; + private final Set> eventHandlers = ConcurrentHashMap.newKeySet(); + private final Map toolHandlers = new ConcurrentHashMap<>(); + private final AtomicReference permissionHandler = new AtomicReference<>(); + + /** + * Creates a new session with the given ID and RPC client. + *

+ * This constructor is package-private. Sessions should be created via + * {@link CopilotClient#createSession} or {@link CopilotClient#resumeSession}. + * + * @param sessionId + * the unique session identifier + * @param rpc + * the JSON-RPC client for communication + */ + CopilotSession(String sessionId, JsonRpcClient rpc) { + this.sessionId = sessionId; + this.rpc = rpc; + } + + /** + * Gets the unique identifier for this session. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sends a simple text message to the Copilot session. + *

+ * This is a convenience method equivalent to + * {@code send(new MessageOptions().setPrompt(prompt))}. + * + * @param prompt + * the message text to send + * @return a future that resolves with the message ID assigned by the server + * @see #send(MessageOptions) + */ + public CompletableFuture send(String prompt) { + return send(new MessageOptions().setPrompt(prompt)); + } + + /** + * Sends a simple text message and waits until the session becomes idle. + *

+ * This is a convenience method equivalent to + * {@code sendAndWait(new MessageOptions().setPrompt(prompt))}. + * + * @param prompt + * the message text to send + * @return a future that resolves with the final assistant message event, or + * {@code null} if no assistant message was received + * @see #sendAndWait(MessageOptions) + */ + public CompletableFuture sendAndWait(String prompt) { + return sendAndWait(new MessageOptions().setPrompt(prompt)); + } + + /** + * Sends a message to the Copilot session. + *

+ * This method sends a message asynchronously and returns immediately. Use + * {@link #sendAndWait(MessageOptions)} to wait for the response. + * + * @param options + * the message options containing the prompt and attachments + * @return a future that resolves with the message ID assigned by the server + * @see #sendAndWait(MessageOptions) + * @see #send(String) + */ + public CompletableFuture send(MessageOptions options) { + SendMessageRequest request = new SendMessageRequest(); + request.setSessionId(sessionId); + request.setPrompt(options.getPrompt()); + request.setAttachments(options.getAttachments()); + request.setMode(options.getMode()); + + return rpc.invoke("session.send", request, SendMessageResponse.class) + .thenApply(SendMessageResponse::getMessageId); + } + + /** + * Sends a message and waits until the session becomes idle. + *

+ * This method blocks until the assistant finishes processing the message or + * until the timeout expires. It's suitable for simple request/response + * interactions where you don't need to process streaming events. + * + * @param options + * the message options containing the prompt and attachments + * @param timeoutMs + * timeout in milliseconds (0 or negative for no timeout) + * @return a future that resolves with the final assistant message event, or + * {@code null} if no assistant message was received. The future + * completes exceptionally with a TimeoutException if the timeout + * expires. + * @see #sendAndWait(MessageOptions) + * @see #send(MessageOptions) + */ + public CompletableFuture sendAndWait(MessageOptions options, long timeoutMs) { + CompletableFuture future = new CompletableFuture<>(); + AtomicReference lastAssistantMessage = new AtomicReference<>(); + + Consumer handler = evt -> { + if (evt instanceof AssistantMessageEvent msg) { + lastAssistantMessage.set(msg); + } else if (evt instanceof SessionIdleEvent) { + future.complete(lastAssistantMessage.get()); + } else if (evt instanceof SessionErrorEvent errorEvent) { + String message = errorEvent.getData() != null ? errorEvent.getData().getMessage() : "session error"; + future.completeExceptionally(new RuntimeException("Session error: " + message)); + } + }; + + Closeable subscription = on(handler); + + send(options).exceptionally(ex -> { + try { + subscription.close(); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error closing subscription", e); + } + future.completeExceptionally(ex); + return null; + }); + + // Set up timeout + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.schedule(() -> { + if (!future.isDone()) { + future.completeExceptionally(new TimeoutException("sendAndWait timed out after " + timeoutMs + "ms")); + } + scheduler.shutdown(); + }, timeoutMs, TimeUnit.MILLISECONDS); + + return future.whenComplete((result, ex) -> { + try { + subscription.close(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error closing subscription", e); + } + scheduler.shutdown(); + }); + } + + /** + * Sends a message and waits until the session becomes idle with default 60 + * second timeout. + * + * @param options + * the message options containing the prompt and attachments + * @return a future that resolves with the final assistant message event, or + * {@code null} if no assistant message was received + * @see #sendAndWait(MessageOptions, long) + */ + public CompletableFuture sendAndWait(MessageOptions options) { + return sendAndWait(options, 60000); + } + + /** + * Registers a callback for session events. + *

+ * The handler will be invoked for all events in this session, including + * assistant messages, tool calls, and session state changes. + * + *

+ * Example: + * + *

{@code
+     * Closeable subscription = session.on(evt -> {
+     * 	if (evt instanceof AssistantMessageEvent msg) {
+     * 		System.out.println(msg.getData().getContent());
+     * 	}
+     * });
+     *
+     * // Later, to unsubscribe:
+     * subscription.close();
+     * }
+ * + * @param handler + * a callback to be invoked when a session event occurs + * @return a Closeable that, when closed, unsubscribes the handler + * @see AbstractSessionEvent + */ + public Closeable on(Consumer handler) { + eventHandlers.add(handler); + return () -> eventHandlers.remove(handler); + } + + /** + * Dispatches an event to all registered handlers. + *

+ * This is called internally when events are received from the server. + * + * @param event + * the event to dispatch + */ + void dispatchEvent(AbstractSessionEvent event) { + for (Consumer handler : eventHandlers) { + try { + handler.accept(event); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error in event handler", e); + } + } + } + + /** + * Registers custom tool handlers for this session. + *

+ * Called internally when creating or resuming a session with tools. + * + * @param tools + * the list of tool definitions with handlers + */ + void registerTools(List tools) { + toolHandlers.clear(); + if (tools != null) { + for (ToolDefinition tool : tools) { + toolHandlers.put(tool.getName(), tool); + } + } + } + + /** + * Retrieves a registered tool by name. + * + * @param name + * the tool name + * @return the tool definition, or {@code null} if not found + */ + ToolDefinition getTool(String name) { + return toolHandlers.get(name); + } + + /** + * Registers a handler for permission requests. + *

+ * Called internally when creating or resuming a session with permission + * handling. + * + * @param handler + * the permission handler + */ + void registerPermissionHandler(PermissionHandler handler) { + permissionHandler.set(handler); + } + + /** + * Handles a permission request from the Copilot CLI. + *

+ * Called internally when the server requests permission for an operation. + * + * @param permissionRequestData + * the JSON data for the permission request + * @return a future that resolves with the permission result + */ + CompletableFuture handlePermissionRequest(JsonNode permissionRequestData) { + PermissionHandler handler = permissionHandler.get(); + if (handler == null) { + PermissionRequestResult result = new PermissionRequestResult(); + result.setKind("denied-no-approval-rule-and-could-not-request-from-user"); + return CompletableFuture.completedFuture(result); + } + + try { + PermissionRequest request = MAPPER.treeToValue(permissionRequestData, PermissionRequest.class); + PermissionInvocation invocation = new PermissionInvocation(); + invocation.setSessionId(sessionId); + return handler.handle(request, invocation); + } catch (JsonProcessingException e) { + LOG.log(Level.SEVERE, "Failed to parse permission request", e); + PermissionRequestResult result = new PermissionRequestResult(); + result.setKind("denied-no-approval-rule-and-could-not-request-from-user"); + return CompletableFuture.completedFuture(result); + } + } + + /** + * Gets the complete list of messages and events in the session. + *

+ * This retrieves the full conversation history, including all user messages, + * assistant responses, tool invocations, and other session events. + * + * @return a future that resolves with a list of all session events + * @see AbstractSessionEvent + */ + public CompletableFuture> getMessages() { + return rpc.invoke("session.getMessages", Map.of("sessionId", sessionId), GetMessagesResponse.class) + .thenApply(response -> { + List events = new ArrayList<>(); + if (response.getEvents() != null) { + for (JsonNode eventNode : response.getEvents()) { + try { + AbstractSessionEvent event = SessionEventParser.parse(eventNode.toString()); + if (event != null) { + events.add(event); + } + } catch (Exception e) { + LOG.log(Level.WARNING, "Failed to parse event", e); + } + } + } + return events; + }); + } + + /** + * Aborts the currently processing message in this session. + *

+ * Use this to cancel a long-running operation or stop the assistant from + * continuing to generate a response. + * + * @return a future that completes when the abort is acknowledged + */ + public CompletableFuture abort() { + return rpc.invoke("session.abort", Map.of("sessionId", sessionId), Void.class); + } + + /** + * Disposes the session and releases all associated resources. + *

+ * This destroys the session on the server, clears all event handlers, and + * releases tool and permission handlers. After calling this method, the session + * cannot be used again. + */ + @Override + public void close() { + try { + rpc.invoke("session.destroy", Map.of("sessionId", sessionId), Void.class).get(5, TimeUnit.SECONDS); + } catch (Exception e) { + LOG.log(Level.FINE, "Error destroying session", e); + } + + eventHandlers.clear(); + toolHandlers.clear(); + permissionHandler.set(null); + } + +} diff --git a/java/src/main/java/com/github/copilot/sdk/JsonRpcClient.java b/java/src/main/java/com/github/copilot/sdk/JsonRpcClient.java new file mode 100644 index 0000000..bf52754 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/JsonRpcClient.java @@ -0,0 +1,327 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.github.copilot.sdk.json.JsonRpcError; +import com.github.copilot.sdk.json.JsonRpcRequest; +import com.github.copilot.sdk.json.JsonRpcResponse; + +/** + * JSON-RPC 2.0 client implementation for communicating with the Copilot CLI. + */ +class JsonRpcClient implements AutoCloseable { + + private static final Logger LOG = Logger.getLogger(JsonRpcClient.class.getName()); + private static final ObjectMapper MAPPER = createObjectMapper(); + + private final InputStream inputStream; + private final OutputStream outputStream; + private final Socket socket; + private final Process process; + private final AtomicLong requestIdCounter = new AtomicLong(0); + private final Map> pendingRequests = new ConcurrentHashMap<>(); + private final Map> notificationHandlers = new ConcurrentHashMap<>(); + private final ExecutorService readerExecutor; + private volatile boolean running = true; + + private JsonRpcClient(InputStream inputStream, OutputStream outputStream, Socket socket, Process process) { + this.inputStream = inputStream; + this.outputStream = outputStream; + this.socket = socket; + this.process = process; + this.readerExecutor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "jsonrpc-reader"); + t.setDaemon(true); + return t; + }); + startReader(); + } + + static ObjectMapper createObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL); + return mapper; + } + + public static ObjectMapper getObjectMapper() { + return MAPPER; + } + + /** + * Creates a JSON-RPC client using stdio with a process. + */ + public static JsonRpcClient fromProcess(Process process) { + return new JsonRpcClient(process.getInputStream(), process.getOutputStream(), null, process); + } + + /** + * Creates a JSON-RPC client using TCP socket. + */ + public static JsonRpcClient fromSocket(Socket socket) throws IOException { + return new JsonRpcClient(socket.getInputStream(), socket.getOutputStream(), socket, null); + } + + /** + * Registers a handler for JSON-RPC method calls (requests/notifications from + * server). + */ + public void registerMethodHandler(String method, BiConsumer handler) { + notificationHandlers.put(method, handler); + } + + /** + * Sends a JSON-RPC request and waits for the response. + */ + public CompletableFuture invoke(String method, Object params, Class responseType) { + long id = requestIdCounter.incrementAndGet(); + CompletableFuture future = new CompletableFuture<>(); + pendingRequests.put(id, future); + + JsonRpcRequest request = new JsonRpcRequest(); + request.setJsonrpc("2.0"); + request.setId(id); + request.setMethod(method); + request.setParams(params); + + try { + sendMessage(request); + } catch (IOException e) { + pendingRequests.remove(id); + future.completeExceptionally(e); + } + + return future.thenApply(result -> { + try { + if (responseType == Void.class || responseType == void.class) { + return null; + } + return MAPPER.treeToValue(result, responseType); + } catch (JsonProcessingException e) { + throw new CompletionException(e); + } + }); + } + + /** + * Sends a JSON-RPC notification (no response expected). + */ + public void notify(String method, Object params) throws IOException { + JsonRpcRequest notification = new JsonRpcRequest(); + notification.setJsonrpc("2.0"); + notification.setMethod(method); + notification.setParams(params); + sendMessage(notification); + } + + /** + * Sends a JSON-RPC response to a server request. + */ + public void sendResponse(Object id, Object result) throws IOException { + JsonRpcResponse response = new JsonRpcResponse(); + response.setJsonrpc("2.0"); + response.setId(id); + response.setResult(result); + sendMessage(response); + } + + /** + * Sends a JSON-RPC error response to a server request. + */ + public void sendErrorResponse(Object id, int code, String message) throws IOException { + JsonRpcResponse response = new JsonRpcResponse(); + response.setJsonrpc("2.0"); + response.setId(id); + JsonRpcError error = new JsonRpcError(); + error.setCode(code); + error.setMessage(message); + response.setError(error); + sendMessage(response); + } + + private synchronized void sendMessage(Object message) throws IOException { + String json = MAPPER.writeValueAsString(message); + byte[] content = json.getBytes(StandardCharsets.UTF_8); + String header = "Content-Length: " + content.length + "\r\n\r\n"; + + outputStream.write(header.getBytes(StandardCharsets.UTF_8)); + outputStream.write(content); + outputStream.flush(); + + LOG.fine("Sent: " + json); + } + + private void startReader() { + readerExecutor.submit(() -> { + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + + while (running) { + String line = reader.readLine(); + if (line == null) { + break; + } + + // Parse headers + int contentLength = -1; + while (!line.isEmpty()) { + if (line.toLowerCase().startsWith("content-length:")) { + contentLength = Integer.parseInt(line.substring(15).trim()); + } + line = reader.readLine(); + if (line == null) { + return; + } + } + + if (contentLength <= 0) { + continue; + } + + // Read content + char[] buffer = new char[contentLength]; + int read = 0; + while (read < contentLength) { + int result = reader.read(buffer, read, contentLength - read); + if (result == -1) { + return; + } + read += result; + } + + String content = new String(buffer); + LOG.fine("Received: " + content); + + handleMessage(content); + } + } catch (Exception e) { + if (running) { + LOG.log(Level.SEVERE, "Error in JSON-RPC reader", e); + } + } + }); + } + + private void handleMessage(String content) { + try { + JsonNode node = MAPPER.readTree(content); + + // Check if this is a response to our request + if (node.has("id") && !node.get("id").isNull() && (node.has("result") || node.has("error"))) { + long id = node.get("id").asLong(); + CompletableFuture future = pendingRequests.remove(id); + if (future != null) { + if (node.has("error")) { + JsonNode errorNode = node.get("error"); + String errorMessage = errorNode.has("message") + ? errorNode.get("message").asText() + : "Unknown error"; + int errorCode = errorNode.has("code") ? errorNode.get("code").asInt() : -1; + future.completeExceptionally(new JsonRpcException(errorCode, errorMessage)); + } else { + future.complete(node.get("result")); + } + } + } + // Check if this is a request from server (has method and id) + else if (node.has("method")) { + String method = node.get("method").asText(); + JsonNode params = node.get("params"); + Object id = node.has("id") && !node.get("id").isNull() ? node.get("id") : null; + + BiConsumer handler = notificationHandlers.get(method); + if (handler != null) { + try { + // Create a context that includes the request ID for responses + handler.accept(id != null ? id.toString() : null, params); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling method " + method, e); + if (id != null) { + try { + sendErrorResponse(id, -32603, e.getMessage()); + } catch (IOException ioe) { + LOG.log(Level.SEVERE, "Failed to send error response", ioe); + } + } + } + } else { + LOG.fine("No handler for method: " + method); + if (id != null) { + try { + sendErrorResponse(id, -32601, "Method not found: " + method); + } catch (IOException ioe) { + LOG.log(Level.SEVERE, "Failed to send error response", ioe); + } + } + } + } + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error parsing JSON-RPC message", e); + } + } + + @Override + public void close() { + running = false; + readerExecutor.shutdownNow(); + + // Cancel all pending requests + pendingRequests.forEach((id, future) -> future.completeExceptionally(new IOException("Client closed"))); + pendingRequests.clear(); + + try { + if (socket != null) { + socket.close(); + } + } catch (IOException e) { + LOG.log(Level.FINE, "Error closing socket", e); + } + + if (process != null) { + process.destroy(); + } + } + + public boolean isConnected() { + if (socket != null) { + return socket.isConnected() && !socket.isClosed(); + } + if (process != null) { + return process.isAlive(); + } + return false; + } + + public Process getProcess() { + return process; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/JsonRpcException.java b/java/src/main/java/com/github/copilot/sdk/JsonRpcException.java new file mode 100644 index 0000000..2afb7a9 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/JsonRpcException.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +/** + * Exception thrown when a JSON-RPC error occurs during communication with the + * Copilot CLI server. + *

+ * This exception wraps error responses from the JSON-RPC protocol, including + * the error code and message returned by the server. + */ +final class JsonRpcException extends RuntimeException { + + private final int code; + + /** + * Creates a new JSON-RPC exception. + * + * @param code + * the JSON-RPC error code + * @param message + * the error message from the server + */ + public JsonRpcException(int code, String message) { + super(message); + this.code = code; + } + + /** + * Returns the JSON-RPC error code. + *

+ * Standard JSON-RPC error codes include: + *

    + *
  • -32700: Parse error
  • + *
  • -32600: Invalid request
  • + *
  • -32601: Method not found
  • + *
  • -32602: Invalid params
  • + *
  • -32603: Internal error
  • + *
+ * + * @return the error code + */ + public int getCode() { + return code; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java b/java/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java new file mode 100644 index 0000000..3539a59 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// Code generated by update-protocol-version.ts. DO NOT EDIT. + +package com.github.copilot.sdk; + +/** + * Provides the SDK protocol version. This must match the version expected by + * the copilot-agent-runtime server. + */ +public enum SdkProtocolVersion { + + LATEST(1); + + private int versionNumber; + + private SdkProtocolVersion(int versionNumber) { + this.versionNumber = versionNumber; + } + + public int getVersionNumber() { + return this.versionNumber; + } + + /** + * Gets the SDK protocol version. + * + * @return the protocol version + */ + public static int get() { + return LATEST.getVersionNumber(); + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/SystemMessageMode.java b/java/src/main/java/com/github/copilot/sdk/SystemMessageMode.java new file mode 100644 index 0000000..355eb84 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/SystemMessageMode.java @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Specifies how the system message should be applied to a session. + *

+ * The system message controls the behavior and personality of the AI assistant. + * This enum determines whether to append custom instructions to the default + * system message or replace it entirely. + * + * @see com.github.copilot.sdk.json.SystemMessageConfig + */ +public enum SystemMessageMode { + /** + * Append the custom content to the default system message. + *

+ * This mode preserves the default guardrails and behaviors while adding + * additional instructions or context. + */ + APPEND("append"), + + /** + * Replace the default system message entirely with the custom content. + *

+ * Warning: This mode removes all default guardrails and + * behaviors. Use with caution. + */ + REPLACE("replace"); + + private final String value; + + SystemMessageMode(String value) { + this.value = value; + } + + /** + * Returns the JSON value for this mode. + * + * @return the string value used in JSON serialization + */ + @JsonValue + public String getValue() { + return value; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/AbortEvent.java b/java/src/main/java/com/github/copilot/sdk/events/AbortEvent.java new file mode 100644 index 0000000..f05177f --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/AbortEvent.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event: abort + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class AbortEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private AbortData data; + + @Override + public String getType() { + return "abort"; + } + + public AbortData getData() { + return data; + } + + public void setData(AbortData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class AbortData { + + @JsonProperty("reason") + private String reason; + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java b/java/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java new file mode 100644 index 0000000..39cd8e4 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java @@ -0,0 +1,166 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.OffsetDateTime; +import java.util.UUID; + +/** + * Base class for all session events in the Copilot SDK. + *

+ * Session events represent all activities that occur during a Copilot + * conversation, including messages from the user and assistant, tool + * executions, and session state changes. Events are delivered to handlers + * registered via + * {@link com.github.copilot.sdk.CopilotSession#on(java.util.function.Consumer)}. + * + *

Event Categories

+ *
    + *
  • Session events: {@link SessionStartEvent}, + * {@link SessionResumeEvent}, {@link SessionErrorEvent}, + * {@link SessionIdleEvent}, etc.
  • + *
  • Assistant events: {@link AssistantMessageEvent}, + * {@link AssistantMessageDeltaEvent}, {@link AssistantTurnStartEvent}, + * etc.
  • + *
  • Tool events: {@link ToolExecutionStartEvent}, + * {@link ToolExecutionCompleteEvent}, etc.
  • + *
  • User events: {@link UserMessageEvent}
  • + *
+ * + *

Example Usage

+ * + *
{@code
+ * session.on(event -> {
+ * 	if (event instanceof AssistantMessageEvent msg) {
+ * 		System.out.println("Assistant: " + msg.getData().getContent());
+ * 	} else if (event instanceof SessionIdleEvent) {
+ * 		System.out.println("Session is idle");
+ * 	}
+ * });
+ * }
+ * + * @see com.github.copilot.sdk.CopilotSession#on(java.util.function.Consumer) + * @see SessionEventParser + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract sealed class AbstractSessionEvent permits + // Session events + SessionStartEvent, SessionResumeEvent, SessionErrorEvent, SessionIdleEvent, SessionInfoEvent, + SessionModelChangeEvent, SessionHandoffEvent, SessionTruncationEvent, SessionUsageInfoEvent, + SessionCompactionStartEvent, SessionCompactionCompleteEvent, + // Assistant events + AssistantTurnStartEvent, AssistantIntentEvent, AssistantReasoningEvent, AssistantReasoningDeltaEvent, + AssistantMessageEvent, AssistantMessageDeltaEvent, AssistantTurnEndEvent, AssistantUsageEvent, AbortEvent, + // Tool events + ToolUserRequestedEvent, ToolExecutionStartEvent, ToolExecutionPartialResultEvent, ToolExecutionCompleteEvent, + // User events + UserMessageEvent, PendingMessagesModifiedEvent, + // Other events + SubagentStartedEvent, SubagentCompletedEvent, SubagentFailedEvent, SubagentSelectedEvent, HookStartEvent, + HookEndEvent, SystemMessageEvent { + + @JsonProperty("id") + private UUID id; + + @JsonProperty("timestamp") + private OffsetDateTime timestamp; + + @JsonProperty("parentId") + private UUID parentId; + + @JsonProperty("ephemeral") + private Boolean ephemeral; + + /** + * Gets the event type discriminator string. + *

+ * This corresponds to the event type in the JSON protocol (e.g., + * "assistant.message", "session.idle"). + * + * @return the event type string + */ + public abstract String getType(); + + /** + * Gets the unique identifier for this event. + * + * @return the event UUID + */ + public UUID getId() { + return id; + } + + /** + * Sets the event identifier. + * + * @param id + * the event UUID + */ + public void setId(UUID id) { + this.id = id; + } + + /** + * Gets the timestamp when this event occurred. + * + * @return the event timestamp + */ + public OffsetDateTime getTimestamp() { + return timestamp; + } + + /** + * Sets the event timestamp. + * + * @param timestamp + * the event timestamp + */ + public void setTimestamp(OffsetDateTime timestamp) { + this.timestamp = timestamp; + } + + /** + * Gets the parent event ID, if this event is a child of another. + * + * @return the parent event UUID, or {@code null} + */ + public UUID getParentId() { + return parentId; + } + + /** + * Sets the parent event ID. + * + * @param parentId + * the parent event UUID + */ + public void setParentId(UUID parentId) { + this.parentId = parentId; + } + + /** + * Returns whether this is an ephemeral event. + *

+ * Ephemeral events are not persisted in session history. + * + * @return {@code true} if ephemeral, {@code false} otherwise + */ + public Boolean getEphemeral() { + return ephemeral; + } + + /** + * Sets whether this is an ephemeral event. + * + * @param ephemeral + * {@code true} if ephemeral + */ + public void setEphemeral(Boolean ephemeral) { + this.ephemeral = ephemeral; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java b/java/src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java new file mode 100644 index 0000000..b1ee77b --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event: assistant.intent + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class AssistantIntentEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private AssistantIntentData data; + + @Override + public String getType() { + return "assistant.intent"; + } + + public AssistantIntentData getData() { + return data; + } + + public void setData(AssistantIntentData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class AssistantIntentData { + + @JsonProperty("intent") + private String intent; + + public String getIntent() { + return intent; + } + + public void setIntent(String intent) { + this.intent = intent; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java b/java/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java new file mode 100644 index 0000000..fe25edc --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event: assistant.message_delta + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class AssistantMessageDeltaEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private AssistantMessageDeltaData data; + + @Override + public String getType() { + return "assistant.message_delta"; + } + + public AssistantMessageDeltaData getData() { + return data; + } + + public void setData(AssistantMessageDeltaData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class AssistantMessageDeltaData { + + @JsonProperty("messageId") + private String messageId; + + @JsonProperty("deltaContent") + private String deltaContent; + + @JsonProperty("totalResponseSizeBytes") + private Double totalResponseSizeBytes; + + @JsonProperty("parentToolCallId") + private String parentToolCallId; + + public String getMessageId() { + return messageId; + } + + public void setMessageId(String messageId) { + this.messageId = messageId; + } + + public String getDeltaContent() { + return deltaContent; + } + + public void setDeltaContent(String deltaContent) { + this.deltaContent = deltaContent; + } + + public Double getTotalResponseSizeBytes() { + return totalResponseSizeBytes; + } + + public void setTotalResponseSizeBytes(Double totalResponseSizeBytes) { + this.totalResponseSizeBytes = totalResponseSizeBytes; + } + + public String getParentToolCallId() { + return parentToolCallId; + } + + public void setParentToolCallId(String parentToolCallId) { + this.parentToolCallId = parentToolCallId; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java b/java/src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java new file mode 100644 index 0000000..6dc7379 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java @@ -0,0 +1,234 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Event representing a complete message from the assistant. + *

+ * This event is fired when the assistant has finished generating a response. + * For streaming responses, use {@link AssistantMessageDeltaEvent} instead. + * + *

Example Usage

+ * + *
{@code
+ * session.on(event -> {
+ * 	if (event instanceof AssistantMessageEvent msg) {
+ * 		String content = msg.getData().getContent();
+ * 		System.out.println("Assistant: " + content);
+ * 	}
+ * });
+ * }
+ * + * @see AssistantMessageDeltaEvent + * @see AbstractSessionEvent + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class AssistantMessageEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private AssistantMessageData data; + + /** + * {@inheritDoc} + * + * @return "assistant.message" + */ + @Override + public String getType() { + return "assistant.message"; + } + + /** + * Gets the message data. + * + * @return the message data containing content and tool requests + */ + public AssistantMessageData getData() { + return data; + } + + /** + * Sets the message data. + * + * @param data + * the message data + */ + public void setData(AssistantMessageData data) { + this.data = data; + } + + /** + * Contains the assistant message content and metadata. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static class AssistantMessageData { + + @JsonProperty("messageId") + private String messageId; + + @JsonProperty("content") + private String content; + + @JsonProperty("toolRequests") + private List toolRequests; + + @JsonProperty("parentToolCallId") + private String parentToolCallId; + + /** + * Gets the unique message identifier. + * + * @return the message ID + */ + public String getMessageId() { + return messageId; + } + + /** + * Sets the message identifier. + * + * @param messageId + * the message ID + */ + public void setMessageId(String messageId) { + this.messageId = messageId; + } + + /** + * Gets the text content of the assistant's message. + * + * @return the message content + */ + public String getContent() { + return content; + } + + /** + * Sets the message content. + * + * @param content + * the message content + */ + public void setContent(String content) { + this.content = content; + } + + /** + * Gets the list of tool requests made by the assistant. + * + * @return the tool requests, or {@code null} if none + */ + public List getToolRequests() { + return toolRequests; + } + + /** + * Sets the tool requests. + * + * @param toolRequests + * the tool requests + */ + public void setToolRequests(List toolRequests) { + this.toolRequests = toolRequests; + } + + /** + * Gets the parent tool call ID if this message is in response to a tool. + * + * @return the parent tool call ID, or {@code null} + */ + public String getParentToolCallId() { + return parentToolCallId; + } + + /** + * Sets the parent tool call ID. + * + * @param parentToolCallId + * the parent tool call ID + */ + public void setParentToolCallId(String parentToolCallId) { + this.parentToolCallId = parentToolCallId; + } + + /** + * Represents a request from the assistant to invoke a tool. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ToolRequest { + + @JsonProperty("toolCallId") + private String toolCallId; + + @JsonProperty("name") + private String name; + + @JsonProperty("arguments") + private Object arguments; + + /** + * Gets the unique tool call identifier. + * + * @return the tool call ID + */ + public String getToolCallId() { + return toolCallId; + } + + /** + * Sets the tool call identifier. + * + * @param toolCallId + * the tool call ID + */ + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + /** + * Gets the name of the tool to invoke. + * + * @return the tool name + */ + public String getName() { + return name; + } + + /** + * Sets the tool name. + * + * @param name + * the tool name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Gets the arguments to pass to the tool. + * + * @return the tool arguments (typically a Map or JsonNode) + */ + public Object getArguments() { + return arguments; + } + + /** + * Sets the tool arguments. + * + * @param arguments + * the tool arguments + */ + public void setArguments(Object arguments) { + this.arguments = arguments; + } + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/AssistantReasoningDeltaEvent.java b/java/src/main/java/com/github/copilot/sdk/events/AssistantReasoningDeltaEvent.java new file mode 100644 index 0000000..1786e15 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/AssistantReasoningDeltaEvent.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event: assistant.reasoning_delta + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class AssistantReasoningDeltaEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private AssistantReasoningDeltaData data; + + @Override + public String getType() { + return "assistant.reasoning_delta"; + } + + public AssistantReasoningDeltaData getData() { + return data; + } + + public void setData(AssistantReasoningDeltaData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class AssistantReasoningDeltaData { + + @JsonProperty("reasoningId") + private String reasoningId; + + @JsonProperty("deltaContent") + private String deltaContent; + + public String getReasoningId() { + return reasoningId; + } + + public void setReasoningId(String reasoningId) { + this.reasoningId = reasoningId; + } + + public String getDeltaContent() { + return deltaContent; + } + + public void setDeltaContent(String deltaContent) { + this.deltaContent = deltaContent; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/AssistantReasoningEvent.java b/java/src/main/java/com/github/copilot/sdk/events/AssistantReasoningEvent.java new file mode 100644 index 0000000..24c4e9b --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/AssistantReasoningEvent.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event: assistant.reasoning + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class AssistantReasoningEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private AssistantReasoningData data; + + @Override + public String getType() { + return "assistant.reasoning"; + } + + public AssistantReasoningData getData() { + return data; + } + + public void setData(AssistantReasoningData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class AssistantReasoningData { + + @JsonProperty("reasoningId") + private String reasoningId; + + @JsonProperty("content") + private String content; + + public String getReasoningId() { + return reasoningId; + } + + public void setReasoningId(String reasoningId) { + this.reasoningId = reasoningId; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/AssistantTurnEndEvent.java b/java/src/main/java/com/github/copilot/sdk/events/AssistantTurnEndEvent.java new file mode 100644 index 0000000..12aa289 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/AssistantTurnEndEvent.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event: assistant.turn_end + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class AssistantTurnEndEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private AssistantTurnEndData data; + + @Override + public String getType() { + return "assistant.turn_end"; + } + + public AssistantTurnEndData getData() { + return data; + } + + public void setData(AssistantTurnEndData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class AssistantTurnEndData { + + @JsonProperty("turnId") + private String turnId; + + public String getTurnId() { + return turnId; + } + + public void setTurnId(String turnId) { + this.turnId = turnId; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java b/java/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java new file mode 100644 index 0000000..5d7725c --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event: assistant.turn_start + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class AssistantTurnStartEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private AssistantTurnStartData data; + + @Override + public String getType() { + return "assistant.turn_start"; + } + + public AssistantTurnStartData getData() { + return data; + } + + public void setData(AssistantTurnStartData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class AssistantTurnStartData { + + @JsonProperty("turnId") + private String turnId; + + public String getTurnId() { + return turnId; + } + + public void setTurnId(String turnId) { + this.turnId = turnId; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java b/java/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java new file mode 100644 index 0000000..546bc63 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java @@ -0,0 +1,158 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +/** + * Event: assistant.usage + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class AssistantUsageEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private AssistantUsageData data; + + @Override + public String getType() { + return "assistant.usage"; + } + + public AssistantUsageData getData() { + return data; + } + + public void setData(AssistantUsageData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class AssistantUsageData { + + @JsonProperty("model") + private String model; + + @JsonProperty("inputTokens") + private Double inputTokens; + + @JsonProperty("outputTokens") + private Double outputTokens; + + @JsonProperty("cacheReadTokens") + private Double cacheReadTokens; + + @JsonProperty("cacheWriteTokens") + private Double cacheWriteTokens; + + @JsonProperty("cost") + private Double cost; + + @JsonProperty("duration") + private Double duration; + + @JsonProperty("initiator") + private String initiator; + + @JsonProperty("apiCallId") + private String apiCallId; + + @JsonProperty("providerCallId") + private String providerCallId; + + @JsonProperty("quotaSnapshots") + private Map quotaSnapshots; + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public Double getInputTokens() { + return inputTokens; + } + + public void setInputTokens(Double inputTokens) { + this.inputTokens = inputTokens; + } + + public Double getOutputTokens() { + return outputTokens; + } + + public void setOutputTokens(Double outputTokens) { + this.outputTokens = outputTokens; + } + + public Double getCacheReadTokens() { + return cacheReadTokens; + } + + public void setCacheReadTokens(Double cacheReadTokens) { + this.cacheReadTokens = cacheReadTokens; + } + + public Double getCacheWriteTokens() { + return cacheWriteTokens; + } + + public void setCacheWriteTokens(Double cacheWriteTokens) { + this.cacheWriteTokens = cacheWriteTokens; + } + + public Double getCost() { + return cost; + } + + public void setCost(Double cost) { + this.cost = cost; + } + + public Double getDuration() { + return duration; + } + + public void setDuration(Double duration) { + this.duration = duration; + } + + public String getInitiator() { + return initiator; + } + + public void setInitiator(String initiator) { + this.initiator = initiator; + } + + public String getApiCallId() { + return apiCallId; + } + + public void setApiCallId(String apiCallId) { + this.apiCallId = apiCallId; + } + + public String getProviderCallId() { + return providerCallId; + } + + public void setProviderCallId(String providerCallId) { + this.providerCallId = providerCallId; + } + + public Map getQuotaSnapshots() { + return quotaSnapshots; + } + + public void setQuotaSnapshots(Map quotaSnapshots) { + this.quotaSnapshots = quotaSnapshots; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/HookEndEvent.java b/java/src/main/java/com/github/copilot/sdk/events/HookEndEvent.java new file mode 100644 index 0000000..cb8cdb4 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/HookEndEvent.java @@ -0,0 +1,116 @@ +/*--------------------------------------------------------------------------------------------- + * 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: hook.end + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class HookEndEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private HookEndData data; + + @Override + public String getType() { + return "hook.end"; + } + + public HookEndData getData() { + return data; + } + + public void setData(HookEndData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class HookEndData { + + @JsonProperty("hookInvocationId") + private String hookInvocationId; + + @JsonProperty("hookType") + private String hookType; + + @JsonProperty("output") + private Object output; + + @JsonProperty("success") + private boolean success; + + @JsonProperty("error") + private HookError error; + + public String getHookInvocationId() { + return hookInvocationId; + } + + public void setHookInvocationId(String hookInvocationId) { + this.hookInvocationId = hookInvocationId; + } + + public String getHookType() { + return hookType; + } + + public void setHookType(String hookType) { + this.hookType = hookType; + } + + public Object getOutput() { + return output; + } + + public void setOutput(Object output) { + this.output = output; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public HookError getError() { + return error; + } + + public void setError(HookError error) { + this.error = error; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class HookError { + + @JsonProperty("message") + private String message; + + @JsonProperty("stack") + private String stack; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getStack() { + return stack; + } + + public void setStack(String stack) { + this.stack = stack; + } + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/HookStartEvent.java b/java/src/main/java/com/github/copilot/sdk/events/HookStartEvent.java new file mode 100644 index 0000000..3a3d620 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/HookStartEvent.java @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * 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: hook.start + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class HookStartEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private HookStartData data; + + @Override + public String getType() { + return "hook.start"; + } + + public HookStartData getData() { + return data; + } + + public void setData(HookStartData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class HookStartData { + + @JsonProperty("hookInvocationId") + private String hookInvocationId; + + @JsonProperty("hookType") + private String hookType; + + @JsonProperty("input") + private Object input; + + public String getHookInvocationId() { + return hookInvocationId; + } + + public void setHookInvocationId(String hookInvocationId) { + this.hookInvocationId = hookInvocationId; + } + + public String getHookType() { + return hookType; + } + + public void setHookType(String hookType) { + this.hookType = hookType; + } + + public Object getInput() { + return input; + } + + public void setInput(Object input) { + this.input = input; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/PendingMessagesModifiedEvent.java b/java/src/main/java/com/github/copilot/sdk/events/PendingMessagesModifiedEvent.java new file mode 100644 index 0000000..59f72f6 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/PendingMessagesModifiedEvent.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * 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: pending_messages.modified + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class PendingMessagesModifiedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private PendingMessagesModifiedData data; + + @Override + public String getType() { + return "pending_messages.modified"; + } + + public PendingMessagesModifiedData getData() { + return data; + } + + public void setData(PendingMessagesModifiedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class PendingMessagesModifiedData { + // Empty data + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SessionCompactionCompleteEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SessionCompactionCompleteEvent.java new file mode 100644 index 0000000..be6c9bf --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SessionCompactionCompleteEvent.java @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * 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: session.compaction_complete + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SessionCompactionCompleteEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SessionCompactionCompleteData data; + + @Override + public String getType() { + return "session.compaction_complete"; + } + + public SessionCompactionCompleteData getData() { + return data; + } + + public void setData(SessionCompactionCompleteData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SessionCompactionCompleteData { + + @JsonProperty("success") + private boolean success; + + @JsonProperty("error") + private String error; + + @JsonProperty("preCompactionTokens") + private Double preCompactionTokens; + + @JsonProperty("postCompactionTokens") + private Double postCompactionTokens; + + @JsonProperty("preCompactionMessagesLength") + private Double preCompactionMessagesLength; + + @JsonProperty("messagesRemoved") + private Double messagesRemoved; + + @JsonProperty("tokensRemoved") + private Double tokensRemoved; + + @JsonProperty("summaryContent") + private String summaryContent; + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public Double getPreCompactionTokens() { + return preCompactionTokens; + } + + public void setPreCompactionTokens(Double preCompactionTokens) { + this.preCompactionTokens = preCompactionTokens; + } + + public Double getPostCompactionTokens() { + return postCompactionTokens; + } + + public void setPostCompactionTokens(Double postCompactionTokens) { + this.postCompactionTokens = postCompactionTokens; + } + + public Double getPreCompactionMessagesLength() { + return preCompactionMessagesLength; + } + + public void setPreCompactionMessagesLength(Double preCompactionMessagesLength) { + this.preCompactionMessagesLength = preCompactionMessagesLength; + } + + public Double getMessagesRemoved() { + return messagesRemoved; + } + + public void setMessagesRemoved(Double messagesRemoved) { + this.messagesRemoved = messagesRemoved; + } + + public Double getTokensRemoved() { + return tokensRemoved; + } + + public void setTokensRemoved(Double tokensRemoved) { + this.tokensRemoved = tokensRemoved; + } + + public String getSummaryContent() { + return summaryContent; + } + + public void setSummaryContent(String summaryContent) { + this.summaryContent = summaryContent; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SessionCompactionStartEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SessionCompactionStartEvent.java new file mode 100644 index 0000000..e74e627 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SessionCompactionStartEvent.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * 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: session.compaction_start + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SessionCompactionStartEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SessionCompactionStartData data; + + @Override + public String getType() { + return "session.compaction_start"; + } + + public SessionCompactionStartData getData() { + return data; + } + + public void setData(SessionCompactionStartData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SessionCompactionStartData { + // Empty data + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SessionErrorEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SessionErrorEvent.java new file mode 100644 index 0000000..9ecfd16 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SessionErrorEvent.java @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * 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: session.error + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SessionErrorEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SessionErrorData data; + + @Override + public String getType() { + return "session.error"; + } + + public SessionErrorData getData() { + return data; + } + + public void setData(SessionErrorData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SessionErrorData { + + @JsonProperty("errorType") + private String errorType; + + @JsonProperty("message") + private String message; + + @JsonProperty("stack") + private String stack; + + public String getErrorType() { + return errorType; + } + + public void setErrorType(String errorType) { + this.errorType = errorType; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getStack() { + return stack; + } + + public void setStack(String stack) { + this.stack = stack; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java b/java/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java new file mode 100644 index 0000000..e54beb0 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import java.util.HashMap; +import java.util.Map; + +/** + * Parser for session events that handles polymorphic deserialization. + *

+ * This class deserializes JSON event data into the appropriate + * {@link AbstractSessionEvent} subclass based on the "type" field. It is used + * internally by the SDK to convert server events to Java objects. + * + *

Supported Event Types

+ *
    + *
  • Session: session.start, session.resume, session.error, + * session.idle, session.info, etc.
  • + *
  • Assistant: assistant.message, assistant.message_delta, + * assistant.turn_start, assistant.turn_end, etc.
  • + *
  • Tool: tool.execution_start, tool.execution_complete, + * etc.
  • + *
  • User: user.message, pending_messages.modified
  • + *
  • Subagent: subagent.started, subagent.completed, + * etc.
  • + *
+ * + * @see AbstractSessionEvent + */ +public class SessionEventParser { + + private static final Logger LOG = Logger.getLogger(SessionEventParser.class.getName()); + private static final ObjectMapper MAPPER; + private static final Map> TYPE_MAP = new HashMap<>(); + + static { + MAPPER = new ObjectMapper(); + MAPPER.registerModule(new JavaTimeModule()); + MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + TYPE_MAP.put("session.start", SessionStartEvent.class); + TYPE_MAP.put("session.resume", SessionResumeEvent.class); + TYPE_MAP.put("session.error", SessionErrorEvent.class); + TYPE_MAP.put("session.idle", SessionIdleEvent.class); + TYPE_MAP.put("session.info", SessionInfoEvent.class); + TYPE_MAP.put("session.model_change", SessionModelChangeEvent.class); + TYPE_MAP.put("session.handoff", SessionHandoffEvent.class); + TYPE_MAP.put("session.truncation", SessionTruncationEvent.class); + TYPE_MAP.put("session.usage_info", SessionUsageInfoEvent.class); + TYPE_MAP.put("session.compaction_start", SessionCompactionStartEvent.class); + TYPE_MAP.put("session.compaction_complete", SessionCompactionCompleteEvent.class); + TYPE_MAP.put("user.message", UserMessageEvent.class); + TYPE_MAP.put("pending_messages.modified", PendingMessagesModifiedEvent.class); + TYPE_MAP.put("assistant.turn_start", AssistantTurnStartEvent.class); + TYPE_MAP.put("assistant.intent", AssistantIntentEvent.class); + TYPE_MAP.put("assistant.reasoning", AssistantReasoningEvent.class); + TYPE_MAP.put("assistant.reasoning_delta", AssistantReasoningDeltaEvent.class); + TYPE_MAP.put("assistant.message", AssistantMessageEvent.class); + TYPE_MAP.put("assistant.message_delta", AssistantMessageDeltaEvent.class); + TYPE_MAP.put("assistant.turn_end", AssistantTurnEndEvent.class); + TYPE_MAP.put("assistant.usage", AssistantUsageEvent.class); + TYPE_MAP.put("abort", AbortEvent.class); + TYPE_MAP.put("tool.user_requested", ToolUserRequestedEvent.class); + TYPE_MAP.put("tool.execution_start", ToolExecutionStartEvent.class); + TYPE_MAP.put("tool.execution_partial_result", ToolExecutionPartialResultEvent.class); + TYPE_MAP.put("tool.execution_complete", ToolExecutionCompleteEvent.class); + TYPE_MAP.put("subagent.started", SubagentStartedEvent.class); + TYPE_MAP.put("subagent.completed", SubagentCompletedEvent.class); + TYPE_MAP.put("subagent.failed", SubagentFailedEvent.class); + TYPE_MAP.put("subagent.selected", SubagentSelectedEvent.class); + TYPE_MAP.put("hook.start", HookStartEvent.class); + TYPE_MAP.put("hook.end", HookEndEvent.class); + TYPE_MAP.put("system.message", SystemMessageEvent.class); + } + + /** + * Parses a JSON string into the appropriate SessionEvent subclass. + * + * @param json + * the JSON string representing an event + * @return the parsed event, or {@code null} if parsing fails or type is unknown + */ + public static AbstractSessionEvent parse(String json) { + try { + JsonNode node = MAPPER.readTree(json); + String type = node.has("type") ? node.get("type").asText() : null; + + if (type == null) { + LOG.warning("Missing 'type' field in event: " + json); + return null; + } + + Class eventClass = TYPE_MAP.get(type); + if (eventClass == null) { + LOG.fine("Unknown event type: " + type); + return null; + } + + return MAPPER.treeToValue(node, eventClass); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed to parse session event", e); + return null; + } + } + + /** + * Parses a JsonNode into the appropriate SessionEvent subclass. + * + * @param node + * the JSON node representing an event + * @return the parsed event, or {@code null} if parsing fails or type is unknown + */ + public static AbstractSessionEvent parse(JsonNode node) { + try { + String type = node.has("type") ? node.get("type").asText() : null; + + if (type == null) { + LOG.warning("Missing 'type' field in event"); + return null; + } + + Class eventClass = TYPE_MAP.get(type); + if (eventClass == null) { + LOG.fine("Unknown event type: " + type); + return null; + } + + return MAPPER.treeToValue(node, eventClass); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed to parse session event", e); + return null; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SessionHandoffEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SessionHandoffEvent.java new file mode 100644 index 0000000..3dbb22e --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SessionHandoffEvent.java @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.OffsetDateTime; + +/** + * Event: session.handoff + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SessionHandoffEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SessionHandoffData data; + + @Override + public String getType() { + return "session.handoff"; + } + + public SessionHandoffData getData() { + return data; + } + + public void setData(SessionHandoffData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SessionHandoffData { + + @JsonProperty("handoffTime") + private OffsetDateTime handoffTime; + + @JsonProperty("sourceType") + private String sourceType; + + @JsonProperty("repository") + private Repository repository; + + @JsonProperty("context") + private String context; + + @JsonProperty("summary") + private String summary; + + @JsonProperty("remoteSessionId") + private String remoteSessionId; + + public OffsetDateTime getHandoffTime() { + return handoffTime; + } + + public void setHandoffTime(OffsetDateTime handoffTime) { + this.handoffTime = handoffTime; + } + + public String getSourceType() { + return sourceType; + } + + public void setSourceType(String sourceType) { + this.sourceType = sourceType; + } + + public Repository getRepository() { + return repository; + } + + public void setRepository(Repository repository) { + this.repository = repository; + } + + public String getContext() { + return context; + } + + public void setContext(String context) { + this.context = context; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public String getRemoteSessionId() { + return remoteSessionId; + } + + public void setRemoteSessionId(String remoteSessionId) { + this.remoteSessionId = remoteSessionId; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Repository { + + @JsonProperty("owner") + private String owner; + + @JsonProperty("name") + private String name; + + @JsonProperty("branch") + private String branch; + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getBranch() { + return branch; + } + + public void setBranch(String branch) { + this.branch = branch; + } + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SessionIdleEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SessionIdleEvent.java new file mode 100644 index 0000000..23e9dcd --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SessionIdleEvent.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * 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: session.idle + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SessionIdleEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SessionIdleData data; + + @Override + public String getType() { + return "session.idle"; + } + + public SessionIdleData getData() { + return data; + } + + public void setData(SessionIdleData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SessionIdleData { + // Empty data + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SessionInfoEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SessionInfoEvent.java new file mode 100644 index 0000000..664d184 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SessionInfoEvent.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * 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: session.info + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SessionInfoEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SessionInfoData data; + + @Override + public String getType() { + return "session.info"; + } + + public SessionInfoData getData() { + return data; + } + + public void setData(SessionInfoData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SessionInfoData { + + @JsonProperty("infoType") + private String infoType; + + @JsonProperty("message") + private String message; + + public String getInfoType() { + return infoType; + } + + public void setInfoType(String infoType) { + this.infoType = infoType; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SessionModelChangeEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SessionModelChangeEvent.java new file mode 100644 index 0000000..c87cae7 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SessionModelChangeEvent.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * 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: session.model_change + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SessionModelChangeEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SessionModelChangeData data; + + @Override + public String getType() { + return "session.model_change"; + } + + public SessionModelChangeData getData() { + return data; + } + + public void setData(SessionModelChangeData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SessionModelChangeData { + + @JsonProperty("previousModel") + private String previousModel; + + @JsonProperty("newModel") + private String newModel; + + public String getPreviousModel() { + return previousModel; + } + + public void setPreviousModel(String previousModel) { + this.previousModel = previousModel; + } + + public String getNewModel() { + return newModel; + } + + public void setNewModel(String newModel) { + this.newModel = newModel; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SessionResumeEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SessionResumeEvent.java new file mode 100644 index 0000000..12c2e2c --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SessionResumeEvent.java @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.OffsetDateTime; + +/** + * Event: session.resume + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SessionResumeEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SessionResumeData data; + + @Override + public String getType() { + return "session.resume"; + } + + public SessionResumeData getData() { + return data; + } + + public void setData(SessionResumeData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SessionResumeData { + + @JsonProperty("resumeTime") + private OffsetDateTime resumeTime; + + @JsonProperty("eventCount") + private double eventCount; + + public OffsetDateTime getResumeTime() { + return resumeTime; + } + + public void setResumeTime(OffsetDateTime resumeTime) { + this.resumeTime = resumeTime; + } + + public double getEventCount() { + return eventCount; + } + + public void setEventCount(double eventCount) { + this.eventCount = eventCount; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SessionStartEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SessionStartEvent.java new file mode 100644 index 0000000..6602407 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SessionStartEvent.java @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.OffsetDateTime; + +/** + * Event: session.start + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SessionStartEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SessionStartData data; + + @Override + public String getType() { + return "session.start"; + } + + public SessionStartData getData() { + return data; + } + + public void setData(SessionStartData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SessionStartData { + + @JsonProperty("sessionId") + private String sessionId; + + @JsonProperty("version") + private double version; + + @JsonProperty("producer") + private String producer; + + @JsonProperty("copilotVersion") + private String copilotVersion; + + @JsonProperty("startTime") + private OffsetDateTime startTime; + + @JsonProperty("selectedModel") + private String selectedModel; + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + public double getVersion() { + return version; + } + + public void setVersion(double version) { + this.version = version; + } + + public String getProducer() { + return producer; + } + + public void setProducer(String producer) { + this.producer = producer; + } + + public String getCopilotVersion() { + return copilotVersion; + } + + public void setCopilotVersion(String copilotVersion) { + this.copilotVersion = copilotVersion; + } + + public OffsetDateTime getStartTime() { + return startTime; + } + + public void setStartTime(OffsetDateTime startTime) { + this.startTime = startTime; + } + + public String getSelectedModel() { + return selectedModel; + } + + public void setSelectedModel(String selectedModel) { + this.selectedModel = selectedModel; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SessionTruncationEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SessionTruncationEvent.java new file mode 100644 index 0000000..5fbee74 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SessionTruncationEvent.java @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * 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: session.truncation + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SessionTruncationEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SessionTruncationData data; + + @Override + public String getType() { + return "session.truncation"; + } + + public SessionTruncationData getData() { + return data; + } + + public void setData(SessionTruncationData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SessionTruncationData { + + @JsonProperty("tokenLimit") + private double tokenLimit; + + @JsonProperty("preTruncationTokensInMessages") + private double preTruncationTokensInMessages; + + @JsonProperty("preTruncationMessagesLength") + private double preTruncationMessagesLength; + + @JsonProperty("postTruncationTokensInMessages") + private double postTruncationTokensInMessages; + + @JsonProperty("postTruncationMessagesLength") + private double postTruncationMessagesLength; + + @JsonProperty("tokensRemovedDuringTruncation") + private double tokensRemovedDuringTruncation; + + @JsonProperty("messagesRemovedDuringTruncation") + private double messagesRemovedDuringTruncation; + + @JsonProperty("performedBy") + private String performedBy; + + public double getTokenLimit() { + return tokenLimit; + } + + public void setTokenLimit(double tokenLimit) { + this.tokenLimit = tokenLimit; + } + + public double getPreTruncationTokensInMessages() { + return preTruncationTokensInMessages; + } + + public void setPreTruncationTokensInMessages(double preTruncationTokensInMessages) { + this.preTruncationTokensInMessages = preTruncationTokensInMessages; + } + + public double getPreTruncationMessagesLength() { + return preTruncationMessagesLength; + } + + public void setPreTruncationMessagesLength(double preTruncationMessagesLength) { + this.preTruncationMessagesLength = preTruncationMessagesLength; + } + + public double getPostTruncationTokensInMessages() { + return postTruncationTokensInMessages; + } + + public void setPostTruncationTokensInMessages(double postTruncationTokensInMessages) { + this.postTruncationTokensInMessages = postTruncationTokensInMessages; + } + + public double getPostTruncationMessagesLength() { + return postTruncationMessagesLength; + } + + public void setPostTruncationMessagesLength(double postTruncationMessagesLength) { + this.postTruncationMessagesLength = postTruncationMessagesLength; + } + + public double getTokensRemovedDuringTruncation() { + return tokensRemovedDuringTruncation; + } + + public void setTokensRemovedDuringTruncation(double tokensRemovedDuringTruncation) { + this.tokensRemovedDuringTruncation = tokensRemovedDuringTruncation; + } + + public double getMessagesRemovedDuringTruncation() { + return messagesRemovedDuringTruncation; + } + + public void setMessagesRemovedDuringTruncation(double messagesRemovedDuringTruncation) { + this.messagesRemovedDuringTruncation = messagesRemovedDuringTruncation; + } + + public String getPerformedBy() { + return performedBy; + } + + public void setPerformedBy(String performedBy) { + this.performedBy = performedBy; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SessionUsageInfoEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SessionUsageInfoEvent.java new file mode 100644 index 0000000..5077302 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SessionUsageInfoEvent.java @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * 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: session.usage_info + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SessionUsageInfoEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SessionUsageInfoData data; + + @Override + public String getType() { + return "session.usage_info"; + } + + public SessionUsageInfoData getData() { + return data; + } + + public void setData(SessionUsageInfoData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SessionUsageInfoData { + + @JsonProperty("tokenLimit") + private double tokenLimit; + + @JsonProperty("currentTokens") + private double currentTokens; + + @JsonProperty("messagesLength") + private double messagesLength; + + public double getTokenLimit() { + return tokenLimit; + } + + public void setTokenLimit(double tokenLimit) { + this.tokenLimit = tokenLimit; + } + + public double getCurrentTokens() { + return currentTokens; + } + + public void setCurrentTokens(double currentTokens) { + this.currentTokens = currentTokens; + } + + public double getMessagesLength() { + return messagesLength; + } + + public void setMessagesLength(double messagesLength) { + this.messagesLength = messagesLength; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SubagentCompletedEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SubagentCompletedEvent.java new file mode 100644 index 0000000..4caaa2d --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SubagentCompletedEvent.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * 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: subagent.completed + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SubagentCompletedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SubagentCompletedData data; + + @Override + public String getType() { + return "subagent.completed"; + } + + public SubagentCompletedData getData() { + return data; + } + + public void setData(SubagentCompletedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SubagentCompletedData { + + @JsonProperty("toolCallId") + private String toolCallId; + + @JsonProperty("agentName") + private String agentName; + + public String getToolCallId() { + return toolCallId; + } + + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + public String getAgentName() { + return agentName; + } + + public void setAgentName(String agentName) { + this.agentName = agentName; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SubagentFailedEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SubagentFailedEvent.java new file mode 100644 index 0000000..ca6fb2c --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SubagentFailedEvent.java @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * 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: subagent.failed + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SubagentFailedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SubagentFailedData data; + + @Override + public String getType() { + return "subagent.failed"; + } + + public SubagentFailedData getData() { + return data; + } + + public void setData(SubagentFailedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SubagentFailedData { + + @JsonProperty("toolCallId") + private String toolCallId; + + @JsonProperty("agentName") + private String agentName; + + @JsonProperty("error") + private String error; + + public String getToolCallId() { + return toolCallId; + } + + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + public String getAgentName() { + return agentName; + } + + public void setAgentName(String agentName) { + this.agentName = agentName; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SubagentSelectedEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SubagentSelectedEvent.java new file mode 100644 index 0000000..9d1c159 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SubagentSelectedEvent.java @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * 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: subagent.selected + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SubagentSelectedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SubagentSelectedData data; + + @Override + public String getType() { + return "subagent.selected"; + } + + public SubagentSelectedData getData() { + return data; + } + + public void setData(SubagentSelectedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SubagentSelectedData { + + @JsonProperty("agentName") + private String agentName; + + @JsonProperty("agentDisplayName") + private String agentDisplayName; + + @JsonProperty("tools") + private String[] tools; + + public String getAgentName() { + return agentName; + } + + public void setAgentName(String agentName) { + this.agentName = agentName; + } + + public String getAgentDisplayName() { + return agentDisplayName; + } + + public void setAgentDisplayName(String agentDisplayName) { + this.agentDisplayName = agentDisplayName; + } + + public String[] getTools() { + return tools; + } + + public void setTools(String[] tools) { + this.tools = tools; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SubagentStartedEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SubagentStartedEvent.java new file mode 100644 index 0000000..4d1f22a --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SubagentStartedEvent.java @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event: subagent.started + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SubagentStartedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SubagentStartedData data; + + @Override + public String getType() { + return "subagent.started"; + } + + public SubagentStartedData getData() { + return data; + } + + public void setData(SubagentStartedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SubagentStartedData { + + @JsonProperty("toolCallId") + private String toolCallId; + + @JsonProperty("agentName") + private String agentName; + + @JsonProperty("agentDisplayName") + private String agentDisplayName; + + @JsonProperty("agentDescription") + private String agentDescription; + + public String getToolCallId() { + return toolCallId; + } + + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + public String getAgentName() { + return agentName; + } + + public void setAgentName(String agentName) { + this.agentName = agentName; + } + + public String getAgentDisplayName() { + return agentDisplayName; + } + + public void setAgentDisplayName(String agentDisplayName) { + this.agentDisplayName = agentDisplayName; + } + + public String getAgentDescription() { + return agentDescription; + } + + public void setAgentDescription(String agentDescription) { + this.agentDescription = agentDescription; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/SystemMessageEvent.java b/java/src/main/java/com/github/copilot/sdk/events/SystemMessageEvent.java new file mode 100644 index 0000000..9da3feb --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/SystemMessageEvent.java @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +/** + * Event: system.message + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SystemMessageEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SystemMessageData data; + + @Override + public String getType() { + return "system.message"; + } + + public SystemMessageData getData() { + return data; + } + + public void setData(SystemMessageData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SystemMessageData { + + @JsonProperty("content") + private String content; + + @JsonProperty("type") + private String type; + + @JsonProperty("metadata") + private Map metadata; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/ToolExecutionCompleteEvent.java b/java/src/main/java/com/github/copilot/sdk/events/ToolExecutionCompleteEvent.java new file mode 100644 index 0000000..e196176 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/ToolExecutionCompleteEvent.java @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +/** + * Event: tool.execution_complete + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ToolExecutionCompleteEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private ToolExecutionCompleteData data; + + @Override + public String getType() { + return "tool.execution_complete"; + } + + public ToolExecutionCompleteData getData() { + return data; + } + + public void setData(ToolExecutionCompleteData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ToolExecutionCompleteData { + + @JsonProperty("toolCallId") + private String toolCallId; + + @JsonProperty("success") + private boolean success; + + @JsonProperty("isUserRequested") + private Boolean isUserRequested; + + @JsonProperty("result") + private Result result; + + @JsonProperty("error") + private Error error; + + @JsonProperty("toolTelemetry") + private Map toolTelemetry; + + @JsonProperty("parentToolCallId") + private String parentToolCallId; + + public String getToolCallId() { + return toolCallId; + } + + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public Boolean getIsUserRequested() { + return isUserRequested; + } + + public void setIsUserRequested(Boolean isUserRequested) { + this.isUserRequested = isUserRequested; + } + + public Result getResult() { + return result; + } + + public void setResult(Result result) { + this.result = result; + } + + public Error getError() { + return error; + } + + public void setError(Error error) { + this.error = error; + } + + public Map getToolTelemetry() { + return toolTelemetry; + } + + public void setToolTelemetry(Map toolTelemetry) { + this.toolTelemetry = toolTelemetry; + } + + public String getParentToolCallId() { + return parentToolCallId; + } + + public void setParentToolCallId(String parentToolCallId) { + this.parentToolCallId = parentToolCallId; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Result { + + @JsonProperty("content") + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Error { + + @JsonProperty("message") + private String message; + + @JsonProperty("code") + private String code; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/ToolExecutionPartialResultEvent.java b/java/src/main/java/com/github/copilot/sdk/events/ToolExecutionPartialResultEvent.java new file mode 100644 index 0000000..bd2f164 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/ToolExecutionPartialResultEvent.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * 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: tool.execution_partial_result + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ToolExecutionPartialResultEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private ToolExecutionPartialResultData data; + + @Override + public String getType() { + return "tool.execution_partial_result"; + } + + public ToolExecutionPartialResultData getData() { + return data; + } + + public void setData(ToolExecutionPartialResultData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ToolExecutionPartialResultData { + + @JsonProperty("toolCallId") + private String toolCallId; + + @JsonProperty("partialOutput") + private String partialOutput; + + public String getToolCallId() { + return toolCallId; + } + + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + public String getPartialOutput() { + return partialOutput; + } + + public void setPartialOutput(String partialOutput) { + this.partialOutput = partialOutput; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/ToolExecutionStartEvent.java b/java/src/main/java/com/github/copilot/sdk/events/ToolExecutionStartEvent.java new file mode 100644 index 0000000..dafa24a --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/ToolExecutionStartEvent.java @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event: tool.execution_start + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ToolExecutionStartEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private ToolExecutionStartData data; + + @Override + public String getType() { + return "tool.execution_start"; + } + + public ToolExecutionStartData getData() { + return data; + } + + public void setData(ToolExecutionStartData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ToolExecutionStartData { + + @JsonProperty("toolCallId") + private String toolCallId; + + @JsonProperty("toolName") + private String toolName; + + @JsonProperty("arguments") + private Object arguments; + + @JsonProperty("parentToolCallId") + private String parentToolCallId; + + public String getToolCallId() { + return toolCallId; + } + + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + public String getToolName() { + return toolName; + } + + public void setToolName(String toolName) { + this.toolName = toolName; + } + + public Object getArguments() { + return arguments; + } + + public void setArguments(Object arguments) { + this.arguments = arguments; + } + + public String getParentToolCallId() { + return parentToolCallId; + } + + public void setParentToolCallId(String parentToolCallId) { + this.parentToolCallId = parentToolCallId; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/ToolUserRequestedEvent.java b/java/src/main/java/com/github/copilot/sdk/events/ToolUserRequestedEvent.java new file mode 100644 index 0000000..302c7c2 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/ToolUserRequestedEvent.java @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * 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: tool.user_requested + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ToolUserRequestedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private ToolUserRequestedData data; + + @Override + public String getType() { + return "tool.user_requested"; + } + + public ToolUserRequestedData getData() { + return data; + } + + public void setData(ToolUserRequestedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ToolUserRequestedData { + + @JsonProperty("toolCallId") + private String toolCallId; + + @JsonProperty("toolName") + private String toolName; + + @JsonProperty("arguments") + private Object arguments; + + public String getToolCallId() { + return toolCallId; + } + + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + public String getToolName() { + return toolName; + } + + public void setToolName(String toolName) { + this.toolName = toolName; + } + + public Object getArguments() { + return arguments; + } + + public void setArguments(Object arguments) { + this.arguments = arguments; + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/events/UserMessageEvent.java b/java/src/main/java/com/github/copilot/sdk/events/UserMessageEvent.java new file mode 100644 index 0000000..201b2c9 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/events/UserMessageEvent.java @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Event: user.message + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class UserMessageEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private UserMessageData data; + + @Override + public String getType() { + return "user.message"; + } + + public UserMessageData getData() { + return data; + } + + public void setData(UserMessageData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class UserMessageData { + + @JsonProperty("content") + private String content; + + @JsonProperty("transformedContent") + private String transformedContent; + + @JsonProperty("attachments") + private List attachments; + + @JsonProperty("source") + private String source; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getTransformedContent() { + return transformedContent; + } + + public void setTransformedContent(String transformedContent) { + this.transformedContent = transformedContent; + } + + public List getAttachments() { + return attachments; + } + + public void setAttachments(List attachments) { + this.attachments = attachments; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Attachment { + + @JsonProperty("type") + private String type; + + @JsonProperty("path") + private String path; + + @JsonProperty("displayName") + private String displayName; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/Attachment.java b/java/src/main/java/com/github/copilot/sdk/json/Attachment.java new file mode 100644 index 0000000..abcbbc0 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/Attachment.java @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Represents a file attachment to include with a message. + *

+ * Attachments provide additional context to the AI assistant, such as source + * code files, documents, or other relevant content. All setter methods return + * {@code this} for method chaining. + * + *

Example Usage

+ * + *
{@code
+ * var attachment = new Attachment().setType("file").setPath("/path/to/source.java").setDisplayName("Main Source File");
+ * }
+ * + * @see MessageOptions#setAttachments(java.util.List) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Attachment { + + @JsonProperty("type") + private String type; + + @JsonProperty("path") + private String path; + + @JsonProperty("displayName") + private String displayName; + + /** + * Gets the attachment type. + * + * @return the type (e.g., "file") + */ + public String getType() { + return type; + } + + /** + * Sets the attachment type. + *

+ * Currently supported types: + *

    + *
  • "file" - A file from the filesystem
  • + *
+ * + * @param type + * the attachment type + * @return this attachment for method chaining + */ + public Attachment setType(String type) { + this.type = type; + return this; + } + + /** + * Gets the file path. + * + * @return the absolute path to the file + */ + public String getPath() { + return path; + } + + /** + * Sets the file path. + *

+ * This should be an absolute path to the file on the filesystem. + * + * @param path + * the absolute file path + * @return this attachment for method chaining + */ + public Attachment setPath(String path) { + this.path = path; + return this; + } + + /** + * Gets the display name. + * + * @return the display name for the attachment + */ + public String getDisplayName() { + return displayName; + } + + /** + * Sets a human-readable display name for the attachment. + *

+ * This name is shown to the assistant and may be used when referring to the + * file in responses. + * + * @param displayName + * the display name + * @return this attachment for method chaining + */ + public Attachment setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/AzureOptions.java b/java/src/main/java/com/github/copilot/sdk/json/AzureOptions.java new file mode 100644 index 0000000..41f98ba --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/AzureOptions.java @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Azure OpenAI-specific configuration options. + *

+ * When using a BYOK (Bring Your Own Key) setup with Azure OpenAI, this class + * allows you to specify Azure-specific settings such as the API version to use. + * + *

Example Usage

+ * + *
{@code
+ * var provider = new ProviderConfig().setType("azure-openai").setHost("your-resource.openai.azure.com")
+ * 		.setApiKey("your-api-key").setAzure(new AzureOptions().setApiVersion("2024-02-01"));
+ * }
+ * + * @see ProviderConfig#setAzure(AzureOptions) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AzureOptions { + + @JsonProperty("apiVersion") + private String apiVersion; + + /** + * Gets the Azure OpenAI API version. + * + * @return the API version string + */ + public String getApiVersion() { + return apiVersion; + } + + /** + * Sets the Azure OpenAI API version to use. + *

+ * Examples: {@code "2024-02-01"}, {@code "2023-12-01-preview"} + * + * @param apiVersion + * the API version string + * @return this options object for method chaining + */ + public AzureOptions setApiVersion(String apiVersion) { + this.apiVersion = apiVersion; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/java/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java new file mode 100644 index 0000000..368ab89 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -0,0 +1,296 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Map; +import java.util.logging.Logger; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Configuration options for creating a + * {@link com.github.copilot.sdk.CopilotClient}. + *

+ * This class provides a fluent API for configuring how the client connects to + * and manages the Copilot CLI server. All setter methods return {@code this} + * for method chaining. + * + *

Example Usage

+ * + *
{@code
+ * var options = new CopilotClientOptions().setCliPath("/usr/local/bin/copilot").setLogLevel("debug")
+ * 		.setAutoStart(true);
+ *
+ * var client = new CopilotClient(options);
+ * }
+ * + * @see com.github.copilot.sdk.CopilotClient + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CopilotClientOptions { + + private String cliPath; + private String[] cliArgs; + private String cwd; + private int port; + private boolean useStdio = true; + private String cliUrl; + private String logLevel = "info"; + private boolean autoStart = true; + private boolean autoRestart = true; + private Map environment; + private Logger logger; + + /** + * Gets the path to the Copilot CLI executable. + * + * @return the CLI path, or {@code null} to use "copilot" from PATH + */ + public String getCliPath() { + return cliPath; + } + + /** + * Sets the path to the Copilot CLI executable. + * + * @param cliPath + * the path to the CLI executable, or {@code null} to use "copilot" + * from PATH + * @return this options instance for method chaining + */ + public CopilotClientOptions setCliPath(String cliPath) { + this.cliPath = cliPath; + return this; + } + + /** + * Gets the extra CLI arguments. + * + * @return the extra arguments to pass to the CLI + */ + public String[] getCliArgs() { + return cliArgs; + } + + /** + * Sets extra arguments to pass to the CLI process. + *

+ * These arguments are prepended before SDK-managed flags. + * + * @param cliArgs + * the extra arguments to pass + * @return this options instance for method chaining + */ + public CopilotClientOptions setCliArgs(String[] cliArgs) { + this.cliArgs = cliArgs; + return this; + } + + /** + * Gets the working directory for the CLI process. + * + * @return the working directory path + */ + public String getCwd() { + return cwd; + } + + /** + * Sets the working directory for the CLI process. + * + * @param cwd + * the working directory path + * @return this options instance for method chaining + */ + public CopilotClientOptions setCwd(String cwd) { + this.cwd = cwd; + return this; + } + + /** + * Gets the TCP port for the CLI server. + * + * @return the port number, or 0 for a random port + */ + public int getPort() { + return port; + } + + /** + * Sets the TCP port for the CLI server to listen on. + *

+ * This is only used when {@link #isUseStdio()} is {@code false}. + * + * @param port + * the port number, or 0 for a random port + * @return this options instance for method chaining + */ + public CopilotClientOptions setPort(int port) { + this.port = port; + return this; + } + + /** + * Returns whether to use stdio transport instead of TCP. + * + * @return {@code true} to use stdio (default), {@code false} to use TCP + */ + public boolean isUseStdio() { + return useStdio; + } + + /** + * Sets whether to use stdio transport instead of TCP. + *

+ * Stdio transport is more efficient and is the default. TCP transport can be + * useful for debugging or connecting to remote servers. + * + * @param useStdio + * {@code true} to use stdio, {@code false} to use TCP + * @return this options instance for method chaining + */ + public CopilotClientOptions setUseStdio(boolean useStdio) { + this.useStdio = useStdio; + return this; + } + + /** + * Gets the URL of an existing CLI server to connect to. + * + * @return the CLI server URL, or {@code null} to spawn a new process + */ + public String getCliUrl() { + return cliUrl; + } + + /** + * Sets the URL of an existing CLI server to connect to. + *

+ * When provided, the client will not spawn a CLI process but will connect to + * the specified URL instead. Format: "host:port" or "http://host:port". + *

+ * Note: This is mutually exclusive with + * {@link #setUseStdio(boolean)} and {@link #setCliPath(String)}. + * + * @param cliUrl + * the CLI server URL to connect to + * @return this options instance for method chaining + */ + public CopilotClientOptions setCliUrl(String cliUrl) { + this.cliUrl = cliUrl; + return this; + } + + /** + * Gets the log level for the CLI process. + * + * @return the log level (default: "info") + */ + public String getLogLevel() { + return logLevel; + } + + /** + * Sets the log level for the CLI process. + *

+ * Valid levels include: "error", "warn", "info", "debug", "trace". + * + * @param logLevel + * the log level + * @return this options instance for method chaining + */ + public CopilotClientOptions setLogLevel(String logLevel) { + this.logLevel = logLevel; + return this; + } + + /** + * Returns whether the client should automatically start the server. + * + * @return {@code true} to auto-start (default), {@code false} for manual start + */ + public boolean isAutoStart() { + return autoStart; + } + + /** + * Sets whether the client should automatically start the CLI server when the + * first request is made. + * + * @param autoStart + * {@code true} to auto-start, {@code false} for manual start + * @return this options instance for method chaining + */ + public CopilotClientOptions setAutoStart(boolean autoStart) { + this.autoStart = autoStart; + return this; + } + + /** + * Returns whether the client should automatically restart the server on crash. + * + * @return {@code true} to auto-restart (default), {@code false} otherwise + */ + public boolean isAutoRestart() { + return autoRestart; + } + + /** + * Sets whether the client should automatically restart the CLI server if it + * crashes unexpectedly. + * + * @param autoRestart + * {@code true} to auto-restart, {@code false} otherwise + * @return this options instance for method chaining + */ + public CopilotClientOptions setAutoRestart(boolean autoRestart) { + this.autoRestart = autoRestart; + return this; + } + + /** + * Gets the environment variables for the CLI process. + * + * @return the environment variables map + */ + public Map getEnvironment() { + return environment; + } + + /** + * Sets environment variables to pass to the CLI process. + *

+ * When set, these environment variables replace the inherited environment. + * + * @param environment + * the environment variables map + * @return this options instance for method chaining + */ + public CopilotClientOptions setEnvironment(Map environment) { + this.environment = environment; + return this; + } + + /** + * Gets the custom logger for the client. + * + * @return the logger instance + */ + public Logger getLogger() { + return logger; + } + + /** + * Sets a custom logger for the client. + * + * @param logger + * the logger instance to use + * @return this options instance for method chaining + */ + public CopilotClientOptions setLogger(Logger logger) { + this.logger = logger; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java b/java/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java new file mode 100644 index 0000000..c725a23 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal request object for creating a new session. + *

+ * This is a low-level class for JSON-RPC communication. For creating sessions, + * use + * {@link com.github.copilot.sdk.CopilotClient#createSession(SessionConfig)}. + * + * @see com.github.copilot.sdk.CopilotClient#createSession(SessionConfig) + * @see SessionConfig + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class CreateSessionRequest { + + @JsonProperty("model") + private String model; + + @JsonProperty("sessionId") + private String sessionId; + + @JsonProperty("tools") + private List tools; + + @JsonProperty("systemMessage") + private SystemMessageConfig systemMessage; + + @JsonProperty("availableTools") + private List availableTools; + + @JsonProperty("excludedTools") + private List excludedTools; + + @JsonProperty("provider") + private ProviderConfig provider; + + @JsonProperty("requestPermission") + private Boolean requestPermission; + + @JsonProperty("streaming") + private Boolean streaming; + + @JsonProperty("mcpServers") + private Map mcpServers; + + @JsonProperty("customAgents") + private List customAgents; + + /** Gets the model name. @return the model */ + public String getModel() { + return model; + } + + /** Sets the model name. @param model the model */ + public void setModel(String model) { + this.model = model; + } + + /** Gets the session ID. @return the session ID */ + public String getSessionId() { + return sessionId; + } + + /** Sets the session ID. @param sessionId the session ID */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** Gets the tools. @return the tool definitions */ + public List getTools() { + return tools; + } + + /** Sets the tools. @param tools the tool definitions */ + public void setTools(List tools) { + this.tools = tools; + } + + /** Gets the system message config. @return the config */ + public SystemMessageConfig getSystemMessage() { + return systemMessage; + } + + /** Sets the system message config. @param systemMessage the config */ + public void setSystemMessage(SystemMessageConfig systemMessage) { + this.systemMessage = systemMessage; + } + + /** Gets available tools. @return the tool names */ + public List getAvailableTools() { + return availableTools; + } + + /** Sets available tools. @param availableTools the tool names */ + public void setAvailableTools(List availableTools) { + this.availableTools = availableTools; + } + + /** Gets excluded tools. @return the tool names */ + public List getExcludedTools() { + return excludedTools; + } + + /** Sets excluded tools. @param excludedTools the tool names */ + public void setExcludedTools(List excludedTools) { + this.excludedTools = excludedTools; + } + + /** Gets the provider config. @return the provider */ + public ProviderConfig getProvider() { + return provider; + } + + /** Sets the provider config. @param provider the provider */ + public void setProvider(ProviderConfig provider) { + this.provider = provider; + } + + /** Gets request permission flag. @return the flag */ + public Boolean getRequestPermission() { + return requestPermission; + } + + /** Sets request permission flag. @param requestPermission the flag */ + public void setRequestPermission(Boolean requestPermission) { + this.requestPermission = requestPermission; + } + + /** Gets streaming flag. @return the flag */ + public Boolean getStreaming() { + return streaming; + } + + /** Sets streaming flag. @param streaming the flag */ + public void setStreaming(Boolean streaming) { + this.streaming = streaming; + } + + /** Gets MCP servers. @return the servers map */ + public Map getMcpServers() { + return mcpServers; + } + + /** Sets MCP servers. @param mcpServers the servers map */ + public void setMcpServers(Map mcpServers) { + this.mcpServers = mcpServers; + } + + /** Gets custom agents. @return the agents */ + public List getCustomAgents() { + return customAgents; + } + + /** Sets custom agents. @param customAgents the agents */ + public void setCustomAgents(List customAgents) { + this.customAgents = customAgents; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java b/java/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java new file mode 100644 index 0000000..9304178 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java @@ -0,0 +1,17 @@ +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class CreateSessionResponse { + @JsonProperty("sessionId") + private String sessionId; + + public String getSessionId() { + return sessionId; + } + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java b/java/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java new file mode 100644 index 0000000..ff4a20c --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java @@ -0,0 +1,212 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Configuration for a custom agent in a Copilot session. + *

+ * Custom agents extend the capabilities of the base Copilot assistant with + * specialized behavior, tools, and prompts. Each agent can be referenced in + * messages using the {@code @agent-name} mention syntax. + * + *

Example Usage

+ * + *
{@code
+ * var agent = new CustomAgentConfig().setName("code-reviewer").setDisplayName("Code Reviewer")
+ * 		.setDescription("Reviews code for best practices").setPrompt("You are a code review expert...")
+ * 		.setTools(List.of("read_file", "search_code"));
+ *
+ * var config = new SessionConfig().setCustomAgents(List.of(agent));
+ * }
+ * + * @see SessionConfig#setCustomAgents(List) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CustomAgentConfig { + + @JsonProperty("name") + private String name; + + @JsonProperty("displayName") + private String displayName; + + @JsonProperty("description") + private String description; + + @JsonProperty("tools") + private List tools; + + @JsonProperty("prompt") + private String prompt; + + @JsonProperty("mcpServers") + private Map mcpServers; + + @JsonProperty("infer") + private Boolean infer; + + /** + * Gets the unique identifier name for this agent. + * + * @return the agent name used for {@code @mentions} + */ + public String getName() { + return name; + } + + /** + * Sets the unique identifier name for this agent. + *

+ * This name is used to mention the agent in messages (e.g., + * {@code @code-reviewer}). + * + * @param name + * the agent identifier (alphanumeric and hyphens) + * @return this config for method chaining + */ + public CustomAgentConfig setName(String name) { + this.name = name; + return this; + } + + /** + * Gets the human-readable display name. + * + * @return the display name shown to users + */ + public String getDisplayName() { + return displayName; + } + + /** + * Sets the human-readable display name. + * + * @param displayName + * the friendly name for the agent + * @return this config for method chaining + */ + public CustomAgentConfig setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + + /** + * Gets the agent description. + * + * @return the description of what this agent does + */ + public String getDescription() { + return description; + } + + /** + * Sets a description of the agent's capabilities. + *

+ * This helps users understand when to use this agent. + * + * @param description + * the agent description + * @return this config for method chaining + */ + public CustomAgentConfig setDescription(String description) { + this.description = description; + return this; + } + + /** + * Gets the list of tool names available to this agent. + * + * @return the list of tool identifiers + */ + public List getTools() { + return tools; + } + + /** + * Sets the tools available to this agent. + *

+ * These can reference both built-in tools and custom tools registered in the + * session. + * + * @param tools + * the list of tool names + * @return this config for method chaining + */ + public CustomAgentConfig setTools(List tools) { + this.tools = tools; + return this; + } + + /** + * Gets the system prompt for this agent. + * + * @return the agent's system prompt + */ + public String getPrompt() { + return prompt; + } + + /** + * Sets the system prompt that defines this agent's behavior. + *

+ * This prompt is used to customize the agent's responses and capabilities. + * + * @param prompt + * the system prompt + * @return this config for method chaining + */ + public CustomAgentConfig setPrompt(String prompt) { + this.prompt = prompt; + return this; + } + + /** + * Gets the MCP server configurations for this agent. + * + * @return the MCP servers map + */ + public Map getMcpServers() { + return mcpServers; + } + + /** + * Sets MCP (Model Context Protocol) servers available to this agent. + * + * @param mcpServers + * the MCP server configurations + * @return this config for method chaining + */ + public CustomAgentConfig setMcpServers(Map mcpServers) { + this.mcpServers = mcpServers; + return this; + } + + /** + * Gets whether inference mode is enabled. + * + * @return the infer flag, or {@code null} if not set + */ + public Boolean getInfer() { + return infer; + } + + /** + * Sets whether to enable inference mode for this agent. + * + * @param infer + * {@code true} to enable inference mode + * @return this config for method chaining + */ + public CustomAgentConfig setInfer(Boolean infer) { + this.infer = infer; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/DeleteSessionResponse.java b/java/src/main/java/com/github/copilot/sdk/json/DeleteSessionResponse.java new file mode 100644 index 0000000..0ef2c2f --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/DeleteSessionResponse.java @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal response object from deleting a session. + *

+ * This is a low-level class for JSON-RPC communication containing the result of + * a session deletion operation. + * + * @see com.github.copilot.sdk.CopilotClient#deleteSession(String) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class DeleteSessionResponse { + + @JsonProperty("success") + private boolean success; + + @JsonProperty("error") + private String error; + + /** + * Returns whether the deletion was successful. + * + * @return {@code true} if the session was deleted successfully + */ + public boolean isSuccess() { + return success; + } + + /** + * Sets whether the deletion was successful. + * + * @param success + * {@code true} if successful + */ + public void setSuccess(boolean success) { + this.success = success; + } + + /** + * Gets the error message if the deletion failed. + * + * @return the error message, or {@code null} if successful + */ + public String getError() { + return error; + } + + /** + * Sets the error message. + * + * @param error + * the error message + */ + public void setError(String error) { + this.error = error; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/GetLastSessionIdResponse.java b/java/src/main/java/com/github/copilot/sdk/json/GetLastSessionIdResponse.java new file mode 100644 index 0000000..b861820 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/GetLastSessionIdResponse.java @@ -0,0 +1,17 @@ +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class GetLastSessionIdResponse { + @JsonProperty("sessionId") + private String sessionId; + + public String getSessionId() { + return sessionId; + } + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/GetMessagesResponse.java b/java/src/main/java/com/github/copilot/sdk/json/GetMessagesResponse.java new file mode 100644 index 0000000..30470f2 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/GetMessagesResponse.java @@ -0,0 +1,22 @@ +package com.github.copilot.sdk.json; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class GetMessagesResponse { + + @JsonProperty("events") + private List events; + + public List getEvents() { + return events; + } + + public void setEvents(List events) { + this.events = events; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/JsonRpcError.java b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcError.java new file mode 100644 index 0000000..f599079 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcError.java @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * JSON-RPC 2.0 error structure. + *

+ * This is an internal class representing an error in a JSON-RPC response. It + * contains an error code, message, and optional additional data. + * + *

Standard Error Codes

+ *
    + *
  • -32700: Parse error
  • + *
  • -32600: Invalid Request
  • + *
  • -32601: Method not found
  • + *
  • -32602: Invalid params
  • + *
  • -32603: Internal error
  • + *
+ * + * @see JsonRpcResponse + * @see JSON-RPC + * Error Object + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class JsonRpcError { + + @JsonProperty("code") + private int code; + + @JsonProperty("message") + private String message; + + @JsonProperty("data") + private Object data; + + /** + * Gets the error code. + * + * @return the integer error code + */ + public int getCode() { + return code; + } + + /** + * Sets the error code. + * + * @param code + * the integer error code + */ + public void setCode(int code) { + this.code = code; + } + + /** + * Gets the error message. + * + * @return the human-readable error message + */ + public String getMessage() { + return message; + } + + /** + * Sets the error message. + * + * @param message + * the error message + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * Gets the additional error data. + * + * @return the additional data, or {@code null} if none + */ + public Object getData() { + return data; + } + + /** + * Sets the additional error data. + * + * @param data + * the additional data + */ + public void setData(Object data) { + this.data = data; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/JsonRpcRequest.java b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcRequest.java new file mode 100644 index 0000000..e4f4692 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcRequest.java @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * JSON-RPC 2.0 request structure. + *

+ * This is an internal class representing the wire format of a JSON-RPC request. + * It follows the JSON-RPC 2.0 specification. + * + * @see JsonRpcResponse + * @see JSON-RPC 2.0 + * Specification + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class JsonRpcRequest { + + @JsonProperty("jsonrpc") + private String jsonrpc; + + @JsonProperty("id") + private Long id; + + @JsonProperty("method") + private String method; + + @JsonProperty("params") + private Object params; + + /** + * Gets the JSON-RPC version. + * + * @return the version string (should be "2.0") + */ + public String getJsonrpc() { + return jsonrpc; + } + + /** + * Sets the JSON-RPC version. + * + * @param jsonrpc + * the version string + */ + public void setJsonrpc(String jsonrpc) { + this.jsonrpc = jsonrpc; + } + + /** + * Gets the request ID. + * + * @return the request identifier + */ + public Long getId() { + return id; + } + + /** + * Sets the request ID. + * + * @param id + * the request identifier + */ + public void setId(Long id) { + this.id = id; + } + + /** + * Gets the method name. + * + * @return the RPC method to invoke + */ + public String getMethod() { + return method; + } + + /** + * Sets the method name. + * + * @param method + * the RPC method to invoke + */ + public void setMethod(String method) { + this.method = method; + } + + /** + * Gets the method parameters. + * + * @return the parameters object + */ + public Object getParams() { + return params; + } + + /** + * Sets the method parameters. + * + * @param params + * the parameters object + */ + public void setParams(Object params) { + this.params = params; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/JsonRpcResponse.java b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcResponse.java new file mode 100644 index 0000000..9eb3dd9 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcResponse.java @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * JSON-RPC 2.0 response structure. + *

+ * This is an internal class representing the wire format of a JSON-RPC + * response. It follows the JSON-RPC 2.0 specification. A response contains + * either a result or an error, but not both. + * + * @see JsonRpcRequest + * @see JsonRpcError + * @see JSON-RPC 2.0 + * Specification + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class JsonRpcResponse { + + @JsonProperty("jsonrpc") + private String jsonrpc; + + @JsonProperty("id") + private Object id; + + @JsonProperty("result") + private Object result; + + @JsonProperty("error") + private JsonRpcError error; + + /** + * Gets the JSON-RPC version. + * + * @return the version string (should be "2.0") + */ + public String getJsonrpc() { + return jsonrpc; + } + + /** + * Sets the JSON-RPC version. + * + * @param jsonrpc + * the version string + */ + public void setJsonrpc(String jsonrpc) { + this.jsonrpc = jsonrpc; + } + + /** + * Gets the response ID. + * + * @return the request identifier this response corresponds to + */ + public Object getId() { + return id; + } + + /** + * Sets the response ID. + * + * @param id + * the response identifier + */ + public void setId(Object id) { + this.id = id; + } + + /** + * Gets the result of the RPC call. + * + * @return the result object, or {@code null} if there was an error + */ + public Object getResult() { + return result; + } + + /** + * Sets the result of the RPC call. + * + * @param result + * the result object + */ + public void setResult(Object result) { + this.result = result; + } + + /** + * Gets the error if the RPC call failed. + * + * @return the error object, or {@code null} if successful + */ + public JsonRpcError getError() { + return error; + } + + /** + * Sets the error for a failed RPC call. + * + * @param error + * the error object + */ + public void setError(JsonRpcError error) { + this.error = error; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ListSessionsResponse.java b/java/src/main/java/com/github/copilot/sdk/json/ListSessionsResponse.java new file mode 100644 index 0000000..1c4490e --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ListSessionsResponse.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal response object from listing sessions. + *

+ * This is a low-level class for JSON-RPC communication containing the list of + * available sessions. + * + * @see com.github.copilot.sdk.CopilotClient#listSessions() + * @see SessionMetadata + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class ListSessionsResponse { + + @JsonProperty("sessions") + private List sessions; + + /** + * Gets the list of sessions. + * + * @return the list of session metadata + */ + public List getSessions() { + return sessions; + } + + /** + * Sets the list of sessions. + * + * @param sessions + * the list of session metadata + */ + public void setSessions(List sessions) { + this.sessions = sessions; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/MessageOptions.java b/java/src/main/java/com/github/copilot/sdk/json/MessageOptions.java new file mode 100644 index 0000000..b553e90 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/MessageOptions.java @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Options for sending a message to a Copilot session. + *

+ * This class specifies the message content and optional attachments to send to + * the assistant. All setter methods return {@code this} for method chaining. + * + *

Example Usage

+ * + *
{@code
+ * var options = new MessageOptions().setPrompt("Explain this code")
+ * 		.setAttachments(List.of(new Attachment().setType("file").setPath("/path/to/file.java")));
+ *
+ * session.send(options).get();
+ * }
+ * + * @see com.github.copilot.sdk.CopilotSession#send(MessageOptions) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MessageOptions { + + private String prompt; + private List attachments; + private String mode; + + /** + * Gets the message prompt. + * + * @return the prompt text + */ + public String getPrompt() { + return prompt; + } + + /** + * Sets the message prompt to send to the assistant. + * + * @param prompt + * the message text + * @return this options instance for method chaining + */ + public MessageOptions setPrompt(String prompt) { + this.prompt = prompt; + return this; + } + + /** + * Gets the file attachments. + * + * @return the list of attachments + */ + public List getAttachments() { + return attachments; + } + + /** + * Sets file attachments to include with the message. + *

+ * Attachments provide additional context to the assistant, such as source code + * files, documents, or other relevant files. + * + * @param attachments + * the list of file attachments + * @return this options instance for method chaining + * @see Attachment + */ + public MessageOptions setAttachments(List attachments) { + this.attachments = attachments; + return this; + } + + /** + * Sets the message delivery mode. + *

+ * Valid modes: + *

    + *
  • "enqueue" - Queue the message for processing (default)
  • + *
  • "immediate" - Process the message immediately
  • + *
+ * + * @param mode + * the delivery mode + * @return this options instance for method chaining + */ + public MessageOptions setMode(String mode) { + this.mode = mode; + return this; + } + + /** + * Gets the delivery mode. + * + * @return the delivery mode + */ + public String getMode() { + return mode; + } + +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java b/java/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java new file mode 100644 index 0000000..5facbd0 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Functional interface for handling permission requests from the AI assistant. + *

+ * When the assistant needs permission to perform certain actions (such as + * executing tools or accessing resources), this handler is invoked to approve + * or deny the request. + * + *

Example Implementation

+ * + *
{@code
+ * PermissionHandler handler = (request, invocation) -> {
+ * 	// Check the permission kind
+ * 	if ("dangerous-action".equals(request.getKind())) {
+ * 		// Deny dangerous actions
+ * 		return CompletableFuture.completedFuture(new PermissionRequestResult().setKind("user-denied"));
+ * 	}
+ *
+ * 	// Approve other requests
+ * 	return CompletableFuture.completedFuture(new PermissionRequestResult().setKind("user-approved"));
+ * };
+ * }
+ * + * @see SessionConfig#setOnPermissionRequest(PermissionHandler) + * @see PermissionRequest + * @see PermissionRequestResult + */ +@FunctionalInterface +public interface PermissionHandler { + + /** + * Handles a permission request from the assistant. + *

+ * The handler should evaluate the request and return a result indicating + * whether the permission is granted or denied. + * + * @param request + * the permission request details + * @param invocation + * the invocation context with session information + * @return a future that completes with the permission decision + */ + CompletableFuture handle(PermissionRequest request, PermissionInvocation invocation); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PermissionInvocation.java b/java/src/main/java/com/github/copilot/sdk/json/PermissionInvocation.java new file mode 100644 index 0000000..baec1c6 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PermissionInvocation.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Context information for a permission request invocation. + *

+ * This object provides context about the session where the permission request + * originated. + * + * @see PermissionHandler + */ +public final class PermissionInvocation { + + private String sessionId; + + /** + * Gets the session ID where the permission was requested. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the session ID. + * + * @param sessionId + * the session ID + * @return this invocation for method chaining + */ + public PermissionInvocation setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PermissionRequest.java b/java/src/main/java/com/github/copilot/sdk/json/PermissionRequest.java new file mode 100644 index 0000000..bcdcecf --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PermissionRequest.java @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a permission request from the AI assistant. + *

+ * When the assistant needs permission to perform certain actions, this object + * contains the details of the request, including the kind of permission and any + * associated tool call. + * + * @see PermissionHandler + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class PermissionRequest { + + @JsonProperty("kind") + private String kind; + + @JsonProperty("toolCallId") + private String toolCallId; + + private Map extensionData; + + /** + * Gets the kind of permission being requested. + * + * @return the permission kind + */ + public String getKind() { + return kind; + } + + /** + * Sets the permission kind. + * + * @param kind + * the permission kind + */ + public void setKind(String kind) { + this.kind = kind; + } + + /** + * Gets the associated tool call ID, if applicable. + * + * @return the tool call ID, or {@code null} if not a tool-related request + */ + public String getToolCallId() { + return toolCallId; + } + + /** + * Sets the tool call ID. + * + * @param toolCallId + * the tool call ID + */ + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + /** + * Gets additional extension data for the request. + * + * @return the extension data map + */ + public Map getExtensionData() { + return extensionData; + } + + /** + * Sets additional extension data for the request. + * + * @param extensionData + * the extension data map + */ + public void setExtensionData(Map extensionData) { + this.extensionData = extensionData; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PermissionRequestResult.java b/java/src/main/java/com/github/copilot/sdk/json/PermissionRequestResult.java new file mode 100644 index 0000000..6a99bc6 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PermissionRequestResult.java @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Result of a permission request decision. + *

+ * This object indicates whether a permission request was approved or denied, + * and may include additional rules for future similar requests. + * + *

Common Result Kinds

+ *
    + *
  • "user-approved" - User approved the permission request
  • + *
  • "user-denied" - User denied the permission request
  • + *
  • "denied-no-approval-rule-and-could-not-request-from-user" - No handler + * and couldn't ask user
  • + *
+ * + * @see PermissionHandler + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class PermissionRequestResult { + + @JsonProperty("kind") + private String kind; + + @JsonProperty("rules") + private List rules; + + /** + * Gets the result kind. + * + * @return the result kind indicating approval or denial + */ + public String getKind() { + return kind; + } + + /** + * Sets the result kind. + * + * @param kind + * the result kind + * @return this result for method chaining + */ + public PermissionRequestResult setKind(String kind) { + this.kind = kind; + return this; + } + + /** + * Gets the approval rules. + * + * @return the list of rules for future similar requests + */ + public List getRules() { + return rules; + } + + /** + * Sets approval rules for future similar requests. + * + * @param rules + * the list of rules + * @return this result for method chaining + */ + public PermissionRequestResult setRules(List rules) { + this.rules = rules; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PingResponse.java b/java/src/main/java/com/github/copilot/sdk/json/PingResponse.java new file mode 100644 index 0000000..50e6be4 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PingResponse.java @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Response from a ping request to the Copilot CLI server. + *

+ * The ping response confirms connectivity and provides information about the + * server, including the protocol version. + * + * @see com.github.copilot.sdk.CopilotClient#ping(String) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class PingResponse { + + @JsonProperty("message") + private String message; + + @JsonProperty("timestamp") + private long timestamp; + + @JsonProperty("protocolVersion") + private Integer protocolVersion; + + /** + * Gets the echo message from the server. + * + * @return the message echoed back by the server + */ + public String getMessage() { + return message; + } + + /** + * Sets the message. + * + * @param message + * the message + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * Gets the server timestamp. + * + * @return the timestamp in milliseconds since epoch + */ + public long getTimestamp() { + return timestamp; + } + + /** + * Sets the timestamp. + * + * @param timestamp + * the timestamp + */ + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + /** + * Gets the SDK protocol version supported by the server. + *

+ * The SDK validates that this version matches the expected version to ensure + * compatibility. + * + * @return the protocol version, or {@code null} if not reported + */ + public Integer getProtocolVersion() { + return protocolVersion; + } + + /** + * Sets the protocol version. + * + * @param protocolVersion + * the protocol version + */ + public void setProtocolVersion(Integer protocolVersion) { + this.protocolVersion = protocolVersion; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java b/java/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java new file mode 100644 index 0000000..bd7d50e --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java @@ -0,0 +1,192 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Configuration for a custom API provider (BYOK - Bring Your Own Key). + *

+ * This allows using your own OpenAI, Azure OpenAI, or other compatible API + * endpoints instead of the default Copilot backend. All setter methods return + * {@code this} for method chaining. + * + *

Example Usage - OpenAI

+ * + *
{@code
+ * var provider = new ProviderConfig().setType("openai").setBaseUrl("https://api.openai.com/v1").setApiKey("sk-...");
+ * }
+ * + *

Example Usage - Azure OpenAI

+ * + *
{@code
+ * var provider = new ProviderConfig().setType("azure")
+ * 		.setAzure(new AzureOptions().setEndpoint("https://my-resource.openai.azure.com").setDeployment("gpt-4"));
+ * }
+ * + * @see SessionConfig#setProvider(ProviderConfig) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ProviderConfig { + + @JsonProperty("type") + private String type; + + @JsonProperty("wireApi") + private String wireApi; + + @JsonProperty("baseUrl") + private String baseUrl; + + @JsonProperty("apiKey") + private String apiKey; + + @JsonProperty("bearerToken") + private String bearerToken; + + @JsonProperty("azure") + private AzureOptions azure; + + /** + * Gets the provider type. + * + * @return the provider type (e.g., "openai", "azure") + */ + public String getType() { + return type; + } + + /** + * Sets the provider type. + *

+ * Supported types include: + *

    + *
  • "openai" - OpenAI API
  • + *
  • "azure" - Azure OpenAI Service
  • + *
+ * + * @param type + * the provider type + * @return this config for method chaining + */ + public ProviderConfig setType(String type) { + this.type = type; + return this; + } + + /** + * Gets the wire API format. + * + * @return the wire API format + */ + public String getWireApi() { + return wireApi; + } + + /** + * Sets the wire API format for custom providers. + *

+ * This specifies the API format when using a custom provider that has a + * different wire protocol. + * + * @param wireApi + * the wire API format + * @return this config for method chaining + */ + public ProviderConfig setWireApi(String wireApi) { + this.wireApi = wireApi; + return this; + } + + /** + * Gets the base URL for the API. + * + * @return the API base URL + */ + public String getBaseUrl() { + return baseUrl; + } + + /** + * Sets the base URL for the API. + *

+ * For OpenAI, this is typically "https://api.openai.com/v1". + * + * @param baseUrl + * the API base URL + * @return this config for method chaining + */ + public ProviderConfig setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * Gets the API key. + * + * @return the API key + */ + public String getApiKey() { + return apiKey; + } + + /** + * Sets the API key for authentication. + * + * @param apiKey + * the API key + * @return this config for method chaining + */ + public ProviderConfig setApiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + /** + * Gets the bearer token. + * + * @return the bearer token + */ + public String getBearerToken() { + return bearerToken; + } + + /** + * Sets a bearer token for authentication. + *

+ * This is an alternative to API key authentication. + * + * @param bearerToken + * the bearer token + * @return this config for method chaining + */ + public ProviderConfig setBearerToken(String bearerToken) { + this.bearerToken = bearerToken; + return this; + } + + /** + * Gets the Azure-specific options. + * + * @return the Azure options + */ + public AzureOptions getAzure() { + return azure; + } + + /** + * Sets Azure-specific options for Azure OpenAI Service. + * + * @param azure + * the Azure options + * @return this config for method chaining + * @see AzureOptions + */ + public ProviderConfig setAzure(AzureOptions azure) { + this.azure = azure; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java new file mode 100644 index 0000000..750e876 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java @@ -0,0 +1,169 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Configuration for resuming an existing Copilot session. + *

+ * This class provides options for configuring a resumed session, including tool + * registration, provider configuration, and streaming. All setter methods + * return {@code this} for method chaining. + * + *

Example Usage

+ * + *
{@code
+ * var config = new ResumeSessionConfig().setStreaming(true).setTools(List.of(myTool));
+ *
+ * var session = client.resumeSession(sessionId, config).get();
+ * }
+ * + * @see com.github.copilot.sdk.CopilotClient#resumeSession(String, + * ResumeSessionConfig) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ResumeSessionConfig { + + private List tools; + private ProviderConfig provider; + private PermissionHandler onPermissionRequest; + private boolean streaming; + private Map mcpServers; + private List customAgents; + + /** + * Gets the custom tools for this session. + * + * @return the list of tool definitions + */ + public List getTools() { + return tools; + } + + /** + * Sets custom tools that the assistant can invoke during the session. + * + * @param tools + * the list of tool definitions + * @return this config for method chaining + * @see ToolDefinition + */ + public ResumeSessionConfig setTools(List tools) { + this.tools = tools; + return this; + } + + /** + * Gets the custom API provider configuration. + * + * @return the provider configuration + */ + public ProviderConfig getProvider() { + return provider; + } + + /** + * Sets a custom API provider for BYOK scenarios. + * + * @param provider + * the provider configuration + * @return this config for method chaining + * @see ProviderConfig + */ + public ResumeSessionConfig setProvider(ProviderConfig provider) { + this.provider = provider; + return this; + } + + /** + * Gets the permission request handler. + * + * @return the permission handler + */ + public PermissionHandler getOnPermissionRequest() { + return onPermissionRequest; + } + + /** + * Sets a handler for permission requests from the assistant. + * + * @param onPermissionRequest + * the permission handler + * @return this config for method chaining + * @see PermissionHandler + */ + public ResumeSessionConfig setOnPermissionRequest(PermissionHandler onPermissionRequest) { + this.onPermissionRequest = onPermissionRequest; + return this; + } + + /** + * Returns whether streaming is enabled. + * + * @return {@code true} if streaming is enabled + */ + public boolean isStreaming() { + return streaming; + } + + /** + * Sets whether to enable streaming of response chunks. + * + * @param streaming + * {@code true} to enable streaming + * @return this config for method chaining + */ + public ResumeSessionConfig setStreaming(boolean streaming) { + this.streaming = streaming; + return this; + } + + /** + * Gets the MCP server configurations. + * + * @return the MCP servers map + */ + public Map getMcpServers() { + return mcpServers; + } + + /** + * Sets MCP (Model Context Protocol) server configurations. + * + * @param mcpServers + * the MCP servers configuration map + * @return this config for method chaining + */ + public ResumeSessionConfig setMcpServers(Map mcpServers) { + this.mcpServers = mcpServers; + return this; + } + + /** + * Gets the custom agent configurations. + * + * @return the list of custom agent configurations + */ + public List getCustomAgents() { + return customAgents; + } + + /** + * Sets custom agent configurations. + * + * @param customAgents + * the list of custom agent configurations + * @return this config for method chaining + * @see CustomAgentConfig + */ + public ResumeSessionConfig setCustomAgents(List customAgents) { + this.customAgents = customAgents; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java b/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java new file mode 100644 index 0000000..92e160f --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal request object for resuming an existing session. + *

+ * This is a low-level class for JSON-RPC communication. For resuming sessions, + * use + * {@link com.github.copilot.sdk.CopilotClient#resumeSession(String, ResumeSessionConfig)}. + * + * @see com.github.copilot.sdk.CopilotClient#resumeSession(String, + * ResumeSessionConfig) + * @see ResumeSessionConfig + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class ResumeSessionRequest { + + @JsonProperty("sessionId") + private String sessionId; + + @JsonProperty("tools") + private List tools; + + @JsonProperty("provider") + private ProviderConfig provider; + + @JsonProperty("requestPermission") + private Boolean requestPermission; + + @JsonProperty("streaming") + private Boolean streaming; + + @JsonProperty("mcpServers") + private Map mcpServers; + + @JsonProperty("customAgents") + private List customAgents; + + /** Gets the session ID. @return the session ID */ + public String getSessionId() { + return sessionId; + } + + /** Sets the session ID. @param sessionId the session ID */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** Gets the tools. @return the tool definitions */ + public List getTools() { + return tools; + } + + /** Sets the tools. @param tools the tool definitions */ + public void setTools(List tools) { + this.tools = tools; + } + + /** Gets the provider config. @return the provider */ + public ProviderConfig getProvider() { + return provider; + } + + /** Sets the provider config. @param provider the provider */ + public void setProvider(ProviderConfig provider) { + this.provider = provider; + } + + /** Gets request permission flag. @return the flag */ + public Boolean getRequestPermission() { + return requestPermission; + } + + /** Sets request permission flag. @param requestPermission the flag */ + public void setRequestPermission(Boolean requestPermission) { + this.requestPermission = requestPermission; + } + + /** Gets streaming flag. @return the flag */ + public Boolean getStreaming() { + return streaming; + } + + /** Sets streaming flag. @param streaming the flag */ + public void setStreaming(Boolean streaming) { + this.streaming = streaming; + } + + /** Gets MCP servers. @return the servers map */ + public Map getMcpServers() { + return mcpServers; + } + + /** Sets MCP servers. @param mcpServers the servers map */ + public void setMcpServers(Map mcpServers) { + this.mcpServers = mcpServers; + } + + /** Gets custom agents. @return the agents */ + public List getCustomAgents() { + return customAgents; + } + + /** Sets custom agents. @param customAgents the agents */ + public void setCustomAgents(List customAgents) { + this.customAgents = customAgents; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java b/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java new file mode 100644 index 0000000..615bf46 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java @@ -0,0 +1,17 @@ +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class ResumeSessionResponse { + @JsonProperty("sessionId") + private String sessionId; + + public String getSessionId() { + return sessionId; + } + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SendMessageRequest.java b/java/src/main/java/com/github/copilot/sdk/json/SendMessageRequest.java new file mode 100644 index 0000000..0b37131 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SendMessageRequest.java @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal request object for sending a message to a session. + *

+ * This is a low-level class for JSON-RPC communication. For sending messages, + * use {@link com.github.copilot.sdk.CopilotSession#send(String)} or + * {@link com.github.copilot.sdk.CopilotSession#sendAndWait(String)}. + * + * @see com.github.copilot.sdk.CopilotSession + * @see MessageOptions + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class SendMessageRequest { + + @JsonProperty("sessionId") + private String sessionId; + + @JsonProperty("prompt") + private String prompt; + + @JsonProperty("attachments") + private List attachments; + + @JsonProperty("mode") + private String mode; + + /** Gets the session ID. @return the session ID */ + public String getSessionId() { + return sessionId; + } + + /** Sets the session ID. @param sessionId the session ID */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** Gets the message prompt. @return the prompt text */ + public String getPrompt() { + return prompt; + } + + /** Sets the message prompt. @param prompt the prompt text */ + public void setPrompt(String prompt) { + this.prompt = prompt; + } + + /** Gets the attachments. @return the list of attachments */ + public List getAttachments() { + return attachments; + } + + /** Sets the attachments. @param attachments the list of attachments */ + public void setAttachments(List attachments) { + this.attachments = attachments; + } + + /** Gets the mode. @return the message mode */ + public String getMode() { + return mode; + } + + /** Sets the mode. @param mode the message mode */ + public void setMode(String mode) { + this.mode = mode; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SendMessageResponse.java b/java/src/main/java/com/github/copilot/sdk/json/SendMessageResponse.java new file mode 100644 index 0000000..878498b --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SendMessageResponse.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sending a message. + *

+ * This is a low-level class for JSON-RPC communication containing the message + * ID assigned by the server. + * + * @see SendMessageRequest + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class SendMessageResponse { + + @JsonProperty("messageId") + private String messageId; + + /** + * Gets the message ID assigned by the server. + * + * @return the message ID + */ + public String getMessageId() { + return messageId; + } + + /** + * Sets the message ID. + * + * @param messageId + * the message ID + */ + public void setMessageId(String messageId) { + this.messageId = messageId; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/java/src/main/java/com/github/copilot/sdk/json/SessionConfig.java new file mode 100644 index 0000000..b1ea7df --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionConfig.java @@ -0,0 +1,312 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Configuration for creating a new Copilot session. + *

+ * This class provides options for customizing session behavior, including model + * selection, tool registration, system message customization, and more. All + * setter methods return {@code this} for method chaining. + * + *

Example Usage

+ * + *
{@code
+ * var config = new SessionConfig().setModel("gpt-5").setStreaming(true).setSystemMessage(
+ * 		new SystemMessageConfig().setMode(SystemMessageMode.APPEND).setContent("Be concise in your responses."));
+ *
+ * var session = client.createSession(config).get();
+ * }
+ * + * @see com.github.copilot.sdk.CopilotClient#createSession(SessionConfig) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SessionConfig { + + private String sessionId; + private String model; + private List tools; + private SystemMessageConfig systemMessage; + private List availableTools; + private List excludedTools; + private ProviderConfig provider; + private PermissionHandler onPermissionRequest; + private boolean streaming; + private Map mcpServers; + private List customAgents; + + /** + * Gets the custom session ID. + * + * @return the session ID, or {@code null} to generate automatically + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets a custom session ID. + *

+ * If not provided, a unique session ID will be generated automatically. + * + * @param sessionId + * the custom session ID + * @return this config instance for method chaining + */ + public SessionConfig setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + /** + * Gets the AI model to use. + * + * @return the model name + */ + public String getModel() { + return model; + } + + /** + * Sets the AI model to use for this session. + *

+ * Examples: "gpt-5", "claude-sonnet-4.5", "o3-mini". + * + * @param model + * the model name + * @return this config instance for method chaining + */ + public SessionConfig setModel(String model) { + this.model = model; + return this; + } + + /** + * Gets the custom tools for this session. + * + * @return the list of tool definitions + */ + public List getTools() { + return tools; + } + + /** + * Sets custom tools that the assistant can invoke during the session. + *

+ * Tools allow the assistant to call back into your application to perform + * actions or retrieve information. + * + * @param tools + * the list of tool definitions + * @return this config instance for method chaining + * @see ToolDefinition + */ + public SessionConfig setTools(List tools) { + this.tools = tools; + return this; + } + + /** + * Gets the system message configuration. + * + * @return the system message config + */ + public SystemMessageConfig getSystemMessage() { + return systemMessage; + } + + /** + * Sets the system message configuration. + *

+ * The system message controls the behavior and personality of the assistant. + * Use {@link com.github.copilot.sdk.SystemMessageMode#APPEND} to add + * instructions while preserving default behavior, or + * {@link com.github.copilot.sdk.SystemMessageMode#REPLACE} to fully customize. + * + * @param systemMessage + * the system message configuration + * @return this config instance for method chaining + * @see SystemMessageConfig + */ + public SessionConfig setSystemMessage(SystemMessageConfig systemMessage) { + this.systemMessage = systemMessage; + return this; + } + + /** + * Gets the list of allowed tool names. + * + * @return the list of available tool names + */ + public List getAvailableTools() { + return availableTools; + } + + /** + * Sets the list of tool names that are allowed in this session. + *

+ * When specified, only tools in this list will be available to the assistant. + * + * @param availableTools + * the list of allowed tool names + * @return this config instance for method chaining + */ + public SessionConfig setAvailableTools(List availableTools) { + this.availableTools = availableTools; + return this; + } + + /** + * Gets the list of excluded tool names. + * + * @return the list of excluded tool names + */ + public List getExcludedTools() { + return excludedTools; + } + + /** + * Sets the list of tool names to exclude from this session. + *

+ * Tools in this list will not be available to the assistant. + * + * @param excludedTools + * the list of tool names to exclude + * @return this config instance for method chaining + */ + public SessionConfig setExcludedTools(List excludedTools) { + this.excludedTools = excludedTools; + return this; + } + + /** + * Gets the custom API provider configuration. + * + * @return the provider configuration + */ + public ProviderConfig getProvider() { + return provider; + } + + /** + * Sets a custom API provider for BYOK (Bring Your Own Key) scenarios. + *

+ * This allows using your own OpenAI, Azure OpenAI, or other compatible API + * endpoints instead of the default Copilot backend. + * + * @param provider + * the provider configuration + * @return this config instance for method chaining + * @see ProviderConfig + */ + public SessionConfig setProvider(ProviderConfig provider) { + this.provider = provider; + return this; + } + + /** + * Gets the permission request handler. + * + * @return the permission handler + */ + public PermissionHandler getOnPermissionRequest() { + return onPermissionRequest; + } + + /** + * Sets a handler for permission requests from the assistant. + *

+ * When the assistant needs permission to perform certain actions, this handler + * will be invoked to approve or deny the request. + * + * @param onPermissionRequest + * the permission handler + * @return this config instance for method chaining + * @see PermissionHandler + */ + public SessionConfig setOnPermissionRequest(PermissionHandler onPermissionRequest) { + this.onPermissionRequest = onPermissionRequest; + return this; + } + + /** + * Returns whether streaming is enabled. + * + * @return {@code true} if streaming is enabled + */ + public boolean isStreaming() { + return streaming; + } + + /** + * Sets whether to enable streaming of response chunks. + *

+ * When enabled, the session will emit {@code AssistantMessageDeltaEvent} events + * as the response is generated, allowing for real-time display of partial + * responses. + * + * @param streaming + * {@code true} to enable streaming + * @return this config instance for method chaining + */ + public SessionConfig setStreaming(boolean streaming) { + this.streaming = streaming; + return this; + } + + /** + * Gets the MCP server configurations. + * + * @return the MCP servers map + */ + public Map getMcpServers() { + return mcpServers; + } + + /** + * Sets MCP (Model Context Protocol) server configurations. + *

+ * MCP servers extend the assistant's capabilities by providing additional + * context sources and tools. + * + * @param mcpServers + * the MCP servers configuration map + * @return this config instance for method chaining + */ + public SessionConfig setMcpServers(Map mcpServers) { + this.mcpServers = mcpServers; + return this; + } + + /** + * Gets the custom agent configurations. + * + * @return the list of custom agent configurations + */ + public List getCustomAgents() { + return customAgents; + } + + /** + * Sets custom agent configurations. + *

+ * Custom agents allow extending the assistant with specialized behaviors and + * capabilities. + * + * @param customAgents + * the list of custom agent configurations + * @return this config instance for method chaining + * @see CustomAgentConfig + */ + public SessionConfig setCustomAgents(List customAgents) { + this.customAgents = customAgents; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionMetadata.java b/java/src/main/java/com/github/copilot/sdk/json/SessionMetadata.java new file mode 100644 index 0000000..278d59e --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionMetadata.java @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Metadata about an existing Copilot session. + *

+ * This class represents session information returned when listing available + * sessions via {@link com.github.copilot.sdk.CopilotClient#listSessions()}. It + * includes timing information, a summary of the conversation, and whether the + * session is stored remotely. + * + *

Example Usage

+ * + *
{@code
+ * var sessions = client.listSessions().get();
+ * for (var meta : sessions) {
+ * 	System.out.println("Session: " + meta.getSessionId());
+ * 	System.out.println("  Started: " + meta.getStartTime());
+ * 	System.out.println("  Summary: " + meta.getSummary());
+ * }
+ * }
+ * + * @see com.github.copilot.sdk.CopilotClient#listSessions() + * @see com.github.copilot.sdk.CopilotClient#resumeSession(String, + * ResumeSessionConfig) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SessionMetadata { + + @JsonProperty("sessionId") + private String sessionId; + + @JsonProperty("startTime") + private String startTime; + + @JsonProperty("modifiedTime") + private String modifiedTime; + + @JsonProperty("summary") + private String summary; + + @JsonProperty("isRemote") + private boolean isRemote; + + /** + * Gets the unique identifier for this session. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the session identifier. + * + * @param sessionId + * the session ID + */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** + * Gets the timestamp when the session was created. + * + * @return the start time as an ISO 8601 formatted string + */ + public String getStartTime() { + return startTime; + } + + /** + * Sets the session start time. + * + * @param startTime + * the start time as an ISO 8601 formatted string + */ + public void setStartTime(String startTime) { + this.startTime = startTime; + } + + /** + * Gets the timestamp when the session was last modified. + * + * @return the modified time as an ISO 8601 formatted string + */ + public String getModifiedTime() { + return modifiedTime; + } + + /** + * Sets the session modified time. + * + * @param modifiedTime + * the modified time as an ISO 8601 formatted string + */ + public void setModifiedTime(String modifiedTime) { + this.modifiedTime = modifiedTime; + } + + /** + * Gets a brief summary of the session's conversation. + *

+ * This is typically an AI-generated summary of the session content. + * + * @return the session summary, or {@code null} if not available + */ + public String getSummary() { + return summary; + } + + /** + * Sets the session summary. + * + * @param summary + * the session summary + */ + public void setSummary(String summary) { + this.summary = summary; + } + + /** + * Returns whether this session is stored remotely. + * + * @return {@code true} if the session is stored on the server, {@code false} if + * it's stored locally + */ + public boolean isRemote() { + return isRemote; + } + + /** + * Sets whether this session is stored remotely. + * + * @param remote + * {@code true} if stored remotely + */ + public void setRemote(boolean remote) { + isRemote = remote; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SystemMessageConfig.java b/java/src/main/java/com/github/copilot/sdk/json/SystemMessageConfig.java new file mode 100644 index 0000000..9369bf1 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SystemMessageConfig.java @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.github.copilot.sdk.SystemMessageMode; + +/** + * Configuration for customizing the system message. + *

+ * The system message controls the behavior and personality of the AI assistant. + * This configuration allows you to either append to or replace the default + * system message. + * + *

Example - Append Mode

+ * + *
{@code
+ * var config = new SystemMessageConfig().setMode(SystemMessageMode.APPEND)
+ * 		.setContent("Always respond in a formal tone.");
+ * }
+ * + *

Example - Replace Mode

+ * + *
{@code
+ * var config = new SystemMessageConfig().setMode(SystemMessageMode.REPLACE)
+ * 		.setContent("You are a helpful coding assistant.");
+ * }
+ * + * @see SessionConfig#setSystemMessage(SystemMessageConfig) + * @see SystemMessageMode + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SystemMessageConfig { + + private SystemMessageMode mode; + private String content; + + /** + * Gets the system message mode. + * + * @return the mode (APPEND or REPLACE) + */ + public SystemMessageMode getMode() { + return mode; + } + + /** + * Sets the system message mode. + *

+ * Use {@link SystemMessageMode#APPEND} to add to the default system message + * while preserving guardrails, or {@link SystemMessageMode#REPLACE} to fully + * customize the system message. + * + * @param mode + * the mode (APPEND or REPLACE) + * @return this config for method chaining + */ + public SystemMessageConfig setMode(SystemMessageMode mode) { + this.mode = mode; + return this; + } + + /** + * Gets the system message content. + * + * @return the content to append or use as replacement + */ + public String getContent() { + return content; + } + + /** + * Sets the system message content. + *

+ * This is the text that will be appended to or replace the default system + * message, depending on the configured mode. + * + * @param content + * the system message content + * @return this config for method chaining + */ + public SystemMessageConfig setContent(String content) { + this.content = content; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ToolBinaryResult.java b/java/src/main/java/com/github/copilot/sdk/json/ToolBinaryResult.java new file mode 100644 index 0000000..a2436be --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ToolBinaryResult.java @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Binary result from a tool execution. + *

+ * This class represents binary data (such as images) returned by a tool. The + * data is base64-encoded for JSON transmission. + * + *

Example Usage

+ * + *
{@code
+ * var binaryResult = new ToolBinaryResult().setType("image").setMimeType("image/png")
+ * 		.setData(Base64.getEncoder().encodeToString(imageBytes)).setDescription("Generated chart");
+ * }
+ * + * @see ToolResultObject#setBinaryResultsForLlm(java.util.List) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class ToolBinaryResult { + + @JsonProperty("data") + private String data; + + @JsonProperty("mimeType") + private String mimeType; + + @JsonProperty("type") + private String type; + + @JsonProperty("description") + private String description; + + /** + * Gets the base64-encoded binary data. + * + * @return the base64-encoded data string + */ + public String getData() { + return data; + } + + /** + * Sets the base64-encoded binary data. + * + * @param data + * the base64-encoded data + * @return this result for method chaining + */ + public ToolBinaryResult setData(String data) { + this.data = data; + return this; + } + + /** + * Gets the MIME type of the binary data. + * + * @return the MIME type (e.g., "image/png", "application/pdf") + */ + public String getMimeType() { + return mimeType; + } + + /** + * Sets the MIME type of the binary data. + * + * @param mimeType + * the MIME type + * @return this result for method chaining + */ + public ToolBinaryResult setMimeType(String mimeType) { + this.mimeType = mimeType; + return this; + } + + /** + * Gets the type of binary content. + * + * @return the content type (e.g., "image", "file") + */ + public String getType() { + return type; + } + + /** + * Sets the type of binary content. + * + * @param type + * the content type + * @return this result for method chaining + */ + public ToolBinaryResult setType(String type) { + this.type = type; + return this; + } + + /** + * Gets the description of the binary content. + * + * @return the content description + */ + public String getDescription() { + return description; + } + + /** + * Sets a description of the binary content. + *

+ * This helps the assistant understand the content. + * + * @param description + * the content description + * @return this result for method chaining + */ + public ToolBinaryResult setDescription(String description) { + this.description = description; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ToolDef.java b/java/src/main/java/com/github/copilot/sdk/json/ToolDef.java new file mode 100644 index 0000000..8477e24 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ToolDef.java @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Low-level tool definition for JSON-RPC communication. + *

+ * This is an internal class representing the wire format of a tool. For + * registering tools with the SDK, use {@link ToolDefinition} instead. + * + * @see ToolDefinition + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class ToolDef { + + @JsonProperty("name") + private String name; + + @JsonProperty("description") + private String description; + + @JsonProperty("parameters") + private Object parameters; + + /** + * Creates an empty tool definition. + */ + public ToolDef() { + } + + /** + * Creates a tool definition with all fields. + * + * @param name + * the unique tool identifier + * @param description + * the tool description + * @param parameters + * the JSON Schema for tool parameters + */ + public ToolDef(String name, String description, Object parameters) { + this.name = name; + this.description = description; + this.parameters = parameters; + } + + /** + * Gets the tool name. + * + * @return the tool name + */ + public String getName() { + return name; + } + + /** + * Sets the tool name. + * + * @param name + * the tool name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Gets the tool description. + * + * @return the tool description + */ + public String getDescription() { + return description; + } + + /** + * Sets the tool description. + * + * @param description + * the tool description + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the JSON Schema for tool parameters. + * + * @return the parameters schema + */ + public Object getParameters() { + return parameters; + } + + /** + * Sets the JSON Schema for tool parameters. + * + * @param parameters + * the parameters schema + */ + public void setParameters(Object parameters) { + this.parameters = parameters; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ToolDefinition.java b/java/src/main/java/com/github/copilot/sdk/json/ToolDefinition.java new file mode 100644 index 0000000..3d07f89 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ToolDefinition.java @@ -0,0 +1,196 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Defines a tool that can be invoked by the AI assistant. + *

+ * Tools extend the assistant's capabilities by allowing it to call back into + * your application to perform actions or retrieve information. Each tool has a + * name, description, parameter schema, and a handler function that executes + * when the tool is invoked. + * + *

Example Usage

+ * + *
{@code
+ * var tool = ToolDefinition.create("get_weather", "Get the current weather for a location",
+ * 		Map.of("type", "object", "properties",
+ * 				Map.of("location", Map.of("type", "string", "description", "City name")), "required",
+ * 				List.of("location")),
+ * 		invocation -> {
+ * 			String location = ((Map) invocation.getArguments()).get("location").toString();
+ * 			return CompletableFuture.completedFuture(getWeatherData(location));
+ * 		});
+ * }
+ * + * @see SessionConfig#setTools(java.util.List) + * @see ToolHandler + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ToolDefinition { + + @JsonProperty("name") + private String name; + + @JsonProperty("description") + private String description; + + @JsonProperty("parameters") + private Object parameters; + + private transient ToolHandler handler; + + /** + * Creates an empty tool definition. + *

+ * Use the setter methods to configure the tool. + */ + public ToolDefinition() { + } + + /** + * Creates a tool definition with all properties. + * + * @param name + * the unique name of the tool + * @param description + * a description of what the tool does + * @param parameters + * the JSON Schema defining the tool's parameters + * @param handler + * the handler function to execute when invoked + */ + public ToolDefinition(String name, String description, Object parameters, ToolHandler handler) { + this.name = name; + this.description = description; + this.parameters = parameters; + this.handler = handler; + } + + /** + * Gets the tool name. + * + * @return the unique name of the tool + */ + public String getName() { + return name; + } + + /** + * Sets the tool name. + *

+ * The name should be unique within a session and follow naming conventions + * similar to function names (e.g., "get_user", "search_files"). + * + * @param name + * the unique name of the tool + * @return this tool definition for method chaining + */ + public ToolDefinition setName(String name) { + this.name = name; + return this; + } + + /** + * Gets the tool description. + * + * @return the description of what the tool does + */ + public String getDescription() { + return description; + } + + /** + * Sets the tool description. + *

+ * The description helps the AI understand when and how to use the tool. Be + * clear and specific about the tool's purpose and any constraints. + * + * @param description + * the description of what the tool does + * @return this tool definition for method chaining + */ + public ToolDefinition setDescription(String description) { + this.description = description; + return this; + } + + /** + * Gets the parameter schema. + * + * @return the JSON Schema for the tool's parameters + */ + public Object getParameters() { + return parameters; + } + + /** + * Sets the parameter schema. + *

+ * The schema should follow JSON Schema format and define the structure of + * arguments the tool accepts. This is typically a {@code Map} with "type", + * "properties", and "required" fields. + * + * @param parameters + * the JSON Schema for the tool's parameters + * @return this tool definition for method chaining + */ + public ToolDefinition setParameters(Object parameters) { + this.parameters = parameters; + return this; + } + + /** + * Gets the tool handler. + * + * @return the handler function that executes when the tool is invoked + */ + public ToolHandler getHandler() { + return handler; + } + + /** + * Sets the tool handler. + *

+ * The handler is called when the assistant invokes this tool. It receives a + * {@link ToolInvocation} with the arguments and should return a + * {@code CompletableFuture} with the result. + * + * @param handler + * the handler function + * @return this tool definition for method chaining + * @see ToolHandler + */ + public ToolDefinition setHandler(ToolHandler handler) { + this.handler = handler; + return this; + } + + /** + * Creates a tool definition with a JSON schema for parameters. + *

+ * This is a convenience factory method for creating tools with a + * {@code Map}-based parameter schema. + * + * @param name + * the unique name of the tool + * @param description + * a description of what the tool does + * @param schema + * the JSON Schema as a {@code Map} + * @param handler + * the handler function to execute when invoked + * @return a new tool definition + */ + public static ToolDefinition create(String name, String description, Map schema, + ToolHandler handler) { + return new ToolDefinition(name, description, schema, handler); + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ToolHandler.java b/java/src/main/java/com/github/copilot/sdk/json/ToolHandler.java new file mode 100644 index 0000000..868a003 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ToolHandler.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Functional interface for handling tool invocations from the AI assistant. + *

+ * When the assistant decides to use a tool, it invokes this handler with the + * tool's arguments. The handler should perform the requested action and return + * the result. + * + *

Example Implementation

+ * + *
{@code
+ * ToolHandler handler = invocation -> {
+ * 	Map args = (Map) invocation.getArguments();
+ * 	String query = args.get("query").toString();
+ *
+ * 	// Perform the tool's action
+ * 	String result = performSearch(query);
+ *
+ * 	return CompletableFuture.completedFuture(result);
+ * };
+ * }
+ * + * @see ToolDefinition + * @see ToolInvocation + */ +@FunctionalInterface +public interface ToolHandler { + + /** + * Invokes the tool with the given invocation context. + *

+ * The returned object will be serialized to JSON and sent back to the assistant + * as the tool's result. This can be a {@code String}, {@code Map}, or any + * JSON-serializable object. + * + * @param invocation + * the invocation context containing arguments + * @return a future that completes with the tool's result + */ + CompletableFuture invoke(ToolInvocation invocation); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ToolInvocation.java b/java/src/main/java/com/github/copilot/sdk/json/ToolInvocation.java new file mode 100644 index 0000000..a338934 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ToolInvocation.java @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Represents a tool invocation request from the AI assistant. + *

+ * When the assistant invokes a tool, this object contains the context including + * the session ID, tool call ID, tool name, and arguments parsed from the + * assistant's request. + * + * @see ToolHandler + * @see ToolDefinition + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class ToolInvocation { + + private String sessionId; + private String toolCallId; + private String toolName; + private Object arguments; + + /** + * Gets the session ID where the tool was invoked. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the session ID. + * + * @param sessionId + * the session ID + * @return this invocation for method chaining + */ + public ToolInvocation setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + /** + * Gets the unique identifier for this tool call. + *

+ * This ID correlates the tool invocation with its response. + * + * @return the tool call ID + */ + public String getToolCallId() { + return toolCallId; + } + + /** + * Sets the tool call ID. + * + * @param toolCallId + * the tool call ID + * @return this invocation for method chaining + */ + public ToolInvocation setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + return this; + } + + /** + * Gets the name of the tool being invoked. + * + * @return the tool name + */ + public String getToolName() { + return toolName; + } + + /** + * Sets the tool name. + * + * @param toolName + * the tool name + * @return this invocation for method chaining + */ + public ToolInvocation setToolName(String toolName) { + this.toolName = toolName; + return this; + } + + /** + * Gets the arguments passed to the tool. + *

+ * This is typically a {@code Map} matching the parameter schema + * defined in the tool's {@link ToolDefinition}. + * + * @return the arguments object + */ + public Object getArguments() { + return arguments; + } + + /** + * Sets the tool arguments. + * + * @param arguments + * the arguments object + * @return this invocation for method chaining + */ + public ToolInvocation setArguments(Object arguments) { + this.arguments = arguments; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ToolResultObject.java b/java/src/main/java/com/github/copilot/sdk/json/ToolResultObject.java new file mode 100644 index 0000000..4d52bc6 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ToolResultObject.java @@ -0,0 +1,173 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Result object returned from a tool execution. + *

+ * This class represents the structured result of a tool invocation, including + * text output, binary data, error information, and telemetry. + * + *

Example: Success Result

+ * + *
{@code
+ * return new ToolResultObject().setResultType("success").setTextResultForLlm("File contents: " + content);
+ * }
+ * + *

Example: Error Result

+ * + *
{@code
+ * return new ToolResultObject().setResultType("error").setError("File not found: " + path);
+ * }
+ * + * @see ToolHandler + * @see ToolBinaryResult + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class ToolResultObject { + + @JsonProperty("textResultForLlm") + private String textResultForLlm; + + @JsonProperty("binaryResultsForLlm") + private List binaryResultsForLlm; + + @JsonProperty("resultType") + private String resultType = "success"; + + @JsonProperty("error") + private String error; + + @JsonProperty("sessionLog") + private String sessionLog; + + @JsonProperty("toolTelemetry") + private Map toolTelemetry; + + /** + * Gets the text result to be sent to the LLM. + * + * @return the text result + */ + public String getTextResultForLlm() { + return textResultForLlm; + } + + /** + * Sets the text result to be sent to the LLM. + * + * @param textResultForLlm + * the text result + * @return this result for method chaining + */ + public ToolResultObject setTextResultForLlm(String textResultForLlm) { + this.textResultForLlm = textResultForLlm; + return this; + } + + /** + * Gets the binary results to be sent to the LLM. + * + * @return the list of binary results + */ + public List getBinaryResultsForLlm() { + return binaryResultsForLlm; + } + + /** + * Sets binary results (images, files) to be sent to the LLM. + * + * @param binaryResultsForLlm + * the list of binary results + * @return this result for method chaining + */ + public ToolResultObject setBinaryResultsForLlm(List binaryResultsForLlm) { + this.binaryResultsForLlm = binaryResultsForLlm; + return this; + } + + /** + * Gets the result type. + * + * @return the result type ("success" or "error") + */ + public String getResultType() { + return resultType; + } + + /** + * Sets the result type. + * + * @param resultType + * "success" or "error" + * @return this result for method chaining + */ + public ToolResultObject setResultType(String resultType) { + this.resultType = resultType; + return this; + } + + /** + * Gets the error message. + * + * @return the error message, or {@code null} if successful + */ + public String getError() { + return error; + } + + /** + * Sets an error message for failed tool execution. + * + * @param error + * the error message + * @return this result for method chaining + */ + public ToolResultObject setError(String error) { + this.error = error; + return this; + } + + /** + * Gets the session log entry. + * + * @return the session log text + */ + public String getSessionLog() { + return sessionLog; + } + + /** + * Sets a log entry to be recorded in the session. + * + * @param sessionLog + * the log entry + * @return this result for method chaining + */ + public ToolResultObject setSessionLog(String sessionLog) { + this.sessionLog = sessionLog; + return this; + } + + /** + * Gets the tool telemetry data. + * + * @return the telemetry map + */ + public Map getToolTelemetry() { + return toolTelemetry; + } + + public ToolResultObject setToolTelemetry(Map toolTelemetry) { + this.toolTelemetry = toolTelemetry; + return this; + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/CapiProxy.java b/java/src/test/java/com/github/copilot/sdk/CapiProxy.java new file mode 100644 index 0000000..3992d49 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/CapiProxy.java @@ -0,0 +1,319 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Manages a replaying proxy server for E2E tests. + * + *

+ * This spawns the shared test harness server from test/harness/server.ts which + * acts as a replaying proxy to AI endpoints. It captures and stores + * request/response pairs in YAML snapshot files and replays stored responses on + * subsequent runs for deterministic testing. + *

+ * + *

+ * Usage example: + *

+ * + *
+ * {@code
+ * CapiProxy proxy = new CapiProxy();
+ * String proxyUrl = proxy.start();
+ *
+ * // Configure for a specific test
+ * proxy.configure("test/snapshots/tools/my_test.yaml", workDir);
+ *
+ * // ... run tests with proxyUrl ...
+ *
+ * // Get captured exchanges
+ * List> exchanges = proxy.getExchanges();
+ *
+ * proxy.stop();
+ * }
+ * 
+ */ +public class CapiProxy implements AutoCloseable { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final Pattern LISTENING_PATTERN = Pattern.compile("Listening: (http://[^\\s]+)"); + + private Process process; + private String proxyUrl; + private final HttpClient httpClient; + private BufferedReader stdoutReader; + + public CapiProxy() { + this.httpClient = HttpClient.newHttpClient(); + } + + /** + * Starts the proxy server and returns its URL. + * + * @return the proxy URL (e.g., "http://localhost:12345") + * @throws IOException + * if the server fails to start + * @throws InterruptedException + * if the startup is interrupted + */ + public String start() throws IOException, InterruptedException { + if (proxyUrl != null) { + return proxyUrl; + } + + // Find the repo root by looking for the test/harness directory + Path harnessDir = findHarnessDirectory(); + if (harnessDir == null) { + throw new IOException("Could not find test/harness directory. " + + "Make sure you are running from within the copilot-sdk repository."); + } + + // Start the harness server using npx tsx + ProcessBuilder pb = new ProcessBuilder("npx", "tsx", "server.ts"); + pb.directory(harnessDir.toFile()); + pb.redirectErrorStream(false); + + process = pb.start(); + + // Read stdout to get the listening URL + // Note: We keep the reader open to avoid closing the process input stream + stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream())); + + // Also consume stderr in a background thread to prevent blocking + Thread stderrThread = new Thread(() -> { + try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String errLine; + while ((errLine = errReader.readLine()) != null) { + System.err.println("[CapiProxy stderr] " + errLine); + } + } catch (IOException e) { + // Ignore + } + }); + stderrThread.setDaemon(true); + stderrThread.start(); + + String line = stdoutReader.readLine(); + if (line == null) { + // Try to get error info + StringBuilder errInfo = new StringBuilder(); + try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String errLine; + while ((errLine = errReader.readLine()) != null) { + errInfo.append(errLine).append("\n"); + } + } + process.destroyForcibly(); + throw new IOException("Failed to read proxy URL - server may have crashed. Stderr: " + errInfo); + } + + Matcher matcher = LISTENING_PATTERN.matcher(line); + if (!matcher.find()) { + process.destroyForcibly(); + throw new IOException("Unexpected proxy output: " + line); + } + + proxyUrl = matcher.group(1); + return proxyUrl; + } + + /** + * Configures the proxy for a specific test file. + * + * @param filePath + * the path to the YAML snapshot file (relative to repo root) + * @param workDir + * the working directory for path normalization + * @throws IOException + * if the configuration fails + * @throws InterruptedException + * if the request is interrupted + */ + public void configure(String filePath, String workDir) throws IOException, InterruptedException { + configure(filePath, workDir, null); + } + + /** + * Configures the proxy for a specific test file. + * + * @param filePath + * the path to the YAML snapshot file (relative to repo root) + * @param workDir + * the working directory for path normalization + * @param testInfo + * optional test information (file and line number) + * @throws IOException + * if the configuration fails + * @throws InterruptedException + * if the request is interrupted + */ + public void configure(String filePath, String workDir, TestInfo testInfo) throws IOException, InterruptedException { + if (proxyUrl == null) { + throw new IllegalStateException("Proxy not started"); + } + + Map config = new java.util.HashMap<>(); + config.put("filePath", filePath); + config.put("workDir", workDir); + if (testInfo != null) { + config.put("testInfo", Map.of("file", testInfo.file(), "line", testInfo.line())); + } + + String body = MAPPER.writeValueAsString(config); + + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(proxyUrl + "/config")) + .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body)).build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new IOException("Proxy config failed with status " + response.statusCode() + ": " + response.body()); + } + } + + /** + * Gets the captured HTTP exchanges from the proxy. + * + * @return list of exchange maps containing request/response data + * @throws IOException + * if the request fails + * @throws InterruptedException + * if the request is interrupted + */ + public List> getExchanges() throws IOException, InterruptedException { + if (proxyUrl == null) { + throw new IllegalStateException("Proxy not started"); + } + + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(proxyUrl + "/exchanges")).GET().build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new IOException("Failed to get exchanges: " + response.statusCode()); + } + + return MAPPER.readValue(response.body(), new TypeReference>>() { + }); + } + + /** + * Stops the proxy server gracefully. + * + * @throws IOException + * if the stop request fails + * @throws InterruptedException + * if the request is interrupted + */ + public void stop() throws IOException, InterruptedException { + stop(false); + } + + /** + * Stops the proxy server. + * + * @param skipWritingCache + * if true, won't write captured exchanges to disk + * @throws IOException + * if the stop request fails + * @throws InterruptedException + * if the request is interrupted + */ + public void stop(boolean skipWritingCache) throws IOException, InterruptedException { + if (process == null) { + return; + } + + // Send stop request to the server + if (proxyUrl != null) { + try { + String stopUrl = proxyUrl + "/stop"; + if (skipWritingCache) { + stopUrl += "?skipWritingCache=true"; + } + + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(stopUrl)) + .POST(HttpRequest.BodyPublishers.noBody()).build(); + + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (Exception e) { + // Best effort - ignore errors + } + } + + // Wait for the process to exit + process.waitFor(5, TimeUnit.SECONDS); + if (process.isAlive()) { + process.destroyForcibly(); + } + + // Close the stdout reader + if (stdoutReader != null) { + try { + stdoutReader.close(); + } catch (IOException e) { + // Ignore + } + stdoutReader = null; + } + + process = null; + proxyUrl = null; + } + + /** + * Gets the proxy URL. + * + * @return the proxy URL, or null if not started + */ + public String getProxyUrl() { + return proxyUrl; + } + + @Override + public void close() throws Exception { + stop(); + } + + /** + * Finds the test/harness directory by walking up from the current directory. + */ + private Path findHarnessDirectory() { + Path current = Paths.get(System.getProperty("user.dir")); + + // Walk up the directory tree looking for test/harness + while (current != null) { + Path harnessDir = current.resolve("test").resolve("harness"); + if (harnessDir.toFile().exists() && harnessDir.resolve("server.ts").toFile().exists()) { + return harnessDir; + } + current = current.getParent(); + } + + return null; + } + + /** + * Test information record for configuring the proxy. + */ + public record TestInfo(String file, int line) { + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/java/src/test/java/com/github/copilot/sdk/CopilotClientTest.java new file mode 100644 index 0000000..7093081 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -0,0 +1,166 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.PingResponse; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for CopilotClient. + * + * Note: These tests require the Copilot CLI to be installed. Set the + * COPILOT_CLI_PATH environment variable to the path to the CLI, or run 'npm + * install' in the nodejs directory. + */ +public class CopilotClientTest { + + private static String cliPath; + + @BeforeAll + static void setup() { + cliPath = getCliPath(); + } + + private static String getCliPath() { + // First, try to find 'copilot' in PATH + String copilotInPath = findCopilotInPath(); + if (copilotInPath != null) { + return copilotInPath; + } + + // Fall back to COPILOT_CLI_PATH environment variable + String envPath = System.getenv("COPILOT_CLI_PATH"); + if (envPath != null && !envPath.isEmpty()) { + return envPath; + } + + // Search for the CLI in the parent directories (nodejs module) + Path current = Paths.get(System.getProperty("user.dir")); + while (current != null) { + Path cliPath = current.resolve("nodejs/node_modules/@github/copilot/index.js"); + if (cliPath.toFile().exists()) { + return cliPath.toString(); + } + current = current.getParent(); + } + + return null; + } + + private static String findCopilotInPath() { + try { + // Use 'where' on Windows, 'which' on Unix-like systems + String command = System.getProperty("os.name").toLowerCase().contains("win") ? "where" : "which"; + ProcessBuilder pb = new ProcessBuilder(command, "copilot"); + pb.redirectErrorStream(true); + Process process = pb.start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line = reader.readLine(); + int exitCode = process.waitFor(); + if (exitCode == 0 && line != null && !line.isEmpty()) { + return line.trim(); + } + } + } catch (Exception e) { + // Ignore - copilot not found in PATH + } + return null; + } + + @Test + void testClientConstruction() { + CopilotClient client = new CopilotClient(); + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + client.close(); + } + + @Test + void testClientConstructionWithOptions() { + CopilotClientOptions options = new CopilotClientOptions().setCliPath("/path/to/cli").setLogLevel("debug") + .setAutoStart(false); + + CopilotClient client = new CopilotClient(options); + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + client.close(); + } + + @Test + void testCliUrlMutualExclusion() { + CopilotClientOptions options = new CopilotClientOptions().setCliUrl("localhost:3000").setUseStdio(true); + + assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); + } + + @Test + void testCliUrlMutualExclusionWithCliPath() { + CopilotClientOptions options = new CopilotClientOptions().setCliUrl("localhost:3000").setCliPath("/path/to/cli") + .setUseStdio(false); + + assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); + } + + @Test + void testStartAndConnectUsingStdio() throws Exception { + if (cliPath == null) { + System.out.println("Skipping test: CLI not found"); + return; + } + + try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setUseStdio(true))) { + client.start().get(); + assertEquals(ConnectionState.CONNECTED, client.getState()); + + PingResponse pong = client.ping("test message").get(); + assertEquals("pong: test message", pong.getMessage()); + assertTrue(pong.getTimestamp() >= 0); + + client.stop().get(); + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + } + } + + @Test + void testStartAndConnectUsingTcp() throws Exception { + if (cliPath == null) { + System.out.println("Skipping test: CLI not found"); + return; + } + + try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setUseStdio(false))) { + client.start().get(); + assertEquals(ConnectionState.CONNECTED, client.getState()); + + PingResponse pong = client.ping("test message").get(); + assertEquals("pong: test message", pong.getMessage()); + + client.stop().get(); + } + } + + @Test + void testForceStopWithoutCleanup() throws Exception { + if (cliPath == null) { + System.out.println("Skipping test: CLI not found"); + return; + } + + try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath))) { + client.createSession().get(); + client.forceStop().get(); + + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java b/java/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java new file mode 100644 index 0000000..d3b007d --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java @@ -0,0 +1,423 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +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.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.events.AbstractSessionEvent; +import com.github.copilot.sdk.events.AssistantMessageDeltaEvent; +import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.events.SessionIdleEvent; +import com.github.copilot.sdk.events.SessionStartEvent; +import com.github.copilot.sdk.events.UserMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SystemMessageConfig; + +/** + * Tests for CopilotSession. + * + *

+ * These tests use the shared CapiProxy infrastructure for deterministic API + * response replay. Snapshots are stored in test/snapshots/session/. + *

+ */ +public class CopilotSessionTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + @Test + void testCreateAndDestroySession() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + + assertNotNull(session.getSessionId()); + assertTrue(session.getSessionId().matches("^[a-f0-9-]+$")); + + List messages = session.getMessages().get(); + assertFalse(messages.isEmpty()); + assertTrue(messages.get(0) instanceof SessionStartEvent); + + session.close(); + + // Session should no longer be accessible + try { + session.getMessages().get(); + fail("Expected exception for closed session"); + } catch (Exception e) { + assertTrue(e.getMessage().toLowerCase().contains("not found") + || e.getCause().getMessage().toLowerCase().contains("not found")); + } + } + } + + @Test + void testStatefulConversation() throws Exception { + ctx.configureForTest("session", "should_have_stateful_conversation"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession().get(); + + AssistantMessageEvent response1 = session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?"), 60000) + .get(90, TimeUnit.SECONDS); + + assertNotNull(response1); + assertTrue(response1.getData().getContent().contains("2"), + "Response should contain 2: " + response1.getData().getContent()); + + AssistantMessageEvent response2 = session + .sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?"), 60000) + .get(90, TimeUnit.SECONDS); + + assertNotNull(response2); + assertTrue(response2.getData().getContent().contains("4"), + "Response should contain 4: " + response2.getData().getContent()); + + session.close(); + } + } + + @Test + void testReceiveSessionEvents() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession().get(); + + List receivedEvents = new ArrayList<>(); + CompletableFuture idleReceived = new CompletableFuture<>(); + + session.on(evt -> { + receivedEvents.add(evt); + if (evt instanceof SessionIdleEvent) { + idleReceived.complete(null); + } + }); + + session.send(new MessageOptions().setPrompt("What is 100+200?")).get(); + + idleReceived.get(60, TimeUnit.SECONDS); + + assertFalse(receivedEvents.isEmpty()); + assertTrue(receivedEvents.stream().anyMatch(e -> e instanceof UserMessageEvent)); + assertTrue(receivedEvents.stream().anyMatch(e -> e instanceof AssistantMessageEvent)); + assertTrue(receivedEvents.stream().anyMatch(e -> e instanceof SessionIdleEvent)); + + // Find the assistant message + AssistantMessageEvent assistantMsg = receivedEvents.stream().filter(e -> e instanceof AssistantMessageEvent) + .map(e -> (AssistantMessageEvent) e).findFirst().orElse(null); + + assertNotNull(assistantMsg); + assertTrue(assistantMsg.getData().getContent().contains("300"), + "Response should contain 300: " + assistantMsg.getData().getContent()); + + session.close(); + } + } + + @Test + void testSendReturnsImmediately() throws Exception { + ctx.configureForTest("session", "send_returns_immediately_while_events_stream_in_background"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession().get(); + + List events = new ArrayList<>(); + AtomicReference lastMessage = new AtomicReference<>(); + CompletableFuture done = new CompletableFuture<>(); + + session.on(evt -> { + events.add(evt.getType()); + if (evt instanceof AssistantMessageEvent msg) { + lastMessage.set(msg); + } else if (evt instanceof SessionIdleEvent) { + done.complete(null); + } + }); + + // Use a slow command so we can verify send() returns before completion + session.send(new MessageOptions().setPrompt("Run 'sleep 2 && echo done'")).get(); + + // At this point, we might not have received session.idle yet + // The event handling happens asynchronously + + // Wait for completion + done.get(60, TimeUnit.SECONDS); + + assertTrue(events.contains("session.idle")); + assertTrue(events.contains("assistant.message")); + assertNotNull(lastMessage.get()); + assertTrue(lastMessage.get().getData().getContent().contains("done"), + "Response should contain done: " + lastMessage.get().getData().getContent()); + + session.close(); + } + } + + @Test + void testSendAndWaitBlocksUntilIdle() throws Exception { + ctx.configureForTest("session", "sendandwait_blocks_until_session_idle_and_returns_final_assistant_message"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession().get(); + + List events = new ArrayList<>(); + session.on(evt -> events.add(evt.getType())); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + assertEquals("assistant.message", response.getType()); + assertTrue(response.getData().getContent().contains("4"), + "Response should contain 4: " + response.getData().getContent()); + assertTrue(events.contains("session.idle")); + assertTrue(events.contains("assistant.message")); + + session.close(); + } + } + + @Test + void testResumeSessionWithSameClient() throws Exception { + ctx.configureForTest("session", "should_resume_a_session_using_the_same_client"); + + try (CopilotClient client = ctx.createClient()) { + // Create initial session + CopilotSession session1 = client.createSession().get(); + String sessionId = session1.getSessionId(); + + AssistantMessageEvent answer = session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, + TimeUnit.SECONDS); + assertNotNull(answer); + assertTrue(answer.getData().getContent().contains("2"), + "Response should contain 2: " + answer.getData().getContent()); + + // Resume using the same client + CopilotSession session2 = client.resumeSession(sessionId).get(); + + assertEquals(sessionId, session2.getSessionId()); + + // Verify resumed session has the previous messages + List messages = session2.getMessages().get(60, TimeUnit.SECONDS); + boolean hasAssistantMessage = messages.stream().filter(m -> m instanceof AssistantMessageEvent) + .map(m -> (AssistantMessageEvent) m).anyMatch(m -> m.getData().getContent().contains("2")); + assertTrue(hasAssistantMessage, "Should find previous assistant message containing 2"); + + session2.close(); + } + } + + @Test + void testResumeSessionWithNewClient() throws Exception { + ctx.configureForTest("session", "should_resume_a_session_using_a_new_client"); + + String sessionId; + + // First client - create session + try (CopilotClient client1 = ctx.createClient()) { + CopilotSession session1 = client1.createSession().get(); + sessionId = session1.getSessionId(); + + AssistantMessageEvent answer = session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, + TimeUnit.SECONDS); + assertNotNull(answer); + assertTrue(answer.getData().getContent().contains("2"), + "Response should contain 2: " + answer.getData().getContent()); + } + + // Second client - resume session + try (CopilotClient client2 = ctx.createClient()) { + CopilotSession session2 = client2.resumeSession(sessionId).get(); + + assertEquals(sessionId, session2.getSessionId()); + + // When resuming with a new client, validate messages contain expected types + List messages = session2.getMessages().get(60, TimeUnit.SECONDS); + assertTrue(messages.stream().anyMatch(m -> m instanceof UserMessageEvent), + "Should contain user.message event"); + assertTrue(messages.stream().anyMatch(m -> "session.resume".equals(m.getType())), + "Should contain session.resume event"); + + session2.close(); + } + } + + @Test + void testSessionWithAppendedSystemMessage() throws Exception { + ctx.configureForTest("session", "should_create_a_session_with_appended_systemmessage_config"); + + try (CopilotClient client = ctx.createClient()) { + String systemMessageSuffix = "End each response with the phrase 'Have a nice day!'"; + SessionConfig config = new SessionConfig().setSystemMessage( + new SystemMessageConfig().setContent(systemMessageSuffix).setMode(SystemMessageMode.APPEND)); + + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("What is your full name?")).get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().getContent().contains("GitHub"), + "Response should contain GitHub: " + response.getData().getContent()); + assertTrue(response.getData().getContent().contains("Have a nice day!"), + "Response should end with 'Have a nice day!': " + response.getData().getContent()); + session.close(); + } + } + + @Test + void testSessionWithReplacedSystemMessage() throws Exception { + ctx.configureForTest("session", "should_create_a_session_with_replaced_systemmessage_config"); + + try (CopilotClient client = ctx.createClient()) { + String testSystemMessage = "You are an assistant called Testy McTestface. Reply succinctly."; + SessionConfig config = new SessionConfig().setSystemMessage( + new SystemMessageConfig().setContent(testSystemMessage).setMode(SystemMessageMode.REPLACE)); + + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("What is your full name?")).get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().getContent().contains("Testy McTestface"), + "Response should contain 'Testy McTestface': " + response.getData().getContent()); + session.close(); + } + } + + @Test + void testSessionWithStreamingEnabled() throws Exception { + ctx.configureForTest("session", "should_receive_streaming_delta_events_when_streaming_is_enabled"); + + try (CopilotClient client = ctx.createClient()) { + SessionConfig config = new SessionConfig().setStreaming(true); + + CopilotSession session = client.createSession(config).get(); + + List receivedEvents = new ArrayList<>(); + CompletableFuture idleReceived = new CompletableFuture<>(); + + session.on(evt -> { + receivedEvents.add(evt); + if (evt instanceof SessionIdleEvent) { + idleReceived.complete(null); + } + }); + + session.send(new MessageOptions().setPrompt("What is 2+2?")).get(); + + idleReceived.get(60, TimeUnit.SECONDS); + + // Should have received delta events when streaming is enabled + boolean hasDeltaEvents = receivedEvents.stream().anyMatch(e -> e instanceof AssistantMessageDeltaEvent); + assertTrue(hasDeltaEvents, "Should receive streaming delta events when streaming is enabled"); + + session.close(); + } + } + + @Test + void testAbortSession() throws Exception { + ctx.configureForTest("session", "should_abort_a_session"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession().get(); + assertNotNull(session.getSessionId()); + + // Send a message (non-blocking) + session.send(new MessageOptions().setPrompt("What is 1+1?")).get(); + + // Abort the session immediately + session.abort(); + + // The session should still be alive and usable after abort + List messages = session.getMessages().get(60, TimeUnit.SECONDS); + assertFalse(messages.isEmpty()); + + // We should be able to send another message + AssistantMessageEvent answer = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60, + TimeUnit.SECONDS); + assertNotNull(answer); + assertTrue(answer.getData().getContent().contains("4"), + "Response should contain 4: " + answer.getData().getContent()); + + session.close(); + } + } + + @Test + void testSessionWithAvailableTools() throws Exception { + ctx.configureForTest("session", "should_create_a_session_with_availabletools"); + + try (CopilotClient client = ctx.createClient()) { + SessionConfig config = new SessionConfig().setAvailableTools(List.of("view", "edit")); + + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + session.close(); + } + } + + @Test + void testSessionWithExcludedTools() throws Exception { + ctx.configureForTest("session", "should_create_a_session_with_excludedtools"); + + try (CopilotClient client = ctx.createClient()) { + SessionConfig config = new SessionConfig().setExcludedTools(List.of("view")); + + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().getContent().contains("2"), + "Response should contain 2: " + response.getData().getContent()); + session.close(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/E2ETestContext.java b/java/src/test/java/com/github/copilot/sdk/E2ETestContext.java new file mode 100644 index 0000000..90c2692 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/E2ETestContext.java @@ -0,0 +1,258 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import com.github.copilot.sdk.json.CopilotClientOptions; + +/** + * E2E test context that manages the test environment including the CapiProxy, + * working directories, and CLI path. + * + *

+ * This provides a complete test environment similar to the Node.js, .NET, Go, + * and Python SDK test harnesses. It manages: + *

+ *
    + *
  • A replaying CapiProxy for deterministic API responses
  • + *
  • Temporary home and work directories for test isolation
  • + *
  • Environment variables for the Copilot CLI
  • + *
+ * + *

+ * Usage example: + *

+ * + *
+ * {@code
+ * try (E2ETestContext ctx = E2ETestContext.create()) {
+ * 	ctx.configureForTest("tools", "my_test_name");
+ *
+ * 	try (CopilotClient client = ctx.createClient()) {
+ * 		CopilotSession session = client.createSession().get();
+ * 		// ... run test ...
+ * 	}
+ * }
+ * }
+ * 
+ */ +public class E2ETestContext implements AutoCloseable { + + private static final Pattern SNAKE_CASE = Pattern.compile("[^a-zA-Z0-9]"); + + private final String cliPath; + private final Path homeDir; + private final Path workDir; + private final String proxyUrl; + private final CapiProxy proxy; + private final Path repoRoot; + + private E2ETestContext(String cliPath, Path homeDir, Path workDir, String proxyUrl, CapiProxy proxy, + Path repoRoot) { + this.cliPath = cliPath; + this.homeDir = homeDir; + this.workDir = workDir; + this.proxyUrl = proxyUrl; + this.proxy = proxy; + this.repoRoot = repoRoot; + } + + /** + * Creates a new E2E test context. + * + * @return the test context + * @throws IOException + * if setup fails + * @throws InterruptedException + * if setup is interrupted + */ + public static E2ETestContext create() throws IOException, InterruptedException { + Path repoRoot = findRepoRoot(); + String cliPath = getCliPath(repoRoot); + + Path tempDir = Paths.get(System.getProperty("java.io.tmpdir")); + Path homeDir = Files.createTempDirectory(tempDir, "copilot-test-config-"); + Path workDir = Files.createTempDirectory(tempDir, "copilot-test-work-"); + + CapiProxy proxy = new CapiProxy(); + String proxyUrl = proxy.start(); + + return new E2ETestContext(cliPath, homeDir, workDir, proxyUrl, proxy, repoRoot); + } + + /** + * Gets the Copilot CLI path. + */ + public String getCliPath() { + return cliPath; + } + + /** + * Gets the temporary home directory for test isolation. + */ + public Path getHomeDir() { + return homeDir; + } + + /** + * Gets the temporary working directory for tests. + */ + public Path getWorkDir() { + return workDir; + } + + /** + * Gets the proxy URL. + */ + public String getProxyUrl() { + return proxyUrl; + } + + /** + * Configures the proxy for a specific test. + * + * @param testFile + * the test category folder (e.g., "tools", "session", "permissions") + * @param testName + * the test method name (will be converted to snake_case) + * @throws IOException + * if configuration fails + * @throws InterruptedException + * if configuration is interrupted + */ + public void configureForTest(String testFile, String testName) throws IOException, InterruptedException { + // Convert test method names to lowercase snake_case for snapshot filenames + // to avoid case collisions on case-insensitive filesystems (macOS/Windows) + String sanitizedName = SNAKE_CASE.matcher(testName).replaceAll("_").toLowerCase(); + String snapshotPath = repoRoot.resolve("test").resolve("snapshots").resolve(testFile) + .resolve(sanitizedName + ".yaml").toString(); + proxy.configure(snapshotPath, workDir.toString()); + } + + /** + * Gets the captured HTTP exchanges from the proxy. + * + * @return list of exchange maps + * @throws IOException + * if the request fails + * @throws InterruptedException + * if the request is interrupted + */ + public List> getExchanges() throws IOException, InterruptedException { + return proxy.getExchanges(); + } + + /** + * Gets the environment variables needed for the Copilot CLI. + * + * @return map of environment variables + */ + public Map getEnvironment() { + Map env = new HashMap<>(System.getenv()); + env.put("COPILOT_API_URL", proxyUrl); + env.put("XDG_CONFIG_HOME", homeDir.toString()); + env.put("XDG_STATE_HOME", homeDir.toString()); + return env; + } + + /** + * Creates a CopilotClient configured for this test context. + * + * @return a new CopilotClient + */ + public CopilotClient createClient() { + return new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setCwd(workDir.toString()) + .setEnvironment(getEnvironment())); + } + + @Override + public void close() throws Exception { + proxy.stop(); + + // Clean up temp directories (best effort) + deleteRecursively(homeDir); + deleteRecursively(workDir); + } + + private static Path findRepoRoot() throws IOException { + Path dir = Paths.get(System.getProperty("user.dir")); + while (dir != null) { + if (Files.exists(dir.resolve("nodejs")) && Files.exists(dir.resolve("test").resolve("harness"))) { + return dir; + } + dir = dir.getParent(); + } + throw new IOException("Could not find repository root"); + } + + private static String getCliPath(Path repoRoot) throws IOException { + // First, try to find 'copilot' in PATH + String copilotInPath = findCopilotInPath(); + if (copilotInPath != null) { + return copilotInPath; + } + + // Try environment variable + String envPath = System.getenv("COPILOT_CLI_PATH"); + if (envPath != null && !envPath.isEmpty()) { + return envPath; + } + + // Try nodejs installation + Path cliPath = repoRoot.resolve("nodejs/node_modules/@github/copilot/index.js"); + if (Files.exists(cliPath)) { + return cliPath.toString(); + } + + throw new IOException("CLI not found. Either install 'copilot' globally, set COPILOT_CLI_PATH, " + + "or run 'npm install' in the nodejs directory."); + } + + private static String findCopilotInPath() { + try { + String command = System.getProperty("os.name").toLowerCase().contains("win") ? "where" : "which"; + ProcessBuilder pb = new ProcessBuilder(command, "copilot"); + pb.redirectErrorStream(true); + Process process = pb.start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line = reader.readLine(); + int exitCode = process.waitFor(); + if (exitCode == 0 && line != null && !line.isEmpty()) { + return line.trim(); + } + } + } catch (Exception e) { + // Ignore - copilot not found in PATH + } + return null; + } + + private static void deleteRecursively(Path path) { + try { + if (Files.exists(path)) { + Files.walk(path).sorted((a, b) -> b.compareTo(a)) // Reverse order to delete children first + .forEach(p -> { + try { + Files.delete(p); + } catch (IOException e) { + // Best effort + } + }); + } + } catch (IOException e) { + // Best effort + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java b/java/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java new file mode 100644 index 0000000..662d1cc --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java @@ -0,0 +1,276 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.json.CustomAgentConfig; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * Tests for MCP Servers and Custom Agents functionality. + * + *

+ * These tests use the shared CapiProxy infrastructure for deterministic API + * response replay. Snapshots are stored in test/snapshots/mcp-and-agents/. + *

+ */ +public class McpAndAgentsTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + // Helper method to create an MCP local server configuration + private Map createLocalMcpServer(String command, List args) { + Map server = new HashMap<>(); + server.put("type", "local"); + server.put("command", command); + server.put("args", args); + server.put("tools", List.of("*")); + return server; + } + + // ============ MCP Server Tests ============ + + @Test + void testAcceptMcpServerConfigurationOnSessionCreate() throws Exception { + ctx.configureForTest("mcp-and-agents", "should_accept_mcp_server_configuration_on_session_create"); + + Map mcpServers = new HashMap<>(); + mcpServers.put("test-server", createLocalMcpServer("echo", List.of("hello"))); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setMcpServers(mcpServers)).get(); + + assertNotNull(session.getSessionId()); + + // Simple interaction to verify session works + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().getContent().contains("4"), + "Response should contain 4: " + response.getData().getContent()); + + session.close(); + } + } + + @Test + void testAcceptMcpServerConfigurationOnSessionResume() throws Exception { + ctx.configureForTest("mcp-and-agents", "should_accept_mcp_server_configuration_on_session_resume"); + + try (CopilotClient client = ctx.createClient()) { + // Create a session first + CopilotSession session1 = client.createSession().get(); + String sessionId = session1.getSessionId(); + session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + // Resume with MCP servers + Map mcpServers = new HashMap<>(); + mcpServers.put("test-server", createLocalMcpServer("echo", List.of("hello"))); + + CopilotSession session2 = client + .resumeSession(sessionId, new ResumeSessionConfig().setMcpServers(mcpServers)).get(); + + assertEquals(sessionId, session2.getSessionId()); + + AssistantMessageEvent response = session2.sendAndWait(new MessageOptions().setPrompt("What is 3+3?")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().getContent().contains("6"), + "Response should contain 6: " + response.getData().getContent()); + + session2.close(); + } + } + + @Test + void testHandleMultipleMcpServers() throws Exception { + // Use same snapshot as single MCP server test since it doesn't depend on server + // count + ctx.configureForTest("mcp-and-agents", "should_accept_mcp_server_configuration_on_session_create"); + + Map mcpServers = new HashMap<>(); + mcpServers.put("server1", createLocalMcpServer("echo", List.of("server1"))); + mcpServers.put("server2", createLocalMcpServer("echo", List.of("server2"))); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setMcpServers(mcpServers)).get(); + + assertNotNull(session.getSessionId()); + session.close(); + } + } + + // ============ Custom Agent Tests ============ + + @Test + void testAcceptCustomAgentConfigurationOnSessionCreate() throws Exception { + ctx.configureForTest("mcp-and-agents", "should_accept_custom_agent_configuration_on_session_create"); + + List customAgents = List.of(new CustomAgentConfig().setName("test-agent") + .setDisplayName("Test Agent").setDescription("A test agent for SDK testing") + .setPrompt("You are a helpful test agent.").setInfer(true)); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setCustomAgents(customAgents)).get(); + + assertNotNull(session.getSessionId()); + + // Simple interaction to verify session works + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 5+5?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().getContent().contains("10"), + "Response should contain 10: " + response.getData().getContent()); + + session.close(); + } + } + + @Test + void testAcceptCustomAgentConfigurationOnSessionResume() throws Exception { + ctx.configureForTest("mcp-and-agents", "should_accept_custom_agent_configuration_on_session_resume"); + + try (CopilotClient client = ctx.createClient()) { + // Create a session first + CopilotSession session1 = client.createSession().get(); + String sessionId = session1.getSessionId(); + session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + // Resume with custom agents + List customAgents = List + .of(new CustomAgentConfig().setName("resume-agent").setDisplayName("Resume Agent") + .setDescription("An agent added on resume").setPrompt("You are a resume test agent.")); + + CopilotSession session2 = client + .resumeSession(sessionId, new ResumeSessionConfig().setCustomAgents(customAgents)).get(); + + assertEquals(sessionId, session2.getSessionId()); + + AssistantMessageEvent response = session2.sendAndWait(new MessageOptions().setPrompt("What is 6+6?")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().getContent().contains("12"), + "Response should contain 12: " + response.getData().getContent()); + + session2.close(); + } + } + + @Test + void testCustomAgentWithToolsConfiguration() throws Exception { + // Use same snapshot as create test since this just verifies configuration + // acceptance + ctx.configureForTest("mcp-and-agents", "should_accept_custom_agent_configuration_on_session_create"); + + List customAgents = List.of(new CustomAgentConfig().setName("tool-agent") + .setDisplayName("Tool Agent").setDescription("An agent with specific tools") + .setPrompt("You are an agent with specific tools.").setTools(List.of("bash", "edit")).setInfer(true)); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setCustomAgents(customAgents)).get(); + + assertNotNull(session.getSessionId()); + session.close(); + } + } + + @Test + void testCustomAgentWithMcpServers() throws Exception { + // Use combined snapshot since this uses both MCP servers and custom agents + ctx.configureForTest("mcp-and-agents", "should_accept_both_mcp_servers_and_custom_agents"); + + Map agentMcpServers = new HashMap<>(); + agentMcpServers.put("agent-server", createLocalMcpServer("echo", List.of("agent-mcp"))); + + List customAgents = List.of(new CustomAgentConfig().setName("mcp-agent") + .setDisplayName("MCP Agent").setDescription("An agent with its own MCP servers") + .setPrompt("You are an agent with MCP servers.").setMcpServers(agentMcpServers)); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setCustomAgents(customAgents)).get(); + + assertNotNull(session.getSessionId()); + session.close(); + } + } + + @Test + void testMultipleCustomAgents() throws Exception { + // Use same snapshot as create test + ctx.configureForTest("mcp-and-agents", "should_accept_custom_agent_configuration_on_session_create"); + + List customAgents = List.of( + new CustomAgentConfig().setName("agent1").setDisplayName("Agent One").setDescription("First agent") + .setPrompt("You are agent one."), + new CustomAgentConfig().setName("agent2").setDisplayName("Agent Two").setDescription("Second agent") + .setPrompt("You are agent two.").setInfer(false)); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setCustomAgents(customAgents)).get(); + + assertNotNull(session.getSessionId()); + session.close(); + } + } + + // ============ Combined Configuration Tests ============ + + @Test + void testAcceptBothMcpServersAndCustomAgents() throws Exception { + ctx.configureForTest("mcp-and-agents", "should_accept_both_mcp_servers_and_custom_agents"); + + Map mcpServers = new HashMap<>(); + mcpServers.put("shared-server", createLocalMcpServer("echo", List.of("shared"))); + + List customAgents = List.of(new CustomAgentConfig().setName("combined-agent") + .setDisplayName("Combined Agent").setDescription("An agent using shared MCP servers") + .setPrompt("You are a combined test agent.")); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setMcpServers(mcpServers).setCustomAgents(customAgents)).get(); + + assertNotNull(session.getSessionId()); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 7+7?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().getContent().contains("14"), + "Response should contain 14: " + response.getData().getContent()); + + session.close(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/PermissionsTest.java b/java/src/test/java/com/github/copilot/sdk/PermissionsTest.java new file mode 100644 index 0000000..c4654e4 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/PermissionsTest.java @@ -0,0 +1,263 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.json.PermissionRequest; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.MessageOptions; + +/** + * Tests for permission callback functionality. + * + *

+ * These tests use the shared CapiProxy infrastructure for deterministic API + * response replay. Snapshots are stored in test/snapshots/permissions/. + *

+ */ +public class PermissionsTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + @Test + void testPermissionHandlerForWriteOperations(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "permission_handler_for_write_operations"); + + List permissionRequests = new ArrayList<>(); + + final String[] sessionIdHolder = new String[1]; + + SessionConfig config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { + permissionRequests.add(request); + assertEquals(sessionIdHolder[0], invocation.getSessionId()); + // Approve the permission + return CompletableFuture.completedFuture(new PermissionRequestResult().setKind("approved")); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + sessionIdHolder[0] = session.getSessionId(); + + // Write a test file + Path testFile = ctx.getWorkDir().resolve("test.txt"); + Files.writeString(testFile, "original content"); + + session.sendAndWait(new MessageOptions().setPrompt("Edit test.txt and replace 'original' with 'modified'")) + .get(60, TimeUnit.SECONDS); + + // Should have received at least one permission request + assertFalse(permissionRequests.isEmpty(), "Should have received permission requests"); + + // Should include write permission request + boolean hasWriteRequest = permissionRequests.stream().anyMatch(req -> "write".equals(req.getKind())); + assertTrue(hasWriteRequest, "Should have received a write permission request"); + + session.close(); + } + } + + @Test + void testDenyPermission(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "deny_permission"); + + SessionConfig config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { + // Deny all permissions + return CompletableFuture + .completedFuture(new PermissionRequestResult().setKind("denied-interactively-by-user")); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + String originalContent = "protected content"; + Path testFile = ctx.getWorkDir().resolve("protected.txt"); + Files.writeString(testFile, originalContent); + + session.sendAndWait( + new MessageOptions().setPrompt("Edit protected.txt and replace 'protected' with 'hacked'.")) + .get(60, TimeUnit.SECONDS); + + // Verify the file was NOT modified + String content = Files.readString(testFile); + assertEquals(originalContent, content, "File should not have been modified"); + + session.close(); + } + } + + @Test + void testWithoutPermissionHandler(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "without_permission_handler"); + + try (CopilotClient client = ctx.createClient()) { + // Create session without onPermissionRequest handler + CopilotSession session = client.createSession().get(); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().getContent().contains("4"), + "Response should contain 4: " + response.getData().getContent()); + + session.close(); + } + } + + @Test + void testAsyncPermissionHandler(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "async_permission_handler"); + + List permissionRequests = new ArrayList<>(); + + SessionConfig config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { + permissionRequests.add(request); + + // Simulate async permission check with delay + return CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(10); // Small delay to simulate async check + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return new PermissionRequestResult().setKind("approved"); + }); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + session.sendAndWait(new MessageOptions().setPrompt("Run 'echo test' and tell me what happens")).get(60, + TimeUnit.SECONDS); + + // Should have received permission requests + assertFalse(permissionRequests.isEmpty(), "Should have received permission requests"); + + session.close(); + } + } + + @Test + void testResumeSessionWithPermissionHandler(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "resume_session_with_permission_handler"); + + List permissionRequests = new ArrayList<>(); + + try (CopilotClient client = ctx.createClient()) { + // Create session without permission handler + CopilotSession session1 = client.createSession().get(); + String sessionId = session1.getSessionId(); + session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + // Resume with permission handler + ResumeSessionConfig resumeConfig = new ResumeSessionConfig() + .setOnPermissionRequest((request, invocation) -> { + permissionRequests.add(request); + return CompletableFuture.completedFuture(new PermissionRequestResult().setKind("approved")); + }); + + CopilotSession session2 = client.resumeSession(sessionId, resumeConfig).get(); + + assertEquals(sessionId, session2.getSessionId()); + + session2.sendAndWait(new MessageOptions().setPrompt("Run 'echo resumed' for me")).get(60, TimeUnit.SECONDS); + + // Should have permission requests from resumed session + assertFalse(permissionRequests.isEmpty(), "Should have received permission requests from resumed session"); + + session2.close(); + } + } + + @Test + void testToolCallIdInPermissionRequests(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "tool_call_id_in_permission_requests"); + + final boolean[] receivedToolCallId = {false}; + + SessionConfig config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { + if (request.getToolCallId() != null) { + receivedToolCallId[0] = true; + assertFalse(request.getToolCallId().isEmpty(), "Tool call ID should not be empty"); + } + return CompletableFuture.completedFuture(new PermissionRequestResult().setKind("approved")); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + session.sendAndWait(new MessageOptions().setPrompt("Run 'echo test'")).get(60, TimeUnit.SECONDS); + + assertTrue(receivedToolCallId[0], "Should have received toolCallId in permission request"); + + session.close(); + } + } + + /** + * Note: This test verifies error handling in permission handlers. When the + * handler throws an exception, the SDK should deny the permission and the + * assistant should indicate it couldn't complete the task. + * + * Currently disabled because the test is flaky and requires proper error + * handling infrastructure that returns a response promptly when permission is + * denied due to errors. + */ + @Disabled("Requires improved error handling for permission handler exceptions") + @Test + void testPermissionHandlerErrorsGracefully(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "permission_handler_errors_gracefully"); + + SessionConfig config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { + // Throw an error in the handler + throw new RuntimeException("Handler error"); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Run 'echo test'. If you can't, say 'failed'.")) + .get(60, TimeUnit.SECONDS); + + // Should handle the error and deny permission + assertNotNull(response); + String content = response.getData().getContent().toLowerCase(); + assertTrue(content.contains("fail") || content.contains("cannot") || content.contains("unable") + || content.contains("permission"), "Response should indicate failure: " + content); + + session.close(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/ToolsTest.java b/java/src/test/java/com/github/copilot/sdk/ToolsTest.java new file mode 100644 index 0000000..8aae004 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/ToolsTest.java @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.ToolDefinition; + +/** + * Tests for custom tools functionality. + * + *

+ * These tests use the shared CapiProxy infrastructure for deterministic API + * response replay. Snapshots are stored in test/snapshots/tools/. + *

+ */ +public class ToolsTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + @Test + void testInvokesBuiltInTools(TestInfo testInfo) throws Exception { + ctx.configureForTest("tools", "invokes_built_in_tools"); + + // Write a test file + Path readmeFile = ctx.getWorkDir().resolve("README.md"); + Files.writeString(readmeFile, "# ELIZA, the only chatbot you'll ever need"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession().get(); + + AssistantMessageEvent response = session + .sendAndWait( + new MessageOptions().setPrompt("What's the first line of README.md in this directory?")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().getContent().contains("ELIZA"), + "Response should contain ELIZA: " + response.getData().getContent()); + + session.close(); + } + } + + @Test + void testInvokesCustomTool(TestInfo testInfo) throws Exception { + ctx.configureForTest("tools", "invokes_custom_tool"); + + // Define a simple encrypt_string tool + Map parameters = new HashMap<>(); + Map properties = new HashMap<>(); + Map inputProp = new HashMap<>(); + inputProp.put("type", "string"); + inputProp.put("description", "String to encrypt"); + properties.put("input", inputProp); + parameters.put("type", "object"); + parameters.put("properties", properties); + parameters.put("required", List.of("input")); + + ToolDefinition encryptTool = ToolDefinition.create("encrypt_string", "Encrypts a string", parameters, + (invocation) -> { + JsonNode argsNode = (JsonNode) invocation.getArguments(); + String input = argsNode.get("input").asText(); + return CompletableFuture.completedFuture(input.toUpperCase()); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(encryptTool))).get(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Use encrypt_string to encrypt this string: Hello")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().getContent().contains("HELLO"), + "Response should contain HELLO: " + response.getData().getContent()); + + session.close(); + } + } + + @Test + void testHandlesToolCallingErrors(TestInfo testInfo) throws Exception { + ctx.configureForTest("tools", "handles_tool_calling_errors"); + + // Define a tool that throws an error + Map parameters = new HashMap<>(); + parameters.put("type", "object"); + parameters.put("properties", new HashMap<>()); + + ToolDefinition errorTool = ToolDefinition.create("get_user_location", "Gets the user's location", parameters, + (invocation) -> { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new RuntimeException("Melbourne")); + return future; + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(errorTool))).get(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions() + .setPrompt("What is my location? If you can't find out, just say 'unknown'.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + // The error message should NOT be exposed to the assistant + String content = response.getData().getContent().toLowerCase(); + assertFalse(content.contains("melbourne"), "Error details should not be exposed in response: " + content); + assertTrue(content.contains("unknown") || content.contains("unable") || content.contains("cannot"), + "Response should indicate inability to get location: " + content); + + session.close(); + } + } + + @Test + void testCanReceiveAndReturnComplexTypes(TestInfo testInfo) throws Exception { + ctx.configureForTest("tools", "can_receive_and_return_complex_types"); + + // Define a db_query tool with complex parameter and return types + Map querySchema = new HashMap<>(); + Map queryProps = new HashMap<>(); + queryProps.put("table", Map.of("type", "string")); + queryProps.put("ids", Map.of("type", "array", "items", Map.of("type", "integer"))); + queryProps.put("sortAscending", Map.of("type", "boolean")); + querySchema.put("type", "object"); + querySchema.put("properties", queryProps); + querySchema.put("required", List.of("table", "ids", "sortAscending")); + + Map parameters = new HashMap<>(); + Map properties = new HashMap<>(); + properties.put("query", querySchema); + parameters.put("type", "object"); + parameters.put("properties", properties); + parameters.put("required", List.of("query")); + + ToolDefinition dbQueryTool = ToolDefinition.create("db_query", "Performs a database query", parameters, + (invocation) -> { + JsonNode argsNode = (JsonNode) invocation.getArguments(); + JsonNode queryNode = argsNode.get("query"); + + assertEquals("cities", queryNode.get("table").asText()); + + // Return complex data structure + List> results = List.of( + Map.of("countryId", 19, "cityName", "Passos", "population", 135460), + Map.of("countryId", 12, "cityName", "San Lorenzo", "population", 204356)); + + return CompletableFuture.completedFuture(results); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(dbQueryTool))).get(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt( + "Perform a DB query for the 'cities' table using IDs 12 and 19, sorting ascending. " + + "Reply only with lines of the form: [cityname] [population]")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + String content = response.getData().getContent(); + assertTrue(content.contains("Passos"), "Response should contain Passos: " + content); + assertTrue(content.contains("San Lorenzo"), "Response should contain San Lorenzo: " + content); + + session.close(); + } + } +} diff --git a/justfile b/justfile index e214ce1..b517f62 100644 --- a/justfile +++ b/justfile @@ -3,13 +3,13 @@ default: @just --list # Format all code across all languages -format: format-go format-python format-nodejs format-dotnet +format: format-go format-python format-nodejs format-dotnet format-java # Lint all code across all languages -lint: lint-go lint-python lint-nodejs lint-dotnet +lint: lint-go lint-python lint-nodejs lint-dotnet lint-java # Run tests for all languages -test: test-go test-python test-nodejs test-dotnet +test: test-go test-python test-nodejs test-dotnet test-java # Format Go code format-go: @@ -73,6 +73,21 @@ test-dotnet: @echo "=== Testing .NET code ===" @cd dotnet && dotnet test test/GitHub.Copilot.SDK.Test.csproj +# Format Java code +format-java: + @echo "=== Formatting Java code ===" + @cd java && mvn spotless:apply -q + +# Lint Java code +lint-java: + @echo "=== Linting Java code ===" + @cd java && mvn spotless:check compile -q + +# Test Java code +test-java: + @echo "=== Testing Java code ===" + @cd java && mvn test -q + # Install all dependencies install: @echo "=== Installing dependencies ===" @@ -80,6 +95,7 @@ install: @cd python && uv pip install -e ".[dev]" @cd go && go mod download @cd dotnet && dotnet restore + @cd java && mvn dependency:resolve -q @echo "✅ All dependencies installed" # Run interactive SDK playground From a4808ca3ab1d7ad74fcd670b8bd9434481be5450 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 19 Jan 2026 16:42:43 -0500 Subject: [PATCH 2/2] Update java/src/main/java/com/github/copilot/sdk/CopilotClient.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../com/github/copilot/sdk/CopilotClient.java | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/java/src/main/java/com/github/copilot/sdk/CopilotClient.java b/java/src/main/java/com/github/copilot/sdk/CopilotClient.java index 7d775ea..831f530 100644 --- a/java/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/java/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -654,26 +654,27 @@ private ProcessInfo startCliServer() throws IOException, InterruptedException { Integer detectedPort = null; if (!options.isUseStdio()) { // Wait for port announcement - BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); - Pattern portPattern = Pattern.compile("listening on port (\\d+)", Pattern.CASE_INSENSITIVE); - long deadline = System.currentTimeMillis() + 30000; - - while (System.currentTimeMillis() < deadline) { - String line = reader.readLine(); - if (line == null) { - throw new IOException("CLI process exited unexpectedly"); - } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + Pattern portPattern = Pattern.compile("listening on port (\\d+)", Pattern.CASE_INSENSITIVE); + long deadline = System.currentTimeMillis() + 30000; + + while (System.currentTimeMillis() < deadline) { + String line = reader.readLine(); + if (line == null) { + throw new IOException("CLI process exited unexpectedly"); + } - Matcher matcher = portPattern.matcher(line); - if (matcher.find()) { - detectedPort = Integer.parseInt(matcher.group(1)); - break; + Matcher matcher = portPattern.matcher(line); + if (matcher.find()) { + detectedPort = Integer.parseInt(matcher.group(1)); + break; + } } - } - if (detectedPort == null) { - process.destroyForcibly(); - throw new IOException("Timeout waiting for CLI to announce port"); + if (detectedPort == null) { + process.destroyForcibly(); + throw new IOException("Timeout waiting for CLI to announce port"); + } } }