From 46cc1e8a8a4f2b04674e73a8f037606fa1f78ecf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:52:48 +0000 Subject: [PATCH 001/104] Initial plan From 6db81dcf7cfd7eca24644bbdee2db2b3509c6995 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:54:50 +0000 Subject: [PATCH 002/104] Remove non-existent test coverage workflow section from README Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- README.md | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/README.md b/README.md index bf943979c..7bf218eb8 100644 --- a/README.md +++ b/README.md @@ -135,27 +135,6 @@ This SDK tracks the official [Copilot SDK](https://github.com/github/copilot-sdk - **VS Code Copilot Chat** — type `/agentic-merge-upstream` - **GitHub Copilot CLI** — use `copilot` CLI with the same skill reference -### Automated Test Coverage Improvement - -The repository includes an [Agentic Workflow](https://github.com/github/gh-aw) that automatically identifies untested code and creates comprehensive tests to increase coverage. The workflow is triggered manually: - -**To run the workflow:** -1. Navigate to the repository on GitHub -2. Go to **Actions** → **Increase Test Coverage** workflow -3. Click "Run workflow" -4. Optionally specify: - - `target_package` — Focus on a specific package (e.g., `com.github.copilot.sdk.json`) - - `min_coverage_threshold` — Target coverage percentage (default: 80%) - -**What it does:** -- Analyzes current test coverage using JaCoCo -- Identifies methods, branches, and edge cases lacking tests -- Creates high-quality tests following repository patterns -- Groups related tests logically -- Creates a PR for human review with coverage metrics - -The workflow follows all repository conventions (Spotless formatting, existing test patterns, E2E test infrastructure) and ensures no production code is modified unnecessarily. - ### Development Setup ```bash From 011936df1cc0c5835b9078582fe3ff49c5f5c188 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Feb 2026 12:47:33 +0000 Subject: [PATCH 003/104] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b0b72ab2f..d86e46be7 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ io.github.copilot-community-sdk copilot-sdk - 1.0.9 + 1.0.10-SNAPSHOT jar GitHub Copilot Community SDK :: Java @@ -33,7 +33,7 @@ scm:git:https://github.com/copilot-community-sdk/copilot-sdk-java.git scm:git:https://github.com/copilot-community-sdk/copilot-sdk-java.git https://github.com/copilot-community-sdk/copilot-sdk-java - v1.0.9 + HEAD From 78caa9c7bc669d909d9d0c5ee23f07d438bd31f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:51:58 +0000 Subject: [PATCH 004/104] Initial plan From 6264fe9f0be5349ca349f3627bd45cd98f42a7dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:55:14 +0000 Subject: [PATCH 005/104] Fix previous tag detection in GitHub Release workflow Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .github/workflows/publish-maven.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml index ea5ee4975..613cbfc1e 100644 --- a/.github/workflows/publish-maven.yml +++ b/.github/workflows/publish-maven.yml @@ -183,7 +183,12 @@ jobs: RELEASE_NOTES=$(envsubst < .github/workflows/notes.template) # Get the previous tag for generating notes - PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + # Fetch all tags and find the previous version tag (excluding 'latest' and the current tag) + CURRENT_TAG="v${VERSION}" + PREV_TAG=$(git tag -l 'v*' --sort=-version:refname | grep -v "^${CURRENT_TAG}$" | grep -v "^latest$" | head -n 1) + + echo "Current tag: ${CURRENT_TAG}" + echo "Previous tag: ${PREV_TAG}" # Build the gh release command GH_ARGS=("v${VERSION}") From d6b7f390d8457ae182c4916887173c6a1f4a4dc8 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 16 Feb 2026 10:13:49 -0500 Subject: [PATCH 006/104] Enhance GitHub Release workflow to skip existing releases and improve tag handling --- .github/workflows/publish-maven.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml index 613cbfc1e..5546f7332 100644 --- a/.github/workflows/publish-maven.yml +++ b/.github/workflows/publish-maven.yml @@ -177,21 +177,28 @@ jobs: VERSION="${{ needs.publish-maven.outputs.version }}" GROUP_ID="io.github.copilot-community-sdk" ARTIFACT_ID="copilot-sdk" + CURRENT_TAG="v${VERSION}" + + if gh release view "${CURRENT_TAG}" >/dev/null 2>&1; then + echo "Release ${CURRENT_TAG} already exists. Skipping creation." + exit 0 + fi # Generate release notes from template export VERSION GROUP_ID ARTIFACT_ID RELEASE_NOTES=$(envsubst < .github/workflows/notes.template) # Get the previous tag for generating notes - # Fetch all tags and find the previous version tag (excluding 'latest' and the current tag) - CURRENT_TAG="v${VERSION}" - PREV_TAG=$(git tag -l 'v*' --sort=-version:refname | grep -v "^${CURRENT_TAG}$" | grep -v "^latest$" | head -n 1) + PREV_TAG=$(git tag --list 'v*' --sort=-version:refname \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ + | grep -Fxv "${CURRENT_TAG}" \ + | head -n 1) echo "Current tag: ${CURRENT_TAG}" echo "Previous tag: ${PREV_TAG}" # Build the gh release command - GH_ARGS=("v${VERSION}") + GH_ARGS=("${CURRENT_TAG}") GH_ARGS+=("--title" "Copilot Java SDK ${VERSION}") GH_ARGS+=("--notes" "${RELEASE_NOTES}") GH_ARGS+=("--generate-notes") From aeed891ece6e74620d6847f44a74831d4827c3a4 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 16 Feb 2026 10:50:18 -0500 Subject: [PATCH 007/104] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 60eff89d4..5a597fcb7 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ [![GitHub Release Date](https://img.shields.io/github/release-date/copilot-community-sdk/copilot-sdk-java)](https://github.com/copilot-community-sdk/copilot-sdk-java/releases) [![GitHub Release](https://img.shields.io/github/v/release/copilot-community-sdk/copilot-sdk-java)](https://github.com/copilot-community-sdk/copilot-sdk-java/releases) [![Maven Central](https://img.shields.io/maven-central/v/io.github.copilot-community-sdk/copilot-sdk)](https://central.sonatype.com/artifact/io.github.copilot-community-sdk/copilot-sdk) -[![Documentation](https://img.shields.io/badge/docs-online-brightgreen)](https://copilot-community-sdk.github.io/copilot-sdk-java/latest/) +[![Documentation](https://img.shields.io/badge/docs-latest-brightgreen)](https://copilot-community-sdk.github.io/copilot-sdk-java/latest/) [![Javadoc](https://javadoc.io/badge2/io.github.copilot-community-sdk/copilot-sdk/javadoc.svg?q=1)](https://javadoc.io/doc/io.github.copilot-community-sdk/copilot-sdk/latest/index.html) ## Overview From e27f282025e80e91b00dffa67eb504ae38fdf370 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:59:09 +0000 Subject: [PATCH 008/104] Initial plan From 1796f4c571218c30b7cef951f4ffa8fd47a9da8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:06:11 +0000 Subject: [PATCH 009/104] Add test for closing session after client stops Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../github/copilot/sdk/CopilotClientTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java index 05e7290b3..1dde9fd01 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -424,6 +424,28 @@ void testForceStopWithNoConnectionCompletes() throws Exception { } } + @Test + void testCloseSessionAfterStoppingClientDoesNotThrow() throws Exception { + if (cliPath == null) { + System.out.println("Skipping test: CLI not found"); + return; + } + + try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath))) { + var session = client.createSession().get(); + + // Stop the client first (which closes the RPC connection) + client.stop().get(); + + // Then close the session - should not throw even though RPC is closed + assertDoesNotThrow(() -> session.close(), "Closing session after client.stop() should not throw exception"); + + // Verify session is terminated + assertThrows(IllegalStateException.class, () -> session.send("test"), + "Session should be terminated after close()"); + } + } + // ===== start() idempotency ===== @Test From 4a84ca9924ef816f963d9cdd53a3f5ee9638f94f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:06:28 +0000 Subject: [PATCH 010/104] Update .lastmerge to 304d812 Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .lastmerge | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lastmerge b/.lastmerge index 6d9098bab..04846a767 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -e40d57c86e18b495722adbf42045288c03924342 +304d812cd4c98755159da427c6701bfb7e0b7c32 From 1af066aa2f8278c6867bf14f406c1e189520bc0a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 09:32:54 +0000 Subject: [PATCH 011/104] Initial plan From 21f956026f0e37f46a3eabcc4884a2f381c5d4b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 09:41:30 +0000 Subject: [PATCH 012/104] Add clone() methods to config classes Port upstream commit ce54e43: Add copy constructors and Clone methods to the various .NET config option bags (#422) Added clone() methods to: - CopilotClientOptions - SessionConfig - ResumeSessionConfig - MessageOptions These methods create shallow clones where mutable collection properties (lists, maps, arrays) are copied into new instances to ensure independence, while other reference-type properties are shared. Also added comprehensive tests in ConfigCloneTest to verify the clone behavior. Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../github/copilot/sdk/CliServerManager.java | 7 ++ .../sdk/json/CopilotClientOptions.java | 27 ++++ .../copilot/sdk/json/MessageOptions.java | 19 +++ .../copilot/sdk/json/ResumeSessionConfig.java | 36 ++++++ .../copilot/sdk/json/SessionConfig.java | 36 ++++++ .../github/copilot/sdk/ConfigCloneTest.java | 118 ++++++++++++++++++ 6 files changed, 243 insertions(+) create mode 100644 src/test/java/com/github/copilot/sdk/ConfigCloneTest.java diff --git a/src/main/java/com/github/copilot/sdk/CliServerManager.java b/src/main/java/com/github/copilot/sdk/CliServerManager.java index 1ac43c719..f4f570d49 100644 --- a/src/main/java/com/github/copilot/sdk/CliServerManager.java +++ b/src/main/java/com/github/copilot/sdk/CliServerManager.java @@ -85,6 +85,13 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { var pb = new ProcessBuilder(command); pb.redirectErrorStream(false); + // Note: On Windows, console window visibility depends on how the parent Java + // process was launched. GUI applications started with 'javaw' will not create + // visible console windows for subprocesses. Console applications started with + // 'java' will share their console with subprocesses. Java's ProcessBuilder + // doesn't provide explicit CREATE_NO_WINDOW flags like native Windows APIs, + // but the default behavior is appropriate for most use cases. + if (options.getCwd() != null) { pb.directory(new File(options.getCwd())); } diff --git a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java index b51ab7a19..05945edc3 100644 --- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -323,4 +323,31 @@ public CopilotClientOptions setUseLoggedInUser(Boolean useLoggedInUser) { this.useLoggedInUser = useLoggedInUser; return this; } + + /** + * Creates a shallow clone of this {@code CopilotClientOptions} instance. + *

+ * Array properties (like {@code cliArgs}) are copied into new arrays so that + * modifications to the clone do not affect the original. Other reference-type + * properties (like {@code environment}) are shared between the original and + * clone. + * + * @return a clone of this options instance + */ + public CopilotClientOptions clone() { + CopilotClientOptions copy = new CopilotClientOptions(); + copy.cliPath = this.cliPath; + copy.cliArgs = this.cliArgs != null ? this.cliArgs.clone() : null; + copy.cwd = this.cwd; + copy.port = this.port; + copy.useStdio = this.useStdio; + copy.cliUrl = this.cliUrl; + copy.logLevel = this.logLevel; + copy.autoStart = this.autoStart; + copy.autoRestart = this.autoRestart; + copy.environment = this.environment; + copy.githubToken = this.githubToken; + copy.useLoggedInUser = this.useLoggedInUser; + return copy; + } } diff --git a/src/main/java/com/github/copilot/sdk/json/MessageOptions.java b/src/main/java/com/github/copilot/sdk/json/MessageOptions.java index 99c4214b8..4302b213e 100644 --- a/src/main/java/com/github/copilot/sdk/json/MessageOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/MessageOptions.java @@ -4,6 +4,7 @@ package com.github.copilot.sdk.json; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -107,4 +108,22 @@ public String getMode() { return mode; } + /** + * Creates a shallow clone of this {@code MessageOptions} instance. + *

+ * Mutable collection properties are copied into new collection instances so + * that modifications to those collections on the clone do not affect the + * original. Other reference-type properties (like attachment items) are not + * deep-cloned; the original and the clone will share those objects. + * + * @return a clone of this options instance + */ + public MessageOptions clone() { + MessageOptions copy = new MessageOptions(); + copy.prompt = this.prompt; + copy.attachments = this.attachments != null ? new ArrayList<>(this.attachments) : null; + copy.mode = this.mode; + return copy; + } + } diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java index 8b7d841f8..a75c90d0a 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java @@ -4,6 +4,7 @@ package com.github.copilot.sdk.json; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -475,4 +476,39 @@ public ResumeSessionConfig setInfiniteSessions(InfiniteSessionConfig infiniteSes this.infiniteSessions = infiniteSessions; return this; } + + /** + * Creates a shallow clone of this {@code ResumeSessionConfig} instance. + *

+ * Mutable collection properties are copied into new collection instances so + * that modifications to those collections on the clone do not affect the + * original. Other reference-type properties (like provider configuration, + * system messages, hooks, infinite session configuration, and delegates) are + * not deep-cloned; the original and the clone will share those objects. + * + * @return a clone of this config instance + */ + public ResumeSessionConfig clone() { + ResumeSessionConfig copy = new ResumeSessionConfig(); + copy.model = this.model; + copy.tools = this.tools != null ? new ArrayList<>(this.tools) : null; + copy.systemMessage = this.systemMessage; + copy.availableTools = this.availableTools != null ? new ArrayList<>(this.availableTools) : null; + copy.excludedTools = this.excludedTools != null ? new ArrayList<>(this.excludedTools) : null; + copy.provider = this.provider; + copy.reasoningEffort = this.reasoningEffort; + copy.onPermissionRequest = this.onPermissionRequest; + copy.onUserInputRequest = this.onUserInputRequest; + copy.hooks = this.hooks; + copy.workingDirectory = this.workingDirectory; + copy.configDir = this.configDir; + copy.disableResume = this.disableResume; + copy.streaming = this.streaming; + copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; + copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; + copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null; + copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; + copy.infiniteSessions = this.infiniteSessions; + return copy; + } } diff --git a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java index 9d7e7367f..34aaab7bd 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java @@ -4,6 +4,7 @@ package com.github.copilot.sdk.json; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -512,4 +513,39 @@ public SessionConfig setConfigDir(String configDir) { this.configDir = configDir; return this; } + + /** + * Creates a shallow clone of this {@code SessionConfig} instance. + *

+ * Mutable collection properties are copied into new collection instances so + * that modifications to those collections on the clone do not affect the + * original. Other reference-type properties (like provider configuration, + * system messages, hooks, infinite session configuration, and delegates) are + * not deep-cloned; the original and the clone will share those objects. + * + * @return a clone of this config instance + */ + public SessionConfig clone() { + SessionConfig copy = new SessionConfig(); + copy.sessionId = this.sessionId; + copy.model = this.model; + copy.reasoningEffort = this.reasoningEffort; + copy.tools = this.tools != null ? new ArrayList<>(this.tools) : null; + copy.systemMessage = this.systemMessage; + copy.availableTools = this.availableTools != null ? new ArrayList<>(this.availableTools) : null; + copy.excludedTools = this.excludedTools != null ? new ArrayList<>(this.excludedTools) : null; + copy.provider = this.provider; + copy.onPermissionRequest = this.onPermissionRequest; + copy.onUserInputRequest = this.onUserInputRequest; + copy.hooks = this.hooks; + copy.workingDirectory = this.workingDirectory; + copy.streaming = this.streaming; + copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; + copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; + copy.infiniteSessions = this.infiniteSessions; + copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null; + copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; + copy.configDir = this.configDir; + return copy; + } } diff --git a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java new file mode 100644 index 000000000..6c834edbb --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.MessageOptions; + +class ConfigCloneTest { + + @Test + void copilotClientOptionsCloneBasic() { + CopilotClientOptions original = new CopilotClientOptions(); + original.setCliPath("/usr/local/bin/copilot"); + original.setLogLevel("debug"); + original.setPort(9000); + + CopilotClientOptions cloned = original.clone(); + + assertEquals(original.getCliPath(), cloned.getCliPath()); + assertEquals(original.getLogLevel(), cloned.getLogLevel()); + assertEquals(original.getPort(), cloned.getPort()); + } + + @Test + void copilotClientOptionsArrayIndependence() { + CopilotClientOptions original = new CopilotClientOptions(); + String[] args = {"--flag1", "--flag2"}; + original.setCliArgs(args); + + CopilotClientOptions cloned = original.clone(); + cloned.getCliArgs()[0] = "--changed"; + + assertEquals("--flag1", original.getCliArgs()[0]); + assertEquals("--changed", cloned.getCliArgs()[0]); + } + + @Test + void sessionConfigCloneBasic() { + SessionConfig original = new SessionConfig(); + original.setSessionId("my-session"); + original.setModel("gpt-4o"); + original.setStreaming(true); + + SessionConfig cloned = original.clone(); + + assertEquals(original.getSessionId(), cloned.getSessionId()); + assertEquals(original.getModel(), cloned.getModel()); + assertEquals(original.isStreaming(), cloned.isStreaming()); + } + + @Test + void sessionConfigListIndependence() { + SessionConfig original = new SessionConfig(); + List toolList = new ArrayList<>(); + toolList.add("grep"); + toolList.add("bash"); + original.setAvailableTools(toolList); + + SessionConfig cloned = original.clone(); + + List clonedTools = new ArrayList<>(cloned.getAvailableTools()); + clonedTools.add("web"); + cloned.setAvailableTools(clonedTools); + + assertEquals(2, original.getAvailableTools().size()); + assertEquals(3, cloned.getAvailableTools().size()); + } + + @Test + void resumeSessionConfigCloneBasic() { + ResumeSessionConfig original = new ResumeSessionConfig(); + original.setModel("o1"); + original.setStreaming(false); + + ResumeSessionConfig cloned = original.clone(); + + assertEquals(original.getModel(), cloned.getModel()); + assertEquals(original.isStreaming(), cloned.isStreaming()); + } + + @Test + void messageOptionsCloneBasic() { + MessageOptions original = new MessageOptions(); + original.setPrompt("What is 2+2?"); + original.setMode("immediate"); + + MessageOptions cloned = original.clone(); + + assertEquals(original.getPrompt(), cloned.getPrompt()); + assertEquals(original.getMode(), cloned.getMode()); + } + + @Test + void clonePreservesNullFields() { + CopilotClientOptions opts = new CopilotClientOptions(); + CopilotClientOptions optsClone = opts.clone(); + assertNull(optsClone.getCliPath()); + + SessionConfig cfg = new SessionConfig(); + SessionConfig cfgClone = cfg.clone(); + assertNull(cfgClone.getModel()); + + MessageOptions msg = new MessageOptions(); + MessageOptions msgClone = msg.clone(); + assertNull(msgClone.getMode()); + } +} From 4bf82ebec4fbd7c647386cd30e07e110d4b6126b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 09:41:50 +0000 Subject: [PATCH 013/104] Update .lastmerge to 5016587 Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .lastmerge | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lastmerge b/.lastmerge index 04846a767..2e1ed67d3 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -304d812cd4c98755159da427c6701bfb7e0b7c32 +5016587a62652f3d184b3c6958dfc63359921aa8 From 962f97bd1db9646436ac9cc33b0a48143afcf596 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 17 Feb 2026 08:31:09 -0500 Subject: [PATCH 014/104] Update src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/github/copilot/sdk/json/ResumeSessionConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java index a75c90d0a..e58ae3ebb 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java @@ -488,6 +488,7 @@ public ResumeSessionConfig setInfiniteSessions(InfiniteSessionConfig infiniteSes * * @return a clone of this config instance */ + @Override public ResumeSessionConfig clone() { ResumeSessionConfig copy = new ResumeSessionConfig(); copy.model = this.model; From 8dbd48fce39d7ad978c3f4c2dafbb67d39040a3f Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 17 Feb 2026 08:31:32 -0500 Subject: [PATCH 015/104] Update src/main/java/com/github/copilot/sdk/json/SessionConfig.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main/java/com/github/copilot/sdk/json/SessionConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java index 34aaab7bd..93ab81bda 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java @@ -520,7 +520,7 @@ public SessionConfig setConfigDir(String configDir) { * Mutable collection properties are copied into new collection instances so * that modifications to those collections on the clone do not affect the * original. Other reference-type properties (like provider configuration, - * system messages, hooks, infinite session configuration, and delegates) are + * system messages, hooks, infinite session configuration, and handlers) are * not deep-cloned; the original and the clone will share those objects. * * @return a clone of this config instance From bd64fb81f9511b2ded1f57ef84d0164f0c2016d3 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 17 Feb 2026 08:31:43 -0500 Subject: [PATCH 016/104] Update src/main/java/com/github/copilot/sdk/json/MessageOptions.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main/java/com/github/copilot/sdk/json/MessageOptions.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/github/copilot/sdk/json/MessageOptions.java b/src/main/java/com/github/copilot/sdk/json/MessageOptions.java index 4302b213e..6ba629b7b 100644 --- a/src/main/java/com/github/copilot/sdk/json/MessageOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/MessageOptions.java @@ -118,6 +118,7 @@ public String getMode() { * * @return a clone of this options instance */ + @Override public MessageOptions clone() { MessageOptions copy = new MessageOptions(); copy.prompt = this.prompt; From 64ab5c195959ed05e569f9122f6bbeecd3ae003d Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 17 Feb 2026 09:40:54 -0500 Subject: [PATCH 017/104] Potential fix for pull request finding 'Missing Override annotation' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../java/com/github/copilot/sdk/json/CopilotClientOptions.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java index 05945edc3..28a75bc6f 100644 --- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -334,6 +334,7 @@ public CopilotClientOptions setUseLoggedInUser(Boolean useLoggedInUser) { * * @return a clone of this options instance */ + @Override public CopilotClientOptions clone() { CopilotClientOptions copy = new CopilotClientOptions(); copy.cliPath = this.cliPath; From 003d5f7e41601ac8a31f6c7c5df35109ec3c5c7e Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 17 Feb 2026 09:44:53 -0500 Subject: [PATCH 018/104] Update src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/github/copilot/sdk/json/CopilotClientOptions.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java index 28a75bc6f..89b196dd8 100644 --- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -335,6 +335,7 @@ public CopilotClientOptions setUseLoggedInUser(Boolean useLoggedInUser) { * @return a clone of this options instance */ @Override + @Override public CopilotClientOptions clone() { CopilotClientOptions copy = new CopilotClientOptions(); copy.cliPath = this.cliPath; From 5cf778bb0aa9b404fa1a7c27d84049e1564bf252 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 17 Feb 2026 09:45:13 -0500 Subject: [PATCH 019/104] Update src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/github/copilot/sdk/json/ResumeSessionConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java index e58ae3ebb..182467c23 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java @@ -483,7 +483,7 @@ public ResumeSessionConfig setInfiniteSessions(InfiniteSessionConfig infiniteSes * Mutable collection properties are copied into new collection instances so * that modifications to those collections on the clone do not affect the * original. Other reference-type properties (like provider configuration, - * system messages, hooks, infinite session configuration, and delegates) are + * system messages, hooks, infinite session configuration, and handlers) are * not deep-cloned; the original and the clone will share those objects. * * @return a clone of this config instance From 7bbabef8cc2fa613977b25b49f2e2585e23cc828 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 17 Feb 2026 09:45:32 -0500 Subject: [PATCH 020/104] Update src/main/java/com/github/copilot/sdk/json/SessionConfig.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main/java/com/github/copilot/sdk/json/SessionConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java index 93ab81bda..6b29c5bb4 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java @@ -525,6 +525,7 @@ public SessionConfig setConfigDir(String configDir) { * * @return a clone of this config instance */ + @Override public SessionConfig clone() { SessionConfig copy = new SessionConfig(); copy.sessionId = this.sessionId; From 063be9470dce41d41c64f8ad9f9e1869ec9e8e34 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 17 Feb 2026 09:50:56 -0500 Subject: [PATCH 021/104] Potential fix for pull request finding 'Missing Override annotation' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../java/com/github/copilot/sdk/json/CopilotClientOptions.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java index 89b196dd8..28a75bc6f 100644 --- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -335,7 +335,6 @@ public CopilotClientOptions setUseLoggedInUser(Boolean useLoggedInUser) { * @return a clone of this options instance */ @Override - @Override public CopilotClientOptions clone() { CopilotClientOptions copy = new CopilotClientOptions(); copy.cliPath = this.cliPath; From 6e2e9919387a3a10f2c32defe50d84746bfe73cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:56:53 +0000 Subject: [PATCH 022/104] Fix test and clone implementation per PR feedback - Fix sessionConfigListIndependence test to properly validate list independence by mutating original list after cloning - Clone environment map in CopilotClientOptions for collection independence - Add copilotClientOptionsEnvironmentIndependence test to validate environment map cloning - Update CopilotClientOptions Javadoc to reflect environment map is now copied - Apply spotless formatting Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../sdk/json/CopilotClientOptions.java | 8 +-- .../copilot/sdk/json/ResumeSessionConfig.java | 4 +- .../copilot/sdk/json/SessionConfig.java | 4 +- .../github/copilot/sdk/ConfigCloneTest.java | 59 ++++++++++++------- 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java index 28a75bc6f..d83b8b6eb 100644 --- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -328,9 +328,9 @@ public CopilotClientOptions setUseLoggedInUser(Boolean useLoggedInUser) { * Creates a shallow clone of this {@code CopilotClientOptions} instance. *

* Array properties (like {@code cliArgs}) are copied into new arrays so that - * modifications to the clone do not affect the original. Other reference-type - * properties (like {@code environment}) are shared between the original and - * clone. + * modifications to the clone do not affect the original. The + * {@code environment} map is also copied to a new map instance. Other + * reference-type properties are shared between the original and clone. * * @return a clone of this options instance */ @@ -346,7 +346,7 @@ public CopilotClientOptions clone() { copy.logLevel = this.logLevel; copy.autoStart = this.autoStart; copy.autoRestart = this.autoRestart; - copy.environment = this.environment; + copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null; copy.githubToken = this.githubToken; copy.useLoggedInUser = this.useLoggedInUser; return copy; diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java index 182467c23..fc790258a 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java @@ -483,8 +483,8 @@ public ResumeSessionConfig setInfiniteSessions(InfiniteSessionConfig infiniteSes * Mutable collection properties are copied into new collection instances so * that modifications to those collections on the clone do not affect the * original. Other reference-type properties (like provider configuration, - * system messages, hooks, infinite session configuration, and handlers) are - * not deep-cloned; the original and the clone will share those objects. + * system messages, hooks, infinite session configuration, and handlers) are not + * deep-cloned; the original and the clone will share those objects. * * @return a clone of this config instance */ diff --git a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java index 6b29c5bb4..064fee9f3 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java @@ -520,8 +520,8 @@ public SessionConfig setConfigDir(String configDir) { * Mutable collection properties are copied into new collection instances so * that modifications to those collections on the clone do not affect the * original. Other reference-type properties (like provider configuration, - * system messages, hooks, infinite session configuration, and handlers) are - * not deep-cloned; the original and the clone will share those objects. + * system messages, hooks, infinite session configuration, and handlers) are not + * deep-cloned; the original and the clone will share those objects. * * @return a clone of this config instance */ diff --git a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java index 6c834edbb..3bd1b2344 100644 --- a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java +++ b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java @@ -7,7 +7,9 @@ import static org.junit.jupiter.api.Assertions.*; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; @@ -24,9 +26,9 @@ void copilotClientOptionsCloneBasic() { original.setCliPath("/usr/local/bin/copilot"); original.setLogLevel("debug"); original.setPort(9000); - + CopilotClientOptions cloned = original.clone(); - + assertEquals(original.getCliPath(), cloned.getCliPath()); assertEquals(original.getLogLevel(), cloned.getLogLevel()); assertEquals(original.getPort(), cloned.getPort()); @@ -37,23 +39,40 @@ void copilotClientOptionsArrayIndependence() { CopilotClientOptions original = new CopilotClientOptions(); String[] args = {"--flag1", "--flag2"}; original.setCliArgs(args); - + CopilotClientOptions cloned = original.clone(); cloned.getCliArgs()[0] = "--changed"; - + assertEquals("--flag1", original.getCliArgs()[0]); assertEquals("--changed", cloned.getCliArgs()[0]); } + @Test + void copilotClientOptionsEnvironmentIndependence() { + CopilotClientOptions original = new CopilotClientOptions(); + Map env = new HashMap<>(); + env.put("KEY1", "value1"); + original.setEnvironment(env); + + CopilotClientOptions cloned = original.clone(); + + // Mutate the original environment map to test independence + env.put("KEY2", "value2"); + + // The cloned config should be unaffected by mutations to the original map + assertEquals(1, cloned.getEnvironment().size()); + assertEquals(2, original.getEnvironment().size()); + } + @Test void sessionConfigCloneBasic() { SessionConfig original = new SessionConfig(); original.setSessionId("my-session"); original.setModel("gpt-4o"); original.setStreaming(true); - + SessionConfig cloned = original.clone(); - + assertEquals(original.getSessionId(), cloned.getSessionId()); assertEquals(original.getModel(), cloned.getModel()); assertEquals(original.isStreaming(), cloned.isStreaming()); @@ -66,15 +85,15 @@ void sessionConfigListIndependence() { toolList.add("grep"); toolList.add("bash"); original.setAvailableTools(toolList); - + SessionConfig cloned = original.clone(); - - List clonedTools = new ArrayList<>(cloned.getAvailableTools()); - clonedTools.add("web"); - cloned.setAvailableTools(clonedTools); - - assertEquals(2, original.getAvailableTools().size()); - assertEquals(3, cloned.getAvailableTools().size()); + + // Mutate the original list directly to test independence + toolList.add("web"); + + // The cloned config should be unaffected by mutations to the original list + assertEquals(2, cloned.getAvailableTools().size()); + assertEquals(3, original.getAvailableTools().size()); } @Test @@ -82,9 +101,9 @@ void resumeSessionConfigCloneBasic() { ResumeSessionConfig original = new ResumeSessionConfig(); original.setModel("o1"); original.setStreaming(false); - + ResumeSessionConfig cloned = original.clone(); - + assertEquals(original.getModel(), cloned.getModel()); assertEquals(original.isStreaming(), cloned.isStreaming()); } @@ -94,9 +113,9 @@ void messageOptionsCloneBasic() { MessageOptions original = new MessageOptions(); original.setPrompt("What is 2+2?"); original.setMode("immediate"); - + MessageOptions cloned = original.clone(); - + assertEquals(original.getPrompt(), cloned.getPrompt()); assertEquals(original.getMode(), cloned.getMode()); } @@ -106,11 +125,11 @@ void clonePreservesNullFields() { CopilotClientOptions opts = new CopilotClientOptions(); CopilotClientOptions optsClone = opts.clone(); assertNull(optsClone.getCliPath()); - + SessionConfig cfg = new SessionConfig(); SessionConfig cfgClone = cfg.clone(); assertNull(cfgClone.getModel()); - + MessageOptions msg = new MessageOptions(); MessageOptions msgClone = msg.clone(); assertNull(msgClone.getMode()); From 55d4bda49c8cd4e8eb38818e55b873df2d4c08f1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:50:02 +0000 Subject: [PATCH 023/104] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 388fa4643..468038233 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 91% - 91% + 90.5% + 90.5% From 02a57311080ad09a83b33be42192051659294811 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 17 Feb 2026 12:25:40 -0500 Subject: [PATCH 024/104] Refactor output statements to use static import for System.out --- jbang-example.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/jbang-example.java b/jbang-example.java index 4a02cfb41..20838e288 100644 --- a/jbang-example.java +++ b/jbang-example.java @@ -5,6 +5,8 @@ import com.github.copilot.sdk.json.*; import java.util.concurrent.CompletableFuture; +import static java.lang.System.out; + class CopilotSDK { public static void main(String[] args) throws Exception { // Create and start client @@ -17,16 +19,16 @@ public static void main(String[] args) throws Exception { // Handle assistant message events session.on(AssistantMessageEvent.class, msg -> { - System.out.println(msg.getData().getContent()); + out.println(msg.getData().content()); }); // Handle session usage info events session.on(SessionUsageInfoEvent.class, usage -> { var data = usage.getData(); - System.out.println("\n--- Usage Metrics ---"); - System.out.println("Current tokens: " + (int) data.getCurrentTokens()); - System.out.println("Token limit: " + (int) data.getTokenLimit()); - System.out.println("Messages count: " + (int) data.getMessagesLength()); + out.println("\n--- Usage Metrics ---"); + out.println("Current tokens: " + (int) data.currentTokens()); + out.println("Token limit: " + (int) data.tokenLimit()); + out.println("Messages count: " + (int) data.messagesLength()); }); // Send a message From 9d6ef2ebc61dc73aeedff708da343e1b624c80c1 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 17 Feb 2026 12:47:56 -0500 Subject: [PATCH 025/104] Update docs coverage and hooks reference (#141) --- .gitignore | 1 + src/site/markdown/documentation.md | 17 +++++++++++++++++ src/site/markdown/hooks.md | 25 +++++++++++++++++++++---- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index a36367133..37e4620a8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ target examples-test/ .merge-env +blog-copilotsdk/ \ No newline at end of file diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index 9cdd5b9a2..6881a5e36 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -20,6 +20,7 @@ This guide covers common use cases for the Copilot SDK for Java. For complete AP - [Message Delivery Mode](#Message_Delivery_Mode) - [Session Management](#Session_Management) - [SessionConfig Reference](#SessionConfig_Reference) + - [Cloning SessionConfig](#Cloning_SessionConfig) --- @@ -167,6 +168,7 @@ The SDK supports event types organized by category. All events extend `AbstractS | `SessionUsageInfoEvent` | `session.usage_info` | Token usage information | | `SessionCompactionStartEvent` | `session.compaction_start` | Context compaction started (infinite sessions) | | `SessionCompactionCompleteEvent` | `session.compaction_complete` | Context compaction completed | +| `SessionContextChangedEvent` | `session.context_changed` | Working directory context changed | ### Assistant Events @@ -645,6 +647,21 @@ Complete list of all `SessionConfig` options for `createSession()`: | `disabledSkills` | List<String> | Skills to disable by name | [Skills](advanced.html#Skills_Configuration) | | `configDir` | String | Custom configuration directory | [Config Dir](advanced.html#Custom_Configuration_Directory) | +### Cloning SessionConfig + +Use `clone()` to copy a base configuration before making per-session changes: + +```java +var base = new SessionConfig() + .setModel("gpt-4.1") + .setStreaming(true); + +var derived = base.clone() + .setWorkingDirectory("/repo-a"); +``` + +`clone()` creates a shallow copy. Collection fields are copied into new collection instances, while nested objects/handlers are shared references. + See [SessionConfig](apidocs/com/github/copilot/sdk/json/SessionConfig.html) Javadoc for full details. --- diff --git a/src/site/markdown/hooks.md b/src/site/markdown/hooks.md index 153362c98..4e36c9869 100644 --- a/src/site/markdown/hooks.md +++ b/src/site/markdown/hooks.md @@ -15,6 +15,7 @@ Session hooks allow you to intercept and modify tool execution, user prompts, an | [User Prompt Submitted](#User_Prompt_Submitted_Hook) | When user sends a message | Nothing (observation only) | | [Session Start](#Session_Start_Hook) | When session begins | Nothing (observation only) | | [Session End](#Session_End_Hook) | When session ends | Nothing (observation only) | +| [Checking Whether Hooks Are Registered](#Checking_Whether_Hooks_Are_Registered) | Before session creation | Whether any handlers are configured | --- @@ -133,7 +134,7 @@ Called **after** a tool executes. Use this to: |-------|------|-------------| | `getToolName()` | `String` | Name of the tool that was called | | `getToolArgs()` | `JsonNode` | Arguments that were passed | -| `getToolResult()` | `String` | Result from the tool | +| `getToolResult()` | `JsonNode` | Result from the tool | | `getCwd()` | `String` | Current working directory | | `getTimestamp()` | `long` | Timestamp in milliseconds | @@ -188,7 +189,7 @@ Called when the user submits a prompt, before the LLM processes it. This is an o | Field | Type | Description | |-------|------|-------------| -| `getPrompt()` | `String` | The user's prompt text | +| `prompt()` | `String` | The user's prompt text | | `getTimestamp()` | `long` | Timestamp in milliseconds | ### Output @@ -222,7 +223,7 @@ Called when a session starts (either new or resumed). | Field | Type | Description | |-------|------|-------------| -| `getSource()` | `String` | `"new"` or `"resumed"` | +| `source()` | `String` | `"startup"`, `"resume"`, or `"new"` | | `getTimestamp()` | `long` | Timestamp in milliseconds | ### Output @@ -254,7 +255,7 @@ Called when a session ends. | Field | Type | Description | |-------|------|-------------| -| `getReason()` | `String` | Why the session ended | +| `reason()` | `String` | Why the session ended | | `getTimestamp()` | `long` | Timestamp in milliseconds | ### Output @@ -358,6 +359,22 @@ All hook handlers receive a `HookInvocation` object as the second parameter: This allows you to correlate hooks with specific sessions when managing multiple concurrent sessions. +## Checking Whether Hooks Are Registered + +Use `hasHooks()` to quickly verify that at least one hook handler is configured: + +```java +var hooks = new SessionHooks() + .setOnPreToolUse((input, invocation) -> + CompletableFuture.completedFuture(PreToolUseHookOutput.allow())); + +if (hooks.hasHooks()) { + var session = client.createSession( + new SessionConfig().setHooks(hooks) + ).get(); +} +``` + --- ## Error Handling From 168a6bfb532feb127ca1719dee9c1ea137758bfe Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 17 Feb 2026 13:05:41 -0500 Subject: [PATCH 026/104] Fix repo root resolution in format-and-test script --- .github/scripts/build/format-and-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/build/format-and-test.sh b/.github/scripts/build/format-and-test.sh index 6de3972f3..c827813bb 100755 --- a/.github/scripts/build/format-and-test.sh +++ b/.github/scripts/build/format-and-test.sh @@ -13,7 +13,7 @@ # ────────────────────────────────────────────────────────────── set -euo pipefail -ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)" cd "$ROOT_DIR" FORMAT=true From c18558d79864072c58a69382bed535db510b32d4 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 17 Feb 2026 13:51:42 -0500 Subject: [PATCH 027/104] Port session mode/plan/workspace file change events --- README.md | 2 +- .../github/copilot/sdk/CliServerManager.java | 22 ++++++++ .../com/github/copilot/sdk/CopilotClient.java | 6 ++ .../copilot/sdk/SessionRequestBuilder.java | 2 + .../sdk/events/AbstractSessionEvent.java | 6 +- .../sdk/events/SessionEventParser.java | 3 + .../sdk/events/SessionModeChangedEvent.java | 38 +++++++++++++ .../sdk/events/SessionPlanChangedEvent.java | 37 ++++++++++++ .../SessionWorkspaceFileChangedEvent.java | 38 +++++++++++++ .../sdk/json/CreateSessionRequest.java | 13 +++++ .../sdk/json/ResumeSessionRequest.java | 13 +++++ src/site/markdown/index.md | 2 +- .../github/copilot/sdk/CopilotClientTest.java | 20 +++++++ .../copilot/sdk/SessionEventParserTest.java | 56 ++++++++++++++++++- .../sdk/SessionRequestBuilderTest.java | 12 ++++ 15 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/github/copilot/sdk/events/SessionModeChangedEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/events/SessionPlanChangedEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/events/SessionWorkspaceFileChangedEvent.java diff --git a/README.md b/README.md index 5a597fcb7..ae93d99b8 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A ### Requirements - Java 17 or later -- GitHub Copilot CLI 0.0.409 or later installed and in PATH (or provide custom `cliPath`) +- GitHub Copilot CLI 0.0.411-1 or later installed and in PATH (or provide custom `cliPath`) ### Maven diff --git a/src/main/java/com/github/copilot/sdk/CliServerManager.java b/src/main/java/com/github/copilot/sdk/CliServerManager.java index f4f570d49..42e41245e 100644 --- a/src/main/java/com/github/copilot/sdk/CliServerManager.java +++ b/src/main/java/com/github/copilot/sdk/CliServerManager.java @@ -32,6 +32,7 @@ final class CliServerManager { private static final Logger LOG = Logger.getLogger(CliServerManager.class.getName()); private final CopilotClientOptions options; + private final StringBuilder stderrBuffer = new StringBuilder(); CliServerManager(CopilotClientOptions options) { this.options = options; @@ -47,6 +48,8 @@ final class CliServerManager { * if interrupted while waiting for port detection */ ProcessInfo startCliServer() throws IOException, InterruptedException { + clearStderrBuffer(); + String cliPath = options.getCliPath() != null ? options.getCliPath() : "copilot"; var args = new ArrayList(); @@ -152,6 +155,9 @@ private void startStderrReader(Process process) { new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { + synchronized (stderrBuffer) { + stderrBuffer.append(line).append('\n'); + } LOG.fine("[CLI] " + line); } } catch (IOException e) { @@ -171,6 +177,10 @@ private Integer waitForPortAnnouncement(Process process) throws IOException { while (System.currentTimeMillis() < deadline) { String line = reader.readLine(); if (line == null) { + String stderr = getStderrOutput(); + if (!stderr.isEmpty()) { + throw new IOException("CLI process exited unexpectedly. stderr: " + stderr); + } throw new IOException("CLI process exited unexpectedly"); } @@ -185,6 +195,18 @@ private Integer waitForPortAnnouncement(Process process) throws IOException { } } + String getStderrOutput() { + synchronized (stderrBuffer) { + return stderrBuffer.toString().trim(); + } + } + + private void clearStderrBuffer() { + synchronized (stderrBuffer) { + stderrBuffer.setLength(0); + } + } + private List resolveCliCommand(String cliPath, List args) { boolean isJsFile = cliPath.toLowerCase().endsWith(".js"); diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index 46190c469..8d4806874 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -4,6 +4,7 @@ package com.github.copilot.sdk; +import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; @@ -174,6 +175,11 @@ private CompletableFuture startCore() { LOG.info("Copilot client connected"); return connection; } catch (Exception e) { + String stderr = serverManager.getStderrOutput(); + if (!stderr.isEmpty()) { + throw new CompletionException( + new IOException("CLI process exited unexpectedly. stderr: " + stderr, e)); + } throw new CompletionException(e); } }); diff --git a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java index a59940bcf..61c06e7f0 100644 --- a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java +++ b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java @@ -49,6 +49,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config) { request.setWorkingDirectory(config.getWorkingDirectory()); request.setStreaming(config.isStreaming() ? true : null); request.setMcpServers(config.getMcpServers()); + request.setEnvValueMode("direct"); request.setCustomAgents(config.getCustomAgents()); request.setInfiniteSessions(config.getInfiniteSessions()); request.setSkillDirectories(config.getSkillDirectories()); @@ -90,6 +91,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setDisableResume(config.isDisableResume() ? true : null); request.setStreaming(config.isStreaming() ? true : null); request.setMcpServers(config.getMcpServers()); + request.setEnvValueMode("direct"); request.setCustomAgents(config.getCustomAgents()); request.setSkillDirectories(config.getSkillDirectories()); request.setDisabledSkills(config.getDisabledSkills()); diff --git a/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java b/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java index 00c95ed4b..00295ee06 100644 --- a/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java @@ -52,9 +52,9 @@ public abstract sealed class AbstractSessionEvent permits // Session events SessionStartEvent, SessionResumeEvent, SessionErrorEvent, SessionIdleEvent, SessionInfoEvent, - SessionModelChangeEvent, SessionHandoffEvent, SessionTruncationEvent, SessionSnapshotRewindEvent, - SessionUsageInfoEvent, SessionCompactionStartEvent, SessionCompactionCompleteEvent, SessionShutdownEvent, - SessionContextChangedEvent, + SessionModelChangeEvent, SessionModeChangedEvent, SessionPlanChangedEvent, SessionWorkspaceFileChangedEvent, + SessionHandoffEvent, SessionTruncationEvent, SessionSnapshotRewindEvent, SessionUsageInfoEvent, + SessionCompactionStartEvent, SessionCompactionCompleteEvent, SessionShutdownEvent, SessionContextChangedEvent, // Assistant events AssistantTurnStartEvent, AssistantIntentEvent, AssistantReasoningEvent, AssistantReasoningDeltaEvent, AssistantMessageEvent, AssistantMessageDeltaEvent, AssistantTurnEndEvent, AssistantUsageEvent, AbortEvent, diff --git a/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java b/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java index 4cc1763cf..146e3ee08 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java @@ -55,6 +55,9 @@ public class SessionEventParser { 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.mode_changed", SessionModeChangedEvent.class); + TYPE_MAP.put("session.plan_changed", SessionPlanChangedEvent.class); + TYPE_MAP.put("session.workspace_file_changed", SessionWorkspaceFileChangedEvent.class); TYPE_MAP.put("session.handoff", SessionHandoffEvent.class); TYPE_MAP.put("session.truncation", SessionTruncationEvent.class); TYPE_MAP.put("session.snapshot_rewind", SessionSnapshotRewindEvent.class); diff --git a/src/main/java/com/github/copilot/sdk/events/SessionModeChangedEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionModeChangedEvent.java new file mode 100644 index 000000000..3c5b5d661 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/SessionModeChangedEvent.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * 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.mode_changed + * + * @since 1.0.10 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SessionModeChangedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SessionModeChangedData data; + + @Override + public String getType() { + return "session.mode_changed"; + } + + public SessionModeChangedData getData() { + return data; + } + + public void setData(SessionModeChangedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record SessionModeChangedData(@JsonProperty("previousMode") String previousMode, + @JsonProperty("newMode") String newMode) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/SessionPlanChangedEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionPlanChangedEvent.java new file mode 100644 index 000000000..2010cf146 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/SessionPlanChangedEvent.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * 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.plan_changed + * + * @since 1.0.10 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SessionPlanChangedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SessionPlanChangedData data; + + @Override + public String getType() { + return "session.plan_changed"; + } + + public SessionPlanChangedData getData() { + return data; + } + + public void setData(SessionPlanChangedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record SessionPlanChangedData(@JsonProperty("operation") String operation) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/SessionWorkspaceFileChangedEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionWorkspaceFileChangedEvent.java new file mode 100644 index 000000000..0ed30aeb8 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/SessionWorkspaceFileChangedEvent.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * 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.workspace_file_changed + * + * @since 1.0.10 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SessionWorkspaceFileChangedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SessionWorkspaceFileChangedData data; + + @Override + public String getType() { + return "session.workspace_file_changed"; + } + + public SessionWorkspaceFileChangedData getData() { + return data; + } + + public void setData(SessionWorkspaceFileChangedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record SessionWorkspaceFileChangedData(@JsonProperty("path") String path, + @JsonProperty("operation") String operation) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java index 1e1ee621a..522aabd32 100644 --- a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java @@ -67,6 +67,9 @@ public final class CreateSessionRequest { @JsonProperty("mcpServers") private Map mcpServers; + @JsonProperty("envValueMode") + private String envValueMode; + @JsonProperty("customAgents") private List customAgents; @@ -224,6 +227,16 @@ public void setMcpServers(Map mcpServers) { this.mcpServers = mcpServers; } + /** Gets MCP environment variable value mode. @return the mode */ + public String getEnvValueMode() { + return envValueMode; + } + + /** Sets MCP environment variable value mode. @param envValueMode the mode */ + public void setEnvValueMode(String envValueMode) { + this.envValueMode = envValueMode; + } + /** Gets custom agents. @return the agents */ public List getCustomAgents() { return customAgents == null ? null : Collections.unmodifiableList(customAgents); diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java index 19c51aef0..5d5228a16 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java @@ -74,6 +74,9 @@ public final class ResumeSessionRequest { @JsonProperty("mcpServers") private Map mcpServers; + @JsonProperty("envValueMode") + private String envValueMode; + @JsonProperty("customAgents") private List customAgents; @@ -251,6 +254,16 @@ public void setMcpServers(Map mcpServers) { this.mcpServers = mcpServers; } + /** Gets MCP environment variable value mode. @return the mode */ + public String getEnvValueMode() { + return envValueMode; + } + + /** Sets MCP environment variable value mode. @param envValueMode the mode */ + public void setEnvValueMode(String envValueMode) { + this.envValueMode = envValueMode; + } + /** Gets custom agents. @return the agents */ public List getCustomAgents() { return customAgents == null ? null : Collections.unmodifiableList(customAgents); diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 736405ee4..9ea166715 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -9,7 +9,7 @@ Welcome to the documentation for the **Copilot SDK for Java** — a Java SDK for ### Requirements - Java 17 or later -- GitHub Copilot CLI 0.0.409 or later installed and in PATH (or provide custom `cliPath`) +- GitHub Copilot CLI 0.0.411-1 or later installed and in PATH (or provide custom `cliPath`) ### Installation diff --git a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java index 1dde9fd01..514e68998 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -149,6 +149,26 @@ void testStartAndConnectUsingStdio() throws Exception { } } + @Test + void testShouldReportErrorWithStderrWhenCliFailsToStart() throws Exception { + if (cliPath == null) { + System.out.println("Skipping test: CLI not found"); + return; + } + + var options = new CopilotClientOptions().setCliPath(cliPath) + .setCliArgs(new String[]{"--nonexistent-flag-for-testing"}).setUseStdio(true); + + try (var client = new CopilotClient(options)) { + Exception ex = assertThrows(Exception.class, () -> client.start().get()); + Throwable root = ex instanceof ExecutionException && ex.getCause() != null ? ex.getCause() : ex; + String message = root.getMessage(); + assertNotNull(message); + assertTrue(message.toLowerCase().contains("stderr") || message.toLowerCase().contains("unexpectedly"), + "Error should include stderr or unexpected exit details: " + message); + } + } + @Test void testStartAndConnectUsingTcp() throws Exception { if (cliPath == null) { diff --git a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java index a102dfe27..2186929fb 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java @@ -158,6 +158,59 @@ void testParseSessionModelChangeEvent() throws Exception { assertEquals("session.model_change", event.getType()); } + @Test + void testParseSessionModeChangedEvent() throws Exception { + String json = """ + { + "type": "session.mode_changed", + "data": { + "previousMode": "interactive", + "newMode": "plan" + } + } + """; + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionModeChangedEvent.class, event); + assertEquals("session.mode_changed", event.getType()); + } + + @Test + void testParseSessionPlanChangedEvent() throws Exception { + String json = """ + { + "type": "session.plan_changed", + "data": { + "operation": "update" + } + } + """; + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionPlanChangedEvent.class, event); + assertEquals("session.plan_changed", event.getType()); + } + + @Test + void testParseSessionWorkspaceFileChangedEvent() throws Exception { + String json = """ + { + "type": "session.workspace_file_changed", + "data": { + "path": "plan.md", + "operation": "create" + } + } + """; + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionWorkspaceFileChangedEvent.class, event); + assertEquals("session.workspace_file_changed", event.getType()); + } + @Test void testParseSessionHandoffEvent() throws Exception { String json = """ @@ -861,7 +914,8 @@ void testParseEmptyJson() throws Exception { @Test void testParseAllEventTypes() throws Exception { String[] types = {"session.start", "session.resume", "session.error", "session.idle", "session.info", - "session.model_change", "session.handoff", "session.truncation", "session.snapshot_rewind", + "session.model_change", "session.mode_changed", "session.plan_changed", + "session.workspace_file_changed", "session.handoff", "session.truncation", "session.snapshot_rewind", "session.usage_info", "session.compaction_start", "session.compaction_complete", "user.message", "pending_messages.modified", "assistant.turn_start", "assistant.intent", "assistant.reasoning", "assistant.reasoning_delta", "assistant.message", "assistant.message_delta", "assistant.turn_end", diff --git a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java index f6de79a0f..b1737de55 100644 --- a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java @@ -59,6 +59,12 @@ void testBuildCreateRequestHooksWithHandler() { assertTrue(request.getHooks(), "Should be true when hooks have handlers"); } + @Test + void testBuildCreateRequestSetsEnvValueModeToDirect() { + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(new SessionConfig()); + assertEquals("direct", request.getEnvValueMode()); + } + // ========================================================================= // buildResumeRequest // ========================================================================= @@ -130,6 +136,12 @@ void testBuildResumeRequestStreaming() { assertTrue(request.getStreaming()); } + @Test + void testBuildResumeRequestSetsEnvValueModeToDirect() { + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-8", new ResumeSessionConfig()); + assertEquals("direct", request.getEnvValueMode()); + } + // ========================================================================= // configureSession (ResumeSessionConfig overload) // ========================================================================= From b1d11d27f30e0af7a2910c8f5b48ae3fa12731d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:12:31 +0000 Subject: [PATCH 028/104] Initial plan From 5c073154a8f34aac3ba902b5c02eca313af28ca3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:23:52 +0000 Subject: [PATCH 029/104] Port upstream changes: clientName, deny-by-default permissions, PermissionHandler.APPROVE_ALL Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- CHANGELOG.md | 12 +++- .../copilot/sdk/SessionRequestBuilder.java | 6 +- .../sdk/json/CreateSessionRequest.java | 13 ++++ .../copilot/sdk/json/PermissionHandler.java | 12 ++++ .../copilot/sdk/json/ResumeSessionConfig.java | 25 +++++++ .../sdk/json/ResumeSessionRequest.java | 13 ++++ .../copilot/sdk/json/SessionConfig.java | 25 +++++++ .../github/copilot/sdk/ConfigCloneTest.java | 2 + .../com/github/copilot/sdk/HooksTest.java | 49 +++++++------ .../github/copilot/sdk/PermissionsTest.java | 68 +++++++++++++++++++ .../sdk/SessionRequestBuilderTest.java | 30 ++++++++ 11 files changed, 230 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 649255c77..66d72554f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] -> **Upstream sync:** [`github/copilot-sdk@e40d57c`](https://github.com/github/copilot-sdk/commit/e40d57c86e18b495722adbf42045288c03924342) +> **Upstream sync:** [`github/copilot-sdk@c263dfc`](https://github.com/github/copilot-sdk/commit/c263dfc69055f9f28ee2d4b121cf617fca5a42dc) + +### Added + +- `SessionConfig.setClientName(String)` / `getClientName()` — identifies the application using the SDK; included in the User-Agent header for API requests (upstream: [`397ef66`](https://github.com/github/copilot-sdk/commit/397ef66)) +- `ResumeSessionConfig.setClientName(String)` / `getClientName()` — same for resumed sessions +- `PermissionHandler.APPROVE_ALL` — pre-built handler that approves all permission requests (upstream: [`3e2d2b2`](https://github.com/github/copilot-sdk/commit/3e2d2b2)) + +### Changed + +- **Breaking:** permissions are now denied by default when no `OnPermissionRequest` handler is provided. The `requestPermission` flag is always sent as `true` so the server calls back for every permission request; the SDK returns a deny result when no handler is registered (upstream: [`3e2d2b2`](https://github.com/github/copilot-sdk/commit/3e2d2b2)) ## [1.0.9] - 2026-02-16 diff --git a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java index 61c06e7f0..0d1322dc3 100644 --- a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java +++ b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java @@ -37,13 +37,14 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config) { request.setModel(config.getModel()); request.setSessionId(config.getSessionId()); + request.setClientName(config.getClientName()); request.setReasoningEffort(config.getReasoningEffort()); request.setTools(config.getTools()); request.setSystemMessage(config.getSystemMessage()); request.setAvailableTools(config.getAvailableTools()); request.setExcludedTools(config.getExcludedTools()); request.setProvider(config.getProvider()); - request.setRequestPermission(config.getOnPermissionRequest() != null ? true : null); + request.setRequestPermission(true); request.setRequestUserInput(config.getOnUserInputRequest() != null ? true : null); request.setHooks(config.getHooks() != null && config.getHooks().hasHooks() ? true : null); request.setWorkingDirectory(config.getWorkingDirectory()); @@ -77,13 +78,14 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo } request.setModel(config.getModel()); + request.setClientName(config.getClientName()); request.setReasoningEffort(config.getReasoningEffort()); request.setTools(config.getTools()); request.setSystemMessage(config.getSystemMessage()); request.setAvailableTools(config.getAvailableTools()); request.setExcludedTools(config.getExcludedTools()); request.setProvider(config.getProvider()); - request.setRequestPermission(config.getOnPermissionRequest() != null ? true : null); + request.setRequestPermission(true); request.setRequestUserInput(config.getOnUserInputRequest() != null ? true : null); request.setHooks(config.getHooks() != null && config.getHooks().hasHooks() ? true : null); request.setWorkingDirectory(config.getWorkingDirectory()); diff --git a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java index 522aabd32..d73d82e6a 100644 --- a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java @@ -31,6 +31,9 @@ public final class CreateSessionRequest { @JsonProperty("sessionId") private String sessionId; + @JsonProperty("clientName") + private String clientName; + @JsonProperty("reasoningEffort") private String reasoningEffort; @@ -105,6 +108,16 @@ public void setSessionId(String sessionId) { this.sessionId = sessionId; } + /** Gets the client name. @return the client name */ + public String getClientName() { + return clientName; + } + + /** Sets the client name. @param clientName the client name */ + public void setClientName(String clientName) { + this.clientName = clientName; + } + /** Gets the reasoning effort. @return the reasoning effort level */ public String getReasoningEffort() { return reasoningEffort; diff --git a/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java b/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java index 259c058f3..e987e41ae 100644 --- a/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java +++ b/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java @@ -28,6 +28,10 @@ * }; * } * + *

+ * A pre-built handler that approves all requests is available as + * {@link #APPROVE_ALL}. + * * @see SessionConfig#setOnPermissionRequest(PermissionHandler) * @see PermissionRequest * @see PermissionRequestResult @@ -36,6 +40,14 @@ @FunctionalInterface public interface PermissionHandler { + /** + * A pre-built handler that approves all permission requests. + * + * @since 1.0.11 + */ + PermissionHandler APPROVE_ALL = (request, invocation) -> CompletableFuture + .completedFuture(new PermissionRequestResult().setKind("approved")); + /** * Handles a permission request from the assistant. *

diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java index fc790258a..0682699bc 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java @@ -33,6 +33,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class ResumeSessionConfig { + private String clientName; private String model; private List tools; private SystemMessageConfig systemMessage; @@ -76,6 +77,29 @@ public ResumeSessionConfig setModel(String model) { return this; } + /** + * Gets the client name used to identify the application using the SDK. + * + * @return the client name, or {@code null} if not set + */ + public String getClientName() { + return clientName; + } + + /** + * Sets the client name to identify the application using the SDK. + *

+ * This value is included in the User-Agent header for API requests. + * + * @param clientName + * the client name + * @return this config for method chaining + */ + public ResumeSessionConfig setClientName(String clientName) { + this.clientName = clientName; + return this; + } + /** * Gets the custom tools for this session. * @@ -491,6 +515,7 @@ public ResumeSessionConfig setInfiniteSessions(InfiniteSessionConfig infiniteSes @Override public ResumeSessionConfig clone() { ResumeSessionConfig copy = new ResumeSessionConfig(); + copy.clientName = this.clientName; copy.model = this.model; copy.tools = this.tools != null ? new ArrayList<>(this.tools) : null; copy.systemMessage = this.systemMessage; diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java index 5d5228a16..4216e5eef 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java @@ -29,6 +29,9 @@ public final class ResumeSessionRequest { @JsonProperty("sessionId") private String sessionId; + @JsonProperty("clientName") + private String clientName; + @JsonProperty("model") private String model; @@ -99,6 +102,16 @@ public void setSessionId(String sessionId) { this.sessionId = sessionId; } + /** Gets the client name. @return the client name */ + public String getClientName() { + return clientName; + } + + /** Sets the client name. @param clientName the client name */ + public void setClientName(String clientName) { + this.clientName = clientName; + } + /** Gets the model name. @return the model */ public String getModel() { return model; diff --git a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java index 064fee9f3..bfed0608e 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java @@ -34,6 +34,7 @@ public class SessionConfig { private String sessionId; + private String clientName; private String model; private String reasoningEffort; private List tools; @@ -76,6 +77,29 @@ public SessionConfig setSessionId(String sessionId) { return this; } + /** + * Gets the client name used to identify the application using the SDK. + * + * @return the client name, or {@code null} if not set + */ + public String getClientName() { + return clientName; + } + + /** + * Sets the client name to identify the application using the SDK. + *

+ * This value is included in the User-Agent header for API requests. + * + * @param clientName + * the client name + * @return this config instance for method chaining + */ + public SessionConfig setClientName(String clientName) { + this.clientName = clientName; + return this; + } + /** * Gets the AI model to use. * @@ -529,6 +553,7 @@ public SessionConfig setConfigDir(String configDir) { public SessionConfig clone() { SessionConfig copy = new SessionConfig(); copy.sessionId = this.sessionId; + copy.clientName = this.clientName; copy.model = this.model; copy.reasoningEffort = this.reasoningEffort; copy.tools = this.tools != null ? new ArrayList<>(this.tools) : null; diff --git a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java index 3bd1b2344..1c5ca6e09 100644 --- a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java +++ b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java @@ -68,12 +68,14 @@ void copilotClientOptionsEnvironmentIndependence() { void sessionConfigCloneBasic() { SessionConfig original = new SessionConfig(); original.setSessionId("my-session"); + original.setClientName("my-app"); original.setModel("gpt-4o"); original.setStreaming(true); SessionConfig cloned = original.clone(); assertEquals(original.getSessionId(), cloned.getSessionId()); + assertEquals(original.getClientName(), cloned.getClientName()); assertEquals(original.getModel(), cloned.getModel()); assertEquals(original.isStreaming(), cloned.isStreaming()); } diff --git a/src/test/java/com/github/copilot/sdk/HooksTest.java b/src/test/java/com/github/copilot/sdk/HooksTest.java index c728b9b99..05cd3292b 100644 --- a/src/test/java/com/github/copilot/sdk/HooksTest.java +++ b/src/test/java/com/github/copilot/sdk/HooksTest.java @@ -19,6 +19,7 @@ import org.junit.jupiter.api.Test; import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.PostToolUseHookInput; import com.github.copilot.sdk.json.PreToolUseHookInput; import com.github.copilot.sdk.json.PreToolUseHookOutput; @@ -67,11 +68,12 @@ void testInvokePreToolUseHookWhenModelRunsATool() throws Exception { var preToolUseInputs = new ArrayList(); final String[] sessionIdHolder = new String[1]; - var config = new SessionConfig().setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> { - preToolUseInputs.add(input); - assertEquals(sessionIdHolder[0], invocation.getSessionId()); - return CompletableFuture.completedFuture(PreToolUseHookOutput.allow()); - })); + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> { + preToolUseInputs.add(input); + assertEquals(sessionIdHolder[0], invocation.getSessionId()); + return CompletableFuture.completedFuture(PreToolUseHookOutput.allow()); + })); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); @@ -106,11 +108,12 @@ void testInvokePostToolUseHookAfterModelRunsATool() throws Exception { var postToolUseInputs = new ArrayList(); final String[] sessionIdHolder = new String[1]; - var config = new SessionConfig().setHooks(new SessionHooks().setOnPostToolUse((input, invocation) -> { - postToolUseInputs.add(input); - assertEquals(sessionIdHolder[0], invocation.getSessionId()); - return CompletableFuture.completedFuture(null); - })); + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setHooks(new SessionHooks().setOnPostToolUse((input, invocation) -> { + postToolUseInputs.add(input); + assertEquals(sessionIdHolder[0], invocation.getSessionId()); + return CompletableFuture.completedFuture(null); + })); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); @@ -147,13 +150,14 @@ void testInvokeBothHooksForSingleToolCall() throws Exception { var preToolUseInputs = new ArrayList(); var postToolUseInputs = new ArrayList(); - var config = new SessionConfig().setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> { - preToolUseInputs.add(input); - return CompletableFuture.completedFuture(PreToolUseHookOutput.allow()); - }).setOnPostToolUse((input, invocation) -> { - postToolUseInputs.add(input); - return CompletableFuture.completedFuture(null); - })); + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> { + preToolUseInputs.add(input); + return CompletableFuture.completedFuture(PreToolUseHookOutput.allow()); + }).setOnPostToolUse((input, invocation) -> { + postToolUseInputs.add(input); + return CompletableFuture.completedFuture(null); + })); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); @@ -192,11 +196,12 @@ void testDenyToolExecutionWhenPreToolUseReturnsDeny() throws Exception { var preToolUseInputs = new ArrayList(); - var config = new SessionConfig().setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> { - preToolUseInputs.add(input); - // Deny all tool calls - return CompletableFuture.completedFuture(PreToolUseHookOutput.deny()); - })); + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> { + preToolUseInputs.add(input); + // Deny all tool calls + return CompletableFuture.completedFuture(PreToolUseHookOutput.deny()); + })); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); diff --git a/src/test/java/com/github/copilot/sdk/PermissionsTest.java b/src/test/java/com/github/copilot/sdk/PermissionsTest.java index d07b7c500..b2a1cfe66 100644 --- a/src/test/java/com/github/copilot/sdk/PermissionsTest.java +++ b/src/test/java/com/github/copilot/sdk/PermissionsTest.java @@ -18,6 +18,8 @@ import org.junit.jupiter.api.TestInfo; import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.events.ToolExecutionCompleteEvent; +import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.PermissionRequest; import com.github.copilot.sdk.json.PermissionRequestResult; import com.github.copilot.sdk.json.SessionConfig; @@ -286,4 +288,70 @@ void testShouldHandlePermissionHandlerErrorsGracefully(TestInfo testInfo) throws session.close(); } } + + /** + * Verifies that tool operations are denied by default when no handler is + * provided. + * + * @see Snapshot: + * permissions/should_deny_tool_operations_by_default_when_no_handler_is_provided + */ + @Test + void testShouldDenyToolOperationsByDefaultWhenNoHandlerIsProvided(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "should_deny_tool_operations_by_default_when_no_handler_is_provided"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession().get(); + + final boolean[] permissionDenied = {false}; + session.on(ToolExecutionCompleteEvent.class, evt -> { + if (!evt.getData().success() && evt.getData().error() != null && evt.getData().error().message() != null + && evt.getData().error().message().contains("Permission denied")) { + permissionDenied[0] = true; + } + }); + + session.sendAndWait(new MessageOptions().setPrompt("Run 'node --version'")).get(60, TimeUnit.SECONDS); + + assertTrue(permissionDenied[0], "Expected a tool.execution_complete event with Permission denied result"); + + session.close(); + } + } + + /** + * Verifies that tool operations are denied by default when no handler is + * provided after resuming a session. + * + * @see Snapshot: + * permissions/should_deny_tool_operations_by_default_when_no_handler_is_provided_after_resume + */ + @Test + void testShouldDenyToolOperationsByDefaultWhenNoHandlerIsProvidedAfterResume(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", + "should_deny_tool_operations_by_default_when_no_handler_is_provided_after_resume"); + + try (CopilotClient client = ctx.createClient()) { + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL); + CopilotSession session1 = client.createSession(config).get(); + String sessionId = session1.getSessionId(); + session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + CopilotSession session2 = client.resumeSession(sessionId).get(); + + final boolean[] permissionDenied = {false}; + session2.on(ToolExecutionCompleteEvent.class, evt -> { + if (!evt.getData().success() && evt.getData().error() != null && evt.getData().error().message() != null + && evt.getData().error().message().contains("Permission denied")) { + permissionDenied[0] = true; + } + }); + + session2.sendAndWait(new MessageOptions().setPrompt("Run 'node --version'")).get(60, TimeUnit.SECONDS); + + assertTrue(permissionDenied[0], "Expected a tool.execution_complete event with Permission denied result"); + + session2.close(); + } + } } diff --git a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java index b1737de55..2f75321b1 100644 --- a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java @@ -65,6 +65,21 @@ void testBuildCreateRequestSetsEnvValueModeToDirect() { assertEquals("direct", request.getEnvValueMode()); } + @Test + void testBuildCreateRequestAlwaysSetsRequestPermissionTrue() { + // No permission handler set - requestPermission should still be true + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(new SessionConfig()); + assertTrue(request.getRequestPermission(), + "requestPermission should always be true to enable deny-by-default behavior"); + } + + @Test + void testBuildCreateRequestSetsClientName() { + var config = new SessionConfig().setClientName("my-app"); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + assertEquals("my-app", request.getClientName()); + } + // ========================================================================= // buildResumeRequest // ========================================================================= @@ -142,6 +157,21 @@ void testBuildResumeRequestSetsEnvValueModeToDirect() { assertEquals("direct", request.getEnvValueMode()); } + @Test + void testBuildResumeRequestAlwaysSetsRequestPermissionTrue() { + // No permission handler set - requestPermission should still be true + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-9", new ResumeSessionConfig()); + assertTrue(request.getRequestPermission(), + "requestPermission should always be true to enable deny-by-default behavior"); + } + + @Test + void testBuildResumeRequestSetsClientName() { + var config = new ResumeSessionConfig().setClientName("my-app"); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-10", config); + assertEquals("my-app", request.getClientName()); + } + // ========================================================================= // configureSession (ResumeSessionConfig overload) // ========================================================================= From 0867d152ca5842420a51cd4eba0f4bdae979196b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:54:14 +0000 Subject: [PATCH 030/104] Fix requestPermission always-true for null config, add APPROVE_ALL to tool tests Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .lastmerge | 2 +- .../github/copilot/sdk/SessionRequestBuilder.java | 12 ++++++++---- .../com/github/copilot/sdk/CopilotSessionTest.java | 4 +++- .../com/github/copilot/sdk/SessionEventsE2ETest.java | 8 ++++++-- .../copilot/sdk/SessionRequestBuilderTest.java | 4 ++++ src/test/java/com/github/copilot/sdk/ToolsTest.java | 4 +++- 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/.lastmerge b/.lastmerge index 2e1ed67d3..e8f667ef7 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -5016587a62652f3d184b3c6958dfc63359921aa8 +c263dfc69055f9f28ee2d4b121cf617fca5a42dc diff --git a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java index 0d1322dc3..90f3c71d8 100644 --- a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java +++ b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java @@ -31,6 +31,10 @@ private SessionRequestBuilder() { */ static CreateSessionRequest buildCreateRequest(SessionConfig config) { var request = new CreateSessionRequest(); + // Always request permission callbacks to enable deny-by-default behavior + request.setRequestPermission(true); + // Always send envValueMode=direct for MCP servers + request.setEnvValueMode("direct"); if (config == null) { return request; } @@ -44,13 +48,11 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config) { request.setAvailableTools(config.getAvailableTools()); request.setExcludedTools(config.getExcludedTools()); request.setProvider(config.getProvider()); - request.setRequestPermission(true); request.setRequestUserInput(config.getOnUserInputRequest() != null ? true : null); request.setHooks(config.getHooks() != null && config.getHooks().hasHooks() ? true : null); request.setWorkingDirectory(config.getWorkingDirectory()); request.setStreaming(config.isStreaming() ? true : null); request.setMcpServers(config.getMcpServers()); - request.setEnvValueMode("direct"); request.setCustomAgents(config.getCustomAgents()); request.setInfiniteSessions(config.getInfiniteSessions()); request.setSkillDirectories(config.getSkillDirectories()); @@ -72,6 +74,10 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config) { static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionConfig config) { var request = new ResumeSessionRequest(); request.setSessionId(sessionId); + // Always request permission callbacks to enable deny-by-default behavior + request.setRequestPermission(true); + // Always send envValueMode=direct for MCP servers + request.setEnvValueMode("direct"); if (config == null) { return request; @@ -85,7 +91,6 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setAvailableTools(config.getAvailableTools()); request.setExcludedTools(config.getExcludedTools()); request.setProvider(config.getProvider()); - request.setRequestPermission(true); request.setRequestUserInput(config.getOnUserInputRequest() != null ? true : null); request.setHooks(config.getHooks() != null && config.getHooks().hasHooks() ? true : null); request.setWorkingDirectory(config.getWorkingDirectory()); @@ -93,7 +98,6 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setDisableResume(config.isDisableResume() ? true : null); request.setStreaming(config.isStreaming() ? true : null); request.setMcpServers(config.getMcpServers()); - request.setEnvValueMode("direct"); request.setCustomAgents(config.getCustomAgents()); request.setSkillDirectories(config.getSkillDirectories()); request.setDisabledSkills(config.getDisabledSkills()); diff --git a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java index d4357e111..6ac77fdb6 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java @@ -30,6 +30,7 @@ import com.github.copilot.sdk.events.ToolExecutionStartEvent; import com.github.copilot.sdk.events.UserMessageEvent; import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.SessionConfig; import com.github.copilot.sdk.json.SystemMessageConfig; import com.github.copilot.sdk.json.ToolDefinition; @@ -180,7 +181,8 @@ void testSendReturnsImmediatelyWhileEventsStreamInBackground() throws Exception ctx.configureForTest("session", "send_returns_immediately_while_events_stream_in_background"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); var events = new ArrayList(); var lastMessage = new AtomicReference(); diff --git a/src/test/java/com/github/copilot/sdk/SessionEventsE2ETest.java b/src/test/java/com/github/copilot/sdk/SessionEventsE2ETest.java index 5af1b445b..758d57c20 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventsE2ETest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventsE2ETest.java @@ -26,6 +26,8 @@ import com.github.copilot.sdk.events.ToolExecutionStartEvent; import com.github.copilot.sdk.events.UserMessageEvent; import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; /** * E2E tests for session events to verify event lifecycle. @@ -130,7 +132,8 @@ void testInvokesBuiltInTools_toolExecutionCompleteEvent() throws Exception { var toolCompletes = new ArrayList(); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.on(ToolExecutionStartEvent.class, toolStarts::add); session.on(ToolExecutionCompleteEvent.class, toolCompletes::add); @@ -235,7 +238,8 @@ void testInvokesBuiltInTools_eventOrderDuringToolExecution() throws Exception { var eventTypes = new ArrayList(); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.on(event -> eventTypes.add(event.getType())); diff --git a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java index 2f75321b1..7e9d5ee69 100644 --- a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java @@ -37,6 +37,8 @@ void testBuildCreateRequestNullConfig() { CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(null); assertNotNull(request); assertNull(request.getModel()); + assertTrue(request.getRequestPermission(), "requestPermission should be true even for null config"); + assertEquals("direct", request.getEnvValueMode(), "envValueMode should be 'direct' even for null config"); } @Test @@ -89,6 +91,8 @@ void testBuildResumeRequestNullConfig() { ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", null); assertEquals("sid-1", request.getSessionId()); assertNull(request.getModel()); + assertTrue(request.getRequestPermission(), "requestPermission should be true even for null config"); + assertEquals("direct", request.getEnvValueMode(), "envValueMode should be 'direct' even for null config"); } @Test diff --git a/src/test/java/com/github/copilot/sdk/ToolsTest.java b/src/test/java/com/github/copilot/sdk/ToolsTest.java index bc392d9f0..a1c66dc49 100644 --- a/src/test/java/com/github/copilot/sdk/ToolsTest.java +++ b/src/test/java/com/github/copilot/sdk/ToolsTest.java @@ -21,6 +21,7 @@ import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.SessionConfig; import com.github.copilot.sdk.json.ToolDefinition; @@ -62,7 +63,8 @@ void testInvokesBuiltInTools(TestInfo testInfo) throws Exception { Files.writeString(readmeFile, "# ELIZA, the only chatbot you'll ever need"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); AssistantMessageEvent response = session .sendAndWait( From 6e3aa52772617e22b4a7102d39abf69f1f308d1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:54:18 +0000 Subject: [PATCH 031/104] Initial plan From 5f8830c45e2735114ed022e4a8843d37d38e9a37 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:56:45 +0000 Subject: [PATCH 032/104] Update .lastmerge to c4b3b366c4bd8dfba9ba4aa05e4019825360ad78 --- .lastmerge | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lastmerge b/.lastmerge index e8f667ef7..d5389b23c 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -c263dfc69055f9f28ee2d4b121cf617fca5a42dc +c4b3b366c4bd8dfba9ba4aa05e4019825360ad78 From 4e3eca12f0e11b76d7d5d02edd25292f2497ee0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:36:00 +0000 Subject: [PATCH 033/104] Initial plan From 991d1e5598d867d25b2e5a567a75da532b4c5d02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:48:47 +0000 Subject: [PATCH 034/104] Port upstream sync: GitHubToken rename, sendAndWait cancellation fix, Foundry Local docs Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../github/copilot/sdk/CliServerManager.java | 10 ++--- .../com/github/copilot/sdk/CopilotClient.java | 4 +- .../github/copilot/sdk/CopilotSession.java | 20 ++++++++- .../sdk/json/CopilotClientOptions.java | 39 +++++++++++++--- src/site/markdown/advanced.md | 45 +++++++++++++++++++ src/site/markdown/setup.md | 12 ++--- .../copilot/sdk/CliServerManagerTest.java | 10 ++--- .../github/copilot/sdk/ConfigCloneTest.java | 4 ++ .../github/copilot/sdk/CopilotClientTest.java | 14 +++--- .../github/copilot/sdk/E2ETestContext.java | 2 +- 10 files changed, 125 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/CliServerManager.java b/src/main/java/com/github/copilot/sdk/CliServerManager.java index 42e41245e..b2a798ada 100644 --- a/src/main/java/com/github/copilot/sdk/CliServerManager.java +++ b/src/main/java/com/github/copilot/sdk/CliServerManager.java @@ -70,15 +70,15 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { } // Add auth-related flags - if (options.getGithubToken() != null && !options.getGithubToken().isEmpty()) { + if (options.getGitHubToken() != null && !options.getGitHubToken().isEmpty()) { args.add("--auth-token-env"); args.add("COPILOT_SDK_AUTH_TOKEN"); } - // Default UseLoggedInUser to false when GithubToken is provided + // Default UseLoggedInUser to false when GitHubToken is provided boolean useLoggedInUser = options.getUseLoggedInUser() != null ? options.getUseLoggedInUser() - : (options.getGithubToken() == null || options.getGithubToken().isEmpty()); + : (options.getGitHubToken() == null || options.getGitHubToken().isEmpty()); if (!useLoggedInUser) { args.add("--no-auto-login"); } @@ -106,8 +106,8 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { pb.environment().remove("NODE_DEBUG"); // Set auth token in environment if provided - if (options.getGithubToken() != null && !options.getGithubToken().isEmpty()) { - pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGithubToken()); + if (options.getGitHubToken() != null && !options.getGitHubToken().isEmpty()) { + pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGitHubToken()); } Process process = pb.start(); diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index 8d4806874..043483284 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -110,9 +110,9 @@ public CopilotClient(CopilotClientOptions options) { // Validate auth options with external server if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty() - && (this.options.getGithubToken() != null || this.options.getUseLoggedInUser() != null)) { + && (this.options.getGitHubToken() != null || this.options.getUseLoggedInUser() != null)) { throw new IllegalArgumentException( - "GithubToken and UseLoggedInUser cannot be used with CliUrl (external server manages its own auth)"); + "GitHubToken and UseLoggedInUser cannot be used with CliUrl (external server manages its own auth)"); } // Parse CliUrl if provided diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index 722407c50..d40c2d3f1 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -312,6 +312,12 @@ public CompletableFuture send(MessageOptions options) { * 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. + *

+ * The returned future can be cancelled via + * {@link java.util.concurrent.Future#cancel(boolean)}. If cancelled externally, + * the future completes with {@link java.util.concurrent.CancellationException}. + * If the timeout expires first, the future completes exceptionally with a + * {@link TimeoutException}. * * @param options * the message options containing the prompt and attachments @@ -320,7 +326,7 @@ public CompletableFuture send(MessageOptions options) { * @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. + * expires, or with CancellationException if cancelled externally. * @throws IllegalStateException * if this session has been terminated * @see #sendAndWait(MessageOptions) @@ -367,7 +373,7 @@ public CompletableFuture sendAndWait(MessageOptions optio scheduler.shutdown(); }, timeoutMs, TimeUnit.MILLISECONDS); - return future.whenComplete((result, ex) -> { + var resultFuture = future.whenComplete((result, ex) -> { try { subscription.close(); } catch (IOException e) { @@ -375,6 +381,16 @@ public CompletableFuture sendAndWait(MessageOptions optio } scheduler.shutdown(); }); + + // When the returned future is cancelled externally, propagate to the inner + // future so that cleanup (subscription close, scheduler shutdown) runs. + resultFuture.whenComplete((v, ex) -> { + if (resultFuture.isCancelled()) { + future.completeExceptionally(new java.util.concurrent.CancellationException()); + } + }); + + return resultFuture; } /** diff --git a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java index d83b8b6eb..70ce99850 100644 --- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -41,7 +41,7 @@ public class CopilotClientOptions { private boolean autoStart = true; private boolean autoRestart = true; private Map environment; - private String githubToken; + private String gitHubToken; private Boolean useLoggedInUser; /** @@ -279,8 +279,8 @@ public CopilotClientOptions setEnvironment(Map environment) { * * @return the GitHub token, or {@code null} to use other authentication methods */ - public String getGithubToken() { - return githubToken; + public String getGitHubToken() { + return gitHubToken; } /** @@ -289,12 +289,37 @@ public String getGithubToken() { * When provided, the token is passed to the CLI server via environment * variable. This takes priority over other authentication methods. * + * @param gitHubToken + * the GitHub token + * @return this options instance for method chaining + */ + public CopilotClientOptions setGitHubToken(String gitHubToken) { + this.gitHubToken = gitHubToken; + return this; + } + + /** + * Gets the GitHub token for authentication. + * + * @return the GitHub token, or {@code null} to use other authentication methods + * @deprecated Use {@link #getGitHubToken()} instead. + */ + @Deprecated + public String getGithubToken() { + return gitHubToken; + } + + /** + * Sets the GitHub token to use for authentication. + * * @param githubToken * the GitHub token * @return this options instance for method chaining + * @deprecated Use {@link #setGitHubToken(String)} instead. */ + @Deprecated public CopilotClientOptions setGithubToken(String githubToken) { - this.githubToken = githubToken; + this.gitHubToken = githubToken; return this; } @@ -312,8 +337,8 @@ public Boolean getUseLoggedInUser() { * Sets whether to use the logged-in user for authentication. *

* When true, the CLI server will attempt to use stored OAuth tokens or gh CLI - * auth. When false, only explicit tokens (githubToken or environment variables) - * are used. Default: true (but defaults to false when githubToken is provided). + * auth. When false, only explicit tokens (gitHubToken or environment variables) + * are used. Default: true (but defaults to false when gitHubToken is provided). * * @param useLoggedInUser * {@code true} to use logged-in user auth, {@code false} otherwise @@ -347,7 +372,7 @@ public CopilotClientOptions clone() { copy.autoStart = this.autoStart; copy.autoRestart = this.autoRestart; copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null; - copy.githubToken = this.githubToken; + copy.gitHubToken = this.gitHubToken; copy.useLoggedInUser = this.useLoggedInUser; return copy; } diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index 0c43f347e..66339d30e 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -149,6 +149,17 @@ session.send(new MessageOptions() Use your own OpenAI or Azure OpenAI API key instead of GitHub Copilot. +Supported providers: + +| Provider | Type value | Notes | +|----------|-----------|-------| +| OpenAI | `"openai"` | Standard OpenAI API | +| Azure OpenAI / Azure AI Foundry | `"azure"` | Azure-hosted models | +| Anthropic | `"anthropic"` | Claude models | +| Ollama | `"openai"` | Local models via OpenAI-compatible API | +| Microsoft Foundry Local | `"openai"` | Run AI models locally on your device via OpenAI-compatible API | +| Other OpenAI-compatible | `"openai"` | vLLM, LiteLLM, etc. | + ### API Key Authentication ```java @@ -177,6 +188,40 @@ var session = client.createSession( > **Note:** The `bearerToken` option accepts a **static token string** only. The SDK does not refresh this token automatically. If your token expires, requests will fail and you'll need to create a new session with a fresh token. +### Microsoft Foundry Local + +[Microsoft Foundry Local](https://foundrylocal.ai) lets you run AI models locally on your own device with an OpenAI-compatible API. Install it via the Foundry Local CLI, then point the SDK at your local endpoint: + +```java +var session = client.createSession( + new SessionConfig() + .setProvider(new ProviderConfig() + .setType("openai") + .setBaseUrl("http://localhost:/v1")) + // No apiKey needed for local Foundry Local +).get(); +``` + +> **Note:** Foundry Local starts on a **dynamic port** — the port is not fixed. Use `foundry service status` to confirm the port the service is currently listening on, then use that port in your `baseUrl`. + +To get started with Foundry Local: + +```bash +# Windows: Install Foundry Local CLI (requires winget) +winget install Microsoft.FoundryLocal + +# macOS / Linux: see https://foundrylocal.ai for installation instructions + +# List available models +foundry model list + +# Run a model (starts the local server automatically) +foundry model run phi-4-mini + +# Check the port the service is running on +foundry service status +``` + ### Limitations When using BYOK, be aware of these limitations: diff --git a/src/site/markdown/setup.md b/src/site/markdown/setup.md index eebec5bcd..1130808ff 100644 --- a/src/site/markdown/setup.md +++ b/src/site/markdown/setup.md @@ -7,7 +7,7 @@ This guide explains how to configure the Copilot SDK for different deployment sc | Scenario | Configuration | Guide Section | |----------|--------------|---------------| | Local development | Default (no options) | [Local CLI](#local-cli) | -| Multi-user app | `setGithubToken(userToken)` | [GitHub OAuth](#github-oauth-authentication) | +| Multi-user app | `setGitHubToken(userToken)` | [GitHub OAuth](#github-oauth-authentication) | | Server deployment | `setCliUrl("host:port")` | [Backend Services](#backend-services) | | Custom CLI location | `setCliPath("/path/to/copilot")` | [Bundled CLI](#bundled-cli) | | Own model keys | Provider configuration | [BYOK](advanced.html#Bring_Your_Own_Key_BYOK) | @@ -48,7 +48,7 @@ After obtaining a user's GitHub OAuth token, pass it to the SDK: ```java var options = new CopilotClientOptions() - .setGithubToken(userAccessToken) + .setGitHubToken(userAccessToken) .setUseLoggedInUser(false); try (var client = new CopilotClient(options)) { @@ -67,7 +67,7 @@ Your application handles the OAuth flow: 1. Create a GitHub OAuth App in your GitHub settings 2. Redirect users to GitHub's authorization URL 3. Exchange the authorization code for an access token -4. Pass the token to `CopilotClientOptions.setGithubToken()` +4. Pass the token to `CopilotClientOptions.setGitHubToken()` ### Per-User Client Management @@ -76,10 +76,10 @@ Each authenticated user should get their own client instance: ```java private final Map clients = new ConcurrentHashMap<>(); -public CopilotClient getClientForUser(String userId, String githubToken) { +public CopilotClient getClientForUser(String userId, String gitHubToken) { return clients.computeIfAbsent(userId, id -> { var options = new CopilotClientOptions() - .setGithubToken(githubToken) + .setGitHubToken(gitHubToken) .setUseLoggedInUser(false); var client = new CopilotClient(options); try { @@ -340,7 +340,7 @@ Complete list of `CopilotClientOptions` settings: | `cliPath` | String | Path to CLI executable | `"copilot"` from PATH | | `cliUrl` | String | External CLI server URL | `null` (spawn process) | | `cliArgs` | String[] | Extra CLI arguments | `null` | -| `githubToken` | String | GitHub OAuth token | `null` | +| `gitHubToken` | String | GitHub OAuth token | `null` | | `useLoggedInUser` | Boolean | Use system credentials | `true` | | `useStdio` | boolean | Use stdio transport | `true` | | `port` | int | TCP port for CLI | `0` (random) | diff --git a/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java b/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java index 61ce938cf..86d6be875 100644 --- a/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java +++ b/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java @@ -165,9 +165,9 @@ void startCliServerWithExplicitPort() throws Exception { } @Test - void startCliServerWithGithubToken() throws Exception { + void startCliServerWithGitHubToken() throws Exception { // Test the github token branch - var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setGithubToken("ghp_test123") + var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setGitHubToken("ghp_test123") .setUseStdio(true); var manager = new CliServerManager(options); @@ -187,9 +187,9 @@ void startCliServerWithUseLoggedInUserExplicit() throws Exception { } @Test - void startCliServerWithGithubTokenAndNoExplicitUseLoggedInUser() throws Exception { - // When githubToken is set and useLoggedInUser is null, defaults to false - var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setGithubToken("ghp_test123") + void startCliServerWithGitHubTokenAndNoExplicitUseLoggedInUser() throws Exception { + // When gitHubToken is set and useLoggedInUser is null, defaults to false + var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setGitHubToken("ghp_test123") .setUseStdio(true); var manager = new CliServerManager(options); diff --git a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java index 1c5ca6e09..e1269a669 100644 --- a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java +++ b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java @@ -26,12 +26,16 @@ void copilotClientOptionsCloneBasic() { original.setCliPath("/usr/local/bin/copilot"); original.setLogLevel("debug"); original.setPort(9000); + original.setGitHubToken("ghp_test"); + original.setUseLoggedInUser(false); CopilotClientOptions cloned = original.clone(); assertEquals(original.getCliPath(), cloned.getCliPath()); assertEquals(original.getLogLevel(), cloned.getLogLevel()); assertEquals(original.getPort(), cloned.getPort()); + assertEquals(original.getGitHubToken(), cloned.getGitHubToken()); + assertEquals(original.getUseLoggedInUser(), cloned.getUseLoggedInUser()); } @Test diff --git a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java index 514e68998..a5060e615 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -203,10 +203,10 @@ void testForceStopWithoutCleanup() throws Exception { } @Test - void testGithubTokenOptionAccepted() { - var options = new CopilotClientOptions().setCliPath("/path/to/cli").setGithubToken("gho_test_token"); + void testGitHubTokenOptionAccepted() { + var options = new CopilotClientOptions().setCliPath("/path/to/cli").setGitHubToken("gho_test_token"); - assertEquals("gho_test_token", options.getGithubToken()); + assertEquals("gho_test_token", options.getGitHubToken()); } @Test @@ -224,16 +224,16 @@ void testExplicitUseLoggedInUserFalse() { } @Test - void testExplicitUseLoggedInUserTrueWithGithubToken() { - var options = new CopilotClientOptions().setCliPath("/path/to/cli").setGithubToken("gho_test_token") + void testExplicitUseLoggedInUserTrueWithGitHubToken() { + var options = new CopilotClientOptions().setCliPath("/path/to/cli").setGitHubToken("gho_test_token") .setUseLoggedInUser(true); assertEquals(true, options.getUseLoggedInUser()); } @Test - void testGithubTokenWithCliUrlThrows() { - var options = new CopilotClientOptions().setCliUrl("localhost:8080").setGithubToken("gho_test_token"); + void testGitHubTokenWithCliUrlThrows() { + var options = new CopilotClientOptions().setCliUrl("localhost:8080").setGitHubToken("gho_test_token"); assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); } diff --git a/src/test/java/com/github/copilot/sdk/E2ETestContext.java b/src/test/java/com/github/copilot/sdk/E2ETestContext.java index 0ec14dc0b..6bb351901 100644 --- a/src/test/java/com/github/copilot/sdk/E2ETestContext.java +++ b/src/test/java/com/github/copilot/sdk/E2ETestContext.java @@ -265,7 +265,7 @@ public CopilotClient createClient() { // In CI, use a fake token to avoid auth issues String ci = System.getenv("CI"); if (ci != null && !ci.isEmpty()) { - options.setGithubToken("fake-token-for-e2e-tests"); + options.setGitHubToken("fake-token-for-e2e-tests"); } return new CopilotClient(options); From 2076705914b4863c0a45ac240a53dd62c5594fd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:49:09 +0000 Subject: [PATCH 035/104] Update .lastmerge to f0909a78ce6c242f1f07e91b72ab7a7d8c910531 --- .lastmerge | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lastmerge b/.lastmerge index d5389b23c..4d56ec15e 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -c4b3b366c4bd8dfba9ba4aa05e4019825360ad78 +f0909a78ce6c242f1f07e91b72ab7a7d8c910531 From aebf4515a0d7aff6f80b9df494ab4d89a6f50ea0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:53:40 +0000 Subject: [PATCH 036/104] Fix sendAndWait cancellation propagation and Foundry Local docs Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../github/copilot/sdk/CopilotSession.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index d40c2d3f1..e7790ea54 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -373,24 +373,33 @@ public CompletableFuture sendAndWait(MessageOptions optio scheduler.shutdown(); }, timeoutMs, TimeUnit.MILLISECONDS); - var resultFuture = future.whenComplete((result, ex) -> { + var result = new CompletableFuture(); + + // When inner future completes, run cleanup and propagate to result + future.whenComplete((r, ex) -> { try { subscription.close(); } catch (IOException e) { LOG.log(Level.SEVERE, "Error closing subscription", e); } scheduler.shutdown(); + if (!result.isDone()) { + if (ex != null) { + result.completeExceptionally(ex); + } else { + result.complete(r); + } + } }); - // When the returned future is cancelled externally, propagate to the inner - // future so that cleanup (subscription close, scheduler shutdown) runs. - resultFuture.whenComplete((v, ex) -> { - if (resultFuture.isCancelled()) { - future.completeExceptionally(new java.util.concurrent.CancellationException()); + // When result is cancelled externally, cancel inner future to trigger cleanup + result.whenComplete((v, ex) -> { + if (result.isCancelled() && !future.isDone()) { + future.cancel(true); } }); - return resultFuture; + return result; } /** From 69f20998a0980f2c963fbcc1db654c0c2de33aab Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Tue, 24 Feb 2026 16:52:33 -0500 Subject: [PATCH 037/104] On branch edburns/dd-2745172-adr-001-virtual-threads-plan modified: .gitignore - Ignore Claude worktrees. new file: docs/adr/adr-001-semver-pre-general-availability.md - ADR for JDK 21 decision. Signed-off-by: Ed Burns --- .gitignore | 3 ++- ...adr-001-semver-pre-general-availability.md | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 docs/adr/adr-001-semver-pre-general-availability.md diff --git a/.gitignore b/.gitignore index 37e4620a8..5f035eaa5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ target examples-test/ .merge-env -blog-copilotsdk/ \ No newline at end of file +blog-copilotsdk/ +.claude/worktrees diff --git a/docs/adr/adr-001-semver-pre-general-availability.md b/docs/adr/adr-001-semver-pre-general-availability.md new file mode 100644 index 000000000..4be3fa566 --- /dev/null +++ b/docs/adr/adr-001-semver-pre-general-availability.md @@ -0,0 +1,27 @@ +# SemVer requirements pre general-availability of Reference Implementation + +## Context and Problem Statement + +Steve Sanderson agreed that `copilot-sdk-java` will track reference implementation version numbers directly, with one exception: when the Java SDK needs to ship a breaking change before 1.0, the reference implementation will bump its minor version to accommodate, giving our release a clean version number that signals the change to users. + +The reference implementation makes no backward compatibility guarantees pre-1.0 — and neither will we. That said, we're choosing to hold ourselves to a higher standard as a matter of good practice: we'll use minor version bumps as a signal to users when we do ship something breaking. + +The 2026-02 state of `copilot-sdk-java` is that it takes Java 17+ as its baseline. This decision precludes the use of Java 21 features such as virtual threads. Our pre-analisys showed the **possibility** of a significant performance benefit when using Virtual Threads with Java 21. + +We took an architectural decision to enable us to pursue investigating this possibility immediately. + +## Considered Options + +* Track SemVer of reference implementation, with one exception. +* Completely avoid the need for this by doing no breaking changes pre-1.0. +* Abandon the policy of tracking the versions of the reference implementation directly, just do our own thing. + +## Decision Outcome + +Chosen option: "Track SemVer of reference implementation, with one exception.", because this enables us to pursue Virtual Threads without delaying the first public release of `copilot-sdk-java`. Also, we're supposed to be aggressively modernizing our customers. + +To some extent, I would use qualifiers to mark a release as having some feature that is awaiting an upstream full release before it goes full ga, i.e you put out 0.1.46-virtualthreads.3 until upstream is ready to move to 0.2.0 then you release your virtual threads change and go 0.2.0. So I would make your agreement that your version numbers would match with the exception of qualifiers that you might add in exceptional circumstances. + +## Related work items + +- https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2745172 From 309821597dfcadfdee9f67978d37ae79ac8710ce Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 24 Feb 2026 17:04:31 -0500 Subject: [PATCH 038/104] Update docs/adr/adr-001-semver-pre-general-availability.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/adr/adr-001-semver-pre-general-availability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/adr-001-semver-pre-general-availability.md b/docs/adr/adr-001-semver-pre-general-availability.md index 4be3fa566..44c9cf6db 100644 --- a/docs/adr/adr-001-semver-pre-general-availability.md +++ b/docs/adr/adr-001-semver-pre-general-availability.md @@ -6,7 +6,7 @@ Steve Sanderson agreed that `copilot-sdk-java` will track reference implementati The reference implementation makes no backward compatibility guarantees pre-1.0 — and neither will we. That said, we're choosing to hold ourselves to a higher standard as a matter of good practice: we'll use minor version bumps as a signal to users when we do ship something breaking. -The 2026-02 state of `copilot-sdk-java` is that it takes Java 17+ as its baseline. This decision precludes the use of Java 21 features such as virtual threads. Our pre-analisys showed the **possibility** of a significant performance benefit when using Virtual Threads with Java 21. +The 2026-02 state of `copilot-sdk-java` is that it takes Java 17+ as its baseline. This decision precludes the use of Java 21 features such as virtual threads. Our pre-analysis showed the **possibility** of a significant performance benefit when using Virtual Threads with Java 21. We took an architectural decision to enable us to pursue investigating this possibility immediately. From 0e47b4c36ccb912ce1ec9fa263dc980830c08fe2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:29:43 +0000 Subject: [PATCH 039/104] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 468038233..d8962bc1d 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 90.5% - 90.5% + 90.4% + 90.4% From 5dcc57121a929031f56c814bca738d8ef8032f75 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 25 Feb 2026 14:07:45 -0500 Subject: [PATCH 040/104] Add untracked file 'test' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test diff --git a/test b/test new file mode 100644 index 000000000..e69de29bb From 6cfbe4fdac25252e153b513df8790f4f35e83c42 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 2 Mar 2026 17:01:32 -0500 Subject: [PATCH 041/104] updated agentic workflow --- .github/aw/actions-lock.json | 5 + .github/workflows/agentics-maintenance.yml | 82 ++ .../workflows/weekly-upstream-sync.lock.yml | 786 ++++++++++-------- .github/workflows/weekly-upstream-sync.md | 1 + 4 files changed, 507 insertions(+), 367 deletions(-) create mode 100644 .github/workflows/agentics-maintenance.yml diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 36881a0f5..d81867b96 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -14,6 +14,11 @@ "repo": "actions/setup-node", "version": "v4", "sha": "49933ea5288caeca8642d1e84afbd3f7d6820020" + }, + "github/gh-aw/actions/setup@v0.51.6": { + "repo": "github/gh-aw/actions/setup", + "version": "v0.51.6", + "sha": "33cd6c7f1fee588654ef19def2e6a4174be66197" } } } diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml new file mode 100644 index 000000000..bb7a31d5d --- /dev/null +++ b/.github/workflows/agentics-maintenance.yml @@ -0,0 +1,82 @@ +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by pkg/workflow/maintenance_workflow.go (v0.51.6). DO NOT EDIT. +# +# To regenerate this workflow, run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Alternative regeneration methods: +# make recompile +# +# Or use the gh-aw CLI directly: +# ./gh-aw compile --validate --verbose +# +# The workflow is generated when any workflow uses the 'expires' field +# in create-discussions, create-issues, or create-pull-request safe-outputs configuration. +# Schedule frequency is automatically determined by the shortest expiration time. +# +name: Agentic Maintenance + +on: + schedule: + - cron: "37 0 * * *" # Daily (based on minimum expires: 6 days) + workflow_dispatch: + +permissions: {} + +jobs: + close-expired-entities: + if: ${{ !github.event.repository.fork }} + runs-on: ubuntu-slim + permissions: + discussions: write + issues: write + pull-requests: write + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@33cd6c7f1fee588654ef19def2e6a4174be66197 # v0.51.6 + with: + destination: /opt/gh-aw/actions + + - name: Close expired discussions + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/close_expired_discussions.cjs'); + await main(); + + - name: Close expired issues + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/close_expired_issues.cjs'); + await main(); + + - name: Close expired pull requests + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/close_expired_pull_requests.cjs'); + await main(); diff --git a/.github/workflows/weekly-upstream-sync.lock.yml b/.github/workflows/weekly-upstream-sync.lock.yml index 163600e1e..cfd1342fa 100644 --- a/.github/workflows/weekly-upstream-sync.lock.yml +++ b/.github/workflows/weekly-upstream-sync.lock.yml @@ -13,16 +13,18 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.43.2). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.51.6). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile -# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ # # Weekly upstream sync workflow. Checks for new commits in the official # Copilot SDK (github/copilot-sdk) and assigns to Copilot to port changes. # -# frontmatter-hash: 27b0f4284357c75008ce6e8d1d772fe9871a3548be6a6a4996125b6ca980cc2e +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"fc14b09206c7aeafcd52c843adce996a1c14cf15875f9b647ef71f631b3b296e","compiler_version":"v0.51.6"} name: "Weekly Upstream Sync Agentic Workflow" "on": @@ -46,11 +48,50 @@ jobs: outputs: comment_id: "" comment_repo: "" + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.43.2 + uses: github/gh-aw/actions/setup@33cd6c7f1fee588654ef19def2e6a4174be66197 # v0.51.6 with: destination: /opt/gh-aw/actions + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_INFO_VERSION: "" + GH_AW_INFO_AGENT_VERSION: "0.0.420" + GH_AW_INFO_CLI_VERSION: "v0.51.6" + GH_AW_INFO_WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","github"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.23.0" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { main } = require('/opt/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: | + .github + .agents + sparse-checkout-cone-mode: true + fetch-depth: 1 + persist-credentials: false - name: Check workflow file timestamps uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: @@ -61,6 +102,128 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first.sh + { + cat << 'GH_AW_PROMPT_EOF' + + GH_AW_PROMPT_EOF + cat "/opt/gh-aw/prompts/xpia.md" + cat "/opt/gh-aw/prompts/temp_folder_prompt.md" + cat "/opt/gh-aw/prompts/markdown.md" + cat "/opt/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_EOF' + + Tools: add_comment, create_issue, close_issue, assign_to_agent, missing_tool, missing_data, noop + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/weekly-upstream-sync.md}} + GH_AW_PROMPT_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: activation + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + retention-days: 1 agent: needs: activation @@ -80,25 +243,23 @@ jobs: GH_AW_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_WORKFLOW_ID_SANITIZED: weeklyupstreamsync outputs: checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} has_patch: ${{ steps.collect_output.outputs.has_patch }} - model: ${{ steps.generate_aw_info.outputs.model }} + model: ${{ needs.activation.outputs.model }} output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.43.2 + uses: github/gh-aw/actions/setup@33cd6c7f1fee588654ef19def2e6a4174be66197 # v0.51.6 with: destination: /opt/gh-aw/actions - - name: Checkout .github and .agents folders + - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - sparse-checkout: | - .github - .agents - fetch-depth: 1 persist-credentials: false - name: Create gh-aw temp directory run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh @@ -109,6 +270,7 @@ jobs: run: | git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.name "github-actions[bot]" + git config --global am.keepcr true # Re-authenticate git with GitHub token SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" @@ -116,7 +278,7 @@ jobs: - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request + (github.event.pull_request) || (github.event.issue.pull_request) uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -127,81 +289,31 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - - name: Generate agentic run info - id: generate_aw_info - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require('fs'); - - const awInfo = { - engine_id: "copilot", - engine_name: "GitHub Copilot CLI", - model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", - version: "", - agent_version: "0.0.405", - cli_version: "v0.43.2", - workflow_name: "Weekly Upstream Sync Agentic Workflow", - experimental: false, - supports_tools_allowlist: true, - supports_http_transport: true, - run_id: context.runId, - run_number: context.runNumber, - run_attempt: process.env.GITHUB_RUN_ATTEMPT, - repository: context.repo.owner + '/' + context.repo.repo, - ref: context.ref, - sha: context.sha, - actor: context.actor, - event_name: context.eventName, - staged: false, - allowed_domains: ["defaults","github"], - firewall_enabled: true, - awf_version: "v0.13.12", - awmg_version: "", - steps: { - firewall: "squid" - }, - created_at: new Date().toISOString() - }; - - // Write to /tmp/gh-aw directory to avoid inclusion in PR - const tmpPath = '/tmp/gh-aw/aw_info.json'; - fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); - console.log('Generated aw_info.json at:', tmpPath); - console.log(JSON.stringify(awInfo, null, 2)); - - // Set model as output for reuse in other steps/jobs - core.setOutput('model', awInfo.model); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Install GitHub Copilot CLI - run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.420 - name: Install awf binary - run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.13.12 - - name: Determine automatic lockdown mode for GitHub MCP server + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.23.0 + - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - env: - TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - if: env.TOKEN_CHECK != '' uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} with: script: | const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.13.12 ghcr.io/github/gh-aw-firewall/squid:0.13.12 ghcr.io/github/gh-aw-mcpg:v0.0.113 ghcr.io/github/github-mcp-server:v0.30.3 node:lts-alpine + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.23.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.23.0 ghcr.io/github/gh-aw-firewall/squid:0.23.0 ghcr.io/github/gh-aw-mcpg:v0.1.6 ghcr.io/github/github-mcp-server:v0.31.0 node:lts-alpine - name: Write Safe Outputs Config run: | mkdir -p /opt/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' - {"add_comment":{"max":10,"target":"*"},"assign_to_agent":{"default_agent":"copilot","target":"*"},"close_issue":{"max":10,"required_labels":["upstream-sync"]},"create_issue":{"expires":144,"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} - EOF - cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' + cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' + {"add_comment":{"max":10,"target":"*"},"assign_to_agent":{"default_agent":"copilot","max":1,"target":"*"},"close_issue":{"max":10,"required_labels":["upstream-sync"],"target":"*"},"create_issue":{"expires":144,"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + GH_AW_SAFE_OUTPUTS_CONFIG_EOF + cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' [ { "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[upstream-sync] \". Labels [upstream-sync] will be automatically added. Assignees [copilot] will be automatically assigned.", @@ -220,14 +332,15 @@ jobs: "type": "array" }, "parent": { - "description": "Parent issue number for creating sub-issues. This is the numeric ID from the GitHub URL (e.g., 42 in github.com/owner/repo/issues/42). Can also be a temporary_id (e.g., 'aw_abc123def456') from a previously created issue in the same workflow run.", + "description": "Parent issue number for creating sub-issues. This is the numeric ID from the GitHub URL (e.g., 42 in github.com/owner/repo/issues/42). Can also be a temporary_id (e.g., 'aw_abc123', 'aw_Test123') from a previously created issue in the same workflow run.", "type": [ "number", "string" ] }, "temporary_id": { - "description": "Unique temporary identifier for referencing this issue before it's created. Format: 'aw_' followed by 12 hex characters (e.g., 'aw_abc123def456'). Use '#aw_ID' in body text to reference other issues by their temporary_id; these are replaced with actual issue numbers after creation.", + "description": "Unique temporary identifier for referencing this issue before it's created. Format: 'aw_' followed by 3 to 8 alphanumeric characters (e.g., 'aw_abc1', 'aw_Test123'). Use '#aw_ID' in body text to reference other issues by their temporary_id; these are replaced with actual issue numbers after creation.", + "pattern": "^aw_[A-Za-z0-9]{3,8}$", "type": "string" }, "title": { @@ -244,7 +357,7 @@ jobs: "name": "create_issue" }, { - "description": "Close a GitHub issue with a closing comment. Use this when work is complete, the issue is no longer relevant, or it's a duplicate. The closing comment should explain the resolution or reason for closing. CONSTRAINTS: Maximum 10 issue(s) can be closed. Target: *.", + "description": "Close a GitHub issue with a closing comment. You can and should always add a comment when closing an issue to explain the action or provide context. This tool is ONLY for closing issues - use update_issue if you need to change the title, body, labels, or other metadata without closing. Use close_issue when work is complete, the issue is no longer relevant, or it's a duplicate. The closing comment should explain the resolution or reason for closing. If the issue is already closed, a comment will still be posted. CONSTRAINTS: Maximum 10 issue(s) can be closed. Target: *.", "inputSchema": { "additionalProperties": false, "properties": { @@ -268,16 +381,16 @@ jobs: "name": "close_issue" }, { - "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 10 comment(s) can be added. Target: *.", + "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. IMPORTANT: Comments are subject to validation constraints enforced by the MCP server - maximum 65536 characters for the complete comment (including footer which is added automatically), 10 mentions (@username), and 50 links. Exceeding these limits will result in an immediate error with specific guidance. NOTE: By default, this tool requires discussions:write permission. If your GitHub App lacks Discussions permission, set 'discussions: false' in the workflow's safe-outputs.add-comment configuration to exclude this permission. CONSTRAINTS: Maximum 10 comment(s) can be added. Target: *.", "inputSchema": { "additionalProperties": false, "properties": { "body": { - "description": "The comment text in Markdown format. This is the 'body' field - do not use 'comment_body' or other variations. Provide helpful, relevant information that adds value to the conversation.", + "description": "The comment text in Markdown format. This is the 'body' field - do not use 'comment_body' or other variations. Provide helpful, relevant information that adds value to the conversation. CONSTRAINTS: The complete comment (your body text + automatically added footer) must not exceed 65536 characters total. Maximum 10 mentions (@username), maximum 50 links (http/https URLs). A footer (~200-500 characters) is automatically appended with workflow attribution, so leave adequate space. If these limits are exceeded, the tool call will fail with a detailed error message indicating which constraint was violated.", "type": "string" }, "item_number": { - "description": "The issue, pull request, or discussion number to comment on. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123). If omitted, the tool will attempt to resolve the target from the current workflow context (triggering issue, PR, or discussion).", + "description": "The issue, pull request, or discussion number to comment on. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123). If omitted, the tool auto-targets the issue, PR, or discussion that triggered this workflow. Auto-targeting only works for issue, pull_request, discussion, and comment event triggers — it does NOT work for schedule, workflow_dispatch, push, or workflow_run triggers. For those trigger types, always provide item_number explicitly, or the comment will be silently discarded.", "type": "number" } }, @@ -289,7 +402,7 @@ jobs: "name": "add_comment" }, { - "description": "Assign the GitHub Copilot coding agent to work on an issue or pull request. The agent will analyze the issue/PR and attempt to implement a solution, creating a pull request when complete. Use this to delegate coding tasks to Copilot. Example usage: assign_to_agent(issue_number=123, agent=\"copilot\") or assign_to_agent(pull_number=456, agent=\"copilot\")", + "description": "Assign the GitHub Copilot coding agent to work on an issue or pull request. The agent will analyze the issue/PR and attempt to implement a solution, creating a pull request when complete. Use this to delegate coding tasks to Copilot. Example usage: assign_to_agent(issue_number=123, agent=\"copilot\") or assign_to_agent(pull_number=456, agent=\"copilot\", pull_request_repo=\"owner/repo\") CONSTRAINTS: Maximum 1 issue(s) can be assigned to agent.", "inputSchema": { "additionalProperties": false, "properties": { @@ -298,18 +411,22 @@ jobs: "type": "string" }, "issue_number": { - "description": "Issue number to assign the Copilot agent to. This is the numeric ID from the GitHub URL (e.g., 234 in github.com/owner/repo/issues/234). Can also be a temporary_id (e.g., 'aw_abc123def456') from an issue created earlier in the same workflow run. The issue should contain clear, actionable requirements. Either issue_number or pull_number must be provided, but not both.", + "description": "Issue number to assign the Copilot coding agent to. This is the numeric ID from the GitHub URL (e.g., 234 in github.com/owner/repo/issues/234). Can also be a temporary_id (e.g., 'aw_abc123', 'aw_Test123') from an issue created earlier in the same workflow run. The issue should contain clear, actionable requirements. Either issue_number or pull_number must be provided, but not both.", "type": [ "number", "string" ] }, "pull_number": { - "description": "Pull request number to assign the Copilot agent to. This is the numeric ID from the GitHub URL (e.g., 456 in github.com/owner/repo/pull/456). Either issue_number or pull_number must be provided, but not both.", + "description": "Pull request number to assign the Copilot coding agent to. This is the numeric ID from the GitHub URL (e.g., 456 in github.com/owner/repo/pull/456). Either issue_number or pull_number must be provided, but not both.", "type": [ "number", "string" ] + }, + "pull_request_repo": { + "description": "Target repository where the pull request should be created, in 'owner/repo' format. If omitted, the PR will be created in the same repository as the issue. This allows issues and code to live in different repositories. The global pull-request-repo configuration (if set) is automatically allowed; additional repositories must be listed in allowed-pull-request-repos.", + "type": "string" } }, "type": "object" @@ -386,8 +503,8 @@ jobs: "name": "missing_data" } ] - EOF - cat > /opt/gh-aw/safeoutputs/validation.json << 'EOF' + GH_AW_SAFE_OUTPUTS_TOOLS_EOF + cat > /opt/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_EOF' { "add_comment": { "defaultMax": 1, @@ -400,6 +517,10 @@ jobs: }, "item_number": { "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 } } }, @@ -416,6 +537,14 @@ jobs: }, "pull_number": { "optionalPositiveInteger": true + }, + "pull_request_repo": { + "type": "string", + "maxLength": 256 + }, + "repo": { + "type": "string", + "maxLength": 256 } }, "customValidation": "requiresOneOf:issue_number,pull_number" @@ -431,6 +560,10 @@ jobs: }, "issue_number": { "optionalPositiveInteger": true + }, + "repo": { + "type": "string", + "maxLength": 256 } } }, @@ -467,6 +600,31 @@ jobs: } } }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, "missing_tool": { "defaultMax": 20, "fields": { @@ -500,7 +658,7 @@ jobs: } } } - EOF + GH_AW_SAFE_OUTPUTS_VALIDATION_EOF - name: Generate Safe Outputs MCP Server Config id: safe-outputs-config run: | @@ -539,7 +697,7 @@ jobs: bash /opt/gh-aw/actions/start_safe_outputs_server.sh - - name: Start MCP gateway + - name: Start MCP Gateway id: start-mcp-gateway env: GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} @@ -559,18 +717,19 @@ jobs: export MCP_GATEWAY_API_KEY export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" export DEBUG="*" export GH_AW_ENGINE="copilot" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.0.113' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.6' mkdir -p /home/runner/.copilot - cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v0.30.3", + "container": "ghcr.io/github/github-mcp-server:v0.31.0", "env": { "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", @@ -593,131 +752,12 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - MCPCONFIG_EOF - - name: Generate workflow overview - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); - await generateWorkflowOverview(core); - - name: Create prompt with built-in context - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - run: | - bash /opt/gh-aw/actions/create_prompt_first.sh - cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" - - PROMPT_EOF - cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" - cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - - GitHub API Access Instructions - - The gh CLI is NOT authenticated. Do NOT use gh commands for GitHub operations. - - - To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. - - Discover available tools from the safeoutputs MCP server. - - **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. - - **Note**: If you made no other safe output tool calls during this workflow execution, call the "noop" tool to provide a status message indicating completion or that no actions were needed. - - - - The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ - {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - PROMPT_EOF - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - - PROMPT_EOF - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - {{#runtime-import .github/workflows/weekly-upstream-sync.md}} - PROMPT_EOF - - name: Substitute placeholders - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CONFIG_EOF + - name: Download activation artifact + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8 with: - script: | - const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); - - // Call the substitution function - return await substitutePlaceholders({ - file: process.env.GH_AW_PROMPT, - substitutions: { - GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, - GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, - GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, - GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, - GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE - } - }); - - name: Interpolate variables and render templates - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); - await main(); - - name: Validate prompt placeholders - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh - - name: Print prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/print_prompt_summary.sh + name: activation + path: /tmp/gh-aw - name: Clean git credentials run: bash /opt/gh-aw/actions/clean_git_credentials.sh - name: Execute GitHub Copilot CLI @@ -726,9 +766,9 @@ jobs: timeout-minutes: 20 run: | set -o pipefail - sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.13.12 --skip-pull \ - -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' \ - 2>&1 | tee /tmp/gh-aw/agent-stdio.log + # shellcheck disable=SC1003 + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.23.0 --skip-pull --enable-api-proxy \ + -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} @@ -736,8 +776,11 @@ jobs: GH_AW_MODEL_AGENT_COPILOT: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GITHUB_API_URL: ${{ github.api_url }} GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} GITHUB_WORKSPACE: ${{ github.workspace }} XDG_CONFIG_HOME: /home/runner @@ -748,6 +791,7 @@ jobs: run: | git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.name "github-actions[bot]" + git config --global am.keepcr true # Re-authenticate git with GitHub token SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" @@ -769,7 +813,7 @@ jobs: else echo "No session-state directory found at $SESSION_STATE_DIR" fi - - name: Stop MCP gateway + - name: Stop MCP Gateway if: always() continue-on-error: true env: @@ -795,13 +839,14 @@ jobs: SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload Safe Outputs if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 with: name: safe-output path: ${{ env.GH_AW_SAFE_OUTPUTS }} if-no-files-found: warn - name: Ingest agent output id: collect_output + if: always() uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} @@ -816,13 +861,13 @@ jobs: await main(); - name: Upload sanitized agent output if: always() && env.GH_AW_AGENT_OUTPUT - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 with: name: agent-output path: ${{ env.GH_AW_AGENT_OUTPUT }} if-no-files-found: warn - name: Upload engine output files - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 with: name: agent_outputs path: | @@ -840,7 +885,7 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); await main(); - - name: Parse MCP gateway logs for step summary + - name: Parse MCP Gateway logs for step summary if: always() uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: @@ -858,27 +903,146 @@ jobs: # Fix permissions on firewall logs so they can be uploaded as artifacts # AWF runs with sudo, creating files owned by root sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi - name: Upload agent artifacts if: always() continue-on-error: true - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 with: name: agent-artifacts path: | /tmp/gh-aw/aw-prompts/prompt.txt - /tmp/gh-aw/aw_info.json /tmp/gh-aw/mcp-logs/ /tmp/gh-aw/sandbox/firewall/logs/ /tmp/gh-aw/agent-stdio.log /tmp/gh-aw/agent/ if-no-files-found: ignore + # --- Threat Detection (inline) --- + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ steps.collect_output.outputs.output_types }} + HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP configuration for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" + WORKFLOW_DESCRIPTION: "Weekly upstream sync workflow. Checks for new commits in the official\nCopilot SDK (github/copilot-sdk) and assigns to Copilot to port changes." + HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool shell(cat) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(tail) + # --allow-tool shell(wc) + timeout-minutes: 20 + run: | + set -o pipefail + # shellcheck disable=SC1003 + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,raw.githubusercontent.com,registry.npmjs.org,telemetry.enterprise.githubcopilot.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.23.0 --skip-pull --enable-api-proxy \ + -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(wc)'\'' --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"}' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection results + id: parse_detection_results + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: threat-detection.log + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Set detection conclusion + id: detection_conclusion + if: always() + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_SUCCESS: ${{ steps.parse_detection_results.outputs.success }} + run: | + if [[ "$RUN_DETECTION" != "true" ]]; then + echo "conclusion=skipped" >> "$GITHUB_OUTPUT" + echo "success=true" >> "$GITHUB_OUTPUT" + echo "Detection was not needed, marking as skipped" + elif [[ "$DETECTION_SUCCESS" == "true" ]]; then + echo "conclusion=success" >> "$GITHUB_OUTPUT" + echo "success=true" >> "$GITHUB_OUTPUT" + echo "Detection passed successfully" + else + echo "conclusion=failure" >> "$GITHUB_OUTPUT" + echo "success=false" >> "$GITHUB_OUTPUT" + echo "Detection found issues" + fi conclusion: needs: - activation - agent - - detection - safe_outputs if: (always()) && (needs.agent.result != 'skipped') runs-on: ubuntu-slim @@ -893,23 +1057,12 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.43.2 + uses: github/gh-aw/actions/setup@33cd6c7f1fee588654ef19def2e6a4174be66197 # v0.51.6 with: destination: /opt/gh-aw/actions - - name: Debug job inputs - env: - COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} - AGENT_CONCLUSION: ${{ needs.agent.result }} - run: | - echo "Comment ID: $COMMENT_ID" - echo "Comment Repo: $COMMENT_REPO" - echo "Agent Output Types: $AGENT_OUTPUT_TYPES" - echo "Agent Conclusion: $AGENT_CONCLUSION" - name: Download agent output artifact continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8 with: name: agent-output path: /tmp/gh-aw/safeoutputs/ @@ -923,7 +1076,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_NOOP_MAX: 1 + GH_AW_NOOP_MAX: "1" GH_AW_WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -954,10 +1107,11 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_WORKFLOW_ID: "weekly-upstream-sync" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_ASSIGNMENT_ERRORS: ${{ needs.safe_outputs.outputs.assign_to_agent_assignment_errors }} GH_AW_ASSIGNMENT_ERROR_COUNT: ${{ needs.safe_outputs.outputs.assign_to_agent_assignment_error_count }} + GH_AW_GROUP_REPORTS: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -982,131 +1136,10 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); await main(); - - name: Update reaction comment with completion status - id: conclusion - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.result }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/notify_comment_error.cjs'); - await main(); - - detection: - needs: agent - if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' - runs-on: ubuntu-latest - permissions: {} - concurrency: - group: "gh-aw-copilot-${{ github.workflow }}" - timeout-minutes: 10 - outputs: - success: ${{ steps.parse_results.outputs.success }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.43.2 - with: - destination: /opt/gh-aw/actions - - name: Download agent artifacts - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-artifacts - path: /tmp/gh-aw/threat-detection/ - - name: Download agent output artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-output - path: /tmp/gh-aw/threat-detection/ - - name: Echo agent output types - env: - AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} - run: | - echo "Agent output-types: $AGENT_OUTPUT_TYPES" - - name: Setup threat detection - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" - WORKFLOW_DESCRIPTION: "Weekly upstream sync workflow. Checks for new commits in the official\nCopilot SDK (github/copilot-sdk) and assigns to Copilot to port changes." - HAS_PATCH: ${{ needs.agent.outputs.has_patch }} - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); - await main(); - - name: Ensure threat-detection directory and log - run: | - mkdir -p /tmp/gh-aw/threat-detection - touch /tmp/gh-aw/threat-detection/detection.log - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Install GitHub Copilot CLI - run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 - - name: Execute GitHub Copilot CLI - id: agentic_execution - # Copilot CLI tool arguments (sorted): - # --allow-tool shell(cat) - # --allow-tool shell(grep) - # --allow-tool shell(head) - # --allow-tool shell(jq) - # --allow-tool shell(ls) - # --allow-tool shell(tail) - # --allow-tool shell(wc) - timeout-minutes: 20 - run: | - set -o pipefail - COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" - mkdir -p /tmp/ - mkdir -p /tmp/gh-aw/ - mkdir -p /tmp/gh-aw/agent/ - mkdir -p /tmp/gh-aw/sandbox/agent/logs/ - copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log - env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} - GITHUB_WORKSPACE: ${{ github.workspace }} - XDG_CONFIG_HOME: /home/runner - - name: Parse threat detection results - id: parse_results - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - - name: Upload threat detection log - if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: threat-detection.log - path: /tmp/gh-aw/threat-detection/detection.log - if-no-files-found: ignore safe_outputs: - needs: - - agent - - detection - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.detection.outputs.success == 'true') + needs: agent + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true') runs-on: ubuntu-slim permissions: contents: read @@ -1115,6 +1148,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/${{ github.workflow }}" GH_AW_ENGINE_ID: "copilot" GH_AW_WORKFLOW_ID: "weekly-upstream-sync" GH_AW_WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" @@ -1122,18 +1156,24 @@ jobs: assign_to_agent_assigned: ${{ steps.assign_to_agent.outputs.assigned }} assign_to_agent_assignment_error_count: ${{ steps.assign_to_agent.outputs.assignment_error_count }} assign_to_agent_assignment_errors: ${{ steps.assign_to_agent.outputs.assignment_errors }} + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} + comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} + created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.43.2 + uses: github/gh-aw/actions/setup@33cd6c7f1fee588654ef19def2e6a4174be66197 # v0.51.6 with: destination: /opt/gh-aw/actions - name: Download agent output artifact continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8 with: name: agent-output path: /tmp/gh-aw/safeoutputs/ @@ -1147,6 +1187,9 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"close_issue\":{\"max\":10,\"required_labels\":[\"upstream-sync\"],\"target\":\"*\"},\"create_issue\":{\"assignees\":[\"copilot\"],\"expires\":144,\"labels\":[\"upstream-sync\"],\"max\":1,\"title_prefix\":\"[upstream-sync] \"},\"missing_data\":{},\"missing_tool\":{}}" GH_AW_ASSIGN_COPILOT: "true" with: @@ -1168,13 +1211,15 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/assign_copilot_to_created_issues.cjs'); await main(); - - name: Assign To Agent + - name: Assign to agent id: assign_to_agent if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'assign_to_agent')) uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_AGENT_MAX_COUNT: 1 GH_AW_AGENT_DEFAULT: "copilot" + GH_AW_AGENT_DEFAULT_MODEL: "claude-opus-4.6" GH_AW_AGENT_TARGET: "*" GH_AW_TEMPORARY_ID_MAP: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} with: @@ -1184,4 +1229,11 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/assign_to_agent.cjs'); await main(); + - name: Upload safe output items manifest + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-output-items + path: /tmp/safe-output-items.jsonl + if-no-files-found: warn diff --git a/.github/workflows/weekly-upstream-sync.md b/.github/workflows/weekly-upstream-sync.md index 31a445df4..641f29301 100644 --- a/.github/workflows/weekly-upstream-sync.md +++ b/.github/workflows/weekly-upstream-sync.md @@ -36,6 +36,7 @@ safe-outputs: max: 10 assign-to-agent: name: "copilot" + model: "claude-opus-4.6" target: "*" noop: report-as-issue: false From 050e8e472322619cbd1ee984a16c84c26c48e999 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 2 Mar 2026 17:22:58 -0500 Subject: [PATCH 042/104] Upgrade Jackson to 2.21.1 to fix async parser DoS vulnerability Fixes Dependabot alert #1 (GHSA-72hv-8253-57qq): jackson-core async parser bypasses maxNumberLength constraint, allowing DoS via excessive memory/CPU consumption with arbitrarily long numbers. Upgrades: - jackson-databind: 2.20.1 -> 2.21.1 - jackson-annotations: 2.20 -> 2.21 - jackson-datatype-jsr310: 2.20.1 -> 2.21.1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index d86e46be7..17344af45 100644 --- a/pom.xml +++ b/pom.xml @@ -49,17 +49,17 @@ com.fasterxml.jackson.core jackson-databind - 2.20.1 + 2.21.1 com.fasterxml.jackson.core jackson-annotations - 2.20 + 2.21 com.fasterxml.jackson.datatype jackson-datatype-jsr310 - 2.20.1 + 2.21.1 From ba24f60d5ee8db0431bbb985bc3e311b81dfda83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:37:58 +0000 Subject: [PATCH 043/104] Initial plan From 9640cbd342f214a9187d7a36584c2dcca77adc1c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 23:01:56 +0000 Subject: [PATCH 044/104] Port upstream sync: require permission handler, new events, agent/compaction APIs Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .lastmerge | 2 +- CHANGELOG.md | 27 ++++- .../com/github/copilot/sdk/CopilotClient.java | 74 +++++++----- .../github/copilot/sdk/CopilotSession.java | 102 ++++++++++++++++- .../sdk/events/AbstractSessionEvent.java | 8 +- .../events/AssistantMessageDeltaEvent.java | 1 - .../sdk/events/AssistantMessageEvent.java | 1 + .../events/AssistantStreamingDeltaEvent.java | 37 ++++++ .../sdk/events/AssistantTurnStartEvent.java | 3 +- .../sdk/events/AssistantUsageEvent.java | 27 ++++- .../sdk/events/SessionEventParser.java | 3 + .../sdk/events/SessionTaskCompleteEvent.java | 37 ++++++ .../copilot/sdk/events/SkillInvokedEvent.java | 3 +- .../sdk/events/SubagentDeselectedEvent.java | 37 ++++++ .../events/ToolExecutionCompleteEvent.java | 7 +- .../github/copilot/sdk/json/AgentInfo.java | 89 +++++++++++++++ src/site/markdown/documentation.md | 3 + .../com/github/copilot/sdk/AskUserTest.java | 55 ++++----- .../copilot/sdk/ClosedSessionGuardTest.java | 46 +++++--- .../github/copilot/sdk/CompactionTest.java | 7 +- .../github/copilot/sdk/CopilotClientTest.java | 7 +- .../copilot/sdk/CopilotSessionTest.java | 83 +++++++++----- .../github/copilot/sdk/E2ETestContext.java | 3 +- .../github/copilot/sdk/ErrorHandlingTest.java | 10 +- .../github/copilot/sdk/McpAndAgentsTest.java | 39 ++++--- .../github/copilot/sdk/PermissionsTest.java | 51 +++++---- .../copilot/sdk/SessionEventHandlingTest.java | 2 +- .../copilot/sdk/SessionEventParserTest.java | 2 - .../copilot/sdk/SessionEventsE2ETest.java | 12 +- .../com/github/copilot/sdk/SkillsTest.java | 8 +- .../com/github/copilot/sdk/ToolsTest.java | 106 +++++++++++++++++- 31 files changed, 724 insertions(+), 168 deletions(-) create mode 100644 src/main/java/com/github/copilot/sdk/events/AssistantStreamingDeltaEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/events/SessionTaskCompleteEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/events/SubagentDeselectedEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/json/AgentInfo.java diff --git a/.lastmerge b/.lastmerge index 4d56ec15e..4767fe10d 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -f0909a78ce6c242f1f07e91b72ab7a7d8c910531 +b9f746ab1b9c5f31af03a6f8a6cf4e680b3fd6b8 diff --git a/CHANGELOG.md b/CHANGELOG.md index 66d72554f..93cb20567 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,17 +8,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] -> **Upstream sync:** [`github/copilot-sdk@c263dfc`](https://github.com/github/copilot-sdk/commit/c263dfc69055f9f28ee2d4b121cf617fca5a42dc) +> **Upstream sync:** [`github/copilot-sdk@b9f746a`](https://github.com/github/copilot-sdk/commit/b9f746ab1b9c5f31af03a6f8a6cf4e680b3fd6b8) ### Added -- `SessionConfig.setClientName(String)` / `getClientName()` — identifies the application using the SDK; included in the User-Agent header for API requests (upstream: [`397ef66`](https://github.com/github/copilot-sdk/commit/397ef66)) -- `ResumeSessionConfig.setClientName(String)` / `getClientName()` — same for resumed sessions -- `PermissionHandler.APPROVE_ALL` — pre-built handler that approves all permission requests (upstream: [`3e2d2b2`](https://github.com/github/copilot-sdk/commit/3e2d2b2)) +- `CopilotSession.listAgents()` — lists custom agents available for selection (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) +- `CopilotSession.getCurrentAgent()` — gets the currently selected custom agent (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) +- `CopilotSession.selectAgent(String)` — selects a custom agent for the session (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) +- `CopilotSession.deselectAgent()` — deselects the current custom agent (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) +- `CopilotSession.compact()` — triggers immediate session context compaction (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) +- `AgentInfo` — new JSON type representing a custom agent with `name`, `displayName`, and `description` (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) +- New event types: `SessionTaskCompleteEvent` (`session.task_complete`), `AssistantStreamingDeltaEvent` (`assistant.streaming_delta`), `SubagentDeselectedEvent` (`subagent.deselected`) (upstream: various commits) +- `AssistantTurnStartEvent` data now includes `interactionId` field +- `AssistantMessageEvent` data now includes `interactionId` field +- `ToolExecutionCompleteEvent` data now includes `model` and `interactionId` fields +- `SkillInvokedEvent` data now includes `pluginName` and `pluginVersion` fields +- `AssistantUsageEvent` data now includes `copilotUsage` field with `CopilotUsage` and `TokenDetails` nested types +- E2E tests for custom tool permission approval and denial flows (upstream: [`388f2f3`](https://github.com/github/copilot-sdk/commit/388f2f3)) ### Changed -- **Breaking:** permissions are now denied by default when no `OnPermissionRequest` handler is provided. The `requestPermission` flag is always sent as `true` so the server calls back for every permission request; the SDK returns a deny result when no handler is registered (upstream: [`3e2d2b2`](https://github.com/github/copilot-sdk/commit/3e2d2b2)) +- **Breaking:** `createSession(SessionConfig)` now requires a non-null `onPermissionRequest` handler; throws `IllegalArgumentException` if not provided (upstream: [`279f6c4`](https://github.com/github/copilot-sdk/commit/279f6c4)) +- **Breaking:** `resumeSession(String, ResumeSessionConfig)` now requires a non-null `onPermissionRequest` handler; throws `IllegalArgumentException` if not provided (upstream: [`279f6c4`](https://github.com/github/copilot-sdk/commit/279f6c4)) +- **Breaking:** The no-arg `createSession()` and `resumeSession(String)` overloads were removed (upstream: [`279f6c4`](https://github.com/github/copilot-sdk/commit/279f6c4)) +- `AssistantMessageDeltaEvent` data: `totalResponseSizeBytes` field moved to new `AssistantStreamingDeltaEvent` (upstream: various) + +### Fixed + +- Permission checks now also apply to SDK-registered custom tools, invoking the `onPermissionRequest` handler with `kind="custom-tool"` before executing tools (upstream: [`388f2f3`](https://github.com/github/copilot-sdk/commit/388f2f3)) ## [1.0.9] - 2026-02-16 diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index 043483284..74d961f4d 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -47,7 +47,10 @@ * try (var client = new CopilotClient()) { * client.start().get(); * - * var session = client.createSession(new SessionConfig().setModel("gpt-5")).get(); + * var session = client + * .createSession( + * new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")) + * .get(); * * session.on(AssistantMessageEvent.class, msg -> { * System.out.println(msg.getData().content()); @@ -273,14 +276,37 @@ private CompletableFuture cleanupConnection() { *

* The session maintains conversation state and can be used to send messages and * receive responses. Remember to close the session when done. + *

+ * A permission handler is required when creating a session. Use + * {@link com.github.copilot.sdk.json.PermissionHandler#APPROVE_ALL} to approve + * all permission requests, or provide a custom handler to control permissions + * selectively. + * + *

+ * Example: + * + *

{@code
+     * var session = client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
+     * }
* * @param config - * configuration for the session (model, tools, etc.) + * configuration for the session, including the required + * {@link SessionConfig#setOnPermissionRequest(com.github.copilot.sdk.json.PermissionHandler)} + * handler * @return a future that resolves with the created CopilotSession - * @see #createSession() + * @throws IllegalArgumentException + * if {@code config} is {@code null} or does not have a permission + * handler set * @see SessionConfig + * @see com.github.copilot.sdk.json.PermissionHandler#APPROVE_ALL */ public CompletableFuture createSession(SessionConfig config) { + if (config == null || config.getOnPermissionRequest() == null) { + return CompletableFuture.failedFuture( + new IllegalArgumentException("An onPermissionRequest handler is required when creating a session. " + + "For example, to allow all permissions, use: " + + "new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)")); + } return ensureConnected().thenCompose(connection -> { var request = SessionRequestBuilder.buildCreateRequest(config); @@ -293,32 +319,38 @@ public CompletableFuture createSession(SessionConfig config) { }); } - /** - * 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. + *

+ * A permission handler is required when resuming a session. Use + * {@link com.github.copilot.sdk.json.PermissionHandler#APPROVE_ALL} to approve + * all permission requests, or provide a custom handler to control permissions + * selectively. * * @param sessionId * the ID of the session to resume * @param config - * configuration for the resumed session + * configuration for the resumed session, including the required + * {@link ResumeSessionConfig#setOnPermissionRequest(com.github.copilot.sdk.json.PermissionHandler)} + * handler * @return a future that resolves with the resumed CopilotSession - * @see #resumeSession(String) + * @throws IllegalArgumentException + * if {@code config} is {@code null} or does not have a permission + * handler set * @see #listSessions() * @see #getLastSessionId() + * @see com.github.copilot.sdk.json.PermissionHandler#APPROVE_ALL */ public CompletableFuture resumeSession(String sessionId, ResumeSessionConfig config) { + if (config == null || config.getOnPermissionRequest() == null) { + return CompletableFuture.failedFuture( + new IllegalArgumentException("An onPermissionRequest handler is required when resuming a session. " + + "For example, to allow all permissions, use: " + + "new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)")); + } return ensureConnected().thenCompose(connection -> { var request = SessionRequestBuilder.buildResumeRequest(sessionId, config); @@ -331,18 +363,6 @@ public CompletableFuture resumeSession(String sessionId, ResumeS }); } - /** - * 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. * diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index e7790ea54..8e5ef7cda 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -7,6 +7,7 @@ import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -20,6 +21,8 @@ import java.util.logging.Level; import java.util.logging.Logger; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.copilot.sdk.events.AbstractSessionEvent; @@ -27,6 +30,7 @@ import com.github.copilot.sdk.events.SessionErrorEvent; import com.github.copilot.sdk.events.SessionEventParser; import com.github.copilot.sdk.events.SessionIdleEvent; +import com.github.copilot.sdk.json.AgentInfo; import com.github.copilot.sdk.json.GetMessagesResponse; import com.github.copilot.sdk.json.HookInvocation; import com.github.copilot.sdk.json.MessageOptions; @@ -58,8 +62,10 @@ *

Example Usage

* *
{@code
- * // Create a session
- * var session = client.createSession(new SessionConfig().setModel("gpt-5")).get();
+ * // Create a session with a permission handler (required)
+ * var session = client
+ * 		.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5"))
+ * 		.get();
  *
  * // Register type-safe event handlers
  * session.on(AssistantMessageEvent.class, msg -> {
@@ -802,6 +808,84 @@ public CompletableFuture abort() {
         return rpc.invoke("session.abort", Map.of("sessionId", sessionId), Void.class);
     }
 
+    /**
+     * Lists the custom agents available for selection in this session.
+     *
+     * @return a future that resolves with the list of available agents
+     * @throws IllegalStateException
+     *             if this session has been terminated
+     * @since 1.0.11
+     */
+    public CompletableFuture> listAgents() {
+        ensureNotTerminated();
+        return rpc.invoke("session.agent.list", Map.of("sessionId", sessionId), AgentListResponse.class)
+                .thenApply(response -> response.agents() != null
+                        ? Collections.unmodifiableList(response.agents())
+                        : Collections.emptyList());
+    }
+
+    /**
+     * Gets the currently selected custom agent for this session, or {@code null} if
+     * no custom agent is selected.
+     *
+     * @return a future that resolves with the current agent, or {@code null} if
+     *         using the default agent
+     * @throws IllegalStateException
+     *             if this session has been terminated
+     * @since 1.0.11
+     */
+    public CompletableFuture getCurrentAgent() {
+        ensureNotTerminated();
+        return rpc.invoke("session.agent.getCurrent", Map.of("sessionId", sessionId), AgentGetCurrentResponse.class)
+                .thenApply(AgentGetCurrentResponse::agent);
+    }
+
+    /**
+     * Selects a custom agent for this session.
+     *
+     * @param agentName
+     *            the name/identifier of the agent to select
+     * @return a future that resolves with the selected agent information
+     * @throws IllegalStateException
+     *             if this session has been terminated
+     * @since 1.0.11
+     */
+    public CompletableFuture selectAgent(String agentName) {
+        ensureNotTerminated();
+        return rpc.invoke("session.agent.select", Map.of("sessionId", sessionId, "name", agentName),
+                AgentSelectResponse.class).thenApply(AgentSelectResponse::agent);
+    }
+
+    /**
+     * Deselects the currently selected custom agent, returning to the default
+     * agent.
+     *
+     * @return a future that completes when the agent is deselected
+     * @throws IllegalStateException
+     *             if this session has been terminated
+     * @since 1.0.11
+     */
+    public CompletableFuture deselectAgent() {
+        ensureNotTerminated();
+        return rpc.invoke("session.agent.deselect", Map.of("sessionId", sessionId), Void.class);
+    }
+
+    /**
+     * Compacts the session context to reduce token usage.
+     * 

+ * This triggers an immediate session compaction, summarizing the conversation + * history to free up context window space. + * + * @return a future that completes when compaction finishes + * @throws IllegalStateException + * if this session has been terminated + * @since 1.0.11 + */ + public CompletableFuture compact() { + ensureNotTerminated(); + return rpc.invoke("session.compaction.compact", Map.of("sessionId", sessionId), Void.class); + } + /** * Verifies that this session has not yet been terminated. * @@ -843,4 +927,18 @@ public void close() { hooksHandler.set(null); } + // ===== Internal response types for agent API ===== + + @JsonIgnoreProperties(ignoreUnknown = true) + private record AgentListResponse(@JsonProperty("agents") List agents) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private record AgentGetCurrentResponse(@JsonProperty("agent") AgentInfo agent) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private record AgentSelectResponse(@JsonProperty("agent") AgentInfo agent) { + } + } diff --git a/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java b/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java index 00295ee06..6c9d76e99 100644 --- a/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java @@ -55,9 +55,11 @@ public abstract sealed class AbstractSessionEvent permits SessionModelChangeEvent, SessionModeChangedEvent, SessionPlanChangedEvent, SessionWorkspaceFileChangedEvent, SessionHandoffEvent, SessionTruncationEvent, SessionSnapshotRewindEvent, SessionUsageInfoEvent, SessionCompactionStartEvent, SessionCompactionCompleteEvent, SessionShutdownEvent, SessionContextChangedEvent, + SessionTaskCompleteEvent, // Assistant events AssistantTurnStartEvent, AssistantIntentEvent, AssistantReasoningEvent, AssistantReasoningDeltaEvent, - AssistantMessageEvent, AssistantMessageDeltaEvent, AssistantTurnEndEvent, AssistantUsageEvent, AbortEvent, + AssistantMessageEvent, AssistantMessageDeltaEvent, AssistantStreamingDeltaEvent, AssistantTurnEndEvent, + AssistantUsageEvent, AbortEvent, // Tool events ToolUserRequestedEvent, ToolExecutionStartEvent, ToolExecutionPartialResultEvent, ToolExecutionProgressEvent, ToolExecutionCompleteEvent, @@ -66,8 +68,8 @@ public abstract sealed class AbstractSessionEvent permits // Skill events SkillInvokedEvent, // Other events - SubagentStartedEvent, SubagentCompletedEvent, SubagentFailedEvent, SubagentSelectedEvent, HookStartEvent, - HookEndEvent, SystemMessageEvent { + SubagentStartedEvent, SubagentCompletedEvent, SubagentFailedEvent, SubagentSelectedEvent, + SubagentDeselectedEvent, HookStartEvent, HookEndEvent, SystemMessageEvent { @JsonProperty("id") private UUID id; diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java index 901b05236..f9b166186 100644 --- a/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java @@ -34,7 +34,6 @@ public void setData(AssistantMessageDeltaData data) { @JsonIgnoreProperties(ignoreUnknown = true) public record AssistantMessageDeltaData(@JsonProperty("messageId") String messageId, @JsonProperty("deltaContent") String deltaContent, - @JsonProperty("totalResponseSizeBytes") Double totalResponseSizeBytes, @JsonProperty("parentToolCallId") String parentToolCallId) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java index 22191a5f4..075d3cd1b 100644 --- a/src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java @@ -73,6 +73,7 @@ public void setData(AssistantMessageData data) { public record AssistantMessageData(@JsonProperty("messageId") String messageId, @JsonProperty("content") String content, @JsonProperty("toolRequests") List toolRequests, @JsonProperty("parentToolCallId") String parentToolCallId, + @JsonProperty("interactionId") String interactionId, @JsonProperty("reasoningOpaque") String reasoningOpaque, @JsonProperty("reasoningText") String reasoningText, @JsonProperty("encryptedContent") String encryptedContent) { diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantStreamingDeltaEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantStreamingDeltaEvent.java new file mode 100644 index 000000000..ff5c2f646 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/AssistantStreamingDeltaEvent.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * 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.streaming_delta + * + * @since 1.0.11 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class AssistantStreamingDeltaEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private AssistantStreamingDeltaData data; + + @Override + public String getType() { + return "assistant.streaming_delta"; + } + + public AssistantStreamingDeltaData getData() { + return data; + } + + public void setData(AssistantStreamingDeltaData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record AssistantStreamingDeltaData(@JsonProperty("totalResponseSizeBytes") double totalResponseSizeBytes) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java index ab5e18653..3a1d6e5ef 100644 --- a/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java @@ -32,6 +32,7 @@ public void setData(AssistantTurnStartData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public record AssistantTurnStartData(@JsonProperty("turnId") String turnId) { + public record AssistantTurnStartData(@JsonProperty("turnId") String turnId, + @JsonProperty("interactionId") String interactionId) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java index 6e51064e6..9aa21925b 100644 --- a/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -42,7 +43,8 @@ public record AssistantUsageData(@JsonProperty("model") String model, @JsonProperty("duration") Double duration, @JsonProperty("initiator") String initiator, @JsonProperty("apiCallId") String apiCallId, @JsonProperty("providerCallId") String providerCallId, @JsonProperty("parentToolCallId") String parentToolCallId, - @JsonProperty("quotaSnapshots") Map quotaSnapshots) { + @JsonProperty("quotaSnapshots") Map quotaSnapshots, + @JsonProperty("copilotUsage") CopilotUsage copilotUsage) { /** Returns a defensive copy of the quota snapshots map. */ @Override @@ -50,4 +52,27 @@ public Map quotaSnapshots() { return quotaSnapshots == null ? Collections.emptyMap() : Collections.unmodifiableMap(quotaSnapshots); } } + + /** + * Copilot usage details including token cost information. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public record CopilotUsage(@JsonProperty("tokenDetails") List tokenDetails, + @JsonProperty("totalNanoAiu") double totalNanoAiu) { + + /** Returns a defensive copy of the token details list. */ + @Override + public List tokenDetails() { + return tokenDetails == null ? null : Collections.unmodifiableList(tokenDetails); + } + } + + /** + * Token details for a specific token type. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public record TokenDetails(@JsonProperty("batchSize") double batchSize, + @JsonProperty("costPerBatch") double costPerBatch, @JsonProperty("tokenCount") double tokenCount, + @JsonProperty("tokenType") String tokenType) { + } } diff --git a/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java b/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java index 146e3ee08..cfe8d5711 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java @@ -65,6 +65,7 @@ public class SessionEventParser { TYPE_MAP.put("session.compaction_start", SessionCompactionStartEvent.class); TYPE_MAP.put("session.compaction_complete", SessionCompactionCompleteEvent.class); TYPE_MAP.put("session.context_changed", SessionContextChangedEvent.class); + TYPE_MAP.put("session.task_complete", SessionTaskCompleteEvent.class); TYPE_MAP.put("user.message", UserMessageEvent.class); TYPE_MAP.put("pending_messages.modified", PendingMessagesModifiedEvent.class); TYPE_MAP.put("assistant.turn_start", AssistantTurnStartEvent.class); @@ -73,6 +74,7 @@ public class SessionEventParser { 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.streaming_delta", AssistantStreamingDeltaEvent.class); TYPE_MAP.put("assistant.turn_end", AssistantTurnEndEvent.class); TYPE_MAP.put("assistant.usage", AssistantUsageEvent.class); TYPE_MAP.put("abort", AbortEvent.class); @@ -85,6 +87,7 @@ public class SessionEventParser { TYPE_MAP.put("subagent.completed", SubagentCompletedEvent.class); TYPE_MAP.put("subagent.failed", SubagentFailedEvent.class); TYPE_MAP.put("subagent.selected", SubagentSelectedEvent.class); + TYPE_MAP.put("subagent.deselected", SubagentDeselectedEvent.class); TYPE_MAP.put("hook.start", HookStartEvent.class); TYPE_MAP.put("hook.end", HookEndEvent.class); TYPE_MAP.put("system.message", SystemMessageEvent.class); diff --git a/src/main/java/com/github/copilot/sdk/events/SessionTaskCompleteEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionTaskCompleteEvent.java new file mode 100644 index 000000000..d82b516ab --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/SessionTaskCompleteEvent.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * 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.task_complete + * + * @since 1.0.11 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SessionTaskCompleteEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SessionTaskCompleteData data; + + @Override + public String getType() { + return "session.task_complete"; + } + + public SessionTaskCompleteData getData() { + return data; + } + + public void setData(SessionTaskCompleteData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record SessionTaskCompleteData(@JsonProperty("summary") String summary) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/SkillInvokedEvent.java b/src/main/java/com/github/copilot/sdk/events/SkillInvokedEvent.java index febe01253..4ed8e2327 100644 --- a/src/main/java/com/github/copilot/sdk/events/SkillInvokedEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SkillInvokedEvent.java @@ -40,6 +40,7 @@ public void setData(SkillInvokedData data) { */ @JsonIgnoreProperties(ignoreUnknown = true) public record SkillInvokedData(@JsonProperty("name") String name, @JsonProperty("path") String path, - @JsonProperty("content") String content, @JsonProperty("allowedTools") List allowedTools) { + @JsonProperty("content") String content, @JsonProperty("allowedTools") List allowedTools, + @JsonProperty("pluginName") String pluginName, @JsonProperty("pluginVersion") String pluginVersion) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SubagentDeselectedEvent.java b/src/main/java/com/github/copilot/sdk/events/SubagentDeselectedEvent.java new file mode 100644 index 000000000..f49f46330 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/SubagentDeselectedEvent.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * 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.deselected + * + * @since 1.0.11 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SubagentDeselectedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SubagentDeselectedData data; + + @Override + public String getType() { + return "subagent.deselected"; + } + + public SubagentDeselectedData getData() { + return data; + } + + public void setData(SubagentDeselectedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record SubagentDeselectedData() { + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/ToolExecutionCompleteEvent.java b/src/main/java/com/github/copilot/sdk/events/ToolExecutionCompleteEvent.java index d6cc1662f..f085f43ce 100644 --- a/src/main/java/com/github/copilot/sdk/events/ToolExecutionCompleteEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/ToolExecutionCompleteEvent.java @@ -36,9 +36,10 @@ public void setData(ToolExecutionCompleteData data) { @JsonIgnoreProperties(ignoreUnknown = true) public record ToolExecutionCompleteData(@JsonProperty("toolCallId") String toolCallId, - @JsonProperty("success") boolean success, @JsonProperty("isUserRequested") Boolean isUserRequested, - @JsonProperty("result") Result result, @JsonProperty("error") Error error, - @JsonProperty("toolTelemetry") Map toolTelemetry, + @JsonProperty("success") boolean success, @JsonProperty("model") String model, + @JsonProperty("interactionId") String interactionId, + @JsonProperty("isUserRequested") Boolean isUserRequested, @JsonProperty("result") Result result, + @JsonProperty("error") Error error, @JsonProperty("toolTelemetry") Map toolTelemetry, @JsonProperty("parentToolCallId") String parentToolCallId) { /** Returns a defensive copy of the tool telemetry map. */ diff --git a/src/main/java/com/github/copilot/sdk/json/AgentInfo.java b/src/main/java/com/github/copilot/sdk/json/AgentInfo.java new file mode 100644 index 000000000..1cc10a91d --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/AgentInfo.java @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a custom agent available for selection in a session. + * + * @since 1.0.11 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AgentInfo { + + @JsonProperty("name") + private String name; + + @JsonProperty("displayName") + private String displayName; + + @JsonProperty("description") + private String description; + + /** + * Gets the unique identifier of the agent. + * + * @return the agent name/identifier + */ + public String getName() { + return name; + } + + /** + * Sets the unique identifier of the agent. + * + * @param name + * the agent name/identifier + * @return this instance for chaining + */ + public AgentInfo setName(String name) { + this.name = name; + return this; + } + + /** + * Gets the human-readable display name of the agent. + * + * @return the display name + */ + public String getDisplayName() { + return displayName; + } + + /** + * Sets the human-readable display name of the agent. + * + * @param displayName + * the display name + * @return this instance for chaining + */ + public AgentInfo setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + + /** + * Gets the description of the agent's purpose. + * + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of the agent's purpose. + * + * @param description + * the description + * @return this instance for chaining + */ + public AgentInfo setDescription(String description) { + this.description = description; + return this; + } +} diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index 6881a5e36..e5f405307 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -169,6 +169,7 @@ The SDK supports event types organized by category. All events extend `AbstractS | `SessionCompactionStartEvent` | `session.compaction_start` | Context compaction started (infinite sessions) | | `SessionCompactionCompleteEvent` | `session.compaction_complete` | Context compaction completed | | `SessionContextChangedEvent` | `session.context_changed` | Working directory context changed | +| `SessionTaskCompleteEvent` | `session.task_complete` | Task completed with summary | ### Assistant Events @@ -180,6 +181,7 @@ The SDK supports event types organized by category. All events extend `AbstractS | `AssistantReasoningDeltaEvent` | `assistant.reasoning_delta` | Streaming reasoning chunk | | `AssistantMessageEvent` | `assistant.message` | Complete assistant message | | `AssistantMessageDeltaEvent` | `assistant.message_delta` | Streaming message chunk | +| `AssistantStreamingDeltaEvent` | `assistant.streaming_delta` | Streaming progress with size metrics | | `AssistantTurnEndEvent` | `assistant.turn_end` | Assistant finished processing | | `AssistantUsageEvent` | `assistant.usage` | Token usage for this turn | @@ -206,6 +208,7 @@ The SDK supports event types organized by category. All events extend `AbstractS |-------|-------------|-------------| | `SubagentStartedEvent` | `subagent.started` | Subagent was spawned | | `SubagentSelectedEvent` | `subagent.selected` | Subagent was selected for task | +| `SubagentDeselectedEvent` | `subagent.deselected` | Subagent was deselected | | `SubagentCompletedEvent` | `subagent.completed` | Subagent completed its task | | `SubagentFailedEvent` | `subagent.failed` | Subagent failed | diff --git a/src/test/java/com/github/copilot/sdk/AskUserTest.java b/src/test/java/com/github/copilot/sdk/AskUserTest.java index 5d45e3b2a..a2ad13b18 100644 --- a/src/test/java/com/github/copilot/sdk/AskUserTest.java +++ b/src/test/java/com/github/copilot/sdk/AskUserTest.java @@ -16,6 +16,7 @@ import com.github.copilot.sdk.json.MessageOptions; import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.UserInputRequest; import com.github.copilot.sdk.json.UserInputResponse; @@ -56,19 +57,20 @@ void testShouldInvokeUserInputHandlerWhenModelUsesAskUserTool() throws Exception var userInputRequests = new ArrayList(); final String[] sessionIdHolder = new String[1]; - var config = new SessionConfig().setOnUserInputRequest((request, invocation) -> { - userInputRequests.add(request); - assertEquals(sessionIdHolder[0], invocation.getSessionId()); + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnUserInputRequest((request, invocation) -> { + userInputRequests.add(request); + assertEquals(sessionIdHolder[0], invocation.getSessionId()); - // Return the first choice if available, otherwise a freeform answer - String answer = (request.getChoices() != null && !request.getChoices().isEmpty()) - ? request.getChoices().get(0) - : "freeform answer"; - boolean wasFreeform = request.getChoices() == null || request.getChoices().isEmpty(); + // Return the first choice if available, otherwise a freeform answer + String answer = (request.getChoices() != null && !request.getChoices().isEmpty()) + ? request.getChoices().get(0) + : "freeform answer"; + boolean wasFreeform = request.getChoices() == null || request.getChoices().isEmpty(); - return CompletableFuture - .completedFuture(new UserInputResponse().setAnswer(answer).setWasFreeform(wasFreeform)); - }); + return CompletableFuture + .completedFuture(new UserInputResponse().setAnswer(answer).setWasFreeform(wasFreeform)); + }); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); @@ -98,16 +100,18 @@ void testShouldReceiveChoicesInUserInputRequest() throws Exception { var userInputRequests = new ArrayList(); - var config = new SessionConfig().setOnUserInputRequest((request, invocation) -> { - userInputRequests.add(request); + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnUserInputRequest((request, invocation) -> { + userInputRequests.add(request); - // Pick the first choice - String answer = (request.getChoices() != null && !request.getChoices().isEmpty()) - ? request.getChoices().get(0) - : "default"; + // Pick the first choice + String answer = (request.getChoices() != null && !request.getChoices().isEmpty()) + ? request.getChoices().get(0) + : "default"; - return CompletableFuture.completedFuture(new UserInputResponse().setAnswer(answer).setWasFreeform(false)); - }); + return CompletableFuture + .completedFuture(new UserInputResponse().setAnswer(answer).setWasFreeform(false)); + }); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); @@ -137,13 +141,14 @@ void testShouldHandleFreeformUserInputResponse() throws Exception { final var userInputRequests = new ArrayList(); String freeformAnswer = "This is my custom freeform answer that was not in the choices"; - var config = new SessionConfig().setOnUserInputRequest((request, invocation) -> { - userInputRequests.add(request); + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnUserInputRequest((request, invocation) -> { + userInputRequests.add(request); - // Return a freeform answer (not from choices) - return CompletableFuture - .completedFuture(new UserInputResponse().setAnswer(freeformAnswer).setWasFreeform(true)); - }); + // Return a freeform answer (not from choices) + return CompletableFuture + .completedFuture(new UserInputResponse().setAnswer(freeformAnswer).setWasFreeform(true)); + }); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); diff --git a/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java b/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java index f4606c646..4d86c23d8 100644 --- a/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java +++ b/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java @@ -15,6 +15,7 @@ import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.SessionConfig; /** @@ -51,7 +52,8 @@ void testSendStringThrowsAfterTermination() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -70,7 +72,8 @@ void testSendOptionsThrowsAfterTermination() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -89,7 +92,8 @@ void testSendAndWaitStringThrowsAfterTermination() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -108,7 +112,8 @@ void testSendAndWaitOptionsThrowsAfterTermination() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -127,7 +132,8 @@ void testSendAndWaitWithTimeoutThrowsAfterTermination() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -146,7 +152,8 @@ void testOnConsumerThrowsAfterTermination() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -167,7 +174,8 @@ void testOnTypedConsumerThrowsAfterTermination() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -188,7 +196,8 @@ void testGetMessagesThrowsAfterTermination() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -207,7 +216,8 @@ void testAbortThrowsAfterTermination() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -226,7 +236,8 @@ void testSetEventErrorHandlerThrowsAfterTermination() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -247,7 +258,8 @@ void testSetEventErrorPolicyThrowsAfterTermination() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); session.close(); IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { @@ -266,7 +278,8 @@ void testGetSessionIdWorksAfterTermination() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); String sessionIdBeforeClose = session.getSessionId(); session.close(); @@ -284,7 +297,8 @@ void testGetWorkspacePathWorksAfterTermination() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); String pathBeforeClose = session.getWorkspacePath(); session.close(); @@ -301,7 +315,8 @@ void testCloseIsIdempotent() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); // First close should succeed assertDoesNotThrow(() -> session.close()); @@ -322,7 +337,8 @@ void testTryWithResourcesDoubleClose() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); try (session) { // Manual close within try-with-resources diff --git a/src/test/java/com/github/copilot/sdk/CompactionTest.java b/src/test/java/com/github/copilot/sdk/CompactionTest.java index 33cdc56d1..58573a80e 100644 --- a/src/test/java/com/github/copilot/sdk/CompactionTest.java +++ b/src/test/java/com/github/copilot/sdk/CompactionTest.java @@ -20,6 +20,7 @@ import com.github.copilot.sdk.events.SessionCompactionStartEvent; import com.github.copilot.sdk.json.InfiniteSessionConfig; import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.SessionConfig; /** @@ -66,7 +67,8 @@ void testShouldTriggerCompactionWithLowThresholdAndEmitEvents() throws Exception // Block at 1% to ensure compaction runs .setBufferExhaustionThreshold(0.01); - var config = new SessionConfig().setInfiniteSessions(infiniteConfig); + var config = new SessionConfig().setInfiniteSessions(infiniteConfig) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL); var events = new ArrayList(); @@ -132,7 +134,8 @@ void testShouldNotEmitCompactionEventsWhenInfiniteSessionsDisabled() throws Exce var infiniteConfig = new InfiniteSessionConfig().setEnabled(false); - var config = new SessionConfig().setInfiniteSessions(infiniteConfig); + var config = new SessionConfig().setInfiniteSessions(infiniteConfig) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL); var compactionEvents = new ArrayList(); diff --git a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java index a5060e615..1f4f5193b 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -8,7 +8,9 @@ import org.junit.jupiter.api.Test; import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.PingResponse; +import com.github.copilot.sdk.json.SessionConfig; import com.github.copilot.sdk.json.SessionLifecycleEvent; import com.github.copilot.sdk.json.SessionLifecycleEventTypes; @@ -195,7 +197,7 @@ void testForceStopWithoutCleanup() throws Exception { } try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath))) { - client.createSession().get(); + client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); client.forceStop().get(); assertEquals(ConnectionState.DISCONNECTED, client.getState()); @@ -452,7 +454,8 @@ void testCloseSessionAfterStoppingClientDoesNotThrow() throws Exception { } try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath))) { - var session = client.createSession().get(); + var session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); // Stop the client first (which closes the RPC connection) client.stop().get(); diff --git a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java index 6ac77fdb6..332dfe5b7 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java @@ -31,6 +31,7 @@ import com.github.copilot.sdk.events.UserMessageEvent; import com.github.copilot.sdk.json.MessageOptions; import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ResumeSessionConfig; import com.github.copilot.sdk.json.SessionConfig; import com.github.copilot.sdk.json.SystemMessageConfig; import com.github.copilot.sdk.json.ToolDefinition; @@ -69,7 +70,8 @@ void testShouldReceiveSessionEvents_createAndDestroy() 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(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); assertNotNull(session.getSessionId()); assertTrue(session.getSessionId().matches("^[a-f0-9-]+$")); @@ -105,7 +107,8 @@ void testShouldHaveStatefulConversation() throws Exception { ctx.configureForTest("session", "should_have_stateful_conversation"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); AssistantMessageEvent response1 = session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?"), 60000) .get(90, TimeUnit.SECONDS); @@ -137,7 +140,8 @@ void testShouldReceiveSessionEvents() throws Exception { ctx.configureForTest("session", "should_receive_session_events"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); List receivedEvents = new ArrayList<>(); CompletableFuture idleReceived = new CompletableFuture<>(); @@ -229,7 +233,8 @@ void testSendAndWaitBlocksUntilSessionIdleAndReturnsFinalAssistantMessage() thro ctx.configureForTest("session", "sendandwait_blocks_until_session_idle_and_returns_final_assistant_message"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); var events = new ArrayList(); session.on(evt -> events.add(evt.getType())); @@ -259,7 +264,8 @@ void testShouldResumeSessionUsingTheSameClient() throws Exception { try (CopilotClient client = ctx.createClient()) { // Create initial session - CopilotSession session1 = client.createSession().get(); + CopilotSession session1 = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); String sessionId = session1.getSessionId(); AssistantMessageEvent answer = session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, @@ -269,7 +275,8 @@ void testShouldResumeSessionUsingTheSameClient() throws Exception { "Response should contain 2: " + answer.getData().content()); // Resume using the same client - CopilotSession session2 = client.resumeSession(sessionId).get(); + CopilotSession session2 = client.resumeSession(sessionId, + new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); assertEquals(sessionId, session2.getSessionId()); @@ -296,7 +303,8 @@ void testShouldResumeSessionUsingNewClient() throws Exception { // throughout the test, matching the behavior of other SDK implementations try (CopilotClient client1 = ctx.createClient()) { // Create initial session - CopilotSession session1 = client1.createSession().get(); + CopilotSession session1 = client1 + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); String sessionId = session1.getSessionId(); AssistantMessageEvent answer = session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, @@ -307,7 +315,8 @@ void testShouldResumeSessionUsingNewClient() throws Exception { // Resume using a new client (keeping client1 alive) try (CopilotClient client2 = ctx.createClient()) { - CopilotSession session2 = client2.resumeSession(sessionId).get(); + CopilotSession session2 = client2.resumeSession(sessionId, + new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); assertEquals(sessionId, session2.getSessionId()); @@ -335,8 +344,9 @@ void testShouldCreateSessionWithAppendedSystemMessageConfig() throws Exception { 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)); + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSystemMessage(new SystemMessageConfig().setContent(systemMessageSuffix) + .setMode(SystemMessageMode.APPEND)); CopilotSession session = client.createSession(config).get(); @@ -366,8 +376,9 @@ void testShouldCreateSessionWithReplacedSystemMessageConfig() throws Exception { 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)); + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSystemMessage( + new SystemMessageConfig().setContent(testSystemMessage).setMode(SystemMessageMode.REPLACE)); CopilotSession session = client.createSession(config).get(); @@ -394,7 +405,8 @@ void testShouldReceiveStreamingDeltaEventsWhenStreamingIsEnabled() throws Except ctx.configureForTest("session", "should_receive_streaming_delta_events_when_streaming_is_enabled"); try (CopilotClient client = ctx.createClient()) { - SessionConfig config = new SessionConfig().setStreaming(true); + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setStreaming(true); CopilotSession session = client.createSession(config).get(); @@ -430,7 +442,8 @@ void testShouldAbortSession() throws Exception { ctx.configureForTest("session", "should_abort_a_session"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); assertNotNull(session.getSessionId()); // Set up wait for tool execution to start BEFORE sending @@ -487,7 +500,8 @@ void testShouldCreateSessionWithAvailableTools() 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")); + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setAvailableTools(List.of("view", "edit")); CopilotSession session = client.createSession(config).get(); @@ -511,7 +525,8 @@ void testShouldCreateSessionWithExcludedTools() throws Exception { ctx.configureForTest("session", "should_create_a_session_with_excludedtools"); try (CopilotClient client = ctx.createClient()) { - SessionConfig config = new SessionConfig().setExcludedTools(List.of("view")); + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setExcludedTools(List.of("view")); CopilotSession session = client.createSession(config).get(); @@ -538,7 +553,9 @@ void testShouldThrowErrorWhenResumingNonExistentSession() throws Exception { try (CopilotClient client = ctx.createClient()) { try { - client.resumeSession("non-existent-session-id").get(30, TimeUnit.SECONDS); + client.resumeSession("non-existent-session-id", + new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); fail("Expected exception when resuming non-existent session"); } catch (Exception e) { // Should throw an error @@ -559,7 +576,8 @@ void testShouldCreateSessionWithCustomConfigDir() throws Exception { try (CopilotClient client = ctx.createClient()) { String customConfigDir = ctx.getWorkDir().resolve("custom-config").toString(); - SessionConfig config = new SessionConfig().setConfigDir(customConfigDir); + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setConfigDir(customConfigDir); CopilotSession session = client.createSession(config).get(); assertNotNull(session.getSessionId()); @@ -591,7 +609,8 @@ void testSendAndWaitThrowsOnTimeout() throws Exception { ctx.configureForTest("session", "sendandwait_throws_on_timeout"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); // Use a short timeout that will trigger before any response try { @@ -626,10 +645,12 @@ void testShouldListSessions() throws Exception { try (CopilotClient client = ctx.createClient()) { // Create two sessions and send one message to each (matches snapshot format) - CopilotSession session1 = client.createSession().get(); + CopilotSession session1 = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session1.sendAndWait(new MessageOptions().setPrompt("Say hello")).get(60, TimeUnit.SECONDS); - CopilotSession session2 = client.createSession().get(); + CopilotSession session2 = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session2.sendAndWait(new MessageOptions().setPrompt("Say goodbye")).get(60, TimeUnit.SECONDS); // Small delay to ensure session files are written to disk @@ -663,7 +684,8 @@ void testShouldDeleteSession() throws Exception { try (CopilotClient client = ctx.createClient()) { // Create a session - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); String sessionId = session.getSessionId(); session.sendAndWait(new MessageOptions().setPrompt("Hello")).get(60, TimeUnit.SECONDS); @@ -683,7 +705,9 @@ void testShouldDeleteSession() throws Exception { // Trying to resume the deleted session should fail try { - client.resumeSession(sessionId).get(30, TimeUnit.SECONDS); + client.resumeSession(sessionId, + new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); fail("Expected exception when resuming deleted session"); } catch (Exception e) { // Should throw an error indicating session not found @@ -723,7 +747,9 @@ void testShouldCreateSessionWithCustomTool() throws Exception { try (CopilotClient client = ctx.createClient()) { CopilotSession session = client - .createSession(new SessionConfig().setTools(java.util.List.of(getSecretNumberTool))).get(); + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setTools(java.util.List.of(getSecretNumberTool))) + .get(); AssistantMessageEvent response = session .sendAndWait(new MessageOptions().setPrompt("What is the secret number for key ALPHA?")) @@ -748,7 +774,8 @@ void testShouldPassStreamingOptionToSessionCreation() throws Exception { try (CopilotClient client = ctx.createClient()) { // Verify that the streaming option is accepted without errors - CopilotSession session = client.createSession(new SessionConfig().setStreaming(true)).get(); + CopilotSession session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(true)).get(); assertNotNull(session.getSessionId()); assertTrue(session.getSessionId().matches("^[a-f0-9-]+$")); @@ -776,7 +803,8 @@ void testListSessionsIncludesContextWhenAvailable() throws Exception { ctx.configureForTest("session", "should_list_sessions"); try (CopilotClient client = ctx.createClient()) { - var session = client.createSession().get(); + var session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); var sessions = client.listSessions().get(30, TimeUnit.SECONDS); assertNotNull(sessions); @@ -804,7 +832,8 @@ void testSessionListFilterFluentAPI() throws Exception { ctx.configureForTest("session", "should_list_sessions"); try (CopilotClient client = ctx.createClient()) { - var session = client.createSession().get(); + var session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); var filter = new com.github.copilot.sdk.json.SessionListFilter().setCwd("/test/path") .setRepository("owner/repo").setBranch("main").setGitRoot("/test"); diff --git a/src/test/java/com/github/copilot/sdk/E2ETestContext.java b/src/test/java/com/github/copilot/sdk/E2ETestContext.java index 6bb351901..20dfce7c7 100644 --- a/src/test/java/com/github/copilot/sdk/E2ETestContext.java +++ b/src/test/java/com/github/copilot/sdk/E2ETestContext.java @@ -44,7 +44,8 @@ * ctx.configureForTest("tools", "my_test_name"); * * try (CopilotClient client = ctx.createClient()) { - * CopilotSession session = client.createSession().get(); + * CopilotSession session = client + * .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); * // ... run test ... * } * } diff --git a/src/test/java/com/github/copilot/sdk/ErrorHandlingTest.java b/src/test/java/com/github/copilot/sdk/ErrorHandlingTest.java index e111d8854..8adc8a173 100644 --- a/src/test/java/com/github/copilot/sdk/ErrorHandlingTest.java +++ b/src/test/java/com/github/copilot/sdk/ErrorHandlingTest.java @@ -20,6 +20,7 @@ import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.events.SessionErrorEvent; import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.SessionConfig; import com.github.copilot.sdk.json.ToolDefinition; @@ -69,7 +70,8 @@ void testHandlesToolCallingErrors_toolErrorDoesNotCrashSession() throws Exceptio }); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(errorTool))).get(); + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(errorTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.on(event -> allEvents.add(event)); @@ -107,7 +109,8 @@ void testHandlesToolCallingErrors_toolReturnsFailureResult() throws Exception { }); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(failTool))).get(); + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(failTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); AssistantMessageEvent response = session .sendAndWait(new MessageOptions() @@ -216,7 +219,8 @@ void testHandlesToolCallingErrors_sessionContinuesAfterToolError() throws Except }); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(errorTool))).get(); + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(errorTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); // First request that will cause tool error AssistantMessageEvent response = session diff --git a/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java b/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java index faa3deafa..84aa3b341 100644 --- a/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java +++ b/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java @@ -18,6 +18,7 @@ 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.PermissionHandler; import com.github.copilot.sdk.json.ResumeSessionConfig; import com.github.copilot.sdk.json.SessionConfig; @@ -71,7 +72,9 @@ void testShouldAcceptMcpServerConfigurationOnSessionCreate() throws Exception { mcpServers.put("test-server", createLocalMcpServer("echo", List.of("hello"))); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig().setMcpServers(mcpServers)).get(); + CopilotSession session = client.createSession( + new SessionConfig().setMcpServers(mcpServers).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); assertNotNull(session.getSessionId()); @@ -99,7 +102,8 @@ void testShouldAcceptMcpServerConfigurationOnSessionResume() throws Exception { try (CopilotClient client = ctx.createClient()) { // Create a session first - CopilotSession session1 = client.createSession().get(); + CopilotSession session1 = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); String sessionId = session1.getSessionId(); session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); @@ -107,8 +111,8 @@ void testShouldAcceptMcpServerConfigurationOnSessionResume() throws Exception { var mcpServers = new HashMap(); mcpServers.put("test-server", createLocalMcpServer("echo", List.of("hello"))); - CopilotSession session2 = client - .resumeSession(sessionId, new ResumeSessionConfig().setMcpServers(mcpServers)).get(); + CopilotSession session2 = client.resumeSession(sessionId, new ResumeSessionConfig() + .setMcpServers(mcpServers).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); assertEquals(sessionId, session2.getSessionId()); @@ -140,7 +144,9 @@ void testShouldHandleMultipleMcpServers() throws Exception { mcpServers.put("server2", createLocalMcpServer("echo", List.of("server2"))); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig().setMcpServers(mcpServers)).get(); + CopilotSession session = client.createSession( + new SessionConfig().setMcpServers(mcpServers).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); assertNotNull(session.getSessionId()); session.close(); @@ -164,7 +170,8 @@ void testShouldAcceptCustomAgentConfigurationOnSessionCreate() throws Exception .setPrompt("You are a helpful test agent.").setInfer(true)); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig().setCustomAgents(customAgents)).get(); + CopilotSession session = client.createSession(new SessionConfig().setCustomAgents(customAgents) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); assertNotNull(session.getSessionId()); @@ -192,7 +199,8 @@ void testShouldAcceptCustomAgentConfigurationOnSessionResume() throws Exception try (CopilotClient client = ctx.createClient()) { // Create a session first - CopilotSession session1 = client.createSession().get(); + CopilotSession session1 = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); String sessionId = session1.getSessionId(); session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); @@ -201,8 +209,8 @@ void testShouldAcceptCustomAgentConfigurationOnSessionResume() throws Exception .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(); + CopilotSession session2 = client.resumeSession(sessionId, new ResumeSessionConfig() + .setCustomAgents(customAgents).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); assertEquals(sessionId, session2.getSessionId()); @@ -234,7 +242,8 @@ void testShouldAcceptCustomAgentWithToolsConfiguration() throws Exception { .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(); + CopilotSession session = client.createSession(new SessionConfig().setCustomAgents(customAgents) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); assertNotNull(session.getSessionId()); session.close(); @@ -260,7 +269,8 @@ void testShouldAcceptCustomAgentWithMcpServers() throws Exception { .setPrompt("You are an agent with MCP servers.").setMcpServers(agentMcpServers)); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig().setCustomAgents(customAgents)).get(); + CopilotSession session = client.createSession(new SessionConfig().setCustomAgents(customAgents) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); assertNotNull(session.getSessionId()); session.close(); @@ -285,7 +295,8 @@ void testShouldAcceptMultipleCustomAgents() throws Exception { .setPrompt("You are agent two.").setInfer(false)); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig().setCustomAgents(customAgents)).get(); + CopilotSession session = client.createSession(new SessionConfig().setCustomAgents(customAgents) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); assertNotNull(session.getSessionId()); session.close(); @@ -312,8 +323,8 @@ void testShouldAcceptBothMcpServersAndCustomAgents() throws Exception { .setPrompt("You are a combined test agent.")); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client - .createSession(new SessionConfig().setMcpServers(mcpServers).setCustomAgents(customAgents)).get(); + CopilotSession session = client.createSession(new SessionConfig().setMcpServers(mcpServers) + .setCustomAgents(customAgents).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); assertNotNull(session.getSessionId()); diff --git a/src/test/java/com/github/copilot/sdk/PermissionsTest.java b/src/test/java/com/github/copilot/sdk/PermissionsTest.java index b2a1cfe66..af9347e33 100644 --- a/src/test/java/com/github/copilot/sdk/PermissionsTest.java +++ b/src/test/java/com/github/copilot/sdk/PermissionsTest.java @@ -127,17 +127,17 @@ void testDenyPermission(TestInfo testInfo) throws Exception { } /** - * Verifies that sessions work without a permission handler. + * Verifies that sessions work with the approve-all permission handler. * - * @see Snapshot: permissions/without_permission_handler + * @see Snapshot: permissions/should_work_with_approve_all_permission_handler */ @Test - void testWithoutPermissionHandler(TestInfo testInfo) throws Exception { - ctx.configureForTest("permissions", "without_permission_handler"); + void testShouldWorkWithApproveAllPermissionHandler(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "should_work_with_approve_all_permission_handler"); try (CopilotClient client = ctx.createClient()) { - // Create session without onPermissionRequest handler - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60, TimeUnit.SECONDS); @@ -200,8 +200,9 @@ void testResumeSessionWithPermissionHandler(TestInfo testInfo) throws Exception var permissionRequests = new ArrayList(); try (CopilotClient client = ctx.createClient()) { - // Create session without permission handler - CopilotSession session1 = client.createSession().get(); + // Create session with approve-all handler for initial exchange + CopilotSession session1 = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); String sessionId = session1.getSessionId(); session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); @@ -290,18 +291,22 @@ void testShouldHandlePermissionHandlerErrorsGracefully(TestInfo testInfo) throws } /** - * Verifies that tool operations are denied by default when no handler is - * provided. + * Verifies that tool operations are denied when the handler explicitly denies. * * @see Snapshot: - * permissions/should_deny_tool_operations_by_default_when_no_handler_is_provided + * permissions/should_deny_tool_operations_when_handler_explicitly_denies */ @Test - void testShouldDenyToolOperationsByDefaultWhenNoHandlerIsProvided(TestInfo testInfo) throws Exception { - ctx.configureForTest("permissions", "should_deny_tool_operations_by_default_when_no_handler_is_provided"); + void testShouldDenyToolOperationsWhenHandlerExplicitlyDenies(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "should_deny_tool_operations_when_handler_explicitly_denies"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession( + new SessionConfig().setOnPermissionRequest((request, + invocation) -> CompletableFuture.completedFuture(new PermissionRequestResult() + .setKind("denied-no-approval-rule-and-could-not-request-from-user")))) + .get(); final boolean[] permissionDenied = {false}; session.on(ToolExecutionCompleteEvent.class, evt -> { @@ -320,16 +325,15 @@ void testShouldDenyToolOperationsByDefaultWhenNoHandlerIsProvided(TestInfo testI } /** - * Verifies that tool operations are denied by default when no handler is - * provided after resuming a session. + * Verifies that tool operations are denied when the handler explicitly denies + * after resuming a session. * * @see Snapshot: - * permissions/should_deny_tool_operations_by_default_when_no_handler_is_provided_after_resume + * permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume */ @Test - void testShouldDenyToolOperationsByDefaultWhenNoHandlerIsProvidedAfterResume(TestInfo testInfo) throws Exception { - ctx.configureForTest("permissions", - "should_deny_tool_operations_by_default_when_no_handler_is_provided_after_resume"); + void testShouldDenyToolOperationsWhenHandlerExplicitlyDeniesAfterResume(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "should_deny_tool_operations_when_handler_explicitly_denies_after_resume"); try (CopilotClient client = ctx.createClient()) { var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL); @@ -337,7 +341,12 @@ void testShouldDenyToolOperationsByDefaultWhenNoHandlerIsProvidedAfterResume(Tes String sessionId = session1.getSessionId(); session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); - CopilotSession session2 = client.resumeSession(sessionId).get(); + CopilotSession session2 = client + .resumeSession(sessionId, + new ResumeSessionConfig().setOnPermissionRequest((request, + invocation) -> CompletableFuture.completedFuture(new PermissionRequestResult() + .setKind("denied-no-approval-rule-and-could-not-request-from-user")))) + .get(); final boolean[] permissionDenied = {false}; session2.on(ToolExecutionCompleteEvent.class, evt -> { diff --git a/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java b/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java index 6a6737754..2e9da4fd2 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java @@ -862,7 +862,7 @@ private SessionStartEvent createSessionStartEvent(String sessionId) { private AssistantMessageEvent createAssistantMessageEvent(String content) { var event = new AssistantMessageEvent(); - var data = new AssistantMessageEvent.AssistantMessageData(null, content, null, null, null, null, null); + var data = new AssistantMessageEvent.AssistantMessageData(null, content, null, null, null, null, null, null); event.setData(data); return event; } diff --git a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java index 2186929fb..49f8bb9cb 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java @@ -1377,7 +1377,6 @@ void testAssistantMessageDeltaEventAllFields() throws Exception { "data": { "messageId": "msg-delta-1", "deltaContent": "partial text", - "totalResponseSizeBytes": 4096.0, "parentToolCallId": "ptc-1" } } @@ -1388,7 +1387,6 @@ void testAssistantMessageDeltaEventAllFields() throws Exception { var data = event.getData(); assertEquals("msg-delta-1", data.messageId()); assertEquals("partial text", data.deltaContent()); - assertEquals(4096.0, data.totalResponseSizeBytes()); assertEquals("ptc-1", data.parentToolCallId()); } diff --git a/src/test/java/com/github/copilot/sdk/SessionEventsE2ETest.java b/src/test/java/com/github/copilot/sdk/SessionEventsE2ETest.java index 758d57c20..dad3f5907 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventsE2ETest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventsE2ETest.java @@ -65,7 +65,8 @@ void testShouldReceiveSessionEvents_assistantTurnEvents() throws Exception { var allEvents = new ArrayList(); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.on(event -> allEvents.add(event)); @@ -106,7 +107,8 @@ void testShouldReceiveSessionEvents_userMessageEvent() throws Exception { var userMessages = new ArrayList(); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.on(UserMessageEvent.class, userMessages::add); @@ -171,7 +173,8 @@ void testShouldReceiveSessionEvents_assistantUsageEvent() throws Exception { var usageEvents = new ArrayList(); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.on(AssistantUsageEvent.class, usageEvents::add); @@ -197,7 +200,8 @@ void testShouldReceiveSessionEvents_sessionIdleAfterMessage() throws Exception { var allEvents = new ArrayList(); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession().get(); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.on(event -> allEvents.add(event)); diff --git a/src/test/java/com/github/copilot/sdk/SkillsTest.java b/src/test/java/com/github/copilot/sdk/SkillsTest.java index 9367145d7..e2900a927 100644 --- a/src/test/java/com/github/copilot/sdk/SkillsTest.java +++ b/src/test/java/com/github/copilot/sdk/SkillsTest.java @@ -19,6 +19,7 @@ import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.SessionConfig; /** @@ -105,7 +106,8 @@ void testShouldLoadAndApplySkillFromSkillDirectories() throws Exception { Path skillsDir = createSkillDir(); - SessionConfig config = new SessionConfig().setSkillDirectories(List.of(skillsDir.toString())); + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSkillDirectories(List.of(skillsDir.toString())); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); @@ -136,8 +138,8 @@ void testShouldNotApplySkillWhenDisabledViaDisabledSkills() throws Exception { Path skillsDir = createSkillDir(); - SessionConfig config = new SessionConfig().setSkillDirectories(List.of(skillsDir.toString())) - .setDisabledSkills(List.of("test-skill")); + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSkillDirectories(List.of(skillsDir.toString())).setDisabledSkills(List.of("test-skill")); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); diff --git a/src/test/java/com/github/copilot/sdk/ToolsTest.java b/src/test/java/com/github/copilot/sdk/ToolsTest.java index a1c66dc49..d64e245b5 100644 --- a/src/test/java/com/github/copilot/sdk/ToolsTest.java +++ b/src/test/java/com/github/copilot/sdk/ToolsTest.java @@ -8,6 +8,7 @@ import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -22,6 +23,8 @@ import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; import com.github.copilot.sdk.json.PermissionHandler; +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.ToolDefinition; @@ -107,7 +110,8 @@ void testInvokesCustomTool(TestInfo testInfo) throws Exception { }); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(encryptTool))).get(); + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(encryptTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); AssistantMessageEvent response = session .sendAndWait(new MessageOptions().setPrompt("Use encrypt_string to encrypt this string: Hello")) @@ -143,7 +147,8 @@ void testHandlesToolCallingErrors(TestInfo testInfo) throws Exception { }); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(errorTool))).get(); + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(errorTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); AssistantMessageEvent response = session .sendAndWait(new MessageOptions() @@ -204,7 +209,8 @@ void testCanReceiveAndReturnComplexTypes(TestInfo testInfo) throws Exception { }); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(dbQueryTool))).get(); + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(dbQueryTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); AssistantMessageEvent response = session .sendAndWait(new MessageOptions().setPrompt( @@ -220,4 +226,98 @@ void testCanReceiveAndReturnComplexTypes(TestInfo testInfo) throws Exception { session.close(); } } + + /** + * Verifies that a custom tool is invoked with the permission handler being + * called and can inspect the tool name. + * + * @see Snapshot: tools/invokes_custom_tool_with_permission_handler + */ + @Test + void testInvokesCustomToolWithPermissionHandler(TestInfo testInfo) throws Exception { + ctx.configureForTest("tools", "invokes_custom_tool_with_permission_handler"); + + var permissionRequests = new ArrayList(); + + var parameters = new HashMap(); + parameters.put("type", "object"); + var props = new HashMap(); + props.put("input", Map.of("type", "string")); + parameters.put("properties", props); + parameters.put("required", List.of("input")); + + ToolDefinition encryptTool = ToolDefinition.create("encrypt_string", "Encrypts a string", parameters, + (invocation) -> { + Map args = invocation.getArguments(); + String input = (String) args.get("input"); + return CompletableFuture.completedFuture(input.toUpperCase()); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession( + new SessionConfig().setTools(List.of(encryptTool)).setOnPermissionRequest((request, invocation) -> { + permissionRequests.add(request); + return CompletableFuture.completedFuture(new PermissionRequestResult().setKind("approved")); + })).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().content().contains("HELLO"), + "Response should contain HELLO: " + response.getData().content()); + + // Should have received a custom-tool permission request + boolean hasCustomToolRequest = permissionRequests.stream() + .anyMatch(req -> "custom-tool".equals(req.getKind())); + assertTrue(hasCustomToolRequest, "Should have received a custom-tool permission request"); + + session.close(); + } + } + + /** + * Verifies that a custom tool is denied when the permission handler denies it. + * + * @see Snapshot: tools/denies_custom_tool_when_permission_denied + */ + @Test + void testDeniesCustomToolWhenPermissionDenied(TestInfo testInfo) throws Exception { + ctx.configureForTest("tools", "denies_custom_tool_when_permission_denied"); + + final boolean[] toolHandlerCalled = {false}; + + var parameters = new HashMap(); + parameters.put("type", "object"); + var props = new HashMap(); + props.put("input", Map.of("type", "string")); + parameters.put("properties", props); + parameters.put("required", List.of("input")); + + ToolDefinition encryptTool = ToolDefinition.create("encrypt_string", "Encrypts a string", parameters, + (invocation) -> { + toolHandlerCalled[0] = true; + Map args = invocation.getArguments(); + String input = (String) args.get("input"); + return CompletableFuture.completedFuture(input.toUpperCase()); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession( + new SessionConfig().setTools(List.of(encryptTool)) + .setOnPermissionRequest((request, invocation) -> CompletableFuture.completedFuture( + new PermissionRequestResult().setKind("denied-interactively-by-user")))) + .get(); + + session.sendAndWait(new MessageOptions().setPrompt("Use encrypt_string to encrypt this string: Hello")) + .get(60, TimeUnit.SECONDS); + + // The tool handler should NOT have been called since permission was denied + assertFalse(toolHandlerCalled[0], "Tool handler should not be called when permission is denied"); + + session.close(); + } + } } From 72459703f99b29af4c5c75832c3ef6096b28ee58 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 2 Mar 2026 19:26:40 -0500 Subject: [PATCH 045/104] On branch edburns/dd-2785795-t-shirt-size-for-gh-667 compliance with open source process. Resolves issues sized at: | Size | Meaning | |---|---| | **XS** | Trivial / copy-paste template, < 15 min | | **S** | Straightforward, < 1 hour | new file: .github/CODEOWNERS new file: CODE_OF_CONDUCT.md new file: CONTRIBUTING.md modified: README.md new file: SECURITY.md new file: SUPPORT.md --- .github/CODEOWNERS | 1 + CODE_OF_CONDUCT.md | 74 ++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 67 +++++++++++++++++++++++++++++++++++++++++ README.md | 14 ++++++++- SECURITY.md | 31 +++++++++++++++++++ SUPPORT.md | 13 ++++++++ 6 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 .github/CODEOWNERS create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md create mode 100644 SUPPORT.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..0c213e61c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @github/copilot-sdk-java diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..c75895245 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or + advance +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at . All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..fe7c9ec09 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,67 @@ +## Contributing + +[fork]: https://github.com/github/copilot-sdk-java/fork +[pr]: https://github.com/github/copilot-sdk-java/compare + +Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. + +This repository contains the **Copilot SDK for Java**, the official Java variant of the official [GitHub Copilot SDK](https://github.com/github/copilot-sdk). For issues or features related to the upstream SDK, please contribute there instead. + +Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE). + +Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. + +## What kinds of contributions we're looking for + +We'd love your help with: + + * Fixing any bugs in the existing feature set + * Making the SDK more idiomatic and nice to use for Java developers + * Improving documentation + +If you have ideas for entirely new features, please post an issue or start a discussion. We're very open to new features but need to make sure they align with the direction of the upstream [Copilot SDK](https://github.com/github/copilot-sdk) and can be maintained in sync. Note that this repo periodically merges upstream changes — see the [README](README.md#agentic-upstream-merge-and-sync) for details on how that works. + +## Prerequisites for running and testing code + +1. Install [Java 17+](https://openjdk.org/) (JDK) +1. Install [Maven 3.9+](https://maven.apache.org/download.cgi) (or use the included `mvnw` wrapper) +1. Install [Node.js](https://nodejs.org/) (v18+) — required for the E2E test harness + +## Submitting a pull request + +1. [Fork][fork] and clone the repository +1. Enable git hooks: `git config core.hooksPath .githooks` +1. Make sure the tests pass on your machine: `mvn clean verify` +1. Make sure formatting passes: `mvn spotless:check` +1. Create a new branch: `git checkout -b my-branch-name` +1. Make your change, add tests, and make sure the tests and linter still pass +1. Push to your fork and [submit a pull request][pr] +1. Pat yourself on the back and wait for your pull request to be reviewed and merged. + +### Running tests and linters + +```bash +# Build and run all tests +mvn clean verify + +# Run a single test class +mvn test -Dtest=CopilotClientTest + +# Format code (required before commit) +mvn spotless:apply + +# Check formatting only +mvn spotless:check +``` + +Here are a few things you can do that will increase the likelihood of your pull request being accepted: + +- Write tests. +- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. +- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). + +## Resources + +- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) +- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) +- [GitHub Help](https://help.github.com) diff --git a/README.md b/README.md index ae93d99b8..c71fab60e 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ This SDK tracks the official [Copilot SDK](https://github.com/github/copilot-sdk ```bash # Clone the repository -git clone https://github.com/copilot-community-sdk/copilot-sdk-java.git +git clone https://github.com/github/copilot-sdk-java.git cd copilot-sdk-java # Enable git hooks for code formatting @@ -151,6 +151,18 @@ mvn clean verify The tests require the official [copilot-sdk](https://github.com/github/copilot-sdk) test harness, which is automatically cloned during build. +## Support + +See [SUPPORT.md](SUPPORT.md) for how to file issues and get help. + +## Code of Conduct + +This project has adopted the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details. + +## Security + +See [SECURITY.md](SECURITY.md) for reporting security vulnerabilities. + ## License MIT — see [LICENSE](LICENSE) for details. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..ef183e6fc --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,31 @@ +Thanks for helping make GitHub safe for everyone. + +# Security + +GitHub takes the security of our software products and services seriously, including all of the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). + +Even though [open source repositories are outside of the scope of our bug bounty program](https://bounty.github.com/index.html#scope) and therefore not eligible for bounty rewards, we will ensure that your finding gets passed along to the appropriate maintainers for remediation. + +## Reporting Security Issues + +If you believe you have found a security vulnerability in any GitHub-owned repository, please report it to us through coordinated disclosure. + +**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** + +Instead, please send an email to opensource-security[@]github.com. + +Please include as much of the information listed below as you can to help us better understand and resolve the issue: + +- The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) +- Full paths of source file(s) related to the manifestation of the issue +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +## Policy + +See [GitHub's Safe Harbor Policy](https://docs.github.com/en/site-policy/security-policies/github-bug-bounty-program-legal-safe-harbor#1-safe-harbor-terms) diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 000000000..980b3dce8 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,13 @@ +# Support + +## How to file issues and get help + +This project uses GitHub issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new issue. + +For help or questions about using this project, please file an issue. + +**Copilot SDK for Java** is under active development and maintained by the community. We will do our best to respond to support, feature requests, and community questions in a timely manner. + +## GitHub Support Policy + +Support for this project is limited to the resources listed above. From 47fb9bfe25af778bc101969491720c6f3001a9f9 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 2 Mar 2026 19:33:15 -0500 Subject: [PATCH 046/104] On branch edburns/dd-2785795-t-shirt-size-for-gh-667 Your branch is up to date with 'public-fork/edburns/dd-2785795-t-shirt-size-for-gh-667'. Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git restore ..." to discard changes in working directory) modified: .github/CODEOWNERS https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2785871 --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0c213e61c..10ed51d11 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @github/copilot-sdk-java +* @copilot-community-sdk/copilot-sdk-java From c729e0789978263e0dd8dceb613d578e88c7d7e5 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 2 Mar 2026 19:34:18 -0500 Subject: [PATCH 047/104] On branch edburns/dd-2785795-t-shirt-size-for-gh-667 Your branch is up to date with 'public-fork/edburns/dd-2785795-t-shirt-size-for-gh-667'. Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git restore ..." to discard changes in working directory) modified: .github/CODEOWNERS https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2785871 --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 10ed51d11..6fc1e2fe4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @copilot-community-sdk/copilot-sdk-java +* edburns From 16a2bdf37e09ef38cd9a937166c5f9044c2aa35f Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 2 Mar 2026 19:35:31 -0500 Subject: [PATCH 048/104] On branch edburns/dd-2785795-t-shirt-size-for-gh-667 Your branch is up to date with 'public-fork/edburns/dd-2785795-t-shirt-size-for-gh-667'. Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git restore ..." to discard changes in working directory) modified: .github/CODEOWNERS https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2785871 --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6fc1e2fe4..6912dbc77 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* edburns +* edburns@github.com From 07b20bb25971a36f6ed94410ed6879fc6438e785 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 2 Mar 2026 19:37:09 -0500 Subject: [PATCH 049/104] On branch edburns/dd-2785795-t-shirt-size-for-gh-667 modified: README.md https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2785864 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c71fab60e..27b271e86 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ This SDK tracks the official [Copilot SDK](https://github.com/github/copilot-sdk ```bash # Clone the repository -git clone https://github.com/github/copilot-sdk-java.git +git clone https://github.com/copilot-community-sdk/copilot-sdk-java.git cd copilot-sdk-java # Enable git hooks for code formatting From 0ba74455b48e85eb28c53b7ba787b4e0382e07ea Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 2 Mar 2026 19:58:12 -0500 Subject: [PATCH 050/104] On branch edburns/update-license Changes to be committed: (use "git restore --staged ..." to unstage) modified: LICENSE Use LICENSE from reference implementation --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 4fa9fd93f..28a50fa22 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Bruno Borges and the Copilot Community SDK contributors +Copyright GitHub, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From ab0897147429d8252e5653428489d1e1d072d784 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 2 Mar 2026 20:05:35 -0500 Subject: [PATCH 051/104] On branch main Comply with "Update README.md to include sections seen in README.md" Your branch is up to date with 'public-upstream/main'. Changes to be committed: (use "git restore --staged ..." to unstage) modified: README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 27b271e86..30cf07021 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ [![Documentation](https://img.shields.io/badge/docs-latest-brightgreen)](https://copilot-community-sdk.github.io/copilot-sdk-java/latest/) [![Javadoc](https://javadoc.io/badge2/io.github.copilot-community-sdk/copilot-sdk/javadoc.svg?q=1)](https://javadoc.io/doc/io.github.copilot-community-sdk/copilot-sdk/latest/index.html) -## Overview +## Background > ⚠️ **Disclaimer:** This is an **unofficial, community-driven SDK** and is **not supported or endorsed by GitHub**. This SDK may change in breaking ways. Use at your own risk. @@ -167,6 +167,10 @@ See [SECURITY.md](SECURITY.md) for reporting security vulnerabilities. MIT — see [LICENSE](LICENSE) for details. +## Acknowledgement + +- Initially developed with Copilot and [Bruno Borges](https://www.linkedin.com/in/brunocborges/). + ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=copilot-community-sdk/copilot-sdk-java&type=Date)](https://www.star-history.com/#copilot-community-sdk/copilot-sdk-java&Date) From 20dbaaf9ba3c1345f9acf771275dc01b9e64e0ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:19:41 +0000 Subject: [PATCH 052/104] Initial plan From 68804823dc1c0c9f61d3639b0cfb01477df049eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:25:24 +0000 Subject: [PATCH 053/104] Fix CompactionTest prompt mismatch causing sendAndWait timeout Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- src/test/java/com/github/copilot/sdk/CompactionTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/github/copilot/sdk/CompactionTest.java b/src/test/java/com/github/copilot/sdk/CompactionTest.java index 58573a80e..872070c45 100644 --- a/src/test/java/com/github/copilot/sdk/CompactionTest.java +++ b/src/test/java/com/github/copilot/sdk/CompactionTest.java @@ -80,7 +80,7 @@ void testShouldTriggerCompactionWithLowThresholdAndEmitEvents() throws Exception // Send multiple messages to fill up the context window // With such low thresholds, even a few messages should trigger compaction session.sendAndWait( - new MessageOptions().setPrompt("Tell me a long story about a dragon. Be very detailed.")) + new MessageOptions().setPrompt("Tell me a story about a dragon. Be detailed.")) .get(60, TimeUnit.SECONDS); session.sendAndWait( new MessageOptions().setPrompt("Continue the story with more details about the dragon's castle.")) From b01089b088ab9bf81f25cfdf6930f543803427c1 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 3 Mar 2026 10:30:46 -0500 Subject: [PATCH 054/104] Apply formatting fix in CompactionTest Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/test/java/com/github/copilot/sdk/CompactionTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/github/copilot/sdk/CompactionTest.java b/src/test/java/com/github/copilot/sdk/CompactionTest.java index 872070c45..a3a665e95 100644 --- a/src/test/java/com/github/copilot/sdk/CompactionTest.java +++ b/src/test/java/com/github/copilot/sdk/CompactionTest.java @@ -79,9 +79,8 @@ void testShouldTriggerCompactionWithLowThresholdAndEmitEvents() throws Exception // Send multiple messages to fill up the context window // With such low thresholds, even a few messages should trigger compaction - session.sendAndWait( - new MessageOptions().setPrompt("Tell me a story about a dragon. Be detailed.")) - .get(60, TimeUnit.SECONDS); + session.sendAndWait(new MessageOptions().setPrompt("Tell me a story about a dragon. Be detailed.")).get(60, + TimeUnit.SECONDS); session.sendAndWait( new MessageOptions().setPrompt("Continue the story with more details about the dragon's castle.")) .get(60, TimeUnit.SECONDS); From af0e78c59548b1fa0b934058bbc2bb1719fa55bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:14:43 +0000 Subject: [PATCH 055/104] Address review feedback: add parser tests for new fields, fix Javadoc @see refs Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../com/github/copilot/sdk/CopilotClient.java | 6 +- .../copilot/sdk/SessionEventParserTest.java | 85 +++++++++++++++++++ .../com/github/copilot/sdk/ToolsTest.java | 4 +- 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index 74d961f4d..5c4fcfe21 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -460,7 +460,7 @@ public CompletableFuture> listModels() { * * @return a future that resolves with the last session ID, or {@code null} if * no sessions exist - * @see #resumeSession(String) + * @see #resumeSession(String, com.github.copilot.sdk.json.ResumeSessionConfig) */ public CompletableFuture getLastSessionId() { return ensureConnected().thenCompose( @@ -498,7 +498,7 @@ public CompletableFuture deleteSession(String sessionId) { * * @return a future that resolves with a list of session metadata * @see SessionMetadata - * @see #resumeSession(String) + * @see #resumeSession(String, com.github.copilot.sdk.json.ResumeSessionConfig) */ public CompletableFuture> listSessions() { return listSessions(null); @@ -528,7 +528,7 @@ public CompletableFuture> listSessions() { * @return a future that resolves with a list of session metadata * @see SessionMetadata * @see SessionListFilter - * @see #resumeSession(String) + * @see #resumeSession(String, com.github.copilot.sdk.json.ResumeSessionConfig) */ public CompletableFuture> listSessions(SessionListFilter filter) { return ensureConnected().thenCompose(connection -> { diff --git a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java index 49f8bb9cb..65129ce7e 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java @@ -1343,6 +1343,7 @@ void testAssistantMessageEventAllFields() throws Exception { } ], "parentToolCallId": "parent-tc", + "interactionId": "interaction-msg-1", "reasoningOpaque": "opaque-data", "reasoningText": "My reasoning", "encryptedContent": "enc123" @@ -1356,6 +1357,7 @@ void testAssistantMessageEventAllFields() throws Exception { assertEquals("msg-rich", data.messageId()); assertEquals("Full response", data.content()); assertEquals("parent-tc", data.parentToolCallId()); + assertEquals("interaction-msg-1", data.interactionId()); assertEquals("opaque-data", data.reasoningOpaque()); assertEquals("My reasoning", data.reasoningText()); assertEquals("enc123", data.encryptedContent()); @@ -1390,6 +1392,59 @@ void testAssistantMessageDeltaEventAllFields() throws Exception { assertEquals("ptc-1", data.parentToolCallId()); } + @Test + void testAssistantStreamingDeltaEventAllFields() throws Exception { + String json = """ + { + "type": "assistant.streaming_delta", + "data": { + "totalResponseSizeBytes": 4096.0 + } + } + """; + + var event = (AssistantStreamingDeltaEvent) parseJson(json); + assertNotNull(event); + assertEquals("assistant.streaming_delta", event.getType()); + assertEquals(4096.0, event.getData().totalResponseSizeBytes()); + } + + @Test + void testAssistantMessageEventIncludesInteractionId() throws Exception { + String json = """ + { + "type": "assistant.message", + "data": { + "messageId": "msg-with-interaction", + "content": "Response", + "interactionId": "interaction-abc-123" + } + } + """; + + var event = (AssistantMessageEvent) parseJson(json); + assertNotNull(event); + assertEquals("interaction-abc-123", event.getData().interactionId()); + } + + @Test + void testAssistantTurnStartEventIncludesInteractionId() throws Exception { + String json = """ + { + "type": "assistant.turn_start", + "data": { + "turnId": "turn-with-interaction", + "interactionId": "interaction-xyz-456" + } + } + """; + + var event = (AssistantTurnStartEvent) parseJson(json); + assertNotNull(event); + assertEquals("turn-with-interaction", event.getData().turnId()); + assertEquals("interaction-xyz-456", event.getData().interactionId()); + } + @Test void testAssistantUsageEventAllFields() throws Exception { String json = """ @@ -1410,6 +1465,23 @@ void testAssistantUsageEventAllFields() throws Exception { "quotaSnapshots": { "premium": 100, "standard": 500 + }, + "copilotUsage": { + "totalNanoAiu": 1234567.0, + "tokenDetails": [ + { + "tokenType": "input", + "tokenCount": 500.0, + "batchSize": 100.0, + "costPerBatch": 0.001 + }, + { + "tokenType": "output", + "tokenCount": 200.0, + "batchSize": 100.0, + "costPerBatch": 0.002 + } + ] } } } @@ -1431,6 +1503,15 @@ void testAssistantUsageEventAllFields() throws Exception { assertEquals("ptc-usage", data.parentToolCallId()); assertNotNull(data.quotaSnapshots()); assertEquals(2, data.quotaSnapshots().size()); + + // Verify copilotUsage + assertNotNull(data.copilotUsage()); + assertEquals(1234567.0, data.copilotUsage().totalNanoAiu()); + assertNotNull(data.copilotUsage().tokenDetails()); + assertEquals(2, data.copilotUsage().tokenDetails().size()); + assertEquals("input", data.copilotUsage().tokenDetails().get(0).tokenType()); + assertEquals(500.0, data.copilotUsage().tokenDetails().get(0).tokenCount()); + assertEquals("output", data.copilotUsage().tokenDetails().get(1).tokenType()); } @Test @@ -1546,6 +1627,8 @@ void testToolExecutionCompleteEventWithError() throws Exception { "data": { "toolCallId": "tc-err-1", "success": false, + "model": "claude-3-5-sonnet", + "interactionId": "interaction-tool-1", "isUserRequested": true, "error": { "message": "File not found", @@ -1565,6 +1648,8 @@ void testToolExecutionCompleteEventWithError() throws Exception { var data = event.getData(); assertEquals("tc-err-1", data.toolCallId()); assertFalse(data.success()); + assertEquals("claude-3-5-sonnet", data.model()); + assertEquals("interaction-tool-1", data.interactionId()); assertTrue(data.isUserRequested()); assertEquals("ptc-complete", data.parentToolCallId()); diff --git a/src/test/java/com/github/copilot/sdk/ToolsTest.java b/src/test/java/com/github/copilot/sdk/ToolsTest.java index d64e245b5..63908e3ab 100644 --- a/src/test/java/com/github/copilot/sdk/ToolsTest.java +++ b/src/test/java/com/github/copilot/sdk/ToolsTest.java @@ -229,7 +229,7 @@ void testCanReceiveAndReturnComplexTypes(TestInfo testInfo) throws Exception { /** * Verifies that a custom tool is invoked with the permission handler being - * called and can inspect the tool name. + * called and can inspect the permission request kind. * * @see Snapshot: tools/invokes_custom_tool_with_permission_handler */ @@ -257,7 +257,7 @@ void testInvokesCustomToolWithPermissionHandler(TestInfo testInfo) throws Except CopilotSession session = client.createSession( new SessionConfig().setTools(List.of(encryptTool)).setOnPermissionRequest((request, invocation) -> { permissionRequests.add(request); - return CompletableFuture.completedFuture(new PermissionRequestResult().setKind("approved")); + return PermissionHandler.APPROVE_ALL.handle(request, invocation); })).get(); AssistantMessageEvent response = session From d78db830cbbf576f5e92ea5fc556c619a01461a0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:47:05 +0000 Subject: [PATCH 056/104] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index d8962bc1d..815fe9cc7 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -6,13 +6,13 @@ - + coverage coverage - 90.4% - 90.4% + 88.7% + 88.7% From b24e25f8fdf6deae6626da99df067df0a028c275 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:50:18 +0000 Subject: [PATCH 057/104] Initial plan From 0f7caac16a915241712fd6787a8dc361b339b04e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:51:34 +0000 Subject: [PATCH 058/104] Initial plan From 4c7f1fd8708a2170083d97324f142409e33f6a43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:59:42 +0000 Subject: [PATCH 059/104] Update docs and samples for required permission handlers Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- README.md | 2 +- jbang-example.java | 2 +- src/site/markdown/advanced.md | 32 ++++++------- src/site/markdown/cookbook/error-handling.md | 6 +-- .../markdown/cookbook/managing-local-files.md | 4 +- .../markdown/cookbook/multiple-sessions.md | 14 +++--- .../markdown/cookbook/persisting-sessions.md | 14 +++--- .../markdown/cookbook/pr-visualization.md | 2 +- src/site/markdown/documentation.md | 26 +++++----- src/site/markdown/getting-started.md | 8 ++-- src/site/markdown/hooks.md | 6 +-- src/site/markdown/index.md | 4 +- src/site/markdown/mcp.md | 4 +- src/site/markdown/setup.md | 4 +- .../copilot/sdk/DocumentationSamplesTest.java | 48 +++++++++++++++++++ 15 files changed, 112 insertions(+), 64 deletions(-) create mode 100644 src/test/java/com/github/copilot/sdk/DocumentationSamplesTest.java diff --git a/README.md b/README.md index 30cf07021..adb9e0ea3 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ public class CopilotSDK { // Create a session var session = client.createSession( - new SessionConfig().setModel("claude-sonnet-4.5")).get(); + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("claude-sonnet-4.5")).get(); // Handle assistant message events session.on(AssistantMessageEvent.class, msg -> { diff --git a/jbang-example.java b/jbang-example.java index 20838e288..ca30b2c10 100644 --- a/jbang-example.java +++ b/jbang-example.java @@ -15,7 +15,7 @@ public static void main(String[] args) throws Exception { // Create a session var session = client.createSession( - new SessionConfig().setModel("claude-sonnet-4.5")).get(); + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("claude-sonnet-4.5")).get(); // Handle assistant message events session.on(AssistantMessageEvent.class, msg -> { diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index 66339d30e..0b516bdf1 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -66,7 +66,7 @@ var lookupTool = ToolDefinition.create( ); var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setTools(List.of(lookupTool)) ).get(); ``` @@ -85,7 +85,7 @@ Use `APPEND` mode to add constraints while keeping default guardrails: ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setSystemMessage(new SystemMessageConfig() .setMode(SystemMessageMode.APPEND) .setContent(""" @@ -103,7 +103,7 @@ Use `REPLACE` mode for complete control (removes default guardrails): ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setSystemMessage(new SystemMessageConfig() .setMode(SystemMessageMode.REPLACE) .setContent("You are a helpful coding assistant.")) @@ -164,7 +164,7 @@ Supported providers: ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setProvider(new ProviderConfig() .setType("openai") .setBaseUrl("https://api.openai.com/v1") @@ -178,7 +178,7 @@ Some providers require bearer token authentication instead of API keys: ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setProvider(new ProviderConfig() .setType("openai") .setBaseUrl("https://my-custom-endpoint.example.com/v1") @@ -194,7 +194,7 @@ var session = client.createSession( ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setProvider(new ProviderConfig() .setType("openai") .setBaseUrl("http://localhost:/v1")) @@ -248,7 +248,7 @@ When enabled (default), the session automatically compacts older messages as the ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setInfiniteSessions(new InfiniteSessionConfig() .setEnabled(true) .setBackgroundCompactionThreshold(0.80) // Start compacting at 80% @@ -295,7 +295,7 @@ Map server = Map.of( ); var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setMcpServers(Map.of("filesystem", server)) ).get(); ``` @@ -317,7 +317,7 @@ var reviewer = new CustomAgentConfig() .setTools(List.of("read_file", "search_code")); var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setCustomAgents(List.of(reviewer)) ).get(); @@ -355,7 +355,7 @@ var agents = List.of( ); var session = client.createSession( - new SessionConfig().setCustomAgents(agents) + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setCustomAgents(agents) ).get(); ``` @@ -373,7 +373,7 @@ Skills are loaded from `SKILL.md` files in subdirectories of the specified skill ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setSkillDirectories(List.of("/path/to/skills")) ).get(); ``` @@ -397,7 +397,7 @@ Disable specific skills by name: ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setSkillDirectories(List.of("/path/to/skills")) .setDisabledSkills(List.of("my-skill")) ).get(); @@ -411,7 +411,7 @@ Use a custom configuration directory for session settings: ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setConfigDir("/path/to/custom/config") ).get(); ``` @@ -426,7 +426,7 @@ Handle user input requests when the AI uses the `ask_user` tool to gather inform ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setOnUserInputRequest((request, invocation) -> { System.out.println("Agent asks: " + request.getQuestion()); @@ -471,7 +471,7 @@ Approve or deny permission requests from the AI. ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setOnPermissionRequest((request, invocation) -> { // Inspect request and approve/deny var result = new PermissionRequestResult(); @@ -499,7 +499,7 @@ var hooks = new SessionHooks() }); var session = client.createSession( - new SessionConfig().setHooks(hooks) + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setHooks(hooks) ).get(); ``` diff --git a/src/site/markdown/cookbook/error-handling.md b/src/site/markdown/cookbook/error-handling.md index cb66b4934..db1a173f5 100644 --- a/src/site/markdown/cookbook/error-handling.md +++ b/src/site/markdown/cookbook/error-handling.md @@ -41,7 +41,7 @@ public class BasicErrorHandling { client.start().get(); var session = client.createSession( - new SessionConfig().setModel("gpt-5")).get(); + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")).get(); session.on(AssistantMessageEvent.class, msg -> { System.out.println(msg.getData().getContent()); @@ -206,7 +206,7 @@ public class TryWithResources { client.start().get(); try (var session = client.createSession( - new SessionConfig().setModel("gpt-5")).get()) { + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")).get()) { session.on(AssistantMessageEvent.class, msg -> { System.out.println(msg.getData().getContent()); @@ -251,7 +251,7 @@ public class ToolErrorHandling { client.start().get(); var session = client.createSession( - new SessionConfig().setTools(List.of(errorTool))).get(); + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setTools(List.of(errorTool))).get(); session.on(AssistantMessageEvent.class, msg -> { System.out.println(msg.getData().getContent()); diff --git a/src/site/markdown/cookbook/managing-local-files.md b/src/site/markdown/cookbook/managing-local-files.md index e66427704..941cbdded 100644 --- a/src/site/markdown/cookbook/managing-local-files.md +++ b/src/site/markdown/cookbook/managing-local-files.md @@ -48,7 +48,7 @@ public class ManagingLocalFiles { // Create session var session = client.createSession( - new SessionConfig().setModel("gpt-5")).get(); + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")).get(); // Set up event handlers var done = new CountDownLatch(1); @@ -168,7 +168,7 @@ public class InteractiveFileOrganizer { client.start().get(); var session = client.createSession( - new SessionConfig().setModel("gpt-5")).get(); + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")).get(); session.on(AssistantMessageEvent.class, msg -> System.out.println("\nCopilot: " + msg.getData().getContent()) diff --git a/src/site/markdown/cookbook/multiple-sessions.md b/src/site/markdown/cookbook/multiple-sessions.md index be5cb78b6..ff7441255 100644 --- a/src/site/markdown/cookbook/multiple-sessions.md +++ b/src/site/markdown/cookbook/multiple-sessions.md @@ -42,11 +42,11 @@ public class MultipleSessions { // Create multiple independent sessions var session1 = client.createSession( - new SessionConfig().setModel("gpt-5")).get(); + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")).get(); var session2 = client.createSession( - new SessionConfig().setModel("gpt-5")).get(); + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")).get(); var session3 = client.createSession( - new SessionConfig().setModel("claude-sonnet-4.5")).get(); + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("claude-sonnet-4.5")).get(); // Set up event handlers for each session session1.on(AssistantMessageEvent.class, msg -> @@ -90,7 +90,7 @@ Use custom IDs for easier tracking: ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setSessionId("user-123-chat") .setModel("gpt-5") ).get(); @@ -129,9 +129,9 @@ public class ParallelSessions { public static void runParallelSessions(CopilotClient client) throws Exception { // Create sessions in parallel var sessionFutures = List.of( - client.createSession(new SessionConfig().setModel("gpt-5")), - client.createSession(new SessionConfig().setModel("gpt-5")), - client.createSession(new SessionConfig().setModel("claude-sonnet-4.5")) + client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")), + client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")), + client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("claude-sonnet-4.5")) ); // Wait for all sessions to be created diff --git a/src/site/markdown/cookbook/persisting-sessions.md b/src/site/markdown/cookbook/persisting-sessions.md index 47b989940..a281c88f3 100644 --- a/src/site/markdown/cookbook/persisting-sessions.md +++ b/src/site/markdown/cookbook/persisting-sessions.md @@ -42,7 +42,7 @@ public class PersistingSessions { // Create session with a memorable ID var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setSessionId("user-123-conversation") .setModel("gpt-5") ).get(); @@ -73,7 +73,7 @@ public class ResumeSession { client.start().get(); // Resume the previous session - var session = client.resumeSession("user-123-conversation").get(); + var session = client.resumeSession("user-123-conversation", new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.on(AssistantMessageEvent.class, msg -> System.out.println(msg.getData().getContent()) @@ -135,7 +135,7 @@ public class SessionHistory { try (var client = new CopilotClient()) { client.start().get(); - var session = client.resumeSession("user-123-conversation").get(); + var session = client.resumeSession("user-123-conversation", new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); var messages = session.getMessages().get(); for (var event : messages) { @@ -184,7 +184,7 @@ public class SessionManager { System.out.print("Enter session ID: "); String sessionId = scanner.nextLine(); session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setSessionId(sessionId) .setModel("gpt-5") ).get(); @@ -195,7 +195,7 @@ public class SessionManager { System.out.print("Enter session ID to resume: "); String resumeId = scanner.nextLine(); try { - session = client.resumeSession(resumeId).get(); + session = client.resumeSession(resumeId, new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); System.out.println("Resumed session: " + resumeId); } catch (Exception ex) { System.err.println("Failed to resume session: " + ex.getMessage()); @@ -264,13 +264,13 @@ public class CheckSession { if (sessionExists(client, sessionId)) { System.out.println("Session exists, resuming..."); - var session = client.resumeSession(sessionId).get(); + var session = client.resumeSession(sessionId, new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); // ... use session ... session.close(); } else { System.out.println("Session doesn't exist, creating new one..."); var session = client.createSession( - new SessionConfig().setSessionId(sessionId).setModel("gpt-5") + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSessionId(sessionId).setModel("gpt-5") ).get(); // ... use session ... session.close(); diff --git a/src/site/markdown/cookbook/pr-visualization.md b/src/site/markdown/cookbook/pr-visualization.md index 002a9f073..dfe9db85f 100644 --- a/src/site/markdown/cookbook/pr-visualization.md +++ b/src/site/markdown/cookbook/pr-visualization.md @@ -94,7 +94,7 @@ public class PRVisualization { """, owner, repoName, cwd); var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("gpt-5") .setSystemMessage(new SystemMessageConfig().setContent(systemMessage)) ).get(); diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index e5f405307..01aba950f 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -36,7 +36,7 @@ try (var client = new CopilotClient()) { client.start().get(); var session = client.createSession( - new SessionConfig().setModel("gpt-4.1") + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-4.1") ).get(); var response = session.sendAndWait("Explain Java records in one sentence").get(); @@ -232,7 +232,7 @@ Enable streaming to receive response chunks as they're generated: ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("gpt-4.1") .setStreaming(true) ).get(); @@ -343,7 +343,7 @@ for (var model : models) { ```java var session = client.createSession( - new SessionConfig().setModel("claude-sonnet-4") + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("claude-sonnet-4") ).get(); ``` @@ -355,7 +355,7 @@ For models that support it, control how much effort the model spends reasoning b ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("o3-mini") .setReasoningEffort("high") ).get(); @@ -382,7 +382,7 @@ Restrict the session to only specific tools: ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setAvailableTools(List.of("read_file", "search_code", "list_dir")) ).get(); ``` @@ -395,7 +395,7 @@ Allow all tools except specific ones: ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setExcludedTools(List.of("execute_command", "write_file")) ).get(); ``` @@ -410,7 +410,7 @@ Tool filtering applies to built-in tools. Your custom tools (registered via `set var lookupTool = ToolDefinition.create("lookup_issue", "Fetch issue", schema, handler); var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setTools(List.of(lookupTool)) // Custom tool always available .setAvailableTools(List.of("read_file")) // Only allow read_file from built-ins ).get(); @@ -424,7 +424,7 @@ Set the working directory for file operations in the session: ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setWorkingDirectory("/path/to/project") ).get(); ``` @@ -553,11 +553,11 @@ session.send(new MessageOptions() ```java var session1 = client.createSession( - new SessionConfig().setModel("gpt-4.1") + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-4.1") ).get(); var session2 = client.createSession( - new SessionConfig().setModel("claude-sonnet-4") + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("claude-sonnet-4") ).get(); // Send messages concurrently @@ -578,7 +578,7 @@ var lastSessionId = client.getLastSessionId().get(); var sessions = client.listSessions().get(); // Resume a session -var session = client.resumeSession(lastSessionId).get(); +var session = client.resumeSession(lastSessionId, new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); ``` ### Resume Options @@ -606,7 +606,7 @@ When resuming a session, you can optionally reconfigure many settings. This is u ```java // Resume with a different model -var config = new ResumeSessionConfig() +var config = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("claude-sonnet-4") // Switch to a different model .setReasoningEffort("high"); // Increase reasoning effort @@ -655,7 +655,7 @@ Complete list of all `SessionConfig` options for `createSession()`: Use `clone()` to copy a base configuration before making per-session changes: ```java -var base = new SessionConfig() +var base = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("gpt-4.1") .setStreaming(true); diff --git a/src/site/markdown/getting-started.md b/src/site/markdown/getting-started.md index 9077426f7..49281a4fc 100644 --- a/src/site/markdown/getting-started.md +++ b/src/site/markdown/getting-started.md @@ -74,7 +74,7 @@ public class HelloCopilot { client.start().get(); var session = client.createSession( - new SessionConfig().setModel("gpt-4.1") + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-4.1") ).get(); var response = session.sendAndWait( @@ -119,7 +119,7 @@ public class StreamingExample { client.start().get(); var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("gpt-4.1") .setStreaming(true) ).get(); @@ -195,7 +195,7 @@ public class ToolExample { ); var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("gpt-4.1") .setStreaming(true) .setTools(List.of(getWeather)) @@ -270,7 +270,7 @@ public class WeatherAssistant { ); var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("gpt-4.1") .setStreaming(true) .setTools(List.of(getWeather)) diff --git a/src/site/markdown/hooks.md b/src/site/markdown/hooks.md index 4e36c9869..11df1d6e9 100644 --- a/src/site/markdown/hooks.md +++ b/src/site/markdown/hooks.md @@ -35,7 +35,7 @@ var hooks = new SessionHooks() }); var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("gpt-4.1") .setHooks(hooks) ).get(); @@ -333,7 +333,7 @@ public class HooksExample { }); var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("gpt-4.1") .setHooks(hooks) ).get(); @@ -370,7 +370,7 @@ var hooks = new SessionHooks() if (hooks.hasHooks()) { var session = client.createSession( - new SessionConfig().setHooks(hooks) + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setHooks(hooks) ).get(); } ``` diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 9ea166715..27abc27c0 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -47,7 +47,7 @@ public class Example { client.start().get(); var session = client.createSession( - new SessionConfig().setModel("claude-sonnet-4.5") + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("claude-sonnet-4.5") ).get(); var done = new CompletableFuture(); @@ -93,7 +93,7 @@ class hello { public static void main(String[] args) throws Exception { try (var client = new CopilotClient()) { client.start().get(); - var session = client.createSession(new SessionConfig()).get(); + var session = client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); var done = new CompletableFuture(); session.on(AssistantMessageEvent.class, msg -> { System.out.print(msg.getData().getContent()); diff --git a/src/site/markdown/mcp.md b/src/site/markdown/mcp.md index 262b67372..68df0db87 100644 --- a/src/site/markdown/mcp.md +++ b/src/site/markdown/mcp.md @@ -17,7 +17,7 @@ Map server = Map.of( ); var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setMcpServers(Map.of("filesystem", server)) ).get(); @@ -83,7 +83,7 @@ Combine tools from several sources. ```java var session = client.createSession( - new SessionConfig() + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setMcpServers(Map.of( "filesystem", filesystemServer, "github", githubServer, diff --git a/src/site/markdown/setup.md b/src/site/markdown/setup.md index 1130808ff..56a2df4e5 100644 --- a/src/site/markdown/setup.md +++ b/src/site/markdown/setup.md @@ -22,7 +22,7 @@ The simplest setup uses the Copilot CLI already signed in on your development ma try (var client = new CopilotClient()) { client.start().get(); var session = client.createSession( - new SessionConfig().setModel("gpt-4.1") + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-4.1") ).get(); // Use session... } @@ -54,7 +54,7 @@ var options = new CopilotClientOptions() try (var client = new CopilotClient(options)) { client.start().get(); var session = client.createSession( - new SessionConfig().setModel("gpt-4.1") + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-4.1") ).get(); // Requests are made on behalf of the authenticated user } diff --git a/src/test/java/com/github/copilot/sdk/DocumentationSamplesTest.java b/src/test/java/com/github/copilot/sdk/DocumentationSamplesTest.java new file mode 100644 index 000000000..1e7b2f298 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/DocumentationSamplesTest.java @@ -0,0 +1,48 @@ +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +class DocumentationSamplesTest { + + private static final Pattern CREATE_SESSION_WITHOUT_PERMISSION = Pattern + .compile("createSession\\(\\s*new SessionConfig\\(\\)(?!\\.setOnPermissionRequest\\()", Pattern.DOTALL); + private static final Pattern RESUME_CONFIG_WITHOUT_PERMISSION = Pattern.compile( + "new ResumeSessionConfig\\(\\)(?!\\.setOnPermissionRequest\\()", Pattern.DOTALL); + private static final Pattern REMOVED_RESUME_OVERLOAD = Pattern + .compile("resumeSession\\([^,\\n\\)]*\\)\\.get\\(\\)"); + + @Test + void docsAndJbangSamplesUseRequiredPermissionHandler() throws IOException { + for (Path path : documentationFiles()) { + String content = Files.readString(path); + assertFalse(CREATE_SESSION_WITHOUT_PERMISSION.matcher(content).find(), + () -> path + " contains createSession sample without setOnPermissionRequest"); + assertFalse(RESUME_CONFIG_WITHOUT_PERMISSION.matcher(content).find(), + () -> path + " contains ResumeSessionConfig sample without setOnPermissionRequest"); + assertFalse(REMOVED_RESUME_OVERLOAD.matcher(content).find(), + () -> path + " contains removed resumeSession(String) overload"); + } + } + + private static List documentationFiles() throws IOException { + Path root = Path.of("").toAbsolutePath(); + List files = new ArrayList<>(); + files.add(root.resolve("README.md")); + files.add(root.resolve("jbang-example.java")); + + try (Stream markdownFiles = Files.walk(root.resolve("src/site/markdown"))) { + markdownFiles.filter(Files::isRegularFile).filter(path -> path.toString().endsWith(".md")).forEach(files::add); + } + return files; + } +} From b2fbe646dfe39a0d7f32b75440f3f377c9dcbe57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:07:04 +0000 Subject: [PATCH 060/104] Port session.setModel() and built-in tool override support from upstream Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .lastmerge | 2 +- CHANGELOG.md | 5 +- .../github/copilot/sdk/CopilotSession.java | 22 +++++++ .../copilot/sdk/json/ToolDefinition.java | 33 ++++++++++- src/site/markdown/advanced.md | 58 +++++++++++++++++++ .../copilot/sdk/ClosedSessionGuardTest.java | 19 ++++++ .../com/github/copilot/sdk/ToolsTest.java | 40 +++++++++++++ 7 files changed, 175 insertions(+), 4 deletions(-) diff --git a/.lastmerge b/.lastmerge index 4767fe10d..2a538cb8c 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -b9f746ab1b9c5f31af03a6f8a6cf4e680b3fd6b8 +dcd86c189501ce1b46b787ca60d90f3f315f3079 diff --git a/CHANGELOG.md b/CHANGELOG.md index 93cb20567..ae1fdef07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] -> **Upstream sync:** [`github/copilot-sdk@b9f746a`](https://github.com/github/copilot-sdk/commit/b9f746ab1b9c5f31af03a6f8a6cf4e680b3fd6b8) +> **Upstream sync:** [`github/copilot-sdk@dcd86c1`](https://github.com/github/copilot-sdk/commit/dcd86c189501ce1b46b787ca60d90f3f315f3079) ### Added +- `CopilotSession.setModel(String)` — changes the model for an existing session mid-conversation; the new model takes effect for the next message, and conversation history is preserved (upstream: [`bd98e3a`](https://github.com/github/copilot-sdk/commit/bd98e3a)) +- `ToolDefinition.createOverride(String, String, Map, ToolHandler)` — creates a tool definition that overrides a built-in CLI tool with the same name (upstream: [`f843c80`](https://github.com/github/copilot-sdk/commit/f843c80)) +- `ToolDefinition` record now includes `overridesBuiltInTool` field; when `true`, signals to the CLI that the custom tool intentionally replaces a built-in (upstream: [`f843c80`](https://github.com/github/copilot-sdk/commit/f843c80)) - `CopilotSession.listAgents()` — lists custom agents available for selection (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) - `CopilotSession.getCurrentAgent()` — gets the currently selected custom agent (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) - `CopilotSession.selectAgent(String)` — selects a custom agent for the session (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index 8e5ef7cda..1af22d803 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -808,6 +808,28 @@ public CompletableFuture abort() { return rpc.invoke("session.abort", Map.of("sessionId", sessionId), Void.class); } + /** + * Changes the model for this session. + *

+ * The new model takes effect for the next message. Conversation history is + * preserved. + * + *

{@code
+     * session.setModel("gpt-4.1").get();
+     * }
+ * + * @param model + * the model ID to switch to (e.g., {@code "gpt-4.1"}) + * @return a future that completes when the model switch is acknowledged + * @throws IllegalStateException + * if this session has been terminated + * @since 1.0.11 + */ + public CompletableFuture setModel(String model) { + ensureNotTerminated(); + return rpc.invoke("session.model.switchTo", Map.of("sessionId", sessionId, "modelId", model), Void.class); + } + /** * Lists the custom agents available for selection in this session. * diff --git a/src/main/java/com/github/copilot/sdk/json/ToolDefinition.java b/src/main/java/com/github/copilot/sdk/json/ToolDefinition.java index f81c26d90..9b3087a42 100644 --- a/src/main/java/com/github/copilot/sdk/json/ToolDefinition.java +++ b/src/main/java/com/github/copilot/sdk/json/ToolDefinition.java @@ -48,13 +48,18 @@ * the JSON Schema defining the tool's parameters * @param handler * the handler function to execute when invoked + * @param overridesBuiltInTool + * when {@code true}, indicates that this tool intentionally + * overrides a built-in CLI tool with the same name; {@code null} or + * {@code false} means the tool is purely custom * @see SessionConfig#setTools(java.util.List) * @see ToolHandler * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("description") String description, - @JsonProperty("parameters") Object parameters, @JsonIgnore ToolHandler handler) { + @JsonProperty("parameters") Object parameters, @JsonIgnore ToolHandler handler, + @JsonProperty("overridesBuiltInTool") Boolean overridesBuiltInTool) { /** * Creates a tool definition with a JSON schema for parameters. @@ -74,6 +79,30 @@ public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("d */ public static ToolDefinition create(String name, String description, Map schema, ToolHandler handler) { - return new ToolDefinition(name, description, schema, handler); + return new ToolDefinition(name, description, schema, handler, null); + } + + /** + * Creates a tool definition that overrides a built-in CLI tool. + *

+ * Use this factory method when you want your custom tool to replace a built-in + * tool (e.g., {@code grep}, {@code read_file}) with the same name. Setting + * {@code overridesBuiltInTool} to {@code true} signals to the CLI that this is + * intentional. + * + * @param name + * the name of the built-in tool to override + * @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 with the override flag set + * @since 1.0.11 + */ + public static ToolDefinition createOverride(String name, String description, Map schema, + ToolHandler handler) { + return new ToolDefinition(name, description, schema, handler, true); } } diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index 66339d30e..04d515359 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -7,6 +7,8 @@ This guide covers advanced scenarios for extending and customizing your Copilot ## Table of Contents - [Custom Tools](#Custom_Tools) + - [Overriding Built-in Tools](#Overriding_Built-in_Tools) +- [Switching Models Mid-Session](#Switching_Models_Mid-Session) - [System Messages](#System_Messages) - [Adding Rules](#Adding_Rules) - [Full Control](#Full_Control) @@ -73,6 +75,62 @@ var session = client.createSession( See [ToolDefinition](apidocs/com/github/copilot/sdk/json/ToolDefinition.html) Javadoc for schema details. +### Overriding Built-in Tools + +You can replace a built-in CLI tool (such as `grep` or `read_file`) with your own implementation +by using `ToolDefinition.createOverride()`. This signals to the CLI that the name collision is +intentional and your custom implementation should be used instead. + +```java +var customGrep = ToolDefinition.createOverride( + "grep", + "Project-aware search with custom filtering", + Map.of( + "type", "object", + "properties", Map.of( + "query", Map.of("type", "string", "description", "Search query") + ), + "required", List.of("query") + ), + invocation -> { + String query = (String) invocation.getArguments().get("query"); + // Your custom search logic here + return CompletableFuture.completedFuture("Results for: " + query); + } +); + +var session = client.createSession( + new SessionConfig() + .setTools(List.of(customGrep)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) +).get(); +``` + +--- + +## Switching Models Mid-Session + +You can change the model used by an existing session without losing conversation history. +The new model takes effect starting with the next message sent. + +```java +var session = client.createSession( + new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) +).get(); + +// Switch to a different model mid-conversation +session.setModel("gpt-4.1").get(); + +// Next message will use the new model +session.sendAndWait(new MessageOptions().setPrompt("Continue with the new model")).get(); +``` + +The session emits a [`SessionModelChangeEvent`](apidocs/com/github/copilot/sdk/events/SessionModelChangeEvent.html) +when the switch completes, which you can observe with `session.on(SessionModelChangeEvent.class, event -> ...)`. + +See [CopilotSession.setModel()](apidocs/com/github/copilot/sdk/CopilotSession.html#setModel(java.lang.String)) Javadoc for details. + --- ## System Messages diff --git a/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java b/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java index 4d86c23d8..069cd01cb 100644 --- a/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java +++ b/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java @@ -352,4 +352,23 @@ void testTryWithResourcesDoubleClose() throws Exception { }); } } + + /** + * Verifies that setModel() throws IllegalStateException after session is + * terminated. + */ + @Test + void testSetModelThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get(); + session.close(); + + assertThrows(IllegalStateException.class, () -> { + session.setModel("gpt-4.1"); + }); + } + } } diff --git a/src/test/java/com/github/copilot/sdk/ToolsTest.java b/src/test/java/com/github/copilot/sdk/ToolsTest.java index 63908e3ab..eb68be8b0 100644 --- a/src/test/java/com/github/copilot/sdk/ToolsTest.java +++ b/src/test/java/com/github/copilot/sdk/ToolsTest.java @@ -320,4 +320,44 @@ void testDeniesCustomToolWhenPermissionDenied(TestInfo testInfo) throws Exceptio session.close(); } } + + /** + * Verifies that a custom tool can override a built-in CLI tool with the same + * name when {@code overridesBuiltInTool} is set to {@code true}. + * + * @see Snapshot: tools/overrides_built_in_tool_with_custom_tool + */ + @Test + void testOverridesBuiltInToolWithCustomTool(TestInfo testInfo) throws Exception { + ctx.configureForTest("tools", "overrides_built_in_tool_with_custom_tool"); + + var parameters = new HashMap(); + var properties = new HashMap(); + properties.put("query", Map.of("type", "string", "description", "Search query")); + parameters.put("type", "object"); + parameters.put("properties", properties); + parameters.put("required", List.of("query")); + + ToolDefinition customGrep = ToolDefinition.createOverride("grep", "A custom grep implementation", parameters, + (invocation) -> { + Map args = invocation.getArguments(); + String query = (String) args.get("query"); + return CompletableFuture.completedFuture("CUSTOM_GREP_RESULT: " + query); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(customGrep)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Use grep to search for the word 'hello'")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("CUSTOM_GREP_RESULT"), + "Response should contain CUSTOM_GREP_RESULT: " + response.getData().content()); + + session.close(); + } + } } From 53fd8654efb437121972c45813f5de82f3600dd9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:08:49 +0000 Subject: [PATCH 061/104] Harden documentation sample API guard test Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../copilot/sdk/DocumentationSamplesTest.java | 122 ++++++++++++++++-- 1 file changed, 109 insertions(+), 13 deletions(-) diff --git a/src/test/java/com/github/copilot/sdk/DocumentationSamplesTest.java b/src/test/java/com/github/copilot/sdk/DocumentationSamplesTest.java index 1e7b2f298..ac8e7a6b6 100644 --- a/src/test/java/com/github/copilot/sdk/DocumentationSamplesTest.java +++ b/src/test/java/com/github/copilot/sdk/DocumentationSamplesTest.java @@ -7,33 +7,129 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.regex.Pattern; import java.util.stream.Stream; import org.junit.jupiter.api.Test; class DocumentationSamplesTest { - private static final Pattern CREATE_SESSION_WITHOUT_PERMISSION = Pattern - .compile("createSession\\(\\s*new SessionConfig\\(\\)(?!\\.setOnPermissionRequest\\()", Pattern.DOTALL); - private static final Pattern RESUME_CONFIG_WITHOUT_PERMISSION = Pattern.compile( - "new ResumeSessionConfig\\(\\)(?!\\.setOnPermissionRequest\\()", Pattern.DOTALL); - private static final Pattern REMOVED_RESUME_OVERLOAD = Pattern - .compile("resumeSession\\([^,\\n\\)]*\\)\\.get\\(\\)"); - @Test void docsAndJbangSamplesUseRequiredPermissionHandler() throws IOException { for (Path path : documentationFiles()) { - String content = Files.readString(path); - assertFalse(CREATE_SESSION_WITHOUT_PERMISSION.matcher(content).find(), - () -> path + " contains createSession sample without setOnPermissionRequest"); - assertFalse(RESUME_CONFIG_WITHOUT_PERMISSION.matcher(content).find(), + String content = stripStringsAndComments(Files.readString(path)); + assertFalse(hasConfigWithoutPermissionHandler(content, "SessionConfig"), + () -> path + " contains SessionConfig sample without setOnPermissionRequest"); + assertFalse(hasConfigWithoutPermissionHandler(content, "ResumeSessionConfig"), () -> path + " contains ResumeSessionConfig sample without setOnPermissionRequest"); - assertFalse(REMOVED_RESUME_OVERLOAD.matcher(content).find(), + assertFalse(hasSingleArgumentResumeSessionCall(content), () -> path + " contains removed resumeSession(String) overload"); } } + private static boolean hasConfigWithoutPermissionHandler(String content, String configType) { + String constructor = "new " + configType + "()"; + int fromIndex = 0; + while (true) { + int start = content.indexOf(constructor, fromIndex); + if (start < 0) { + return false; + } + int end = content.indexOf(';', start); + if (end < 0) { + end = content.length(); + } + if (!content.substring(start, end).contains("setOnPermissionRequest(")) { + return true; + } + fromIndex = start + constructor.length(); + } + } + + private static boolean hasSingleArgumentResumeSessionCall(String content) { + int fromIndex = 0; + while (true) { + int callStart = content.indexOf("resumeSession(", fromIndex); + if (callStart < 0) { + return false; + } + int index = callStart + "resumeSession(".length(); + int depth = 1; + int topLevelCommaCount = 0; + while (index < content.length() && depth > 0) { + char c = content.charAt(index); + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + } else if (c == ',' && depth == 1) { + topLevelCommaCount++; + } + index++; + } + + if (depth == 0 && topLevelCommaCount == 0) { + return true; + } + fromIndex = callStart + 1; + } + } + + private static String stripStringsAndComments(String input) { + StringBuilder out = new StringBuilder(input.length()); + int i = 0; + while (i < input.length()) { + char c = input.charAt(i); + if (c == '"' || c == '\'') { + char quote = c; + out.append(' '); + i++; + while (i < input.length()) { + char current = input.charAt(i); + out.append(' '); + if (current == '\\') { + i++; + if (i < input.length()) { + out.append(' '); + } + } else if (current == quote) { + i++; + break; + } + i++; + } + continue; + } + if (c == '/' && i + 1 < input.length()) { + char next = input.charAt(i + 1); + if (next == '/') { + out.append(' ').append(' '); + i += 2; + while (i < input.length() && input.charAt(i) != '\n') { + out.append(' '); + i++; + } + continue; + } + if (next == '*') { + out.append(' ').append(' '); + i += 2; + while (i + 1 < input.length() && !(input.charAt(i) == '*' && input.charAt(i + 1) == '/')) { + out.append(' '); + i++; + } + if (i + 1 < input.length()) { + out.append(' ').append(' '); + i += 2; + } + continue; + } + } + out.append(c); + i++; + } + return out.toString(); + } + private static List documentationFiles() throws IOException { Path root = Path.of("").toAbsolutePath(); List files = new ArrayList<>(); From bf3d547df29813f9ea80fbc44f25ad0aa4b52018 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 3 Mar 2026 13:25:11 -0500 Subject: [PATCH 062/104] Potential fix for pull request finding 'Useless parameter' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- src/test/java/com/github/copilot/sdk/ToolsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/github/copilot/sdk/ToolsTest.java b/src/test/java/com/github/copilot/sdk/ToolsTest.java index eb68be8b0..538da74d2 100644 --- a/src/test/java/com/github/copilot/sdk/ToolsTest.java +++ b/src/test/java/com/github/copilot/sdk/ToolsTest.java @@ -328,7 +328,7 @@ void testDeniesCustomToolWhenPermissionDenied(TestInfo testInfo) throws Exceptio * @see Snapshot: tools/overrides_built_in_tool_with_custom_tool */ @Test - void testOverridesBuiltInToolWithCustomTool(TestInfo testInfo) throws Exception { + void testOverridesBuiltInToolWithCustomTool() throws Exception { ctx.configureForTest("tools", "overrides_built_in_tool_with_custom_tool"); var parameters = new HashMap(); From 2a06a6cc8f6f2f393f3375db604c6fe53de9738d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:27:19 +0000 Subject: [PATCH 063/104] Run spotless apply on documentation samples test Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../java/com/github/copilot/sdk/DocumentationSamplesTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/github/copilot/sdk/DocumentationSamplesTest.java b/src/test/java/com/github/copilot/sdk/DocumentationSamplesTest.java index ac8e7a6b6..bb8a6f07e 100644 --- a/src/test/java/com/github/copilot/sdk/DocumentationSamplesTest.java +++ b/src/test/java/com/github/copilot/sdk/DocumentationSamplesTest.java @@ -137,7 +137,8 @@ private static List documentationFiles() throws IOException { files.add(root.resolve("jbang-example.java")); try (Stream markdownFiles = Files.walk(root.resolve("src/site/markdown"))) { - markdownFiles.filter(Files::isRegularFile).filter(path -> path.toString().endsWith(".md")).forEach(files::add); + markdownFiles.filter(Files::isRegularFile).filter(path -> path.toString().endsWith(".md")) + .forEach(files::add); } return files; } From 3216878cd1211455eb48f3ed8a68a24c068dd9f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:34:11 +0000 Subject: [PATCH 064/104] Initial plan From ecc0fed3b50a66fb985a2048d3f448a80dfe2d71 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:36:57 +0000 Subject: [PATCH 065/104] Initial plan From 96e102f0cab3769063a09141b676ccabb013efc1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:37:36 +0000 Subject: [PATCH 066/104] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 815fe9cc7..738a6ad1e 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 88.7% - 88.7% + 88.6% + 88.6% From 27ef548d235cc7c2f5e2e6a9e9468c5651353444 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:40:29 +0000 Subject: [PATCH 067/104] Document new session compaction and agent-selection APIs Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- src/site/markdown/advanced.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index ef71d92e2..1b8724e96 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -15,9 +15,11 @@ This guide covers advanced scenarios for extending and customizing your Copilot - [File Attachments](#File_Attachments) - [Bring Your Own Key (BYOK)](#Bring_Your_Own_Key_BYOK) - [Infinite Sessions](#Infinite_Sessions) + - [Manual Compaction](#Manual_Compaction) - [Compaction Events](#Compaction_Events) - [MCP Servers](#MCP_Servers) - [Custom Agents](#Custom_Agents) + - [Programmatic Agent Selection](#Programmatic_Agent_Selection) - [Skills Configuration](#Skills_Configuration) - [Loading Skills](#Loading_Skills) - [Disabling Skills](#Disabling_Skills) @@ -317,6 +319,14 @@ var session = client.createSession( var workspace = session.getWorkspacePath(); ``` +### Manual Compaction + +Trigger compaction immediately when you want to reduce context usage before the automatic threshold is reached: + +```java +session.compact().get(); +``` + ### Compaction Events When compaction occurs, the session emits events that you can listen for: @@ -419,6 +429,24 @@ var session = client.createSession( See [CustomAgentConfig](apidocs/com/github/copilot/sdk/json/CustomAgentConfig.html) Javadoc for full details. +### Programmatic Agent Selection + +You can inspect and switch agents at runtime: + +```java +var available = session.listAgents().get(); + +var current = session.getCurrentAgent().get(); +if (current != null) { + System.out.println("Current agent: " + current.name()); +} + +var selected = session.selectAgent("reviewer").get(); +System.out.println("Selected: " + selected.name()); + +session.deselectAgent().get(); // Return to the default agent +``` + --- ## Skills Configuration From 33bea438c8a1c604c26de70ff9a14879f943a777 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:42:26 +0000 Subject: [PATCH 068/104] Restructure upstream merge prompt steps and enforce docs update gate Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../prompts/agentic-merge-upstream.prompt.md | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/.github/prompts/agentic-merge-upstream.prompt.md b/.github/prompts/agentic-merge-upstream.prompt.md index 8db8e8446..c020b86a7 100644 --- a/.github/prompts/agentic-merge-upstream.prompt.md +++ b/.github/prompts/agentic-merge-upstream.prompt.md @@ -31,11 +31,15 @@ All scripts write/read a `.merge-env` file (git-ignored) to share state (branch 1. Run `./.github/scripts/upstream-sync/merge-upstream-start.sh` (creates branch, clones upstream, shows summary) 2. Run `./.github/scripts/upstream-sync/merge-upstream-diff.sh` (analyze changes) 3. Update README with minimum CLI version requirement -4. Port changes to Java SDK (commit as you go) -5. Run `./.github/scripts/build/format-and-test.sh` frequently while porting -6. Update documentation -7. Run `./.github/scripts/upstream-sync/merge-upstream-finish.sh` (final test + push) -8. Finalize Pull Request (see note below about coding agent vs. manual workflow) +4. Identify upstream changes to port +5. Apply changes to Java SDK (commit as you go) +6. Port/adjust tests from upstream changes +7. Run `./.github/scripts/build/format-and-test.sh` frequently while porting +8. Build the package +9. Update documentation (**required for every user-facing upstream change**) +10. Run `./.github/scripts/upstream-sync/merge-upstream-finish.sh` (final test + push) +11. Finalize Pull Request (see note below about coding agent vs. manual workflow) +12. Perform final review before handing off --- @@ -104,7 +108,7 @@ For each change in the upstream diff, determine: > **⚠️ Important:** When adding new documentation pages, always update `src/site/site.xml` to include them in the navigation menu. -## Step 7: Apply Changes to Java SDK +## Step 5: Apply Changes to Java SDK When porting changes: @@ -168,7 +172,7 @@ Follow the existing Java SDK patterns: - **Match the style of surrounding code** - Consistency with existing code is more important than upstream patterns - **Prefer existing abstractions** - If the Java SDK already solves a problem differently than .NET, keep the Java approach -## Step 7.5: Port Tests +## Step 6: Port Tests After porting implementation changes, **always check for new or updated tests** in the upstream repository: @@ -215,7 +219,7 @@ If tests fail with errors like `TypeError: Cannot read properties of undefined`, Commit tests separately or together with their corresponding implementation changes. -## Step 8: Format and Run Tests +## Step 7: Format and Run Tests After applying changes, use the convenience script: @@ -246,7 +250,7 @@ Or for quicker iteration during porting: - **Null handling**: Add null checks where C# had nullable types - **JSON serialization**: Verify Jackson annotations are correct -## Step 9: Build the Package +## Step 8: Build the Package Once tests pass, build the complete package: @@ -259,9 +263,11 @@ Verify: - No warnings (if possible) - JAR file is generated in `target/` -## Step 10: Update Documentation +## Step 9: Update Documentation **Documentation is critical for new features.** Every new feature ported from upstream must be documented before the merge is complete. +Do not proceed to the finish/push step until this documentation checklist is complete. +If you determine no docs changes are needed, explicitly document that decision and rationale in the PR body. ### Documentation Checklist @@ -316,7 +322,7 @@ Ensure consistency across all documentation files: - Code examples should use the same patterns and be tested - Links to Javadoc should use correct paths (`apidocs/...`) -## Steps 11-12: Finish, Push, and Finalize Pull Request +## Steps 10-11: Finish, Push, and Finalize Pull Request Run the finish script which updates `.lastmerge`, runs a final build, and pushes the branch: @@ -378,7 +384,7 @@ Ports changes from the official Copilot SDK ([github/copilot-sdk](https://github - Code formatted with Spotless ``` -## Step 13: Final Review +## Step 12: Final Review Before finishing: @@ -411,6 +417,7 @@ Before finishing: - [ ] `src/site/markdown/documentation.md` updated for new basic usage - [ ] `src/site/markdown/advanced.md` updated for new advanced features - [ ] Javadoc added/updated for new public APIs +- [ ] If no documentation files were changed, PR body explicitly explains why documentation changes were not needed - [ ] `src/site/site.xml` updated if new documentation pages were added - [ ] `.lastmerge` file updated with new commit hash - [ ] Branch pushed to remote @@ -429,4 +436,3 @@ Before finishing: - Uses JUnit 5 for testing - **Java SDK design decisions take precedence over upstream patterns** - **Adapt upstream changes to fit Java idioms, not the other way around** - From b01482f20d469b4a6a7891dc2935b6ae08a50804 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:45:16 +0000 Subject: [PATCH 069/104] Align upstream merge prompt step numbering and docs guidance Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../prompts/agentic-merge-upstream.prompt.md | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/prompts/agentic-merge-upstream.prompt.md b/.github/prompts/agentic-merge-upstream.prompt.md index c020b86a7..c1d2c54a0 100644 --- a/.github/prompts/agentic-merge-upstream.prompt.md +++ b/.github/prompts/agentic-merge-upstream.prompt.md @@ -37,13 +37,12 @@ All scripts write/read a `.merge-env` file (git-ignored) to share state (branch 7. Run `./.github/scripts/build/format-and-test.sh` frequently while porting 8. Build the package 9. Update documentation (**required for every user-facing upstream change**) -10. Run `./.github/scripts/upstream-sync/merge-upstream-finish.sh` (final test + push) -11. Finalize Pull Request (see note below about coding agent vs. manual workflow) -12. Perform final review before handing off +10. Run `./.github/scripts/upstream-sync/merge-upstream-finish.sh` (final test + push) and finalize Pull Request (see note below about coding agent vs. manual workflow) +11. Perform final review before handing off --- -## Steps 1-2: Initialize and Analyze +## Step 1: Initialize Upstream Sync Run the start script to create a branch, update the CLI, clone the upstream repo, and see a summary of new commits: @@ -57,7 +56,9 @@ This writes a `.merge-env` file used by the other scripts. It outputs: - The upstream dir path - A short log of upstream commits since `.lastmerge` -Then run the diff script for a detailed breakdown by area: +## Step 2: Analyze Upstream Changes + +Run the diff script for a detailed breakdown by area: ```bash ./.github/scripts/upstream-sync/merge-upstream-diff.sh # stat only @@ -266,8 +267,8 @@ Verify: ## Step 9: Update Documentation **Documentation is critical for new features.** Every new feature ported from upstream must be documented before the merge is complete. -Do not proceed to the finish/push step until this documentation checklist is complete. -If you determine no docs changes are needed, explicitly document that decision and rationale in the PR body. +Review and complete this documentation checklist before proceeding to Step 10. +If you determine no docs changes are needed, document that decision and rationale in the PR body under a clear heading (for example, `Documentation Impact`). ### Documentation Checklist @@ -322,7 +323,7 @@ Ensure consistency across all documentation files: - Code examples should use the same patterns and be tested - Links to Javadoc should use correct paths (`apidocs/...`) -## Steps 10-11: Finish, Push, and Finalize Pull Request +## Step 10: Finish, Push, and Finalize Pull Request Run the finish script which updates `.lastmerge`, runs a final build, and pushes the branch: @@ -384,7 +385,7 @@ Ports changes from the official Copilot SDK ([github/copilot-sdk](https://github - Code formatted with Spotless ``` -## Step 12: Final Review +## Step 11: Final Review Before finishing: @@ -417,7 +418,7 @@ Before finishing: - [ ] `src/site/markdown/documentation.md` updated for new basic usage - [ ] `src/site/markdown/advanced.md` updated for new advanced features - [ ] Javadoc added/updated for new public APIs -- [ ] If no documentation files were changed, PR body explicitly explains why documentation changes were not needed +- [ ] If no documentation files were changed for user-facing upstream changes, PR body explicitly explains why documentation changes were not needed - [ ] `src/site/site.xml` updated if new documentation pages were added - [ ] `.lastmerge` file updated with new commit hash - [ ] Branch pushed to remote From 8dc97a6d609aacab70310f626bfa7be784af1c50 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 3 Mar 2026 15:42:37 -0500 Subject: [PATCH 070/104] Update GPG secret names in publish workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename MAVEN_GPG_PRIVATE_KEY → GPG_SECRET_KEY and MAVEN_GPG_PASSPHRASE → GPG_PASSPHRASE to match updated repository secrets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/publish-maven.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml index 5546f7332..5596cbd11 100644 --- a/.github/workflows/publish-maven.yml +++ b/.github/workflows/publish-maven.yml @@ -59,8 +59,8 @@ jobs: server-id: central server-username: MAVEN_USERNAME server-password: MAVEN_PASSWORD - gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} - gpg-passphrase: MAVEN_GPG_PASSPHRASE + gpg-private-key: ${{ secrets.GPG_SECRET_KEY }} + gpg-passphrase: GPG_PASSPHRASE - name: Determine versions id: versions @@ -141,7 +141,7 @@ jobs: env: MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} MAVEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - name: Perform Release and Deploy to Maven Central run: | @@ -151,7 +151,7 @@ jobs: env: MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} MAVEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - name: Rollback documentation commit on failure if: failure() && steps.update-docs.outputs.docs_commit_sha != '' From c44e63eed3007227b8da67330ae9d7797cb4655d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 3 Mar 2026 20:52:21 +0000 Subject: [PATCH 071/104] docs: update version references to 1.0.10 --- CHANGELOG.md | 8 +++++++- README.md | 4 ++-- jbang-example.java | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae1fdef07..4845a214e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). > **Upstream sync:** [`github/copilot-sdk@dcd86c1`](https://github.com/github/copilot-sdk/commit/dcd86c189501ce1b46b787ca60d90f3f315f3079) +## [1.0.10] - 2026-03-03 + +> **Upstream sync:** [`github/copilot-sdk@dcd86c1`](https://github.com/github/copilot-sdk/commit/dcd86c189501ce1b46b787ca60d90f3f315f3079) ### Added - `CopilotSession.setModel(String)` — changes the model for an existing session mid-conversation; the new model takes effect for the next message, and conversation history is preserved (upstream: [`bd98e3a`](https://github.com/github/copilot-sdk/commit/bd98e3a)) @@ -406,7 +409,10 @@ New types: `GetForegroundSessionResponse`, `SetForegroundSessionResponse` - Pre-commit hook for Spotless code formatting - Comprehensive API documentation -[Unreleased]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.9...HEAD +[Unreleased]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.10...HEAD +[1.0.10]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.9...v1.0.10 +[Unreleased]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.10...HEAD +[1.0.10]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.9...v1.0.10 [1.0.9]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.8...v1.0.9 [1.0.8]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.7...v1.0.8 [1.0.7]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.6...v1.0.7 diff --git a/README.md b/README.md index adb9e0ea3..50d5cc4fd 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,14 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A io.github.copilot-community-sdk copilot-sdk - 1.0.9 + 1.0.10 ``` ### Gradle ```groovy -implementation 'io.github.copilot-community-sdk:copilot-sdk:1.0.9' +implementation 'io.github.copilot-community-sdk:copilot-sdk:1.0.10' ``` ## Quick Start diff --git a/jbang-example.java b/jbang-example.java index ca30b2c10..d0b307e4e 100644 --- a/jbang-example.java +++ b/jbang-example.java @@ -1,5 +1,5 @@ -//DEPS io.github.copilot-community-sdk:copilot-sdk:1.0.9 +//DEPS io.github.copilot-community-sdk:copilot-sdk:1.0.10 import com.github.copilot.sdk.*; import com.github.copilot.sdk.events.*; import com.github.copilot.sdk.json.*; From 41f6ece632591ddb9906d36ce964cbd567a763c5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 3 Mar 2026 20:52:43 +0000 Subject: [PATCH 072/104] [maven-release-plugin] prepare release v1.0.10 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 17344af45..19f69979c 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ io.github.copilot-community-sdk copilot-sdk - 1.0.10-SNAPSHOT + 1.0.10 jar GitHub Copilot Community SDK :: Java @@ -33,7 +33,7 @@ scm:git:https://github.com/copilot-community-sdk/copilot-sdk-java.git scm:git:https://github.com/copilot-community-sdk/copilot-sdk-java.git https://github.com/copilot-community-sdk/copilot-sdk-java - HEAD + v1.0.10 From 615c065cc073be1bc30a85fe2c431930ee8803f1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 3 Mar 2026 20:52:44 +0000 Subject: [PATCH 073/104] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 19f69979c..7ab031ce7 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ io.github.copilot-community-sdk copilot-sdk - 1.0.10 + 1.0.11-SNAPSHOT jar GitHub Copilot Community SDK :: Java @@ -33,7 +33,7 @@ scm:git:https://github.com/copilot-community-sdk/copilot-sdk-java.git scm:git:https://github.com/copilot-community-sdk/copilot-sdk-java.git https://github.com/copilot-community-sdk/copilot-sdk-java - v1.0.10 + HEAD From 13fca3f7937d41f7d40bdec01a4c3373703b621f Mon Sep 17 00:00:00 2001 From: Claudenir Freitas Date: Tue, 3 Mar 2026 22:31:12 +0000 Subject: [PATCH 074/104] issue 170: fix quick links --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 50d5cc4fd..5943afab4 100644 --- a/README.md +++ b/README.md @@ -102,9 +102,9 @@ jbang https://github.com/copilot-community-sdk/copilot-sdk-java/blob/latest/jban ### Quick Links -- [Getting Started](https://copilot-community-sdk.github.io/copilot-sdk-java/documentation.html) -- [Javadoc API Reference](https://copilot-community-sdk.github.io/copilot-sdk-java/apidocs/) -- [MCP Servers Integration](https://copilot-community-sdk.github.io/copilot-sdk-java/mcp.html) +- [Getting Started](https://copilot-community-sdk.github.io/copilot-sdk-java/latest/documentation.html) +- [Javadoc API Reference](https://copilot-community-sdk.github.io/copilot-sdk-java/latest/apidocs/) +- [MCP Servers Integration](https://copilot-community-sdk.github.io/copilot-sdk-java/latest/mcp.html) - [Cookbook](src/site/markdown/cookbook/) — Practical recipes for common use cases ## Projects Using This SDK From 90da1c97dbb0f48e1f6a70f461ae369b20b2f924 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Tue, 3 Mar 2026 18:21:19 -0500 Subject: [PATCH 075/104] On branch dd-2790313-pr-templates Adapted from https://github.com/octokit/octokit-next.js new file: .github/ISSUE_TEMPLATE/bug.yml new file: .github/ISSUE_TEMPLATE/documentation.yml new file: .github/ISSUE_TEMPLATE/feature.yml new file: .github/ISSUE_TEMPLATE/maintenance.yml new file: .github/pull_request_template.md --- .github/ISSUE_TEMPLATE/bug.yml | 41 ++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/documentation.yml | 41 ++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature.yml | 41 ++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/maintenance.yml | 41 ++++++++++++++++++++++++ .github/pull_request_template.md | 29 +++++++++++++++++ 5 files changed, 193 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.yml create mode 100644 .github/ISSUE_TEMPLATE/maintenance.yml create mode 100644 .github/pull_request_template.md diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 000000000..9e5a9238f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,41 @@ +name: Bug +description: File a bug report +title: "[BUG]: " +labels: ["Type: Bug", "Status: Triage"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: textarea + id: what-happened + attributes: + label: What happened? + description: What did you do? What happened? What did you expect to happen? + placeholder: Put your description of the bug here. + validations: + required: true + - type: textarea + id: versions + attributes: + label: Versions + description: What versions of the relevant software are you running? + placeholder: copilot-sdk-java v1.0.0, Java 17.0.10, Maven 3.9.6, Copilot CLI v1.0.0 + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: | + Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + Please check your logs before submission to ensure sensitive information is redacted. + render: shell + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 000000000..7f049a2ed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,41 @@ +name: Documentation +description: Update or add documentation +title: "[DOCS]: " +labels: ["Type: Documentation", "Status: Triage"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill this out! + - type: textarea + id: describe-need + attributes: + label: Describe the need + description: What do you wish was different about our docs? + placeholder: Describe the need for documentation updates here. + validations: + required: true + - type: input + id: sdk_version + attributes: + label: SDK Version + description: Do these docs apply to a specific SDK version? + placeholder: copilot-sdk-java v1.0.0 + validations: + required: false + - type: textarea + id: logs + attributes: + label: Relevant log output + description: | + Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + Please check your logs before submission to ensure sensitive information is redacted. + render: shell + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 000000000..377cd12ab --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,41 @@ +name: Feature +description: Suggest an idea for a new feature or enhancement +title: "[FEAT]: " +labels: ["Type: Feature", "Status: Triage"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill this out! + - type: textarea + id: describe-need + attributes: + label: Describe the need + description: What do you want to happen? What problem are you trying to solve? + placeholder: Describe the need for the feature. + validations: + required: true + - type: input + id: sdk_version + attributes: + label: SDK Version + description: Does this feature suggestion apply to a specific SDK version? + placeholder: copilot-sdk-java v1.0.0 + validations: + required: false + - type: textarea + id: logs + attributes: + label: Relevant log output + description: | + Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + Please check your logs before submission to ensure sensitive information is redacted. + render: shell + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/maintenance.yml b/.github/ISSUE_TEMPLATE/maintenance.yml new file mode 100644 index 000000000..de21996db --- /dev/null +++ b/.github/ISSUE_TEMPLATE/maintenance.yml @@ -0,0 +1,41 @@ +name: Maintenance +description: Dependencies, cleanup, refactoring, reworking of code +title: "[MAINT]: " +labels: ["Type: Maintenance", "Status: Triage"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill this out! + - type: textarea + id: describe-need + attributes: + label: Describe the need + description: What do you want to happen? + placeholder: Describe the maintenance need here. + validations: + required: true + - type: input + id: sdk_version + attributes: + label: SDK Version + description: Does this maintenance apply to a specific SDK version? + placeholder: copilot-sdk-java v1.0.0 + validations: + required: false + - type: textarea + id: logs + attributes: + label: Relevant log output + description: | + Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + Please check your logs before submission to ensure sensitive information is redacted. + render: shell + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..8993ca146 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,29 @@ + + +Resolves #ISSUE_NUMBER + +---- + +### Before the change? + + +* + +### After the change? + + +* + +### Pull request checklist +- [ ] Tests for the changes have been added (for bug fixes / features) +- [ ] Docs have been reviewed and added / updated if needed (for bug fixes / features) +- [ ] `mvn spotless:apply` has been run to format the code +- [ ] `mvn clean verify` passes locally + +### Does this introduce a breaking change? + + +- [ ] Yes +- [ ] No + +---- From b29869ae5d1a7c4d13ce18f9c2b151f35615ebd2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:57:34 +0000 Subject: [PATCH 076/104] Initial plan From c93a9905fa88a2cf8ef6ad4fa4ba8851d349a381 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:11:00 +0000 Subject: [PATCH 077/104] Update .lastmerge to 4246289e484d42155c75267660d448d9ac4f9158 --- .lastmerge | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lastmerge b/.lastmerge index 2a538cb8c..92e0dd366 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -dcd86c189501ce1b46b787ca60d90f3f315f3079 +4246289e484d42155c75267660d448d9ac4f9158 From b523d0a69da6ad8f175a7d75a04169bfca04211a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:11:44 +0000 Subject: [PATCH 078/104] Port upstream changes: PermissionRequestResultKind, CI detection fix, E2E test updates Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../copilot/sdk/RpcHandlerDispatcher.java | 5 +- .../copilot/sdk/json/PermissionHandler.java | 2 +- .../sdk/json/PermissionRequestResult.java | 29 ++- .../sdk/json/PermissionRequestResultKind.java | 99 ++++++++++ src/site/markdown/advanced.md | 26 ++- .../copilot/sdk/CopilotSessionTest.java | 82 +++----- .../github/copilot/sdk/E2ETestContext.java | 4 +- .../sdk/PermissionRequestResultKindTest.java | 124 ++++++++++++ .../copilot/sdk/StreamingFidelityTest.java | 185 ++++++++++++++++++ 9 files changed, 483 insertions(+), 73 deletions(-) create mode 100644 src/main/java/com/github/copilot/sdk/json/PermissionRequestResultKind.java create mode 100644 src/test/java/com/github/copilot/sdk/PermissionRequestResultKindTest.java create mode 100644 src/test/java/com/github/copilot/sdk/StreamingFidelityTest.java diff --git a/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java b/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java index 671aea593..2f041c7e4 100644 --- a/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java +++ b/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java @@ -17,6 +17,7 @@ import com.github.copilot.sdk.events.AbstractSessionEvent; import com.github.copilot.sdk.events.SessionEventParser; import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.PermissionRequestResultKind; import com.github.copilot.sdk.json.SessionLifecycleEvent; import com.github.copilot.sdk.json.SessionLifecycleEventMetadata; import com.github.copilot.sdk.json.ToolDefinition; @@ -183,7 +184,7 @@ private void handlePermissionRequest(JsonRpcClient rpc, String requestId, JsonNo CopilotSession session = sessions.get(sessionId); if (session == null) { var result = new PermissionRequestResult() - .setKind("denied-no-approval-rule-and-could-not-request-from-user"); + .setKind(PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER); rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result)); return; } @@ -197,7 +198,7 @@ private void handlePermissionRequest(JsonRpcClient rpc, String requestId, JsonNo }).exceptionally(ex -> { try { var result = new PermissionRequestResult() - .setKind("denied-no-approval-rule-and-could-not-request-from-user"); + .setKind(PermissionRequestResultKind.DENIED_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); diff --git a/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java b/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java index e987e41ae..21b822e41 100644 --- a/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java +++ b/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java @@ -46,7 +46,7 @@ public interface PermissionHandler { * @since 1.0.11 */ PermissionHandler APPROVE_ALL = (request, invocation) -> CompletableFuture - .completedFuture(new PermissionRequestResult().setKind("approved")); + .completedFuture(new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED)); /** * Handles a permission request from the assistant. diff --git a/src/main/java/com/github/copilot/sdk/json/PermissionRequestResult.java b/src/main/java/com/github/copilot/sdk/json/PermissionRequestResult.java index 12e2ec2c2..3d7390f03 100644 --- a/src/main/java/com/github/copilot/sdk/json/PermissionRequestResult.java +++ b/src/main/java/com/github/copilot/sdk/json/PermissionRequestResult.java @@ -17,13 +17,17 @@ * *

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
  • + *
  • {@link PermissionRequestResultKind#APPROVED} — approved
  • + *
  • {@link PermissionRequestResultKind#DENIED_BY_RULES} — denied by + * rules
  • + *
  • {@link PermissionRequestResultKind#DENIED_COULD_NOT_REQUEST_FROM_USER} — + * no handler and couldn't ask user
  • + *
  • {@link PermissionRequestResultKind#DENIED_INTERACTIVELY_BY_USER} — denied + * by the user interactively
  • *
* * @see PermissionHandler + * @see PermissionRequestResultKind * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) @@ -36,7 +40,7 @@ public final class PermissionRequestResult { private List rules; /** - * Gets the result kind. + * Gets the result kind as a string. * * @return the result kind indicating approval or denial */ @@ -45,11 +49,24 @@ public String getKind() { } /** - * Sets the result kind. + * Sets the result kind using a {@link PermissionRequestResultKind} value. * * @param kind * the result kind * @return this result for method chaining + * @since 1.1.0 + */ + public PermissionRequestResult setKind(PermissionRequestResultKind kind) { + this.kind = kind != null ? kind.getValue() : null; + return this; + } + + /** + * Sets the result kind using a raw string value. + * + * @param kind + * the result kind string + * @return this result for method chaining */ public PermissionRequestResult setKind(String kind) { this.kind = kind; diff --git a/src/main/java/com/github/copilot/sdk/json/PermissionRequestResultKind.java b/src/main/java/com/github/copilot/sdk/json/PermissionRequestResultKind.java new file mode 100644 index 000000000..f85a0df1c --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/PermissionRequestResultKind.java @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Describes the outcome kind of a permission request result. + * + *

+ * This is a string-backed value type that can hold both well-known kinds (via + * the static constants) and arbitrary extension values forwarded by the server. + * Comparisons are case-insensitive to match server behaviour. + * + *

Well-known kinds

+ *
    + *
  • {@link #APPROVED} — the permission was approved.
  • + *
  • {@link #DENIED_BY_RULES} — the permission was denied by policy + * rules.
  • + *
  • {@link #DENIED_COULD_NOT_REQUEST_FROM_USER} — the permission was denied + * because no approval rule was found and the user could not be prompted.
  • + *
  • {@link #DENIED_INTERACTIVELY_BY_USER} — the permission was denied + * interactively by the user.
  • + *
+ * + * @see PermissionRequestResult + * @since 1.1.0 + */ +public final class PermissionRequestResultKind { + + /** The permission was approved. */ + public static final PermissionRequestResultKind APPROVED = new PermissionRequestResultKind("approved"); + + /** The permission was denied by policy rules. */ + public static final PermissionRequestResultKind DENIED_BY_RULES = new PermissionRequestResultKind( + "denied-by-rules"); + + /** + * The permission was denied because no approval rule was found and the user + * could not be prompted. + */ + public static final PermissionRequestResultKind DENIED_COULD_NOT_REQUEST_FROM_USER = new PermissionRequestResultKind( + "denied-no-approval-rule-and-could-not-request-from-user"); + + /** The permission was denied interactively by the user. */ + public static final PermissionRequestResultKind DENIED_INTERACTIVELY_BY_USER = new PermissionRequestResultKind( + "denied-interactively-by-user"); + + private final String value; + + /** + * Creates a new {@code PermissionRequestResultKind} with the given string + * value. Useful for extension kinds not covered by the well-known constants. + * + * @param value + * the string value; {@code null} is treated as an empty string + */ + @JsonCreator + public PermissionRequestResultKind(String value) { + this.value = value != null ? value : ""; + } + + /** + * Returns the underlying string value of this kind. + * + * @return the string value, never {@code null} + */ + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof PermissionRequestResultKind)) { + return false; + } + PermissionRequestResultKind other = (PermissionRequestResultKind) obj; + return value.equalsIgnoreCase(other.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value.toLowerCase(java.util.Locale.ROOT)); + } +} diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index 1b8724e96..dc57f6258 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -557,16 +557,28 @@ Approve or deny permission requests from the AI. ```java var session = client.createSession( - new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setOnPermissionRequest((request, invocation) -> { - // Inspect request and approve/deny - var result = new PermissionRequestResult(); - result.setKind("user-approved"); - return CompletableFuture.completedFuture(result); - }) + new SessionConfig().setOnPermissionRequest((request, invocation) -> { + // Inspect request and approve/deny using typed constants + var result = new PermissionRequestResult(); + result.setKind(PermissionRequestResultKind.APPROVED); + return CompletableFuture.completedFuture(result); + }) ).get(); ``` +The `PermissionRequestResultKind` class provides well-known constants for common outcomes: + +| Constant | Value | Meaning | +|---|---|---| +| `PermissionRequestResultKind.APPROVED` | `"approved"` | The permission was approved | +| `PermissionRequestResultKind.DENIED_BY_RULES` | `"denied-by-rules"` | Denied by policy rules | +| `PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER` | `"denied-no-approval-rule-and-could-not-request-from-user"` | No rule and user could not be prompted | +| `PermissionRequestResultKind.DENIED_INTERACTIVELY_BY_USER` | `"denied-interactively-by-user"` | User denied interactively | + +You can also pass a raw string to `setKind(String)` for custom or extension values. Use +[`PermissionHandler.APPROVE_ALL`](apidocs/com/github/copilot/sdk/json/PermissionHandler.html) to approve all +requests without writing a handler. + --- ## Session Hooks diff --git a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java index 332dfe5b7..1dec8c881 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java @@ -23,7 +23,6 @@ import com.github.copilot.sdk.events.AbstractSessionEvent; import com.github.copilot.sdk.events.AbortEvent; -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; @@ -286,6 +285,14 @@ void testShouldResumeSessionUsingTheSameClient() throws Exception { .map(m -> (AssistantMessageEvent) m).anyMatch(m -> m.getData().content().contains("2")); assertTrue(hasAssistantMessage, "Should find previous assistant message containing 2"); + // Can continue the conversation statefully + AssistantMessageEvent answer2 = session2 + .sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?")) + .get(60, TimeUnit.SECONDS); + assertNotNull(answer2); + assertTrue(answer2.getData().content().contains("4"), + "Follow-up response should contain 4: " + answer2.getData().content()); + session2.close(); } } @@ -327,6 +334,14 @@ void testShouldResumeSessionUsingNewClient() throws Exception { assertTrue(messages.stream().anyMatch(m -> "session.resume".equals(m.getType())), "Should contain session.resume event"); + // Can continue the conversation statefully + AssistantMessageEvent answer2 = session2 + .sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?")) + .get(60, TimeUnit.SECONDS); + assertNotNull(answer2); + assertTrue(answer2.getData().content().contains("4"), + "Follow-up response should contain 4: " + answer2.getData().content()); + session2.close(); } } @@ -394,44 +409,6 @@ void testShouldCreateSessionWithReplacedSystemMessageConfig() throws Exception { } } - /** - * Verifies that streaming delta events are received when streaming is enabled. - * - * @see Snapshot: - * session/should_receive_streaming_delta_events_when_streaming_is_enabled - */ - @Test - void testShouldReceiveStreamingDeltaEventsWhenStreamingIsEnabled() throws Exception { - ctx.configureForTest("session", "should_receive_streaming_delta_events_when_streaming_is_enabled"); - - try (CopilotClient client = ctx.createClient()) { - SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setStreaming(true); - - CopilotSession session = client.createSession(config).get(); - - var receivedEvents = new ArrayList(); - var 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(); - } - } - /** * Verifies that a session can be aborted during tool execution. * @@ -764,29 +741,24 @@ void testShouldCreateSessionWithCustomTool() throws Exception { } /** - * Verifies that streaming option is passed to session creation. + * Verifies that getLastSessionId returns the ID of the most recently used + * session. * - * @see Snapshot: session/should_pass_streaming_option_to_session_creation + * @see Snapshot: session/should_get_last_session_id */ @Test - void testShouldPassStreamingOptionToSessionCreation() throws Exception { - ctx.configureForTest("session", "should_pass_streaming_option_to_session_creation"); + void testShouldGetLastSessionId() throws Exception { + ctx.configureForTest("session", "should_get_last_session_id"); try (CopilotClient client = ctx.createClient()) { - // Verify that the streaming option is accepted without errors - CopilotSession session = client.createSession( - new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(true)).get(); - - assertNotNull(session.getSessionId()); - assertTrue(session.getSessionId().matches("^[a-f0-9-]+$")); + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); - // Session should still work normally - AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, - TimeUnit.SECONDS); + session.sendAndWait(new MessageOptions().setPrompt("Say hello")).get(60, TimeUnit.SECONDS); - assertNotNull(response); - assertTrue(response.getData().content().contains("2"), - "Response should contain 2: " + response.getData().content()); + String lastId = client.getLastSessionId().get(30, TimeUnit.SECONDS); + assertNotNull(lastId, "Last session ID should not be null"); + assertEquals(session.getSessionId(), lastId, "Last session ID should match the current session ID"); session.close(); } diff --git a/src/test/java/com/github/copilot/sdk/E2ETestContext.java b/src/test/java/com/github/copilot/sdk/E2ETestContext.java index 20dfce7c7..74c65245b 100644 --- a/src/test/java/com/github/copilot/sdk/E2ETestContext.java +++ b/src/test/java/com/github/copilot/sdk/E2ETestContext.java @@ -263,8 +263,8 @@ public CopilotClient createClient() { CopilotClientOptions options = new CopilotClientOptions().setCliPath(cliPath).setCwd(workDir.toString()) .setEnvironment(getEnvironment()); - // In CI, use a fake token to avoid auth issues - String ci = System.getenv("CI"); + // In CI (GitHub Actions), use a fake token to avoid auth issues + String ci = System.getenv("GITHUB_ACTIONS"); if (ci != null && !ci.isEmpty()) { options.setGitHubToken("fake-token-for-e2e-tests"); } diff --git a/src/test/java/com/github/copilot/sdk/PermissionRequestResultKindTest.java b/src/test/java/com/github/copilot/sdk/PermissionRequestResultKindTest.java new file mode 100644 index 000000000..b21f96e83 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/PermissionRequestResultKindTest.java @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.PermissionRequestResultKind; + +/** + * Unit tests for {@link PermissionRequestResultKind}. + *

+ * Covers well-known kind values, equality, hash code, serialization, and + * backward-compatible {@link PermissionRequestResult} integration. + */ +public class PermissionRequestResultKindTest { + + @Test + void wellKnownKinds_haveExpectedValues() { + assertEquals("approved", PermissionRequestResultKind.APPROVED.getValue()); + assertEquals("denied-by-rules", PermissionRequestResultKind.DENIED_BY_RULES.getValue()); + assertEquals("denied-no-approval-rule-and-could-not-request-from-user", + PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER.getValue()); + assertEquals("denied-interactively-by-user", + PermissionRequestResultKind.DENIED_INTERACTIVELY_BY_USER.getValue()); + } + + @Test + void equals_sameValue_returnsTrue() { + var a = new PermissionRequestResultKind("approved"); + assertEquals(PermissionRequestResultKind.APPROVED, a); + assertEquals(a, PermissionRequestResultKind.APPROVED); + } + + @Test + void equals_differentValue_returnsFalse() { + assertNotEquals(PermissionRequestResultKind.APPROVED, PermissionRequestResultKind.DENIED_BY_RULES); + } + + @Test + void equals_isCaseInsensitive() { + var upper = new PermissionRequestResultKind("APPROVED"); + assertEquals(PermissionRequestResultKind.APPROVED, upper); + } + + @Test + void hashCode_isCaseInsensitive() { + var upper = new PermissionRequestResultKind("APPROVED"); + assertEquals(PermissionRequestResultKind.APPROVED.hashCode(), upper.hashCode()); + } + + @Test + void toString_returnsValue() { + assertEquals("approved", PermissionRequestResultKind.APPROVED.toString()); + assertEquals("denied-by-rules", PermissionRequestResultKind.DENIED_BY_RULES.toString()); + } + + @Test + void customValue_isPreserved() { + var custom = new PermissionRequestResultKind("custom-kind"); + assertEquals("custom-kind", custom.getValue()); + assertEquals("custom-kind", custom.toString()); + } + + @Test + void constructor_nullValue_treatedAsEmpty() { + var kind = new PermissionRequestResultKind(null); + assertEquals("", kind.getValue()); + assertEquals("", kind.toString()); + } + + @Test + void equals_nonKindObject_returnsFalse() { + assertNotEquals(PermissionRequestResultKind.APPROVED, "approved"); + } + + @Test + void jsonSerialize_writesStringValue() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + var result = new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED); + String json = mapper.writeValueAsString(result); + assertTrue(json.contains("\"kind\":\"approved\""), "Expected kind to be serialized as string: " + json); + } + + @Test + void jsonDeserialize_readsStringValue() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + String json = "{\"kind\":\"denied-by-rules\"}"; + var result = mapper.readValue(json, PermissionRequestResult.class); + assertEquals("denied-by-rules", result.getKind()); + } + + @Test + void permissionRequestResult_setKindWithKindType() { + var result = new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED); + assertEquals("approved", result.getKind()); + } + + @Test + void permissionRequestResult_setKindWithString_backwardCompatible() { + var result = new PermissionRequestResult().setKind("approved"); + assertEquals("approved", result.getKind()); + } + + @Test + void jsonRoundTrip_allWellKnownKinds() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + PermissionRequestResultKind[] kinds = {PermissionRequestResultKind.APPROVED, + PermissionRequestResultKind.DENIED_BY_RULES, + PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER, + PermissionRequestResultKind.DENIED_INTERACTIVELY_BY_USER,}; + for (PermissionRequestResultKind kind : kinds) { + var result = new PermissionRequestResult().setKind(kind); + String json = mapper.writeValueAsString(result); + var deserialized = mapper.readValue(json, PermissionRequestResult.class); + assertEquals(kind.getValue(), deserialized.getKind()); + } + } +} diff --git a/src/test/java/com/github/copilot/sdk/StreamingFidelityTest.java b/src/test/java/com/github/copilot/sdk/StreamingFidelityTest.java new file mode 100644 index 000000000..d6bae399d --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/StreamingFidelityTest.java @@ -0,0 +1,185 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +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 java.util.ArrayList; +import java.util.List; +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.AbstractSessionEvent; +import com.github.copilot.sdk.events.AssistantMessageDeltaEvent; +import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * E2E tests for streaming fidelity — verifying that delta events are produced + * when streaming is enabled and absent when it is disabled. + * + *

+ * Snapshots are stored in {@code test/snapshots/streaming_fidelity/}. + *

+ */ +public class StreamingFidelityTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Verifies that assistant.message_delta events are produced when streaming is + * enabled. + * + * @see Snapshot: + * streaming_fidelity/should_produce_delta_events_when_streaming_is_enabled + */ + @Test + void testShouldProduceDeltaEventsWhenStreamingIsEnabled() throws Exception { + ctx.configureForTest("streaming_fidelity", "should_produce_delta_events_when_streaming_is_enabled"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(true)).get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt("Count from 1 to 5, separated by commas.")).get(60, + TimeUnit.SECONDS); + + List types = events.stream().map(AbstractSessionEvent::getType).toList(); + + // Should have streaming deltas before the final message + List deltaEvents = events.stream() + .filter(e -> e instanceof AssistantMessageDeltaEvent).map(e -> (AssistantMessageDeltaEvent) e) + .toList(); + assertFalse(deltaEvents.isEmpty(), "Should have received delta events when streaming is enabled"); + + // Deltas should have content + for (AssistantMessageDeltaEvent delta : deltaEvents) { + assertFalse(delta.getData().deltaContent() == null || delta.getData().deltaContent().isEmpty(), + "Delta event should have content"); + } + + // Should still have a final assistant.message + assertTrue(types.contains("assistant.message"), "Should have a final assistant.message event"); + + // Deltas should come before the final message + int firstDeltaIdx = types.indexOf("assistant.message_delta"); + int lastAssistantIdx = types.lastIndexOf("assistant.message"); + assertTrue(firstDeltaIdx < lastAssistantIdx, "Delta events should come before the final assistant.message"); + + session.close(); + } + } + + /** + * Verifies that no delta events are produced when streaming is disabled. + * + * @see Snapshot: + * streaming_fidelity/should_not_produce_deltas_when_streaming_is_disabled + */ + @Test + void testShouldNotProduceDeltasWhenStreamingIsDisabled() throws Exception { + ctx.configureForTest("streaming_fidelity", "should_not_produce_deltas_when_streaming_is_disabled"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(false)) + .get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt("Say 'hello world'.")).get(60, TimeUnit.SECONDS); + + List deltaEvents = events.stream() + .filter(e -> e instanceof AssistantMessageDeltaEvent).map(e -> (AssistantMessageDeltaEvent) e) + .toList(); + + // No deltas when streaming is off + assertTrue(deltaEvents.isEmpty(), "Should not receive delta events when streaming is disabled"); + + // But should still have a final assistant.message + List assistantEvents = events.stream() + .filter(e -> e instanceof AssistantMessageEvent).map(e -> (AssistantMessageEvent) e).toList(); + assertFalse(assistantEvents.isEmpty(), + "Should still have a final assistant.message when streaming is disabled"); + + session.close(); + } + } + + /** + * Verifies that delta events are produced after resuming a session with + * streaming enabled. + * + * @see Snapshot: streaming_fidelity/should_produce_deltas_after_session_resume + */ + @Test + void testShouldProduceDeltasAfterSessionResume() throws Exception { + ctx.configureForTest("streaming_fidelity", "should_produce_deltas_after_session_resume"); + + try (CopilotClient client = ctx.createClient()) { + // Create a non-streaming session and send an initial message + CopilotSession session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(false)) + .get(); + session.sendAndWait(new MessageOptions().setPrompt("What is 3 + 6?")).get(60, TimeUnit.SECONDS); + String sessionId = session.getSessionId(); + session.close(); + + // Resume using a new client with streaming enabled + try (CopilotClient newClient = ctx.createClient()) { + CopilotSession session2 = newClient.resumeSession(sessionId, new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(true)).get(); + + List events = new ArrayList<>(); + session2.on(events::add); + + AssistantMessageEvent answer = session2 + .sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?")) + .get(60, TimeUnit.SECONDS); + assertNotNull(answer); + assertTrue(answer.getData().content().contains("18"), + "Follow-up response should contain 18: " + answer.getData().content()); + + // Should have streaming deltas before the final message + List deltaEvents = events.stream() + .filter(e -> e instanceof AssistantMessageDeltaEvent).map(e -> (AssistantMessageDeltaEvent) e) + .toList(); + assertFalse(deltaEvents.isEmpty(), "Should have received delta events after session resume"); + + // Deltas should have content + for (AssistantMessageDeltaEvent delta : deltaEvents) { + assertFalse(delta.getData().deltaContent() == null || delta.getData().deltaContent().isEmpty(), + "Delta event should have content"); + } + + session2.close(); + } + } + } +} From b5e56efe8253000b3a050623ebaa22b739fc7d16 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 23:21:47 +0000 Subject: [PATCH 079/104] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 738a6ad1e..c80aa997f 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 88.6% - 88.6% + 88.9% + 88.9% From 3b84ce811f40f2e03b5539db7ca9e68fb40c9db9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:49:55 +0000 Subject: [PATCH 080/104] Initial plan From d74170f4faefdb4f973e9b9a3c7678858f263953 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:59:16 +0000 Subject: [PATCH 081/104] Update .lastmerge to 4e1499dd23709022c720eaaa5457d00bf0cb3977 --- .lastmerge | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lastmerge b/.lastmerge index 92e0dd366..df5b85477 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -4246289e484d42155c75267660d448d9ac4f9158 +4e1499dd23709022c720eaaa5457d00bf0cb3977 From 1daa34649fb9b5146d62002062b1af8c677e0453 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:01:05 +0000 Subject: [PATCH 082/104] docs: clarify session close vs delete semantics (upstream sync #599) Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../com/github/copilot/sdk/CopilotClient.java | 20 ++++++++++++++++--- .../github/copilot/sdk/CopilotSession.java | 7 +++++++ src/site/markdown/documentation.md | 8 +++++++- .../copilot/sdk/CopilotSessionTest.java | 2 +- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index 5c4fcfe21..023051edd 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -208,7 +208,18 @@ private void verifyProtocolVersion(Connection connection) throws Exception { } /** - * Stops the client and closes all sessions. + * Disconnects from the Copilot server and closes all active sessions. + *

+ * This method performs graceful cleanup: + *

    + *
  1. Closes all active sessions (releases in-memory resources)
  2. + *
  3. Closes the JSON-RPC connection
  4. + *
  5. Terminates the CLI server process (if spawned by this client)
  6. + *
+ *

+ * Note: session data on disk is preserved, so sessions can be resumed later. To + * permanently remove session data before stopping, call + * {@link #deleteSession(String)} for each session first. * * @return A future that completes when the client is stopped */ @@ -469,9 +480,12 @@ public CompletableFuture getLastSessionId() { } /** - * Deletes a session by ID. + * Permanently deletes a session and all its data from disk, including + * conversation history, planning state, and artifacts. *

- * This permanently removes the session and its conversation history. + * Unlike {@link CopilotSession#close()}, which only releases in-memory + * resources and preserves session data for later resumption, this method is + * irreversible. The session cannot be resumed after deletion. * * @param sessionId * the ID of the session to delete diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index 1af22d803..80b2e85ea 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -58,6 +58,13 @@ * A session maintains conversation state, handles events, and manages tool * execution. Sessions are created via {@link CopilotClient#createSession} or * resumed via {@link CopilotClient#resumeSession}. + *

+ * {@code CopilotSession} implements {@link AutoCloseable}. Use the + * try-with-resources pattern for automatic cleanup, or call {@link #close()} + * explicitly. Closing a session releases in-memory resources but preserves + * session data on disk — the conversation can be resumed later via + * {@link CopilotClient#resumeSession}. To permanently delete session data, use + * {@link CopilotClient#deleteSession}. * *

Example Usage

* diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index 01aba950f..26f17ce0f 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -617,8 +617,14 @@ See [ResumeSessionConfig](apidocs/com/github/copilot/sdk/json/ResumeSessionConfi ### Clean Up Sessions +Closing a session releases its in-memory resources but **preserves session data on disk**, so +it can be resumed later. Use `deleteSession()` to permanently remove session data from disk: + ```java -// Delete a specific session +// Close a session (releases in-memory resources; session can be resumed later) +session.close(); + +// Permanently delete a session and all its data from disk (cannot be resumed) client.deleteSession(sessionId).get(); ``` diff --git a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java index 1dec8c881..787312cef 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java @@ -60,7 +60,7 @@ static void teardown() throws Exception { } /** - * Verifies that a session can be created and destroyed properly. + * Verifies that a session can be created and closed properly. * * @see Snapshot: session/should_receive_session_events */ From 1bfb71ac64840053ba70f0bbf0218cc36b354503 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:56:15 +0000 Subject: [PATCH 083/104] Initial plan From 2aea3a02eb0f008d6c77b15067ac700dc0e83409 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:48:34 +0000 Subject: [PATCH 084/104] Port upstream sync: protocol v3, OnListModels, agent, onEvent, new events, register-before-RPC Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .lastmerge | 2 +- CHANGELOG.md | 17 +- .../com/github/copilot/sdk/CopilotClient.java | 73 +++++- .../github/copilot/sdk/CopilotSession.java | 209 +++++++++++++++++- .../copilot/sdk/SdkProtocolVersion.java | 2 +- .../copilot/sdk/SessionRequestBuilder.java | 30 ++- .../sdk/events/AbstractSessionEvent.java | 4 + .../sdk/events/CommandCompletedEvent.java | 37 ++++ .../sdk/events/CommandQueuedEvent.java | 38 ++++ .../events/ExitPlanModeCompletedEvent.java | 37 ++++ .../events/ExitPlanModeRequestedEvent.java | 39 ++++ .../events/ExternalToolCompletedEvent.java | 40 ++++ .../events/ExternalToolRequestedEvent.java | 43 ++++ .../sdk/events/PermissionCompletedEvent.java | 45 ++++ .../sdk/events/PermissionRequestedEvent.java | 43 ++++ .../sdk/events/SessionEventParser.java | 9 + .../sdk/events/SystemNotificationEvent.java | 37 ++++ .../sdk/json/CopilotClientOptions.java | 30 +++ .../sdk/json/CreateSessionRequest.java | 13 ++ .../copilot/sdk/json/ResumeSessionConfig.java | 56 +++++ .../sdk/json/ResumeSessionRequest.java | 13 ++ .../copilot/sdk/json/SessionConfig.java | 59 +++++ .../github/copilot/sdk/ConfigCloneTest.java | 48 +++- .../github/copilot/sdk/CopilotClientTest.java | 65 ++++++ .../github/copilot/sdk/MetadataApiTest.java | 4 +- .../copilot/sdk/SessionEventParserTest.java | 182 +++++++++++++++ .../sdk/SessionRequestBuilderTest.java | 14 ++ 27 files changed, 1169 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/github/copilot/sdk/events/CommandCompletedEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/events/CommandQueuedEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/events/ExitPlanModeCompletedEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/events/ExitPlanModeRequestedEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/events/ExternalToolCompletedEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/events/ExternalToolRequestedEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/events/PermissionCompletedEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/events/PermissionRequestedEvent.java create mode 100644 src/main/java/com/github/copilot/sdk/events/SystemNotificationEvent.java diff --git a/.lastmerge b/.lastmerge index df5b85477..fd2e8b676 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -4e1499dd23709022c720eaaa5457d00bf0cb3977 +9a0a1a5f21111f4ad02b5ce911750ecc75e054c3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4845a214e..b95104175 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] -> **Upstream sync:** [`github/copilot-sdk@dcd86c1`](https://github.com/github/copilot-sdk/commit/dcd86c189501ce1b46b787ca60d90f3f315f3079) +> **Upstream sync:** [`github/copilot-sdk@9a0a1a5`](https://github.com/github/copilot-sdk/commit/9a0a1a5f21111f4ad02b5ce911750ecc75e054c3) + +### Added + +- `CopilotClientOptions.setOnListModels(Supplier>>)` — custom handler for `listModels()` used in BYOK mode to return models from a custom provider instead of querying the CLI (upstream: [`e478657`](https://github.com/github/copilot-sdk/commit/e478657)) +- `SessionConfig.setAgent(String)` — pre-selects a custom agent by name when creating a session (upstream: [`7766b1a`](https://github.com/github/copilot-sdk/commit/7766b1a)) +- `ResumeSessionConfig.setAgent(String)` — pre-selects a custom agent by name when resuming a session (upstream: [`7766b1a`](https://github.com/github/copilot-sdk/commit/7766b1a)) +- `SessionConfig.setOnEvent(Consumer)` — registers an event handler before the `session.create` RPC is issued, ensuring no early events are missed (upstream: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7)) +- `ResumeSessionConfig.setOnEvent(Consumer)` — registers an event handler before the `session.resume` RPC is issued (upstream: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7)) +- New broadcast session event types (protocol v3): `ExternalToolRequestedEvent` (`external_tool.requested`), `ExternalToolCompletedEvent` (`external_tool.completed`), `PermissionRequestedEvent` (`permission.requested`), `PermissionCompletedEvent` (`permission.completed`), `CommandQueuedEvent` (`command.queued`), `CommandCompletedEvent` (`command.completed`), `ExitPlanModeRequestedEvent` (`exit_plan_mode.requested`), `ExitPlanModeCompletedEvent` (`exit_plan_mode.completed`), `SystemNotificationEvent` (`system.notification`) (upstream: [`1653812`](https://github.com/github/copilot-sdk/commit/1653812), [`396e8b3`](https://github.com/github/copilot-sdk/commit/396e8b3)) +- `CopilotSession.log(String)` and `CopilotSession.log(String, String, Boolean)` — log a message to the session timeline (upstream: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7)) + +### Changed + +- **Protocol version bumped to v3.** The SDK now supports CLI servers running v2 or v3 (backward-compatible range). Sessions are now registered in the client's session map *before* the `session.create`/`session.resume` RPC is issued, ensuring broadcast events emitted immediately on session start are never dropped (upstream: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7), [`1653812`](https://github.com/github/copilot-sdk/commit/1653812)) +- In protocol v3, tool calls and permission requests that have a registered handler are now handled automatically via `ExternalToolRequestedEvent` and `PermissionRequestedEvent` broadcast events; results are sent back via `session.tools.handlePendingToolCall` and `session.permissions.handlePendingPermissionRequest` RPC calls (upstream: [`1653812`](https://github.com/github/copilot-sdk/commit/1653812)) ## [1.0.10] - 2026-03-03 diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index 023051edd..39034c910 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -188,6 +188,8 @@ private CompletableFuture startCore() { }); } + private static final int MIN_PROTOCOL_VERSION = 2; + private void verifyProtocolVersion(Connection connection) throws Exception { int expectedVersion = SdkProtocolVersion.get(); var params = new HashMap(); @@ -200,9 +202,10 @@ private void verifyProtocolVersion(Connection connection) throws Exception { + "Please update your server to ensure compatibility."); } - if (pingResponse.protocolVersion() != expectedVersion) { + int serverVersion = pingResponse.protocolVersion(); + if (serverVersion < MIN_PROTOCOL_VERSION || serverVersion > expectedVersion) { throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion - + ", but server reports version " + pingResponse.protocolVersion() + ". " + + " (minimum " + MIN_PROTOCOL_VERSION + "), but server reports version " + serverVersion + ". " + "Please update your SDK or server to ensure compatibility."); } } @@ -319,13 +322,32 @@ public CompletableFuture createSession(SessionConfig config) { + "new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)")); } return ensureConnected().thenCompose(connection -> { - var request = SessionRequestBuilder.buildCreateRequest(config); + // Pre-generate session ID so the session can be registered before the RPC call, + // ensuring no events emitted by the CLI during creation are lost. + String sessionId = config.getSessionId() != null + ? config.getSessionId() + : java.util.UUID.randomUUID().toString(); + + var session = new CopilotSession(sessionId, connection.rpc); + SessionRequestBuilder.configureSession(session, config); + sessions.put(sessionId, session); + + var request = SessionRequestBuilder.buildCreateRequest(config, sessionId); return connection.rpc.invoke("session.create", request, CreateSessionResponse.class).thenApply(response -> { - var session = new CopilotSession(response.sessionId(), connection.rpc, response.workspacePath()); - SessionRequestBuilder.configureSession(session, config); - sessions.put(response.sessionId(), session); + session.setWorkspacePath(response.workspacePath()); + // If the server returned a different sessionId (e.g. a v2 CLI that ignores + // the client-supplied ID), re-key the sessions map. + String returnedId = response.sessionId(); + if (returnedId != null && !returnedId.equals(sessionId)) { + sessions.remove(sessionId); + session.setActiveSessionId(returnedId); + sessions.put(returnedId, session); + } return session; + }).exceptionally(ex -> { + sessions.remove(sessionId); + throw ex instanceof RuntimeException re ? re : new RuntimeException(ex); }); }); } @@ -363,13 +385,26 @@ public CompletableFuture resumeSession(String sessionId, ResumeS + "new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)")); } return ensureConnected().thenCompose(connection -> { + // Register the session before the RPC call to avoid missing early events. + var session = new CopilotSession(sessionId, connection.rpc); + SessionRequestBuilder.configureSession(session, config); + sessions.put(sessionId, session); + var request = SessionRequestBuilder.buildResumeRequest(sessionId, config); return connection.rpc.invoke("session.resume", request, ResumeSessionResponse.class).thenApply(response -> { - var session = new CopilotSession(response.sessionId(), connection.rpc, response.workspacePath()); - SessionRequestBuilder.configureSession(session, config); - sessions.put(response.sessionId(), session); + session.setWorkspacePath(response.workspacePath()); + // If the server returned a different sessionId than what was requested, re-key. + String returnedId = response.sessionId(); + if (returnedId != null && !returnedId.equals(sessionId)) { + sessions.remove(sessionId); + session.setActiveSessionId(returnedId); + sessions.put(returnedId, session); + } return session; + }).exceptionally(ex -> { + sessions.remove(sessionId); + throw ex instanceof RuntimeException re ? re : new RuntimeException(ex); }); }); } @@ -434,6 +469,10 @@ public CompletableFuture getAuthStatus() { *

* Results are cached after the first successful call to avoid rate limiting. * The cache is cleared when the client disconnects. + *

+ * If an {@code onListModels} handler was provided in + * {@link com.github.copilot.sdk.json.CopilotClientOptions}, it is called + * instead of querying the CLI server. This is useful in BYOK mode. * * @return a future that resolves with a list of available models * @see ModelInfo @@ -445,6 +484,22 @@ public CompletableFuture> listModels() { return CompletableFuture.completedFuture(new ArrayList<>(cached)); } + // If a custom handler is configured, use it instead of querying the CLI server + var onListModels = options.getOnListModels(); + if (onListModels != null) { + synchronized (modelsCacheLock) { + if (modelsCache != null) { + return CompletableFuture.completedFuture(new ArrayList<>(modelsCache)); + } + } + return onListModels.get().thenApply(models -> { + synchronized (modelsCacheLock) { + modelsCache = models; + } + return new ArrayList<>(models); + }); + } + return ensureConnected().thenCompose(connection -> { // Double-check cache inside lock synchronized (modelsCacheLock) { diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index 80b2e85ea..c19fe72f3 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -27,6 +27,8 @@ 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.ExternalToolRequestedEvent; +import com.github.copilot.sdk.events.PermissionRequestedEvent; import com.github.copilot.sdk.events.SessionErrorEvent; import com.github.copilot.sdk.events.SessionEventParser; import com.github.copilot.sdk.events.SessionIdleEvent; @@ -46,6 +48,7 @@ import com.github.copilot.sdk.json.SessionHooks; import com.github.copilot.sdk.json.SessionStartHookInput; import com.github.copilot.sdk.json.ToolDefinition; +import com.github.copilot.sdk.json.ToolResultObject; import com.github.copilot.sdk.json.UserInputHandler; import com.github.copilot.sdk.json.UserInputInvocation; import com.github.copilot.sdk.json.UserInputRequest; @@ -100,8 +103,14 @@ 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 String workspacePath; + /** + * The current active session ID. Initialized to the pre-generated value and may + * be updated after session.create / session.resume if the server returns a + * different ID (e.g. when working against a v2 CLI that ignores the + * client-supplied sessionId). + */ + private volatile String sessionId; + private volatile String workspacePath; private final JsonRpcClient rpc; private final Set> eventHandlers = ConcurrentHashMap.newKeySet(); private final Map toolHandlers = new ConcurrentHashMap<>(); @@ -157,6 +166,18 @@ public String getSessionId() { return sessionId; } + /** + * Updates the active session ID. Package-private; called by CopilotClient if + * the server returns a different session ID than the pre-generated one (e.g. + * when a v2 CLI ignores the client-supplied sessionId). + * + * @param sessionId + * the server-confirmed session ID + */ + void setActiveSessionId(String sessionId) { + this.sessionId = sessionId; + } + /** * Gets the path to the session workspace directory when infinite sessions are * enabled. @@ -170,6 +191,17 @@ public String getWorkspacePath() { return workspacePath; } + /** + * Sets the workspace path. Package-private; called by CopilotClient after + * session.create or session.resume RPC response. + * + * @param workspacePath + * the workspace path + */ + void setWorkspacePath(String workspacePath) { + this.workspacePath = workspacePath; + } + /** * Sets a custom error handler for exceptions thrown by event handlers. *

@@ -551,6 +583,10 @@ public Closeable on(Class eventType, Consume * @see #setEventErrorPolicy(EventErrorPolicy) */ void dispatchEvent(AbstractSessionEvent event) { + // Handle broadcast request events (protocol v3) before dispatching to user + // handlers. These are fire-and-forget: the response is sent asynchronously. + handleBroadcastEventAsync(event); + for (Consumer handler : eventHandlers) { try { handler.accept(event); @@ -572,6 +608,120 @@ void dispatchEvent(AbstractSessionEvent event) { } } + /** + * Handles broadcast request events by executing local handlers and responding + * via RPC (protocol v3). + *

+ * Fire-and-forget: the response is sent asynchronously. + * + * @param event + * the event to handle + */ + private void handleBroadcastEventAsync(AbstractSessionEvent event) { + if (event instanceof ExternalToolRequestedEvent toolEvent) { + var data = toolEvent.getData(); + if (data == null || data.requestId() == null || data.toolName() == null) { + return; + } + ToolDefinition tool = getTool(data.toolName()); + if (tool == null) { + return; // This client doesn't handle this tool; another client will + } + executeToolAndRespondAsync(data.requestId(), data.toolName(), data.toolCallId(), data.arguments(), tool); + + } else if (event instanceof PermissionRequestedEvent permEvent) { + var data = permEvent.getData(); + if (data == null || data.requestId() == null || data.permissionRequest() == null) { + return; + } + PermissionHandler handler = permissionHandler.get(); + if (handler == null) { + return; // This client doesn't handle permissions; another client will + } + executePermissionAndRespondAsync(data.requestId(), data.permissionRequest(), handler); + } + } + + /** + * Executes a tool handler and sends the result back via + * {@code session.tools.handlePendingToolCall}. + */ + private void executeToolAndRespondAsync(String requestId, String toolName, String toolCallId, Object arguments, + ToolDefinition tool) { + CompletableFuture.runAsync(() -> { + try { + JsonNode argumentsNode = arguments instanceof JsonNode jn + ? jn + : (arguments != null ? MAPPER.valueToTree(arguments) : null); + var invocation = new com.github.copilot.sdk.json.ToolInvocation().setSessionId(sessionId) + .setToolCallId(toolCallId).setToolName(toolName).setArguments(argumentsNode); + + tool.handler().invoke(invocation).thenAccept(result -> { + try { + ToolResultObject toolResult; + if (result instanceof ToolResultObject tr) { + toolResult = tr; + } else { + toolResult = ToolResultObject + .success(result instanceof String s ? s : MAPPER.writeValueAsString(result)); + } + rpc.invoke("session.tools.handlePendingToolCall", + Map.of("sessionId", sessionId, "requestId", requestId, "result", toolResult), + Object.class); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending tool result for requestId=" + requestId, e); + } + }).exceptionally(ex -> { + try { + rpc.invoke( + "session.tools.handlePendingToolCall", Map.of("sessionId", sessionId, "requestId", + requestId, "error", ex.getMessage() != null ? ex.getMessage() : ex.toString()), + Object.class); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending tool error for requestId=" + requestId, e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error executing tool for requestId=" + requestId, e); + } + }); + } + + /** + * Executes a permission handler and sends the result back via + * {@code session.permissions.handlePendingPermissionRequest}. + */ + private void executePermissionAndRespondAsync(String requestId, PermissionRequest permissionRequest, + PermissionHandler handler) { + CompletableFuture.runAsync(() -> { + try { + var invocation = new PermissionInvocation(); + invocation.setSessionId(sessionId); + handler.handle(permissionRequest, invocation).thenAccept(result -> { + try { + rpc.invoke("session.permissions.handlePendingPermissionRequest", + Map.of("sessionId", sessionId, "requestId", requestId, "result", result), Object.class); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending permission result for requestId=" + requestId, e); + } + }).exceptionally(ex -> { + try { + PermissionRequestResult denied = new PermissionRequestResult(); + denied.setKind("denied-could-not-request-from-user"); + rpc.invoke("session.permissions.handlePendingPermissionRequest", + Map.of("sessionId", sessionId, "requestId", requestId, "result", denied), Object.class); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending permission denied for requestId=" + requestId, e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error executing permission handler for requestId=" + requestId, e); + } + }); + } + /** * Registers custom tool handlers for this session. *

@@ -837,6 +987,61 @@ public CompletableFuture setModel(String model) { return rpc.invoke("session.model.switchTo", Map.of("sessionId", sessionId, "modelId", model), Void.class); } + /** + * Logs a message to the session timeline. + *

+ * The message appears in the session event stream and is visible to SDK + * consumers. Non-ephemeral messages are also persisted to the session event log + * on disk. + * + *

Example Usage

+ * + *
{@code
+     * session.log("Build completed successfully").get();
+     * session.log("Disk space low", "warning", null).get();
+     * session.log("Temporary status", null, true).get();
+     * }
+ * + * @param message + * the message to log + * @param level + * the log severity level ({@code "info"}, {@code "warning"}, + * {@code "error"}), or {@code null} to use the default + * ({@code "info"}) + * @param ephemeral + * when {@code true}, the message is transient and not persisted to + * disk; {@code null} uses default behavior + * @return a future that completes when the message is logged + * @throws IllegalStateException + * if this session has been terminated + */ + public CompletableFuture log(String message, String level, Boolean ephemeral) { + ensureNotTerminated(); + var params = new java.util.HashMap(); + params.put("sessionId", sessionId); + params.put("message", message); + if (level != null) { + params.put("level", level); + } + if (ephemeral != null) { + params.put("ephemeral", ephemeral); + } + return rpc.invoke("session.log", params, Void.class); + } + + /** + * Logs an informational message to the session timeline. + * + * @param message + * the message to log + * @return a future that completes when the message is logged + * @throws IllegalStateException + * if this session has been terminated + */ + public CompletableFuture log(String message) { + return log(message, null, null); + } + /** * Lists the custom agents available for selection in this session. * diff --git a/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java b/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java index 40a7f9d56..3b00a88ae 100644 --- a/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java +++ b/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java @@ -14,7 +14,7 @@ */ public enum SdkProtocolVersion { - LATEST(2); + LATEST(3); private int versionNumber; diff --git a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java index 90f3c71d8..7ea0be880 100644 --- a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java +++ b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java @@ -27,20 +27,22 @@ private SessionRequestBuilder() { * * @param config * the session configuration (may be null) + * @param sessionId + * the pre-generated session ID to use * @return the built request object */ - static CreateSessionRequest buildCreateRequest(SessionConfig config) { + static CreateSessionRequest buildCreateRequest(SessionConfig config, String sessionId) { var request = new CreateSessionRequest(); // Always request permission callbacks to enable deny-by-default behavior request.setRequestPermission(true); // Always send envValueMode=direct for MCP servers request.setEnvValueMode("direct"); + request.setSessionId(sessionId); if (config == null) { return request; } request.setModel(config.getModel()); - request.setSessionId(config.getSessionId()); request.setClientName(config.getClientName()); request.setReasoningEffort(config.getReasoningEffort()); request.setTools(config.getTools()); @@ -54,6 +56,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config) { request.setStreaming(config.isStreaming() ? true : null); request.setMcpServers(config.getMcpServers()); request.setCustomAgents(config.getCustomAgents()); + request.setAgent(config.getAgent()); request.setInfiniteSessions(config.getInfiniteSessions()); request.setSkillDirectories(config.getSkillDirectories()); request.setDisabledSkills(config.getDisabledSkills()); @@ -62,6 +65,22 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config) { return request; } + /** + * Builds a CreateSessionRequest from the given configuration. + * + * @param config + * the session configuration (may be null) + * @return the built request object + * @deprecated Use {@link #buildCreateRequest(SessionConfig, String)} instead. + */ + @Deprecated + static CreateSessionRequest buildCreateRequest(SessionConfig config) { + String sessionId = (config != null && config.getSessionId() != null) + ? config.getSessionId() + : java.util.UUID.randomUUID().toString(); + return buildCreateRequest(config, sessionId); + } + /** * Builds a ResumeSessionRequest from the given session ID and configuration. * @@ -99,6 +118,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setStreaming(config.isStreaming() ? true : null); request.setMcpServers(config.getMcpServers()); request.setCustomAgents(config.getCustomAgents()); + request.setAgent(config.getAgent()); request.setSkillDirectories(config.getSkillDirectories()); request.setDisabledSkills(config.getDisabledSkills()); request.setInfiniteSessions(config.getInfiniteSessions()); @@ -131,6 +151,9 @@ static void configureSession(CopilotSession session, SessionConfig config) { if (config.getHooks() != null) { session.registerHooks(config.getHooks()); } + if (config.getOnEvent() != null) { + session.on(config.getOnEvent()); + } } /** @@ -158,5 +181,8 @@ static void configureSession(CopilotSession session, ResumeSessionConfig config) if (config.getHooks() != null) { session.registerHooks(config.getHooks()); } + if (config.getOnEvent() != null) { + session.on(config.getOnEvent()); + } } } diff --git a/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java b/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java index 6c9d76e99..5127f6eee 100644 --- a/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java @@ -63,6 +63,10 @@ public abstract sealed class AbstractSessionEvent permits // Tool events ToolUserRequestedEvent, ToolExecutionStartEvent, ToolExecutionPartialResultEvent, ToolExecutionProgressEvent, ToolExecutionCompleteEvent, + // Broadcast request/completion events (protocol v3) + ExternalToolRequestedEvent, ExternalToolCompletedEvent, PermissionRequestedEvent, PermissionCompletedEvent, + CommandQueuedEvent, CommandCompletedEvent, ExitPlanModeRequestedEvent, ExitPlanModeCompletedEvent, + SystemNotificationEvent, // User events UserMessageEvent, PendingMessagesModifiedEvent, // Skill events diff --git a/src/main/java/com/github/copilot/sdk/events/CommandCompletedEvent.java b/src/main/java/com/github/copilot/sdk/events/CommandCompletedEvent.java new file mode 100644 index 000000000..f9aeb0f3f --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/CommandCompletedEvent.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event: command.completed + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class CommandCompletedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private CommandCompletedData data; + + @Override + public String getType() { + return "command.completed"; + } + + public CommandCompletedData getData() { + return data; + } + + public void setData(CommandCompletedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record CommandCompletedData(@JsonProperty("requestId") String requestId) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/CommandQueuedEvent.java b/src/main/java/com/github/copilot/sdk/events/CommandQueuedEvent.java new file mode 100644 index 000000000..acd35a89c --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/CommandQueuedEvent.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event: command.queued + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class CommandQueuedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private CommandQueuedData data; + + @Override + public String getType() { + return "command.queued"; + } + + public CommandQueuedData getData() { + return data; + } + + public void setData(CommandQueuedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record CommandQueuedData(@JsonProperty("requestId") String requestId, + @JsonProperty("command") String command) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/ExitPlanModeCompletedEvent.java b/src/main/java/com/github/copilot/sdk/events/ExitPlanModeCompletedEvent.java new file mode 100644 index 000000000..217859e43 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/ExitPlanModeCompletedEvent.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * 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: exit_plan_mode.completed + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ExitPlanModeCompletedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private ExitPlanModeCompletedData data; + + @Override + public String getType() { + return "exit_plan_mode.completed"; + } + + public ExitPlanModeCompletedData getData() { + return data; + } + + public void setData(ExitPlanModeCompletedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record ExitPlanModeCompletedData(@JsonProperty("requestId") String requestId) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/ExitPlanModeRequestedEvent.java b/src/main/java/com/github/copilot/sdk/events/ExitPlanModeRequestedEvent.java new file mode 100644 index 000000000..b0019c3ce --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/ExitPlanModeRequestedEvent.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * 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: exit_plan_mode.requested + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ExitPlanModeRequestedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private ExitPlanModeRequestedData data; + + @Override + public String getType() { + return "exit_plan_mode.requested"; + } + + public ExitPlanModeRequestedData getData() { + return data; + } + + public void setData(ExitPlanModeRequestedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record ExitPlanModeRequestedData(@JsonProperty("requestId") String requestId, + @JsonProperty("summary") String summary, @JsonProperty("planContent") String planContent, + @JsonProperty("actions") String[] actions, @JsonProperty("recommendedAction") String recommendedAction) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/ExternalToolCompletedEvent.java b/src/main/java/com/github/copilot/sdk/events/ExternalToolCompletedEvent.java new file mode 100644 index 000000000..83a582720 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/ExternalToolCompletedEvent.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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: external_tool.completed + *

+ * Broadcast when a pending tool call has been resolved by a client (protocol + * v3). + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ExternalToolCompletedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private ExternalToolCompletedData data; + + @Override + public String getType() { + return "external_tool.completed"; + } + + public ExternalToolCompletedData getData() { + return data; + } + + public void setData(ExternalToolCompletedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record ExternalToolCompletedData(@JsonProperty("requestId") String requestId) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/ExternalToolRequestedEvent.java b/src/main/java/com/github/copilot/sdk/events/ExternalToolRequestedEvent.java new file mode 100644 index 000000000..8eb11f5b8 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/ExternalToolRequestedEvent.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event: external_tool.requested + *

+ * Broadcast when the CLI needs a client to handle a tool call (protocol v3). + * Clients that own the named tool should respond via + * {@code session.tools.handlePendingToolCall}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ExternalToolRequestedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private ExternalToolRequestedData data; + + @Override + public String getType() { + return "external_tool.requested"; + } + + public ExternalToolRequestedData getData() { + return data; + } + + public void setData(ExternalToolRequestedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record ExternalToolRequestedData(@JsonProperty("requestId") String requestId, + @JsonProperty("sessionId") String sessionId, @JsonProperty("toolCallId") String toolCallId, + @JsonProperty("toolName") String toolName, @JsonProperty("arguments") Object arguments) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/PermissionCompletedEvent.java b/src/main/java/com/github/copilot/sdk/events/PermissionCompletedEvent.java new file mode 100644 index 000000000..90daf3b49 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/PermissionCompletedEvent.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * 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: permission.completed + *

+ * Broadcast when a pending permission request has been resolved by a client + * (protocol v3). + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class PermissionCompletedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private PermissionCompletedData data; + + @Override + public String getType() { + return "permission.completed"; + } + + public PermissionCompletedData getData() { + return data; + } + + public void setData(PermissionCompletedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record PermissionCompletedData(@JsonProperty("requestId") String requestId, + @JsonProperty("result") PermissionCompletedResult result) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record PermissionCompletedResult(@JsonProperty("kind") String kind) { + } + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/PermissionRequestedEvent.java b/src/main/java/com/github/copilot/sdk/events/PermissionRequestedEvent.java new file mode 100644 index 000000000..d8f9ec147 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/PermissionRequestedEvent.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.sdk.json.PermissionRequest; + +/** + * Event: permission.requested + *

+ * Broadcast when the CLI needs a client to handle a permission request + * (protocol v3). Clients that have a permission handler should respond via + * {@code session.permissions.handlePendingPermissionRequest}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class PermissionRequestedEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private PermissionRequestedData data; + + @Override + public String getType() { + return "permission.requested"; + } + + public PermissionRequestedData getData() { + return data; + } + + public void setData(PermissionRequestedData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record PermissionRequestedData(@JsonProperty("requestId") String requestId, + @JsonProperty("permissionRequest") PermissionRequest permissionRequest) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java b/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java index cfe8d5711..75971b29e 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java @@ -93,6 +93,15 @@ public class SessionEventParser { TYPE_MAP.put("system.message", SystemMessageEvent.class); TYPE_MAP.put("session.shutdown", SessionShutdownEvent.class); TYPE_MAP.put("skill.invoked", SkillInvokedEvent.class); + TYPE_MAP.put("external_tool.requested", ExternalToolRequestedEvent.class); + TYPE_MAP.put("external_tool.completed", ExternalToolCompletedEvent.class); + TYPE_MAP.put("permission.requested", PermissionRequestedEvent.class); + TYPE_MAP.put("permission.completed", PermissionCompletedEvent.class); + TYPE_MAP.put("command.queued", CommandQueuedEvent.class); + TYPE_MAP.put("command.completed", CommandCompletedEvent.class); + TYPE_MAP.put("exit_plan_mode.requested", ExitPlanModeRequestedEvent.class); + TYPE_MAP.put("exit_plan_mode.completed", ExitPlanModeCompletedEvent.class); + TYPE_MAP.put("system.notification", SystemNotificationEvent.class); } /** diff --git a/src/main/java/com/github/copilot/sdk/events/SystemNotificationEvent.java b/src/main/java/com/github/copilot/sdk/events/SystemNotificationEvent.java new file mode 100644 index 000000000..38711f276 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/SystemNotificationEvent.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * 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: system.notification + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SystemNotificationEvent extends AbstractSessionEvent { + + @JsonProperty("data") + private SystemNotificationData data; + + @Override + public String getType() { + return "system.notification"; + } + + public SystemNotificationData getData() { + return data; + } + + public void setData(SystemNotificationData data) { + this.data = data; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record SystemNotificationData(@JsonProperty("content") String content, @JsonProperty("kind") Object kind) { + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java index 70ce99850..4fd55d3ba 100644 --- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -4,7 +4,10 @@ package com.github.copilot.sdk.json; +import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; import com.fasterxml.jackson.annotation.JsonInclude; @@ -43,6 +46,7 @@ public class CopilotClientOptions { private Map environment; private String gitHubToken; private Boolean useLoggedInUser; + private Supplier>> onListModels; /** * Gets the path to the Copilot CLI executable. @@ -349,6 +353,31 @@ public CopilotClientOptions setUseLoggedInUser(Boolean useLoggedInUser) { return this; } + /** + * Gets the custom handler for listing available models. + * + * @return the handler, or {@code null} if not set + */ + public Supplier>> getOnListModels() { + return onListModels; + } + + /** + * Sets a custom handler for listing available models. + *

+ * When provided, {@code listModels()} calls this handler instead of querying + * the CLI server. Useful in BYOK (Bring Your Own Key) mode to return models + * available from your custom provider. + * + * @param onListModels + * the handler that returns the list of available models + * @return this options instance for method chaining + */ + public CopilotClientOptions setOnListModels(Supplier>> onListModels) { + this.onListModels = onListModels; + return this; + } + /** * Creates a shallow clone of this {@code CopilotClientOptions} instance. *

@@ -374,6 +403,7 @@ public CopilotClientOptions clone() { copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null; copy.gitHubToken = this.gitHubToken; copy.useLoggedInUser = this.useLoggedInUser; + copy.onListModels = this.onListModels; return copy; } } diff --git a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java index d73d82e6a..c0243f14b 100644 --- a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java @@ -76,6 +76,9 @@ public final class CreateSessionRequest { @JsonProperty("customAgents") private List customAgents; + @JsonProperty("agent") + private String agent; + @JsonProperty("infiniteSessions") private InfiniteSessionConfig infiniteSessions; @@ -260,6 +263,16 @@ public void setCustomAgents(List customAgents) { this.customAgents = customAgents; } + /** Gets the pre-selected agent name. @return the agent name */ + public String getAgent() { + return agent; + } + + /** Sets the pre-selected agent name. @param agent the agent name */ + public void setAgent(String agent) { + this.agent = agent; + } + /** Gets infinite sessions config. @return the config */ public InfiniteSessionConfig getInfiniteSessions() { return infiniteSessions; diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java index 0682699bc..eab3c789c 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java @@ -8,9 +8,12 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import com.fasterxml.jackson.annotation.JsonInclude; +import com.github.copilot.sdk.events.AbstractSessionEvent; + /** * Configuration for resuming an existing Copilot session. *

@@ -50,9 +53,11 @@ public class ResumeSessionConfig { private boolean streaming; private Map mcpServers; private List customAgents; + private String agent; private List skillDirectories; private List disabledSkills; private InfiniteSessionConfig infiniteSessions; + private Consumer onEvent; /** * Gets the AI model to use. @@ -436,6 +441,29 @@ public ResumeSessionConfig setCustomAgents(List customAgents) return this; } + /** + * Gets the name of the custom agent to activate at session start. + * + * @return the agent name, or {@code null} if not set + */ + public String getAgent() { + return agent; + } + + /** + * Sets the name of the custom agent to activate when the session starts. + *

+ * Must match the name of one of the agents in {@link #setCustomAgents(List)}. + * + * @param agent + * the agent name to pre-select + * @return this config for method chaining + */ + public ResumeSessionConfig setAgent(String agent) { + this.agent = agent; + return this; + } + /** * Gets the skill directories. * @@ -501,6 +529,32 @@ public ResumeSessionConfig setInfiniteSessions(InfiniteSessionConfig infiniteSes return this; } + /** + * Gets the event handler registered before the session.resume RPC is issued. + * + * @return the event handler, or {@code null} if not set + */ + public Consumer getOnEvent() { + return onEvent; + } + + /** + * Sets an event handler that is registered on the session before the + * {@code session.resume} RPC is issued. + *

+ * Equivalent to calling + * {@link com.github.copilot.sdk.CopilotSession#on(Consumer)} immediately after + * resumption, but executes earlier in the lifecycle so no events are missed. + * + * @param onEvent + * the event handler to register before session resumption + * @return this config for method chaining + */ + public ResumeSessionConfig setOnEvent(Consumer onEvent) { + this.onEvent = onEvent; + return this; + } + /** * Creates a shallow clone of this {@code ResumeSessionConfig} instance. *

@@ -532,9 +586,11 @@ public ResumeSessionConfig clone() { copy.streaming = this.streaming; copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; + copy.agent = this.agent; copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null; copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; copy.infiniteSessions = this.infiniteSessions; + copy.onEvent = this.onEvent; return copy; } } diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java index 4216e5eef..31d88399a 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java @@ -83,6 +83,9 @@ public final class ResumeSessionRequest { @JsonProperty("customAgents") private List customAgents; + @JsonProperty("agent") + private String agent; + @JsonProperty("skillDirectories") private List skillDirectories; @@ -287,6 +290,16 @@ public void setCustomAgents(List customAgents) { this.customAgents = customAgents; } + /** Gets the pre-selected agent name. @return the agent name */ + public String getAgent() { + return agent; + } + + /** Sets the pre-selected agent name. @param agent the agent name */ + public void setAgent(String agent) { + this.agent = agent; + } + /** Gets skill directories. @return the directories */ public List getSkillDirectories() { return skillDirectories == null ? null : Collections.unmodifiableList(skillDirectories); diff --git a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java index bfed0608e..76c15660d 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java @@ -8,9 +8,12 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import com.fasterxml.jackson.annotation.JsonInclude; +import com.github.copilot.sdk.events.AbstractSessionEvent; + /** * Configuration for creating a new Copilot session. *

@@ -49,10 +52,12 @@ public class SessionConfig { private boolean streaming; private Map mcpServers; private List customAgents; + private String agent; private InfiniteSessionConfig infiniteSessions; private List skillDirectories; private List disabledSkills; private String configDir; + private Consumer onEvent; /** * Gets the custom session ID. @@ -438,6 +443,29 @@ public SessionConfig setCustomAgents(List customAgents) { return this; } + /** + * Gets the name of the custom agent to activate at session start. + * + * @return the agent name, or {@code null} if not set + */ + public String getAgent() { + return agent; + } + + /** + * Sets the name of the custom agent to activate when the session starts. + *

+ * Must match the name of one of the agents in {@link #setCustomAgents(List)}. + * + * @param agent + * the agent name to pre-select + * @return this config instance for method chaining + */ + public SessionConfig setAgent(String agent) { + this.agent = agent; + return this; + } + /** * Gets the infinite sessions configuration. * @@ -538,6 +566,35 @@ public SessionConfig setConfigDir(String configDir) { return this; } + /** + * Gets the event handler registered before the session.create RPC is issued. + * + * @return the event handler, or {@code null} if not set + */ + public Consumer getOnEvent() { + return onEvent; + } + + /** + * Sets an event handler that is registered on the session before the + * {@code session.create} RPC is issued. + *

+ * Equivalent to calling + * {@link com.github.copilot.sdk.CopilotSession#on(Consumer)} immediately after + * creation, but executes earlier in the lifecycle so no events are missed. + * Using this property rather than {@code CopilotSession.on()} guarantees that + * early events emitted by the CLI during session creation (e.g. + * {@code session.start}) are delivered to the handler. + * + * @param onEvent + * the event handler to register before session creation + * @return this config instance for method chaining + */ + public SessionConfig setOnEvent(Consumer onEvent) { + this.onEvent = onEvent; + return this; + } + /** * Creates a shallow clone of this {@code SessionConfig} instance. *

@@ -568,10 +625,12 @@ public SessionConfig clone() { copy.streaming = this.streaming; copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; + copy.agent = this.agent; copy.infiniteSessions = this.infiniteSessions; copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null; copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; copy.configDir = this.configDir; + copy.onEvent = this.onEvent; return copy; } } diff --git a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java index e1269a669..f3eceb4c2 100644 --- a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java +++ b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java @@ -10,13 +10,17 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; import org.junit.jupiter.api.Test; +import com.github.copilot.sdk.events.AbstractSessionEvent; import com.github.copilot.sdk.json.CopilotClientOptions; -import com.github.copilot.sdk.json.SessionConfig; -import com.github.copilot.sdk.json.ResumeSessionConfig; import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.ModelInfo; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; class ConfigCloneTest { @@ -68,6 +72,18 @@ void copilotClientOptionsEnvironmentIndependence() { assertEquals(2, original.getEnvironment().size()); } + @Test + void copilotClientOptionsOnListModelsCloned() { + CopilotClientOptions original = new CopilotClientOptions(); + List models = List.of(new ModelInfo()); + original.setOnListModels(() -> CompletableFuture.completedFuture(models)); + + CopilotClientOptions cloned = original.clone(); + + assertNotNull(cloned.getOnListModels()); + assertSame(original.getOnListModels(), cloned.getOnListModels()); + } + @Test void sessionConfigCloneBasic() { SessionConfig original = new SessionConfig(); @@ -102,6 +118,20 @@ void sessionConfigListIndependence() { assertEquals(3, original.getAvailableTools().size()); } + @Test + void sessionConfigAgentAndOnEventCloned() { + Consumer handler = event -> { + }; + SessionConfig original = new SessionConfig(); + original.setAgent("my-agent"); + original.setOnEvent(handler); + + SessionConfig cloned = original.clone(); + + assertEquals("my-agent", cloned.getAgent()); + assertSame(handler, cloned.getOnEvent()); + } + @Test void resumeSessionConfigCloneBasic() { ResumeSessionConfig original = new ResumeSessionConfig(); @@ -114,6 +144,20 @@ void resumeSessionConfigCloneBasic() { assertEquals(original.isStreaming(), cloned.isStreaming()); } + @Test + void resumeSessionConfigAgentAndOnEventCloned() { + Consumer handler = event -> { + }; + ResumeSessionConfig original = new ResumeSessionConfig(); + original.setAgent("my-agent"); + original.setOnEvent(handler); + + ResumeSessionConfig cloned = original.clone(); + + assertEquals("my-agent", cloned.getAgent()); + assertSame(handler, cloned.getOnEvent()); + } + @Test void messageOptionsCloneBasic() { MessageOptions original = new MessageOptions(); diff --git a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java index 1f4f5193b..bc5869fda 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -494,4 +494,69 @@ void testNullOptionsDefaultsToEmpty() { assertEquals(ConnectionState.DISCONNECTED, client.getState()); } } + + // ===== OnListModels ===== + + @Test + void testListModels_WithCustomHandler_CallsHandler() throws Exception { + var customModels = new ArrayList(); + var model = new com.github.copilot.sdk.json.ModelInfo(); + model.setId("my-custom-model"); + customModels.add(model); + + var callCount = new int[]{0}; + var options = new CopilotClientOptions().setOnListModels(() -> { + callCount[0]++; + return CompletableFuture.completedFuture(new ArrayList<>(customModels)); + }); + + try (var client = new CopilotClient(options)) { + var models = client.listModels().get(); + assertEquals(1, callCount[0]); + assertEquals(1, models.size()); + assertEquals("my-custom-model", models.get(0).getId()); + } + } + + @Test + void testListModels_WithCustomHandler_CachesResults() throws Exception { + var customModels = new ArrayList(); + var model = new com.github.copilot.sdk.json.ModelInfo(); + model.setId("cached-model"); + customModels.add(model); + + var callCount = new int[]{0}; + var options = new CopilotClientOptions().setOnListModels(() -> { + callCount[0]++; + return CompletableFuture.completedFuture(new ArrayList<>(customModels)); + }); + + try (var client = new CopilotClient(options)) { + client.listModels().get(); + client.listModels().get(); + assertEquals(1, callCount[0], "Handler should be called only once due to caching"); + } + } + + @Test + void testListModels_WithCustomHandler_WorksWithoutStart() throws Exception { + var customModels = new ArrayList(); + var model = new com.github.copilot.sdk.json.ModelInfo(); + model.setId("no-start-model"); + customModels.add(model); + + var callCount = new int[]{0}; + var options = new CopilotClientOptions().setOnListModels(() -> { + callCount[0]++; + return CompletableFuture.completedFuture(new ArrayList<>(customModels)); + }); + + // No start() needed when onListModels is provided + try (var client = new CopilotClient(options)) { + var models = client.listModels().get(); + assertEquals(1, callCount[0]); + assertEquals(1, models.size()); + assertEquals("no-start-model", models.get(0).getId()); + } + } } diff --git a/src/test/java/com/github/copilot/sdk/MetadataApiTest.java b/src/test/java/com/github/copilot/sdk/MetadataApiTest.java index b3eb8fcb7..580c58153 100644 --- a/src/test/java/com/github/copilot/sdk/MetadataApiTest.java +++ b/src/test/java/com/github/copilot/sdk/MetadataApiTest.java @@ -327,7 +327,7 @@ void testListModels() throws Exception { // ===== Protocol Version Test ===== @Test - void testProtocolVersionIsTwo() { - assertEquals(2, SdkProtocolVersion.get()); + void testProtocolVersionIsThree() { + assertEquals(3, SdkProtocolVersion.get()); } } diff --git a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java index 65129ce7e..d4770a721 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java @@ -2198,4 +2198,186 @@ void testParseJsonNodeUserMessageWithAttachment() throws Exception { assertEquals(0, att.selection().start().line()); assertEquals(14, att.selection().end().character()); } + + @Test + void testParseExternalToolRequestedEvent() throws Exception { + String json = """ + { + "type": "external_tool.requested", + "data": { + "requestId": "req-123", + "sessionId": "sess-456", + "toolCallId": "call-789", + "toolName": "get_weather", + "arguments": {"location": "Seattle"} + } + } + """; + + var event = (ExternalToolRequestedEvent) parseJson(json); + assertNotNull(event); + assertEquals("external_tool.requested", event.getType()); + assertNotNull(event.getData()); + assertEquals("req-123", event.getData().requestId()); + assertEquals("sess-456", event.getData().sessionId()); + assertEquals("call-789", event.getData().toolCallId()); + assertEquals("get_weather", event.getData().toolName()); + } + + @Test + void testParseExternalToolCompletedEvent() throws Exception { + String json = """ + { + "type": "external_tool.completed", + "data": { + "requestId": "req-123" + } + } + """; + + var event = (ExternalToolCompletedEvent) parseJson(json); + assertNotNull(event); + assertEquals("external_tool.completed", event.getType()); + assertEquals("req-123", event.getData().requestId()); + } + + @Test + void testParsePermissionRequestedEvent() throws Exception { + String json = """ + { + "type": "permission.requested", + "data": { + "requestId": "perm-req-456", + "permissionRequest": { + "kind": "shell", + "toolCallId": "call-001" + } + } + } + """; + + var event = (PermissionRequestedEvent) parseJson(json); + assertNotNull(event); + assertEquals("permission.requested", event.getType()); + assertEquals("perm-req-456", event.getData().requestId()); + assertNotNull(event.getData().permissionRequest()); + assertEquals("shell", event.getData().permissionRequest().getKind()); + } + + @Test + void testParsePermissionCompletedEvent() throws Exception { + String json = """ + { + "type": "permission.completed", + "data": { + "requestId": "perm-req-456", + "result": { + "kind": "approved" + } + } + } + """; + + var event = (PermissionCompletedEvent) parseJson(json); + assertNotNull(event); + assertEquals("permission.completed", event.getType()); + assertEquals("perm-req-456", event.getData().requestId()); + assertEquals("approved", event.getData().result().kind()); + } + + @Test + void testParseCommandQueuedEvent() throws Exception { + String json = """ + { + "type": "command.queued", + "data": { + "requestId": "cmd-req-789", + "command": "/help" + } + } + """; + + var event = (CommandQueuedEvent) parseJson(json); + assertNotNull(event); + assertEquals("command.queued", event.getType()); + assertEquals("cmd-req-789", event.getData().requestId()); + assertEquals("/help", event.getData().command()); + } + + @Test + void testParseCommandCompletedEvent() throws Exception { + String json = """ + { + "type": "command.completed", + "data": { + "requestId": "cmd-req-789" + } + } + """; + + var event = (CommandCompletedEvent) parseJson(json); + assertNotNull(event); + assertEquals("command.completed", event.getType()); + assertEquals("cmd-req-789", event.getData().requestId()); + } + + @Test + void testParseExitPlanModeRequestedEvent() throws Exception { + String json = """ + { + "type": "exit_plan_mode.requested", + "data": { + "requestId": "plan-req-001", + "summary": "Plan is ready", + "planContent": "## Plan\\n1. Do thing", + "actions": ["approve", "edit", "reject"], + "recommendedAction": "approve" + } + } + """; + + var event = (ExitPlanModeRequestedEvent) parseJson(json); + assertNotNull(event); + assertEquals("exit_plan_mode.requested", event.getType()); + assertEquals("plan-req-001", event.getData().requestId()); + assertEquals("Plan is ready", event.getData().summary()); + assertEquals(3, event.getData().actions().length); + assertEquals("approve", event.getData().recommendedAction()); + } + + @Test + void testParseExitPlanModeCompletedEvent() throws Exception { + String json = """ + { + "type": "exit_plan_mode.completed", + "data": { + "requestId": "plan-req-001" + } + } + """; + + var event = (ExitPlanModeCompletedEvent) parseJson(json); + assertNotNull(event); + assertEquals("exit_plan_mode.completed", event.getType()); + assertEquals("plan-req-001", event.getData().requestId()); + } + + @Test + void testParseSystemNotificationEvent() throws Exception { + String json = """ + { + "type": "system.notification", + "data": { + "content": "Agent completed", + "kind": {"type": "agent_completed", "agentId": "agent-1", "agentType": "task", "status": "completed"} + } + } + """; + + var event = (SystemNotificationEvent) parseJson(json); + assertNotNull(event); + assertEquals("system.notification", event.getType()); + assertNotNull(event.getData()); + assertTrue(event.getData().content().contains("Agent completed")); + } } diff --git a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java index 7e9d5ee69..c00355211 100644 --- a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java @@ -237,4 +237,18 @@ private CopilotSession createTestSession() throws Exception { constructor.setAccessible(true); return constructor.newInstance("builder-test-session", null, null); } + + @Test + void testBuildCreateRequestWithAgent() { + var config = new SessionConfig().setAgent("my-agent"); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config, "test-session-id"); + assertEquals("my-agent", request.getAgent()); + } + + @Test + void testBuildResumeRequestWithAgent() { + var config = new ResumeSessionConfig().setAgent("my-agent"); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("session-id", config); + assertEquals("my-agent", request.getAgent()); + } } From c67265bfa532c2ce41e7c401f8c23042e1776c82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:22:36 +0000 Subject: [PATCH 085/104] Refactor handleBroadcastEventAsync to use Java 21 pattern matching switch; bump minimum Java to 21 Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- .github/workflows/build-test.yml | 2 +- .github/workflows/publish-maven.yml | 2 +- README.md | 4 +- pom.xml | 2 +- .../github/copilot/sdk/CopilotSession.java | 42 +++++++++++-------- src/site/markdown/getting-started.md | 2 +- src/site/markdown/index.md | 2 +- 7 files changed, 31 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 5749900f6..761b85917 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -38,7 +38,7 @@ jobs: node-version: 22 - uses: actions/setup-java@v5 with: - java-version: "17" + java-version: "21" distribution: "temurin" cache: "maven" diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml index 5596cbd11..fa748c4a2 100644 --- a/.github/workflows/publish-maven.yml +++ b/.github/workflows/publish-maven.yml @@ -53,7 +53,7 @@ jobs: - name: Set up JDK 17 uses: actions/setup-java@v5 with: - java-version: "17" + java-version: "21" distribution: "temurin" cache: "maven" server-id: central diff --git a/README.md b/README.md index 5943afab4..98d7d59c1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Site](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/deploy-site.yml/badge.svg)](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/deploy-site.yml) [![Coverage](.github/badges/jacoco.svg)](https://copilot-community-sdk.github.io/copilot-sdk-java/snapshot/jacoco/index.html) [![Documentation](https://img.shields.io/badge/docs-online-brightgreen)](https://copilot-community-sdk.github.io/copilot-sdk-java/) -[![Java 17+](https://img.shields.io/badge/Java-17%2B-blue?logo=openjdk&logoColor=white)](https://openjdk.org/) +[![Java 21+](https://img.shields.io/badge/Java-21%2B-blue?logo=openjdk&logoColor=white)](https://openjdk.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) #### Latest release @@ -24,7 +24,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A ### Requirements -- Java 17 or later +- Java 21 or later - GitHub Copilot CLI 0.0.411-1 or later installed and in PATH (or provide custom `cliPath`) ### Maven diff --git a/pom.xml b/pom.xml index 7ab031ce7..225b5b1da 100644 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,7 @@ - 17 + 21 UTF-8 ${project.build.directory}/copilot-sdk diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index c19fe72f3..c49deac5a 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -618,27 +618,33 @@ void dispatchEvent(AbstractSessionEvent event) { * the event to handle */ private void handleBroadcastEventAsync(AbstractSessionEvent event) { - if (event instanceof ExternalToolRequestedEvent toolEvent) { - var data = toolEvent.getData(); - if (data == null || data.requestId() == null || data.toolName() == null) { - return; - } - ToolDefinition tool = getTool(data.toolName()); - if (tool == null) { - return; // This client doesn't handle this tool; another client will + switch (event) { + case ExternalToolRequestedEvent toolEvent -> { + var data = toolEvent.getData(); + if (data == null || data.requestId() == null || data.toolName() == null) { + return; + } + ToolDefinition tool = getTool(data.toolName()); + if (tool == null) { + return; // This client doesn't handle this tool; another client will + } + executeToolAndRespondAsync(data.requestId(), data.toolName(), data.toolCallId(), data.arguments(), + tool); } - executeToolAndRespondAsync(data.requestId(), data.toolName(), data.toolCallId(), data.arguments(), tool); - - } else if (event instanceof PermissionRequestedEvent permEvent) { - var data = permEvent.getData(); - if (data == null || data.requestId() == null || data.permissionRequest() == null) { - return; + case PermissionRequestedEvent permEvent -> { + var data = permEvent.getData(); + if (data == null || data.requestId() == null || data.permissionRequest() == null) { + return; + } + PermissionHandler handler = permissionHandler.get(); + if (handler == null) { + return; // This client doesn't handle permissions; another client will + } + executePermissionAndRespondAsync(data.requestId(), data.permissionRequest(), handler); } - PermissionHandler handler = permissionHandler.get(); - if (handler == null) { - return; // This client doesn't handle permissions; another client will + default -> { + // Other event types are not handled here } - executePermissionAndRespondAsync(data.requestId(), data.permissionRequest(), handler); } } diff --git a/src/site/markdown/getting-started.md b/src/site/markdown/getting-started.md index 49281a4fc..8bb9c84b2 100644 --- a/src/site/markdown/getting-started.md +++ b/src/site/markdown/getting-started.md @@ -18,7 +18,7 @@ Copilot: In Tokyo it's 75°F and sunny. Great day to be outside! Before you begin, make sure you have: -- **Java 17+** installed +- **Java 21+** installed - **GitHub Copilot CLI** installed and authenticated ([Installation guide](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)) Verify the CLI is working: diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 27abc27c0..4a0918ce1 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -8,7 +8,7 @@ Welcome to the documentation for the **Copilot SDK for Java** — a Java SDK for ### Requirements -- Java 17 or later +- Java 21 or later - GitHub Copilot CLI 0.0.411-1 or later installed and in PATH (or provide custom `cliPath`) ### Installation From 563e0c3343c764224760fdee02519cfb7d66b441 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:34:24 +0000 Subject: [PATCH 086/104] Revert Java 21 bump and pattern-matching switch; restore Java 17 and if/else instanceof Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- .github/workflows/build-test.yml | 2 +- .github/workflows/publish-maven.yml | 2 +- README.md | 4 +- pom.xml | 2 +- .../github/copilot/sdk/CopilotSession.java | 42 ++++++++----------- src/site/markdown/getting-started.md | 2 +- src/site/markdown/index.md | 2 +- 7 files changed, 25 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 761b85917..5749900f6 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -38,7 +38,7 @@ jobs: node-version: 22 - uses: actions/setup-java@v5 with: - java-version: "21" + java-version: "17" distribution: "temurin" cache: "maven" diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml index fa748c4a2..5596cbd11 100644 --- a/.github/workflows/publish-maven.yml +++ b/.github/workflows/publish-maven.yml @@ -53,7 +53,7 @@ jobs: - name: Set up JDK 17 uses: actions/setup-java@v5 with: - java-version: "21" + java-version: "17" distribution: "temurin" cache: "maven" server-id: central diff --git a/README.md b/README.md index 98d7d59c1..5943afab4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Site](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/deploy-site.yml/badge.svg)](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/deploy-site.yml) [![Coverage](.github/badges/jacoco.svg)](https://copilot-community-sdk.github.io/copilot-sdk-java/snapshot/jacoco/index.html) [![Documentation](https://img.shields.io/badge/docs-online-brightgreen)](https://copilot-community-sdk.github.io/copilot-sdk-java/) -[![Java 21+](https://img.shields.io/badge/Java-21%2B-blue?logo=openjdk&logoColor=white)](https://openjdk.org/) +[![Java 17+](https://img.shields.io/badge/Java-17%2B-blue?logo=openjdk&logoColor=white)](https://openjdk.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) #### Latest release @@ -24,7 +24,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A ### Requirements -- Java 21 or later +- Java 17 or later - GitHub Copilot CLI 0.0.411-1 or later installed and in PATH (or provide custom `cliPath`) ### Maven diff --git a/pom.xml b/pom.xml index 225b5b1da..7ab031ce7 100644 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,7 @@ - 21 + 17 UTF-8 ${project.build.directory}/copilot-sdk diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index c49deac5a..c19fe72f3 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -618,33 +618,27 @@ void dispatchEvent(AbstractSessionEvent event) { * the event to handle */ private void handleBroadcastEventAsync(AbstractSessionEvent event) { - switch (event) { - case ExternalToolRequestedEvent toolEvent -> { - var data = toolEvent.getData(); - if (data == null || data.requestId() == null || data.toolName() == null) { - return; - } - ToolDefinition tool = getTool(data.toolName()); - if (tool == null) { - return; // This client doesn't handle this tool; another client will - } - executeToolAndRespondAsync(data.requestId(), data.toolName(), data.toolCallId(), data.arguments(), - tool); + if (event instanceof ExternalToolRequestedEvent toolEvent) { + var data = toolEvent.getData(); + if (data == null || data.requestId() == null || data.toolName() == null) { + return; } - case PermissionRequestedEvent permEvent -> { - var data = permEvent.getData(); - if (data == null || data.requestId() == null || data.permissionRequest() == null) { - return; - } - PermissionHandler handler = permissionHandler.get(); - if (handler == null) { - return; // This client doesn't handle permissions; another client will - } - executePermissionAndRespondAsync(data.requestId(), data.permissionRequest(), handler); + ToolDefinition tool = getTool(data.toolName()); + if (tool == null) { + return; // This client doesn't handle this tool; another client will + } + executeToolAndRespondAsync(data.requestId(), data.toolName(), data.toolCallId(), data.arguments(), tool); + + } else if (event instanceof PermissionRequestedEvent permEvent) { + var data = permEvent.getData(); + if (data == null || data.requestId() == null || data.permissionRequest() == null) { + return; } - default -> { - // Other event types are not handled here + PermissionHandler handler = permissionHandler.get(); + if (handler == null) { + return; // This client doesn't handle permissions; another client will } + executePermissionAndRespondAsync(data.requestId(), data.permissionRequest(), handler); } } diff --git a/src/site/markdown/getting-started.md b/src/site/markdown/getting-started.md index 8bb9c84b2..49281a4fc 100644 --- a/src/site/markdown/getting-started.md +++ b/src/site/markdown/getting-started.md @@ -18,7 +18,7 @@ Copilot: In Tokyo it's 75°F and sunny. Great day to be outside! Before you begin, make sure you have: -- **Java 21+** installed +- **Java 17+** installed - **GitHub Copilot CLI** installed and authenticated ([Installation guide](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)) Verify the CLI is working: diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 4a0918ce1..27abc27c0 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -8,7 +8,7 @@ Welcome to the documentation for the **Copilot SDK for Java** — a Java SDK for ### Requirements -- Java 21 or later +- Java 17 or later - GitHub Copilot CLI 0.0.411-1 or later installed and in PATH (or provide custom `cliPath`) ### Installation From e592e478bfc188f01bf2ab360a32719aae6e043d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 08:46:52 +0000 Subject: [PATCH 087/104] Fix: use correct permission denial kind and send errors back via RPC (ported from #185) --- .../github/copilot/sdk/CopilotSession.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index c19fe72f3..452e82671 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -40,6 +40,7 @@ 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.PermissionRequestResultKind; import com.github.copilot.sdk.json.PostToolUseHookInput; import com.github.copilot.sdk.json.PreToolUseHookInput; import com.github.copilot.sdk.json.SendMessageRequest; @@ -684,6 +685,14 @@ private void executeToolAndRespondAsync(String requestId, String toolName, Strin }); } catch (Exception e) { LOG.log(Level.WARNING, "Error executing tool for requestId=" + requestId, e); + try { + rpc.invoke( + "session.tools.handlePendingToolCall", Map.of("sessionId", sessionId, "requestId", + requestId, "error", e.getMessage() != null ? e.getMessage() : e.toString()), + Object.class); + } catch (Exception sendEx) { + LOG.log(Level.WARNING, "Error sending tool error for requestId=" + requestId, sendEx); + } } }); } @@ -708,7 +717,7 @@ private void executePermissionAndRespondAsync(String requestId, PermissionReques }).exceptionally(ex -> { try { PermissionRequestResult denied = new PermissionRequestResult(); - denied.setKind("denied-could-not-request-from-user"); + denied.setKind(PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER); rpc.invoke("session.permissions.handlePendingPermissionRequest", Map.of("sessionId", sessionId, "requestId", requestId, "result", denied), Object.class); } catch (Exception e) { @@ -718,6 +727,14 @@ private void executePermissionAndRespondAsync(String requestId, PermissionReques }); } catch (Exception e) { LOG.log(Level.WARNING, "Error executing permission handler for requestId=" + requestId, e); + try { + PermissionRequestResult denied = new PermissionRequestResult(); + denied.setKind(PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER); + rpc.invoke("session.permissions.handlePendingPermissionRequest", + Map.of("sessionId", sessionId, "requestId", requestId, "result", denied), Object.class); + } catch (Exception sendEx) { + LOG.log(Level.WARNING, "Error sending permission denied for requestId=" + requestId, sendEx); + } } }); } From 4e0f289338ef11d29cbece0e89e9b3c314e7b825 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:00:40 +0000 Subject: [PATCH 088/104] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index c80aa997f..8ef59f460 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 88.9% - 88.9% + 87.5% + 87.5% From 48e3960fc8a9d53d915fdbf3bb27ef4828d2ac99 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Thu, 12 Mar 2026 02:39:16 +0100 Subject: [PATCH 089/104] Fix documentation coverage gaps Add missing SessionConfig options (clientName, agent, onEvent) to reference table. Add 13 undocumented event types with new sections for External Tool, Permission, Command, and Plan Mode events. Add session.log() and early event registration sections to advanced.md. Add onListModels to setup.md CopilotClientOptions table. Add disableResume and onEvent to resume options table. --- src/site/markdown/advanced.md | 50 ++++++++++++++++++++++++++++++ src/site/markdown/documentation.md | 38 +++++++++++++++++++++++ src/site/markdown/setup.md | 1 + 3 files changed, 89 insertions(+) diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index dc57f6258..42b2c8964 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -24,6 +24,8 @@ This guide covers advanced scenarios for extending and customizing your Copilot - [Loading Skills](#Loading_Skills) - [Disabling Skills](#Disabling_Skills) - [Custom Configuration Directory](#Custom_Configuration_Directory) +- [Session Logging](#Session_Logging) +- [Early Event Registration](#Early_Event_Registration) - [User Input Handling](#User_Input_Handling) - [Permission Handling](#Permission_Handling) - [Session Hooks](#Session_Hooks) @@ -506,6 +508,54 @@ This is useful when you need to isolate session configuration or use different s --- +## Session Logging + +Send log messages to the session for debugging, status updates, or UI feedback. + +```java +// Simple log message (defaults to "info" level) +session.log("Processing step 1 of 3").get(); + +// Log with explicit level and ephemeral flag +session.log("Downloading dependencies...", "info", true).get(); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `message` | String | The log message text | +| `level` | String | Log level: `"info"`, `"warning"`, `"error"` | +| `ephemeral` | Boolean | If `true`, the message is transient and may not be persisted | + +Use cases: +- Displaying progress in a UI while the session processes a request +- Sending status updates to the session log +- Debugging session behavior with contextual messages + +See [CopilotSession.log()](apidocs/com/github/copilot/sdk/CopilotSession.html#log(java.lang.String)) Javadoc for details. + +--- + +## Early Event Registration + +Register an event handler *before* the `session.create` RPC is issued, ensuring no early events are missed. + +When you register handlers with `session.on()` after `createSession()` returns, you may miss events emitted during session creation (e.g., `SessionStartEvent`). Use `SessionConfig.setOnEvent()` to guarantee delivery of all events from the very start: + +```java +var events = new CopyOnWriteArrayList(); + +var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnEvent(events::add) // Registered before session.create RPC +).get(); + +// events list now includes SessionStartEvent and any other early events +``` + +This is equivalent to calling `session.on(handler)` immediately after creation, but executes earlier in the lifecycle. The same option is available on `ResumeSessionConfig.setOnEvent()` for resumed sessions. + +--- + ## User Input Handling Handle user input requests when the AI uses the `ask_user` tool to gather information from the user. diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index 26f17ce0f..ebfee18fc 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -162,6 +162,9 @@ The SDK supports event types organized by category. All events extend `AbstractS | `SessionInfoEvent` | `session.info` | Informational message from the session | | `SessionShutdownEvent` | `session.shutdown` | Session is shutting down (includes reason and exit code) | | `SessionModelChangeEvent` | `session.model_change` | The model was changed mid-session | +| `SessionModeChangedEvent` | `session.mode_changed` | Session mode changed (e.g., plan mode) | +| `SessionPlanChangedEvent` | `session.plan_changed` | Session plan was updated | +| `SessionWorkspaceFileChangedEvent` | `session.workspace_file_changed` | A file in the workspace was modified | | `SessionHandoffEvent` | `session.handoff` | Session handed off to another agent | | `SessionTruncationEvent` | `session.truncation` | Context was truncated due to limits | | `SessionSnapshotRewindEvent` | `session.snapshot_rewind` | Session rewound to a previous snapshot | @@ -220,8 +223,37 @@ The SDK supports event types organized by category. All events extend `AbstractS | `HookStartEvent` | `hook.start` | Hook execution started | | `HookEndEvent` | `hook.end` | Hook execution completed | | `SystemMessageEvent` | `system.message` | System-level message | +| `SystemNotificationEvent` | `system.notification` | System notification (informational) | | `SkillInvokedEvent` | `skill.invoked` | A skill was invoked | +### External Tool Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `ExternalToolRequestedEvent` | `external_tool.requested` | An external tool invocation was requested | +| `ExternalToolCompletedEvent` | `external_tool.completed` | An external tool invocation completed | + +### Permission Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `PermissionRequestedEvent` | `permission.requested` | A permission request was issued | +| `PermissionCompletedEvent` | `permission.completed` | A permission request was resolved | + +### Command Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `CommandQueuedEvent` | `command.queued` | A command was queued for execution | +| `CommandCompletedEvent` | `command.completed` | A queued command completed | + +### Plan Mode Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `ExitPlanModeRequestedEvent` | `exit_plan_mode.requested` | Exit from plan mode was requested | +| `ExitPlanModeCompletedEvent` | `exit_plan_mode.completed` | Exit from plan mode completed | + See the [events package Javadoc](apidocs/com/github/copilot/sdk/events/package-summary.html) for detailed event data structures. --- @@ -598,9 +630,12 @@ When resuming a session, you can optionally reconfigure many settings. This is u | `configDir` | Override configuration directory | | `mcpServers` | Configure MCP servers | | `customAgents` | Configure custom agents | +| `agent` | Pre-select a custom agent at session start | | `skillDirectories` | Directories to load skills from | | `disabledSkills` | Skills to disable | | `infiniteSessions` | Configure infinite session behavior | +| `disableResume` | When `true`, creates a new session instead of resuming | +| `onEvent` | Event handler registered before session resumption | **Example: Changing Model on Resume** @@ -637,6 +672,7 @@ Complete list of all `SessionConfig` options for `createSession()`: | Option | Type | Description | Guide | |--------|------|-------------|-------| | `sessionId` | String | Custom session ID (auto-generated if omitted) | — | +| `clientName` | String | Client name for User-Agent identification | — | | `model` | String | AI model to use | [Choosing a Model](#Choosing_a_Model) | | `reasoningEffort` | String | Reasoning depth: `"low"`, `"medium"`, `"high"`, `"xhigh"` | [Reasoning Effort](#Reasoning_Effort) | | `tools` | List<ToolDefinition> | Custom tools the assistant can invoke | [Custom Tools](advanced.html#Custom_Tools) | @@ -651,10 +687,12 @@ Complete list of all `SessionConfig` options for `createSession()`: | `streaming` | boolean | Enable streaming response chunks | [Streaming Responses](#Streaming_Responses) | | `mcpServers` | Map<String, Object> | MCP server configurations | [MCP Servers](mcp.html) | | `customAgents` | List<CustomAgentConfig> | Custom agent definitions | [Custom Agents](advanced.html#Custom_Agents) | +| `agent` | String | Pre-select a custom agent at session start | [Custom Agents](advanced.html#Custom_Agents) | | `infiniteSessions` | InfiniteSessionConfig | Auto-compaction for long conversations | [Infinite Sessions](advanced.html#Infinite_Sessions) | | `skillDirectories` | List<String> | Directories to load skills from | [Skills](advanced.html#Skills_Configuration) | | `disabledSkills` | List<String> | Skills to disable by name | [Skills](advanced.html#Skills_Configuration) | | `configDir` | String | Custom configuration directory | [Config Dir](advanced.html#Custom_Configuration_Directory) | +| `onEvent` | Consumer<AbstractSessionEvent> | Event handler registered before session creation | [Early Event Registration](advanced.html#Early_Event_Registration) | ### Cloning SessionConfig diff --git a/src/site/markdown/setup.md b/src/site/markdown/setup.md index 56a2df4e5..10c1cb3d8 100644 --- a/src/site/markdown/setup.md +++ b/src/site/markdown/setup.md @@ -349,6 +349,7 @@ Complete list of `CopilotClientOptions` settings: | `logLevel` | String | CLI log level | `"info"` | | `environment` | Map | Environment variables | inherited | | `cwd` | String | Working directory | current dir | +| `onListModels` | Supplier | Custom model listing implementation | `null` (use CLI) | ### Extra CLI Arguments From df01e4a091703be5916ae6be1d21f1f031ff2f42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 01:47:15 +0000 Subject: [PATCH 090/104] Initial plan From 2c4348d262c5a729ffeb8d663c888e3e7246fa34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 01:50:42 +0000 Subject: [PATCH 091/104] Update .lastmerge to 062b61c8aa63b9b5d45fa1d7b01723e6660ffa83 --- .lastmerge | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lastmerge b/.lastmerge index fd2e8b676..c5649a512 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -9a0a1a5f21111f4ad02b5ce911750ecc75e054c3 +062b61c8aa63b9b5d45fa1d7b01723e6660ffa83 From 7215df0c9c70e52339a286b7f30635b92d7550fc Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Thu, 12 Mar 2026 03:08:37 +0100 Subject: [PATCH 092/104] Update src/site/markdown/documentation.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/site/markdown/documentation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index ebfee18fc..5e6913dbd 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -634,7 +634,7 @@ When resuming a session, you can optionally reconfigure many settings. This is u | `skillDirectories` | Directories to load skills from | | `disabledSkills` | Skills to disable | | `infiniteSessions` | Configure infinite session behavior | -| `disableResume` | When `true`, creates a new session instead of resuming | +| `disableResume` | When `true`, resumes without emitting a `session.resume` event | | `onEvent` | Event handler registered before session resumption | **Example: Changing Model on Resume** From e4e25119f6880f28327b5f512d34ade31c7ee81d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 12 Mar 2026 02:10:39 +0000 Subject: [PATCH 093/104] docs: update version references to 1.0.11 --- CHANGELOG.md | 13 ++++++++++--- README.md | 4 ++-- jbang-example.java | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b95104175..29e6b6281 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] -> **Upstream sync:** [`github/copilot-sdk@9a0a1a5`](https://github.com/github/copilot-sdk/commit/9a0a1a5f21111f4ad02b5ce911750ecc75e054c3) +> **Upstream sync:** [`github/copilot-sdk@062b61c`](https://github.com/github/copilot-sdk/commit/062b61c8aa63b9b5d45fa1d7b01723e6660ffa83) +## [1.0.11] - 2026-03-12 + +> **Upstream sync:** [`github/copilot-sdk@062b61c`](https://github.com/github/copilot-sdk/commit/062b61c8aa63b9b5d45fa1d7b01723e6660ffa83) ### Added - `CopilotClientOptions.setOnListModels(Supplier>>)` — custom handler for `listModels()` used in BYOK mode to return models from a custom provider instead of querying the CLI (upstream: [`e478657`](https://github.com/github/copilot-sdk/commit/e478657)) @@ -424,9 +427,13 @@ New types: `GetForegroundSessionResponse`, `SetForegroundSessionResponse` - Pre-commit hook for Spotless code formatting - Comprehensive API documentation -[Unreleased]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.10...HEAD +[Unreleased]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.11...HEAD +[1.0.11]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.10...v1.0.11 +[Unreleased]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.11...HEAD +[1.0.11]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.10...v1.0.11 [1.0.10]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.9...v1.0.10 -[Unreleased]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.10...HEAD +[Unreleased]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.11...HEAD +[1.0.11]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.10...v1.0.11 [1.0.10]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.9...v1.0.10 [1.0.9]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.8...v1.0.9 [1.0.8]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.7...v1.0.8 diff --git a/README.md b/README.md index 5943afab4..4d31e5b33 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,14 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A io.github.copilot-community-sdk copilot-sdk - 1.0.10 + 1.0.11 ``` ### Gradle ```groovy -implementation 'io.github.copilot-community-sdk:copilot-sdk:1.0.10' +implementation 'io.github.copilot-community-sdk:copilot-sdk:1.0.11' ``` ## Quick Start diff --git a/jbang-example.java b/jbang-example.java index d0b307e4e..c99178c6d 100644 --- a/jbang-example.java +++ b/jbang-example.java @@ -1,5 +1,5 @@ -//DEPS io.github.copilot-community-sdk:copilot-sdk:1.0.10 +//DEPS io.github.copilot-community-sdk:copilot-sdk:1.0.11 import com.github.copilot.sdk.*; import com.github.copilot.sdk.events.*; import com.github.copilot.sdk.json.*; From a1169bc6bb3a66d566b60c2f5421d59c426a5a98 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 12 Mar 2026 02:11:00 +0000 Subject: [PATCH 094/104] [maven-release-plugin] prepare release v1.0.11 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 7ab031ce7..51c08825f 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ io.github.copilot-community-sdk copilot-sdk - 1.0.11-SNAPSHOT + 1.0.11 jar GitHub Copilot Community SDK :: Java @@ -33,7 +33,7 @@ scm:git:https://github.com/copilot-community-sdk/copilot-sdk-java.git scm:git:https://github.com/copilot-community-sdk/copilot-sdk-java.git https://github.com/copilot-community-sdk/copilot-sdk-java - HEAD + v1.0.11 From d8ec7710c14d3cdbb442d362f0ddfadaf9cf6c43 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 12 Mar 2026 02:11:01 +0000 Subject: [PATCH 095/104] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 51c08825f..3058362c9 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ io.github.copilot-community-sdk copilot-sdk - 1.0.11 + 1.0.12-SNAPSHOT jar GitHub Copilot Community SDK :: Java @@ -33,7 +33,7 @@ scm:git:https://github.com/copilot-community-sdk/copilot-sdk-java.git scm:git:https://github.com/copilot-community-sdk/copilot-sdk-java.git https://github.com/copilot-community-sdk/copilot-sdk-java - v1.0.11 + HEAD From 36fcf4b3b743eb8c55dcb5fc41ddef3e25d197da Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Thu, 12 Mar 2026 11:50:39 +0100 Subject: [PATCH 096/104] On branch edburns/dd-2824425-fix-record-vs-getter-discrepancy modified: .github/copilot-instructions.md modified: README.md modified: src/site/markdown/advanced.md modified: src/site/markdown/cookbook/error-handling.md modified: src/site/markdown/cookbook/managing-local-files.md modified: src/site/markdown/cookbook/multiple-sessions.md modified: src/site/markdown/cookbook/persisting-sessions.md modified: src/site/markdown/cookbook/pr-visualization.md modified: src/site/markdown/documentation.md modified: src/site/markdown/getting-started.md modified: src/site/markdown/hooks.md modified: src/site/markdown/index.md modified: src/site/markdown/mcp.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the accessor style changed Commit f5e966262 — February 10, 2026, authored by Bruno Borges. "Refactor data access methods in tests to use new naming conventions" Updated test assertions … to reflect changes in data access methods from getX() to x(). This was a massive breaking change across all 40+ event data types in com.github.copilot.sdk.events. Every public static class FooData with JavaBean getters/setters was converted to a public record FooData(...). This removed all getX() / setX() methods and replaced them with record component accessors (x()). The commit was merged via PR #117 (feat/records-for-data-types) and shipped in v1.0.9 (the first release tag that contains it, alongside v1.0.10 and v1.0.11). Timeline summary: Before f5e966262: AssistantMessageData was a mutable static class with getContent(), setContent(), etc. After f5e966262 (v1.0.9+): all event data types became Java records — content(), currentTokens(), tokenLimit(), messagesLength(), etc. The README's Quick Start code was never updated to match — it still showed getContent() / getCurrentTokens() etc. until the fix applied in our current session. --- .github/copilot-instructions.md | 2 +- README.md | 8 ++++---- src/site/markdown/advanced.md | 4 ++-- src/site/markdown/cookbook/error-handling.md | 8 ++++---- .../markdown/cookbook/managing-local-files.md | 8 ++++---- .../markdown/cookbook/multiple-sessions.md | 6 +++--- .../markdown/cookbook/persisting-sessions.md | 8 ++++---- src/site/markdown/cookbook/pr-visualization.md | 4 ++-- src/site/markdown/documentation.md | 18 +++++++++--------- src/site/markdown/getting-started.md | 2 +- src/site/markdown/hooks.md | 2 +- src/site/markdown/index.md | 4 ++-- src/site/markdown/mcp.md | 2 +- 13 files changed, 38 insertions(+), 38 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index bc28a29fb..7112a3f51 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -124,7 +124,7 @@ session.createSession(new SessionConfig() Sessions emit typed events via `session.on()`: ```java -session.on(AssistantMessageEvent.class, msg -> System.out.println(msg.getData().getContent())); +session.on(AssistantMessageEvent.class, msg -> System.out.println(msg.getData().content())); session.on(SessionIdleEvent.class, idle -> done.complete(null)); ``` diff --git a/README.md b/README.md index 4d31e5b33..1463a61af 100644 --- a/README.md +++ b/README.md @@ -63,16 +63,16 @@ public class CopilotSDK { // Handle assistant message events session.on(AssistantMessageEvent.class, msg -> { - System.out.println(msg.getData().getContent()); + System.out.println(msg.getData().content()); }); // Handle session usage info events session.on(SessionUsageInfoEvent.class, usage -> { var data = usage.getData(); System.out.println("\n--- Usage Metrics ---"); - System.out.println("Current tokens: " + (int) data.getCurrentTokens()); - System.out.println("Token limit: " + (int) data.getTokenLimit()); - System.out.println("Messages count: " + (int) data.getMessagesLength()); + System.out.println("Current tokens: " + (int) data.currentTokens()); + System.out.println("Token limit: " + (int) data.tokenLimit()); + System.out.println("Messages count: " + (int) data.messagesLength()); }); // Send a message diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index 42b2c8964..12015c612 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -340,7 +340,7 @@ session.on(SessionCompactionStartEvent.class, start -> { session.on(SessionCompactionCompleteEvent.class, complete -> { var data = complete.getData(); System.out.println("Compaction completed - success: " + data.isSuccess() - + ", tokens removed: " + data.getTokensRemoved()); + + ", tokens removed: " + data.tokensRemoved()); }); ``` @@ -856,7 +856,7 @@ session.on(AssistantMessageEvent.class, msg -> { session.on(AssistantMessageEvent.class, msg -> { // This handler executes normally despite the exception above - System.out.println(msg.getData().getContent()); + System.out.println(msg.getData().content()); }); ``` diff --git a/src/site/markdown/cookbook/error-handling.md b/src/site/markdown/cookbook/error-handling.md index db1a173f5..1e44e702f 100644 --- a/src/site/markdown/cookbook/error-handling.md +++ b/src/site/markdown/cookbook/error-handling.md @@ -44,7 +44,7 @@ public class BasicErrorHandling { new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")).get(); session.on(AssistantMessageEvent.class, msg -> { - System.out.println(msg.getData().getContent()); + System.out.println(msg.getData().content()); }); session.sendAndWait(new MessageOptions() @@ -110,7 +110,7 @@ public class TimeoutHandling { public static void sendWithTimeout(CopilotSession session) { try { session.on(AssistantMessageEvent.class, msg -> { - System.out.println(msg.getData().getContent()); + System.out.println(msg.getData().content()); }); // Wait up to 30 seconds for response @@ -209,7 +209,7 @@ public class TryWithResources { new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")).get()) { session.on(AssistantMessageEvent.class, msg -> { - System.out.println(msg.getData().getContent()); + System.out.println(msg.getData().content()); }); session.sendAndWait(new MessageOptions() @@ -254,7 +254,7 @@ public class ToolErrorHandling { new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setTools(List.of(errorTool))).get(); session.on(AssistantMessageEvent.class, msg -> { - System.out.println(msg.getData().getContent()); + System.out.println(msg.getData().content()); }); // Session continues even when tool fails diff --git a/src/site/markdown/cookbook/managing-local-files.md b/src/site/markdown/cookbook/managing-local-files.md index 941cbdded..c978ccbad 100644 --- a/src/site/markdown/cookbook/managing-local-files.md +++ b/src/site/markdown/cookbook/managing-local-files.md @@ -54,15 +54,15 @@ public class ManagingLocalFiles { var done = new CountDownLatch(1); session.on(AssistantMessageEvent.class, msg -> - System.out.println("\nCopilot: " + msg.getData().getContent()) + System.out.println("\nCopilot: " + msg.getData().content()) ); session.on(ToolExecutionStartEvent.class, evt -> - System.out.println(" → Running: " + evt.getData().getToolName()) + System.out.println(" → Running: " + evt.getData().toolName()) ); session.on(ToolExecutionCompleteEvent.class, evt -> - System.out.println(" ✓ Completed: " + evt.getData().getToolCallId()) + System.out.println(" ✓ Completed: " + evt.getData().toolCallId()) ); session.on(SessionIdleEvent.class, evt -> done.countDown()); @@ -171,7 +171,7 @@ public class InteractiveFileOrganizer { new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")).get(); session.on(AssistantMessageEvent.class, msg -> - System.out.println("\nCopilot: " + msg.getData().getContent()) + System.out.println("\nCopilot: " + msg.getData().content()) ); System.out.print("Enter folder path to organize: "); diff --git a/src/site/markdown/cookbook/multiple-sessions.md b/src/site/markdown/cookbook/multiple-sessions.md index ff7441255..b03c69c2c 100644 --- a/src/site/markdown/cookbook/multiple-sessions.md +++ b/src/site/markdown/cookbook/multiple-sessions.md @@ -50,11 +50,11 @@ public class MultipleSessions { // Set up event handlers for each session session1.on(AssistantMessageEvent.class, msg -> - System.out.println("Session 1: " + msg.getData().getContent())); + System.out.println("Session 1: " + msg.getData().content())); session2.on(AssistantMessageEvent.class, msg -> - System.out.println("Session 2: " + msg.getData().getContent())); + System.out.println("Session 2: " + msg.getData().content())); session3.on(AssistantMessageEvent.class, msg -> - System.out.println("Session 3: " + msg.getData().getContent())); + System.out.println("Session 3: " + msg.getData().content())); // Each session maintains its own conversation history session1.send(new MessageOptions() diff --git a/src/site/markdown/cookbook/persisting-sessions.md b/src/site/markdown/cookbook/persisting-sessions.md index a281c88f3..a3191005d 100644 --- a/src/site/markdown/cookbook/persisting-sessions.md +++ b/src/site/markdown/cookbook/persisting-sessions.md @@ -48,7 +48,7 @@ public class PersistingSessions { ).get(); session.on(AssistantMessageEvent.class, msg -> - System.out.println(msg.getData().getContent()) + System.out.println(msg.getData().content()) ); session.sendAndWait(new MessageOptions() @@ -76,7 +76,7 @@ public class ResumeSession { var session = client.resumeSession("user-123-conversation", new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.on(AssistantMessageEvent.class, msg -> - System.out.println(msg.getData().getContent()) + System.out.println(msg.getData().content()) ); // Previous context is restored @@ -141,7 +141,7 @@ public class SessionHistory { for (var event : messages) { // Print different event types appropriately if (event instanceof AssistantMessageEvent msg) { - System.out.printf("[assistant] %s%n", msg.getData().getContent()); + System.out.printf("[assistant] %s%n", msg.getData().content()); } else if (event instanceof UserMessageEvent userMsg) { System.out.printf("[user] %s%n", userMsg.getData().content()); } else { @@ -218,7 +218,7 @@ public class SessionManager { if (session != null) { session.on(AssistantMessageEvent.class, msg -> - System.out.println("\nCopilot: " + msg.getData().getContent()) + System.out.println("\nCopilot: " + msg.getData().content()) ); // Interactive conversation loop diff --git a/src/site/markdown/cookbook/pr-visualization.md b/src/site/markdown/cookbook/pr-visualization.md index dfe9db85f..6fabf5c35 100644 --- a/src/site/markdown/cookbook/pr-visualization.md +++ b/src/site/markdown/cookbook/pr-visualization.md @@ -101,11 +101,11 @@ public class PRVisualization { // Set up event handling session.on(AssistantMessageEvent.class, msg -> - System.out.println("\n🤖 " + msg.getData().getContent() + "\n") + System.out.println("\n🤖 " + msg.getData().content() + "\n") ); session.on(ToolExecutionStartEvent.class, evt -> - System.out.println(" ⚙️ " + evt.getData().getToolName()) + System.out.println(" ⚙️ " + evt.getData().toolName()) ); // Initial prompt - let Copilot figure out the details diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index 5e6913dbd..4ded8bd4e 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -40,7 +40,7 @@ try (var client = new CopilotClient()) { ).get(); var response = session.sendAndWait("Explain Java records in one sentence").get(); - System.out.println(response.getData().getContent()); + System.out.println(response.getData().content()); session.close(); } @@ -58,7 +58,7 @@ For straightforward interactions, use `sendAndWait()`: ```java var response = session.sendAndWait("What is the capital of France?").get(); -System.out.println(response.getData().getContent()); +System.out.println(response.getData().content()); ``` ### Event-Based Processing @@ -103,7 +103,7 @@ var done = new CompletableFuture(); // Type-safe event handlers (recommended) session.on(AssistantMessageEvent.class, msg -> { - System.out.println("Response: " + msg.getData().getContent()); + System.out.println("Response: " + msg.getData().content()); }); session.on(SessionErrorEvent.class, err -> { @@ -124,7 +124,7 @@ You can also use a single handler for all events: session.on(event -> { switch (event) { case AssistantMessageEvent msg -> - System.out.println("Response: " + msg.getData().getContent()); + System.out.println("Response: " + msg.getData().content()); case SessionErrorEvent err -> System.err.println("Error: " + err.getData().message()); case SessionIdleEvent idle -> @@ -299,11 +299,11 @@ var history = session.getMessages().get(); for (var event : history) { switch (event) { case UserMessageEvent user -> - System.out.println("User: " + user.getData().getContent()); + System.out.println("User: " + user.getData().content()); case AssistantMessageEvent assistant -> - System.out.println("Assistant: " + assistant.getData().getContent()); + System.out.println("Assistant: " + assistant.getData().content()); case ToolExecutionCompleteEvent tool -> - System.out.println("Tool: " + tool.getData().getToolName()); + System.out.println("Tool: " + tool.getData().toolName()); default -> { } } } @@ -596,8 +596,8 @@ var session2 = client.createSession( var future1 = session1.sendAndWait("Summarize REST APIs"); var future2 = session2.sendAndWait("Summarize GraphQL"); -System.out.println("GPT: " + future1.get().getData().getContent()); -System.out.println("Claude: " + future2.get().getData().getContent()); +System.out.println("GPT: " + future1.get().getData().content()); +System.out.println("Claude: " + future2.get().getData().content()); ``` ### Resume a Previous Session diff --git a/src/site/markdown/getting-started.md b/src/site/markdown/getting-started.md index 49281a4fc..cbbad8d46 100644 --- a/src/site/markdown/getting-started.md +++ b/src/site/markdown/getting-started.md @@ -81,7 +81,7 @@ public class HelloCopilot { new MessageOptions().setPrompt("What is 2 + 2?") ).get(); - System.out.println(response.getData().getContent()); + System.out.println(response.getData().content()); } } } diff --git a/src/site/markdown/hooks.md b/src/site/markdown/hooks.md index 11df1d6e9..511721c33 100644 --- a/src/site/markdown/hooks.md +++ b/src/site/markdown/hooks.md @@ -339,7 +339,7 @@ public class HooksExample { ).get(); var response = session.sendAndWait("List files in /tmp").get(); - System.out.println(response.getData().getContent()); + System.out.println(response.getData().content()); session.close(); } diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 27abc27c0..ad8a3e143 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -52,7 +52,7 @@ public class Example { var done = new CompletableFuture(); session.on(AssistantMessageEvent.class, msg -> { - System.out.println(msg.getData().getContent()); + System.out.println(msg.getData().content()); }); session.on(SessionIdleEvent.class, idle -> done.complete(null)); @@ -96,7 +96,7 @@ class hello { var session = client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); var done = new CompletableFuture(); session.on(AssistantMessageEvent.class, msg -> { - System.out.print(msg.getData().getContent()); + System.out.print(msg.getData().content()); }); session.on(SessionIdleEvent.class, idle -> done.complete(null)); session.send(new MessageOptions().setPrompt("Say hello!")).get(); diff --git a/src/site/markdown/mcp.md b/src/site/markdown/mcp.md index 68df0db87..8f830da57 100644 --- a/src/site/markdown/mcp.md +++ b/src/site/markdown/mcp.md @@ -22,7 +22,7 @@ var session = client.createSession( ).get(); var result = session.sendAndWait("List files in the directory").get(); -System.out.println(result.getData().getContent()); +System.out.println(result.getData().content()); ``` > **Tip:** Browse the [MCP Servers Directory](https://github.com/modelcontextprotocol/servers) for community servers like GitHub, SQLite, and Puppeteer. From f0b2e6351ba746510384a3f8fa454630465e10d7 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Thu, 12 Mar 2026 13:01:27 +0100 Subject: [PATCH 097/104] Update src/site/markdown/advanced.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/site/markdown/advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index 12015c612..41e122d5c 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -339,7 +339,7 @@ session.on(SessionCompactionStartEvent.class, start -> { }); session.on(SessionCompactionCompleteEvent.class, complete -> { var data = complete.getData(); - System.out.println("Compaction completed - success: " + data.isSuccess() + System.out.println("Compaction completed - success: " + data.success() + ", tokens removed: " + data.tokensRemoved()); }); ``` From 26a39e78dc12f5dc8e4f1935a31d6a38545aab29 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Thu, 12 Mar 2026 13:01:43 +0100 Subject: [PATCH 098/104] Update src/site/markdown/documentation.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/site/markdown/documentation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index 4ded8bd4e..7bc543bb5 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -303,7 +303,7 @@ for (var event : history) { case AssistantMessageEvent assistant -> System.out.println("Assistant: " + assistant.getData().content()); case ToolExecutionCompleteEvent tool -> - System.out.println("Tool: " + tool.getData().toolName()); + System.out.println("Tool: " + tool.getData().toolCallId()); default -> { } } } From 3fbefc8e91df44a35f4547a3b040c30460f38ae3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:35:54 +0000 Subject: [PATCH 099/104] Initial plan From b3a2826888cdfe6560e1ba43d1f8107b09d8440a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:38:55 +0000 Subject: [PATCH 100/104] Add Automatic-Module-Name to JAR manifest via maven-jar-plugin Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- pom.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pom.xml b/pom.xml index 3058362c9..cea1f9575 100644 --- a/pom.xml +++ b/pom.xml @@ -108,6 +108,18 @@ maven-compiler-plugin 3.14.1 + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + + com.github.copilot.sdk.java + + + + org.apache.maven.plugins From 86b23e691ee7cf9ef5d7cfdbf98ee08b3c77f9c6 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Thu, 12 Mar 2026 15:25:32 +0100 Subject: [PATCH 101/104] On branch edburns/dd-2824811-smoke-test Execute this prompt to create and execute a smoke test based on the most recently published snapshot release as documented in the top level README. new file: src/test/prompts/PROMPT-smoke-test.md --- src/test/prompts/PROMPT-smoke-test.md | 135 ++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/test/prompts/PROMPT-smoke-test.md diff --git a/src/test/prompts/PROMPT-smoke-test.md b/src/test/prompts/PROMPT-smoke-test.md new file mode 100644 index 000000000..4013002ac --- /dev/null +++ b/src/test/prompts/PROMPT-smoke-test.md @@ -0,0 +1,135 @@ +# Prompt: Generate and Run the copilot-sdk-java Smoke Test + +## Objective + +Create a Maven project that acts as a smoke test for `copilot-sdk-java`. The project must compile, build, and run to completion with exit code 0 as the definition of success. + +## Step 1 — Read the source README + +Read the file `README.md` at the top level of this repository. You will need two sections from it: + +- **"Snapshot Builds"** — provides the Maven GAV (groupId, artifactId, version) and the Maven Central Snapshots repository configuration to use for the dependency under test. +- **"Quick Start"** — provides the exact Java source code for the smoke test program. Use this code verbatim. Do not modify it, fix it, or improve it. If it does not compile or run correctly against the artifact under test, that is itself a smoke test failure and must be reported as such rather than silently corrected. + +## Step 2 — Create the Maven project + +Create the following file layout in a subdirectory named `smoke-test/` at the top level of this repository: + +``` +smoke-test/ + pom.xml + src/main/java/(Class name taken from the code in the "Quick Start" section in the README).java ← verbatim from README "Quick Start" +``` + +### `pom.xml` requirements + +- **groupId**: `com.github` (or any reasonable value) +- **artifactId**: `copilot-sdk-smoketest` +- **version**: `1.0-SNAPSHOT` +- **packaging**: `jar` +- **Java source/target**: (taken from the "Requirements" section in the README) (via `maven.compiler.source` and `maven.compiler.target` properties) +- **`mainClass` property**: (taken from the "Quick Start" section in the README) (the class is in the default package) + +#### Snapshot repository + +Configure the Maven Central Snapshots repository exactly as specified in the "Snapshot Builds" section of `README.md`, and add `always` inside the `` block so that every build fetches the latest snapshot without requiring `-U`: + +```xml + + central-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + true + always + + +``` + +#### Dependency + +Use the GAV from the "Snapshot Builds" section of `README.md` verbatim — do not substitute the release version from the "Maven" section. + +#### Plugins — REQUIRED configuration + +**Do not use `maven-shade-plugin`.** Use the `Class-Path` manifest approach instead: + +1. **`maven-jar-plugin`** (version **3.4.1** — pin explicitly to suppress Maven version warnings): + + ```xml + + org.apache.maven.plugins + maven-jar-plugin + 3.4.1 + + + + ${mainClass} + true + lib/ + false + + + + + ``` + + **Critical**: `false` is mandatory. Without it, the manifest `Class-Path:` entry uses the timestamped SNAPSHOT filename (e.g. `copilot-sdk-java-0.1.33-20260312.125508-3.jar`) while `copy-dependencies` writes the base SNAPSHOT filename (`copilot-sdk-java-0.1.33-SNAPSHOT.jar`), causing `NoClassDefFoundError` at runtime. + +2. **`maven-dependency-plugin`** (version **3.6.1**): + + ```xml + + org.apache.maven.plugins + maven-dependency-plugin + 3.6.1 + + + copy-dependencies + package + copy-dependencies + + ${project.build.directory}/lib + + + + + ``` + + This copies all runtime dependency JARs into `target/lib/`, which is where the manifest `Class-Path:` points. + +## Step 3 — Build + +```bash +mvn -U clean package +``` + +The `-U` flag forces a fresh snapshot metadata check regardless of local cache. The `always` already handles this for normal invocations, but `-U` is the safest choice for CI. + +Build must succeed with `BUILD SUCCESS` before proceeding. + +## Step 4 — Run + +```bash +java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar +``` + +The JAR must be run from the `smoke-test/` directory so that the relative `lib/` path in the manifest resolves correctly. Do not use `-cp` or `-classpath` — the test specifically validates that `java -jar` works with the manifest `Class-Path:` approach. + +## Step 5 — Verify success + +The smoke test passes if and only if the process exits with code **0**. + +The "Quick Start" code in `README.md` already contains the exit-code logic: it captures the last assistant message and calls `System.exit(0)` if it contains `"4"` (the expected answer to "What is 2+2?"), or `System.exit(-1)` otherwise. + +Check the exit code: +```bash +echo "Exit code: $?" +``` + +Expected: `Exit code: 0` + +## Important API notes (do not apply these as fixes — they are here for diagnostic context only) + +If the build fails with compilation errors such as `cannot find symbol` on methods like `getContent()`, `getCurrentTokens()`, `getTokenLimit()`, or `getMessagesLength()`, this indicates a mismatch between the Quick Start code and the SDK implementation. **Do not silently fix the code.** Report the failure. The purpose of this smoke test is precisely to catch such regressions. + +For reference: the data classes in `copilot-sdk-java` are Java **records**. Record accessor methods have no `get` prefix — they are named `content()`, `currentTokens()`, `tokenLimit()`, and `messagesLength()`. If the README Quick Start uses `getContent()` etc., that is a bug in the README that must be surfaced, not silently corrected. From 23618864660598da71c809cfc9651817e51b8547 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Thu, 12 Mar 2026 15:48:55 +0100 Subject: [PATCH 102/104] Consider the content of `smoke-test` to be ignorable. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5f035eaa5..f6b0681d3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ examples-test/ .merge-env blog-copilotsdk/ .claude/worktrees +smoke-test From 083318272b15d1efb32902ecb32540ca8f1280a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 23:02:13 +0000 Subject: [PATCH 103/104] Initial plan From 3748f22f179dc7d6219e4983f3b97758b15f3e44 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 23:05:42 +0000 Subject: [PATCH 104/104] Mark repository as archived, pointing to official github/copilot-sdk-java Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ README.md | 4 ++-- src/site/markdown/advanced.md | 2 +- src/site/markdown/documentation.md | 2 +- src/site/markdown/hooks.md | 2 +- src/site/markdown/index.md | 2 +- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29e6b6281..1404ba336 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). > **Upstream sync:** [`github/copilot-sdk@062b61c`](https://github.com/github/copilot-sdk/commit/062b61c8aa63b9b5d45fa1d7b01723e6660ffa83) +### Notice + +- **This repository is now archived.** The official GitHub Copilot SDK for Java is maintained at [github/copilot-sdk-java](https://github.com/github/copilot-sdk-java). No further changes or releases will be made in this repository. + ## [1.0.11] - 2026-03-12 > **Upstream sync:** [`github/copilot-sdk@062b61c`](https://github.com/github/copilot-sdk/commit/062b61c8aa63b9b5d45fa1d7b01723e6660ffa83) diff --git a/README.md b/README.md index 1463a61af..d4b0a54ab 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Copilot SDK for Java +> 🚨 **This repository is archived.** The official GitHub Copilot SDK for Java is now maintained at **[github/copilot-sdk-java](https://github.com/github/copilot-sdk-java)**. No further changes or releases will be made here. Please use the official SDK going forward. + [![Build](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/build-test.yml/badge.svg)](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/build-test.yml) [![Site](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/deploy-site.yml/badge.svg)](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/deploy-site.yml) [![Coverage](.github/badges/jacoco.svg)](https://copilot-community-sdk.github.io/copilot-sdk-java/snapshot/jacoco/index.html) @@ -16,8 +18,6 @@ ## Background -> ⚠️ **Disclaimer:** This is an **unofficial, community-driven SDK** and is **not supported or endorsed by GitHub**. This SDK may change in breaking ways. Use at your own risk. - Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build AI-powered applications and agentic workflows. ## Installation diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index 41e122d5c..926df5d72 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -1,6 +1,6 @@ # Advanced Usage -> ⚠️ **Disclaimer:** This is an **unofficial, community-driven SDK** and is **not supported or endorsed by GitHub**. Use at your own risk. +> 🚨 **This repository is archived.** The official GitHub Copilot SDK for Java is now maintained at **[github/copilot-sdk-java](https://github.com/github/copilot-sdk-java)**. No further changes or releases will be made here. Please use the official SDK going forward. This guide covers advanced scenarios for extending and customizing your Copilot integration. diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index 7bc543bb5..366b04627 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -1,6 +1,6 @@ # Copilot SDK for Java - Documentation -> ⚠️ **Disclaimer:** This is an **unofficial, community-driven SDK** and is **not supported or endorsed by GitHub**. Use at your own risk. +> 🚨 **This repository is archived.** The official GitHub Copilot SDK for Java is now maintained at **[github/copilot-sdk-java](https://github.com/github/copilot-sdk-java)**. No further changes or releases will be made here. Please use the official SDK going forward. This guide covers common use cases for the Copilot SDK for Java. For complete API details, see the [Javadoc](apidocs/index.html). diff --git a/src/site/markdown/hooks.md b/src/site/markdown/hooks.md index 511721c33..4dde96fdd 100644 --- a/src/site/markdown/hooks.md +++ b/src/site/markdown/hooks.md @@ -1,6 +1,6 @@ # Session Hooks -> ⚠️ **Disclaimer:** This is an **unofficial, community-driven SDK** and is **not supported or endorsed by GitHub**. Use at your own risk. +> 🚨 **This repository is archived.** The official GitHub Copilot SDK for Java is now maintained at **[github/copilot-sdk-java](https://github.com/github/copilot-sdk-java)**. No further changes or releases will be made here. Please use the official SDK going forward. Session hooks allow you to intercept and modify tool execution, user prompts, and session lifecycle events. Use hooks to implement custom logic like logging, security controls, or context injection. diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index ad8a3e143..83a589824 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -1,6 +1,6 @@ # Copilot SDK for Java -> ⚠️ **Disclaimer:** This is an **unofficial, community-driven SDK** and is **not supported or endorsed by GitHub**. Use at your own risk. +> 🚨 **This repository is archived.** The official GitHub Copilot SDK for Java is now maintained at **[github/copilot-sdk-java](https://github.com/github/copilot-sdk-java)**. No further changes or releases will be made here. Please use the official SDK going forward. Welcome to the documentation for the **Copilot SDK for Java** — a Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build AI-powered applications and agentic workflows.