diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0c213e61c0..c6959d3089 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @github/copilot-sdk-java +* @github/copilot-sdk diff --git a/.github/actions/setup-copilot/action.yml b/.github/actions/setup-copilot/action.yml index dcc99b8a70..1288a1d89b 100644 --- a/.github/actions/setup-copilot/action.yml +++ b/.github/actions/setup-copilot/action.yml @@ -4,15 +4,36 @@ outputs: cli-path: description: "Path to the Copilot CLI executable" value: ${{ steps.cli-path.outputs.path }} + cli-version: + description: "Pinned @github/copilot version installed (read from pom.xml)" + value: ${{ steps.cli-version.outputs.version }} runs: using: "composite" steps: - - uses: actions/setup-node@v6 + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v6 with: node-version: 22 - - name: Install Copilot CLI - run: npm install -g @github/copilot + - name: Read pinned @github/copilot version from pom.xml + id: cli-version shell: bash + # The version is the SINGLE SOURCE OF TRUTH for the Copilot CLI version + # used across all CI paths. It is kept in sync with the reference + # implementation pinned in .lastmerge by + # .github/scripts/reference-impl-sync/sync-cli-version-from-reference-impl.sh. + run: | + PROP="readonly-copilot-sdk-ref-impl-version-from-lastmerge-file-updated-by-reference-impl-sync" + VERSION=$(sed -n "s|.*<${PROP}>\(.*\).*|\1|p" pom.xml | head -n 1 | tr -d '[:space:]') + if [[ -z "$VERSION" || "$VERSION" == "PRIMER_TO_REPLACE" ]]; then + echo "::error::Could not read pinned @github/copilot version from pom.xml property <${PROP}>" >&2 + exit 1 + fi + echo "Pinned @github/copilot version: $VERSION" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + - name: Install Copilot CLI (pinned to pom.xml version) + shell: bash + env: + CLI_VERSION: ${{ steps.cli-version.outputs.version }} + run: npm install -g "@github/copilot@${CLI_VERSION}" - name: Set CLI path id: cli-path run: echo "path=$(which copilot)" >> $GITHUB_OUTPUT diff --git a/.github/actions/test-report/action.yml b/.github/actions/test-report/action.yml index 43d45f287a..4f807ed911 100644 --- a/.github/actions/test-report/action.yml +++ b/.github/actions/test-report/action.yml @@ -4,7 +4,7 @@ inputs: report-path: description: "Path to the test report XML files (glob pattern)" required: false - default: "target/surefire-reports/TEST-*.xml" + default: "target/surefire-reports*/TEST-*.xml" jacoco-path: description: "Path to the JaCoCo XML report" required: false @@ -22,10 +22,7 @@ runs: echo "## πŸ§ͺ Copilot Java SDK :: Test Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - REPORT_DIR=$(dirname "${{ inputs.report-path }}") - REPORT_PATTERN=$(basename "${{ inputs.report-path }}") - - if [ -d "$REPORT_DIR" ]; then + if ls ${{ inputs.report-path }} 1>/dev/null 2>&1; then TESTS_RUN=$(grep -h "tests=" ${{ inputs.report-path }} 2>/dev/null | sed 's/.*tests="\([0-9]*\)".*/\1/' | awk '{s+=$1} END {print s}') FAILURES=$(grep -h "failures=" ${{ inputs.report-path }} 2>/dev/null | sed 's/.*failures="\([0-9]*\)".*/\1/' | awk '{s+=$1} END {print s}') ERRORS=$(grep -h "errors=" ${{ inputs.report-path }} 2>/dev/null | sed 's/.*errors="\([0-9]*\)".*/\1/' | awk '{s+=$1} END {print s}') diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 11e06d9b9e..6a6fa53d9f 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -1,5 +1,15 @@ { "entries": { + "actions/checkout@v6.0.2": { + "repo": "actions/checkout", + "version": "v6.0.2", + "sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd" + }, + "actions/download-artifact@v8.0.1": { + "repo": "actions/download-artifact", + "version": "v8.0.1", + "sha": "3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" + }, "actions/github-script@v8": { "repo": "actions/github-script", "version": "v8", @@ -10,6 +20,11 @@ "version": "v9", "sha": "373c709c69115d41ff229c7e5df9f8788daa9553" }, + "actions/github-script@v9.0.0": { + "repo": "actions/github-script", + "version": "v9.0.0", + "sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3" + }, "actions/setup-java@v4": { "repo": "actions/setup-java", "version": "v4", @@ -20,15 +35,15 @@ "version": "v4", "sha": "49933ea5288caeca8642d1e84afbd3f7d6820020" }, - "github/gh-aw-actions/setup-cli@v0.68.3": { - "repo": "github/gh-aw-actions/setup-cli", - "version": "v0.68.3", - "sha": "ba90f2186d7ad780ec640f364005fa24e797b360" + "actions/upload-artifact@v7.0.1": { + "repo": "actions/upload-artifact", + "version": "v7.0.1", + "sha": "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" }, - "github/gh-aw-actions/setup@v0.68.3": { + "github/gh-aw-actions/setup@v0.71.5": { "repo": "github/gh-aw-actions/setup", - "version": "v0.68.3", - "sha": "ba90f2186d7ad780ec640f364005fa24e797b360" + "version": "v0.71.5", + "sha": "b8068426813005612b960b5ab0b8bd2c27142323" }, "github/gh-aw/actions/setup@v0.51.6": { "repo": "github/gh-aw/actions/setup", diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 620c8d52a2..b90843bcc4 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 82.7% - 82.7% + 81.9% + 81.9% diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d029784219..7b304f2677 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -222,6 +222,8 @@ Test method names are converted to lowercase snake_case for snapshot filenames t - **DO NOT** modify test snapshots in `target/copilot-sdk/test/snapshots/` - these come from reference implementation - **DO NOT** alter the Eclipse formatter configuration in `pom.xml` without team consensus - **DO NOT** remove or skip Checkstyle or Spotless checks +- **YOU MUST ALWAYS** run `gh aw compile ` after editing any `.github/workflows/*.md` agentic workflow source file to regenerate the corresponding `.lock.yml`. The lock file contains a content hash of the frontmatter β€” any edit to the `.md` without recompiling will cause the workflow to fail at runtime with a "lock file out of sync" error. +- **DO NOT** edit `.github/workflows/*.lock.yml` directly β€” these are auto-generated by `gh aw compile` from the `.md` source files. ### Security Guidelines diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 03f0f715ae..04d5ab5f47 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,13 +1,5 @@ version: 2 updates: - - package-ecosystem: "npm" - directory: "/scripts/codegen" - schedule: - interval: "daily" - open-pull-requests-limit: 1 - allow: - - dependency-name: "@github/copilot" - - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/prompts/coding-agent-merge-reference-impl-instructions.md b/.github/prompts/coding-agent-merge-reference-impl-instructions.md index c93af9a1a5..5ae1621db6 100644 --- a/.github/prompts/coding-agent-merge-reference-impl-instructions.md +++ b/.github/prompts/coding-agent-merge-reference-impl-instructions.md @@ -1,5 +1,5 @@ - - + + Follow the agentic-merge-reference-impl prompt at .github/prompts/agentic-merge-reference-impl.prompt.md to port reference implementation changes to the Java SDK. diff --git a/.github/scripts/reference-impl-sync/merge-reference-impl-finish.sh b/.github/scripts/reference-impl-sync/merge-reference-impl-finish.sh index 480d43bc0a..2dda8da07f 100755 --- a/.github/scripts/reference-impl-sync/merge-reference-impl-finish.sh +++ b/.github/scripts/reference-impl-sync/merge-reference-impl-finish.sh @@ -5,8 +5,12 @@ # Finalises a reference implementation merge: # 1. Runs format + test + build (via format-and-test.sh) # 2. Updates .lastmerge to reference implementation HEAD -# 3. Commits the .lastmerge update -# 4. Pushes the branch to origin +# 3. Syncs the @github/copilot version property in pom.xml from the +# cloned reference implementation's nodejs/package.json +# 4. Syncs scripts/codegen/package.json to the same @github/copilot +# version so the code generator uses matching schemas +# 5. Commits the .lastmerge + pom.xml + codegen package updates +# 6. Pushes the branch to origin # # Usage: ./.github/scripts/reference-impl-sync/merge-reference-impl-finish.sh # ./.github/scripts/reference-impl-sync/merge-reference-impl-finish.sh --skip-tests @@ -48,8 +52,22 @@ echo "β–Έ Updating .lastmerge…" NEW_COMMIT=$(cd "$REFERENCE_IMPL_DIR" && git rev-parse origin/main) echo "$NEW_COMMIT" > "$ROOT_DIR/.lastmerge" -git add .lastmerge -git commit -m "Update .lastmerge to $NEW_COMMIT" +# ── 2b. Sync pom.xml @github/copilot version ───────────────── +# Keeps the canonical CLI version in pom.xml aligned with what the +# reference implementation pinned in .lastmerge depends on. +echo "β–Έ Syncing @github/copilot version in pom.xml from reference implementation…" +"$ROOT_DIR/.github/scripts/reference-impl-sync/sync-cli-version-from-reference-impl.sh" "$REFERENCE_IMPL_DIR" + +# ── 2c. Sync scripts/codegen @github/copilot version ───────── +# Keeps scripts/codegen/package.json in lockstep so the code generator +# uses schemas from the same CLI version that the tests run against. +# This eliminates the gap where Dependabot could race ahead of the +# reference implementation sync. +echo "β–Έ Syncing @github/copilot version in scripts/codegen/package.json…" +"$ROOT_DIR/.github/scripts/reference-impl-sync/sync-codegen-version.sh" "$REFERENCE_IMPL_DIR" + +git add .lastmerge pom.xml scripts/codegen/package.json scripts/codegen/package-lock.json +git commit -m "Update .lastmerge to $NEW_COMMIT, sync pom.xml CLI version, and update scripts/codegen @github/copilot version" # ── 3. Push branch ─────────────────────────────────────────── echo "β–Έ Pushing branch $BRANCH_NAME to origin…" diff --git a/.github/scripts/reference-impl-sync/sync-cli-version-from-reference-impl.sh b/.github/scripts/reference-impl-sync/sync-cli-version-from-reference-impl.sh new file mode 100755 index 0000000000..1d15d8aee9 --- /dev/null +++ b/.github/scripts/reference-impl-sync/sync-cli-version-from-reference-impl.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# ────────────────────────────────────────────────────────────── +# sync-cli-version-from-reference-impl.sh +# +# Reads the @github/copilot version specifier from the cloned +# reference implementation's nodejs/package.json, and updates the +# corresponding property in pom.xml: +# +# +# +# This keeps the canonical Copilot CLI version (declared in pom.xml) +# in sync with whatever the reference implementation pinned in +# .lastmerge depends on. All workflows that install the Copilot CLI +# (build-test.yml β€” implicitly via cloned SDK, run-smoke-test.yml and +# update-copilot-dependency.yml β€” via the setup-copilot action) read +# this single property so every CI path uses the same CLI version. +# +# Usage: +# ./sync-cli-version-from-reference-impl.sh +# +# Or, when invoked from merge-reference-impl-finish.sh, sources +# REFERENCE_IMPL_DIR from the .merge-env file. +# ────────────────────────────────────────────────────────────── +set -euo pipefail + +# Locate the repo root by walking up from this script until we find a pom.xml. +# This is resilient to the script being moved to a different depth under +# .github/scripts/ in the future. +find_repo_root() { + local dir + dir="$(cd "$(dirname "$0")" && pwd)" + while [[ "$dir" != "/" ]]; do + if [[ -f "$dir/pom.xml" ]]; then + echo "$dir" + return 0 + fi + dir="$(dirname "$dir")" + done + echo "❌ Could not locate repo root (no pom.xml found above $(dirname "$0"))" >&2 + return 1 +} +ROOT_DIR="$(find_repo_root)" + +REFERENCE_IMPL_DIR="${1:-${REFERENCE_IMPL_DIR:-}}" +if [[ -z "$REFERENCE_IMPL_DIR" ]]; then + echo "❌ Usage: $0 " >&2 + echo " or set REFERENCE_IMPL_DIR in the environment." >&2 + exit 1 +fi + +PKG_JSON="$REFERENCE_IMPL_DIR/nodejs/package.json" +if [[ ! -f "$PKG_JSON" ]]; then + echo "❌ Cannot find $PKG_JSON" >&2 + exit 1 +fi + +# node is always available since the reference implementation uses npm. +CLI_VERSION=$(node -e \ + "const fs=require('fs');const p=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));const v=(p.dependencies&&p.dependencies['@github/copilot'])||(p.devDependencies&&p.devDependencies['@github/copilot']);process.stdout.write(v||'');" \ + "$PKG_JSON") + +if [[ -z "$CLI_VERSION" ]]; then + echo "❌ Could not extract @github/copilot version from $PKG_JSON" >&2 + exit 1 +fi + +POM="$ROOT_DIR/pom.xml" +PROP="readonly-copilot-sdk-ref-impl-version-from-lastmerge-file-updated-by-reference-impl-sync" + +if ! grep -q "<${PROP}>" "$POM"; then + echo "❌ Property <${PROP}> not found in $POM" >&2 + exit 1 +fi + +# Use a portable sed invocation (works on both BSD/macOS and GNU/Linux). +TMP="$(mktemp)" +sed -E "s|<${PROP}>[^<]*|<${PROP}>${CLI_VERSION}|" "$POM" > "$TMP" +mv "$TMP" "$POM" + +echo "β–Έ Updated pom.xml: <${PROP}> = ${CLI_VERSION}" diff --git a/.github/scripts/reference-impl-sync/sync-codegen-version.sh b/.github/scripts/reference-impl-sync/sync-codegen-version.sh new file mode 100755 index 0000000000..f3baa0ab5e --- /dev/null +++ b/.github/scripts/reference-impl-sync/sync-codegen-version.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# ────────────────────────────────────────────────────────────── +# sync-codegen-version.sh +# +# Updates the @github/copilot dependency in scripts/codegen/package.json +# to match the version used by the reference implementation. This keeps +# the code generator schemas in lockstep with the CLI version used for +# testing, eliminating the gap where Dependabot could race ahead. +# +# Usage: +# ./sync-codegen-version.sh +# +# Or, when invoked from merge-reference-impl-finish.sh, the directory +# is passed as $1. +# ────────────────────────────────────────────────────────────── +set -euo pipefail + +# Locate the repo root by walking up from this script until we find a pom.xml. +find_repo_root() { + local dir + dir="$(cd "$(dirname "$0")" && pwd)" + while [[ "$dir" != "/" ]]; do + if [[ -f "$dir/pom.xml" ]]; then + echo "$dir" + return 0 + fi + dir="$(dirname "$dir")" + done + echo "❌ Could not locate repo root (no pom.xml found above $(dirname "$0"))" >&2 + return 1 +} +ROOT_DIR="$(find_repo_root)" + +REFERENCE_IMPL_DIR="${1:-${REFERENCE_IMPL_DIR:-}}" +if [[ -z "$REFERENCE_IMPL_DIR" ]]; then + echo "❌ Usage: $0 " >&2 + echo " or set REFERENCE_IMPL_DIR in the environment." >&2 + exit 1 +fi + +PKG_JSON="$REFERENCE_IMPL_DIR/nodejs/package.json" +if [[ ! -f "$PKG_JSON" ]]; then + echo "❌ Cannot find $PKG_JSON" >&2 + exit 1 +fi + +# Extract the @github/copilot version from the reference implementation. +CLI_VERSION=$(node -e \ + "const fs=require('fs');const p=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));const v=(p.dependencies&&p.dependencies['@github/copilot'])||(p.devDependencies&&p.devDependencies['@github/copilot']);process.stdout.write(v||'');" \ + "$PKG_JSON") + +if [[ -z "$CLI_VERSION" ]]; then + echo "❌ Could not extract @github/copilot version from $PKG_JSON" >&2 + exit 1 +fi + +CODEGEN_DIR="$ROOT_DIR/scripts/codegen" +CODEGEN_PKG="$CODEGEN_DIR/package.json" + +if [[ ! -f "$CODEGEN_PKG" ]]; then + echo "❌ Cannot find $CODEGEN_PKG" >&2 + exit 1 +fi + +# Update scripts/codegen/package.json with the new version and regenerate the lock file. +# Write the version string directly into package.json to preserve the exact specifier +# used by the reference implementation (e.g. '^1.0.43-0'). npm install normalises +# caret ranges and would silently strip the prerelease suffix, causing a mismatch +# with pom.xml. +echo "β–Έ Updating scripts/codegen/package.json: @github/copilot β†’ ${CLI_VERSION}" +cd "$CODEGEN_DIR" +node -e " +const fs = require('fs'); +const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); +pkg.dependencies['@github/copilot'] = process.argv[1]; +fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); +" "$CLI_VERSION" +npm install +echo "β–Έ Updated scripts/codegen to @github/copilot@${CLI_VERSION}" diff --git a/.github/scripts/release/test-update-changelog.sh b/.github/scripts/release/test-update-changelog.sh index ade87d6a94..b5fc8486b7 100755 --- a/.github/scripts/release/test-update-changelog.sh +++ b/.github/scripts/release/test-update-changelog.sh @@ -33,10 +33,10 @@ run_test() { if $test_func; then echo -e "${GREEN}PASSED${NC}" - ((passed++)) + passed=$((passed + 1)) else echo -e "${RED}FAILED${NC}" - ((failed++)) + failed=$((failed + 1)) fi } @@ -180,6 +180,71 @@ EOF fi } +# Test 6: Beta-java version format (e.g., 1.0.0-beta-java.N) +test_beta_java_version() { + local test_file="${TEST_DIR}/test6.md" + cat > "$test_file" << 'EOF' +# Changelog + +## [Unreleased] + +### Added +- New feature + +## [1.0.0-beta-java.1] - 2026-05-01 + +[Unreleased]: https://github.com/test/repo/compare/v1.0.0-beta-java.1...HEAD +[1.0.0-beta-java.1]: https://github.com/test/repo/compare/v0.3.0-java.2...v1.0.0-beta-java.1 +[0.3.0-java.2]: https://github.com/test/repo/releases/tag/0.3.0-java.2 +EOF + + CHANGELOG_FILE="$test_file" bash "$UPDATE_SCRIPT" 1.0.0-beta-java.2 > /dev/null 2>&1 + + # The [Unreleased] link should now point to v1.0.0-beta-java.2 + # [1.0.0-beta-java.2] should compare from v1.0.0-beta-java.1 + if grep -q "\[Unreleased\]: https://github.com/test/repo/compare/v1.0.0-beta-java.2...HEAD" "$test_file" && \ + grep -q "\[1.0.0-beta-java.2\]: https://github.com/test/repo/compare/v1.0.0-beta-java.1...v1.0.0-beta-java.2" "$test_file"; then + return 0 + else + return 1 + fi +} + +# Test 7: No duplicate [Unreleased] links when existing [Unreleased] link is present +test_no_duplicate_unreleased_links() { + local test_file="${TEST_DIR}/test7.md" + cat > "$test_file" << 'EOF' +# Changelog + +## [Unreleased] + +### Added +- New feature + +## [1.0.0-beta-java.2] - 2026-05-08 + +## [1.0.0-beta-java.1] - 2026-05-05 + +[Unreleased]: https://github.com/test/repo/compare/v1.0.0-beta-java.2...HEAD +[1.0.0-beta-java.2]: https://github.com/test/repo/compare/v1.0.0-beta-java.1...v1.0.0-beta-java.2 +[1.0.0-beta-java.1]: https://github.com/test/repo/compare/v0.3.0-java.2...v1.0.0-beta-java.1 +[0.3.0-java.2]: https://github.com/test/repo/releases/tag/0.3.0-java.2 +EOF + + CHANGELOG_FILE="$test_file" bash "$UPDATE_SCRIPT" 1.0.0-beta-java.3 > /dev/null 2>&1 + + # Count [Unreleased] link definitions - there should be exactly one + local unreleased_count + unreleased_count=$(grep -c "^\[Unreleased\]:" "$test_file") + if [ "$unreleased_count" -eq 1 ] && \ + grep -q "\[Unreleased\]: https://github.com/test/repo/compare/v1.0.0-beta-java.3...HEAD" "$test_file" && \ + grep -q "\[1.0.0-beta-java.3\]: https://github.com/test/repo/compare/v1.0.0-beta-java.2...v1.0.0-beta-java.3" "$test_file"; then + return 0 + else + return 1 + fi +} + # Run all tests echo "Running CHANGELOG update script tests..." echo "" @@ -189,6 +254,8 @@ run_test "Handle CHANGELOG without Unreleased link" test_no_unreleased_link run_test "Preserve content structure" test_preserve_content run_test "Error handling - no Unreleased section" test_no_unreleased_section run_test "Multiple version handling" test_multiple_versions +run_test "Beta-java version format (e.g., 1.0.0-beta-java.N)" test_beta_java_version +run_test "No duplicate [Unreleased] links when existing link is present" test_no_duplicate_unreleased_links echo "" echo "==========================================" diff --git a/.github/scripts/release/update-changelog.sh b/.github/scripts/release/update-changelog.sh index ff35dec722..e21d03774e 100755 --- a/.github/scripts/release/update-changelog.sh +++ b/.github/scripts/release/update-changelog.sh @@ -45,6 +45,7 @@ BEGIN { links_section = 0 first_version_link = "" repo_url = "" + unreleased_link_handled = 0 } # Track if we are in the links section at the bottom @@ -53,7 +54,7 @@ BEGIN { } # Capture the repository URL from the first version link -links_section && repo_url == "" && /^\[[0-9]+\.[0-9]+\.[0-9]+(-java\.[0-9]+)?\]:/ { +links_section && repo_url == "" && /^\[[0-9]+\.[0-9]+\.[0-9]+(-(beta-)?java(-preview)?\.[0-9]+)?\]:/ { match($0, /(https:\/\/github\.com\/[^\/]+\/[^\/]+)\//, arr) if (arr[1] != "") { repo_url = arr[1] @@ -86,28 +87,30 @@ skip_old_reference_impl && /^[[:space:]]*$/ { next } skip_old_reference_impl && /^> \*\*Reference implementation sync:\*\*/ { next } skip_old_reference_impl && !/^[[:space:]]*$/ && !/^> \*\*Reference implementation sync:\*\*/ { skip_old_reference_impl = 0 } -# Capture the first version link to get the previous version -links_section && first_version_link == "" && /^\[[0-9]+\.[0-9]+\.[0-9]+(-java\.[0-9]+)?\]:/ { - match($0, /\[([0-9]+\.[0-9]+\.[0-9]+(-java\.[0-9]+)?)\]:/, arr) - if (arr[1] != "" && repo_url != "") { - first_version_link = arr[1] - # Insert Unreleased and new version links before first version link - print "[Unreleased]: " repo_url "/compare/v" version "...HEAD" - print "[" version "]: " repo_url "/compare/v" arr[1] "...v" version - } -} - -# Update existing [Unreleased] link if present +# Update existing [Unreleased] link if present (must be checked before the first-version-link block) links_section && /^\[Unreleased\]:/ { # Get the previous version and repo URL from the existing link - match($0, /(https:\/\/github\.com\/[^\/]+\/[^\/]+)\/compare\/v([0-9]+\.[0-9]+\.[0-9]+(-java\.[0-9]+)?)\.\.\.HEAD/, arr) + match($0, /(https:\/\/github\.com\/[^\/]+\/[^\/]+)\/compare\/v([0-9]+\.[0-9]+\.[0-9]+(-(beta-)?java(-preview)?\.[0-9]+)?)\.\.\.HEAD/, arr) if (arr[1] != "" && arr[2] != "") { print "[Unreleased]: " arr[1] "/compare/v" version "...HEAD" print "[" version "]: " arr[1] "/compare/v" arr[2] "...v" version + unreleased_link_handled = 1 next } } +# Capture the first version link to get the previous version +# Only fires if the [Unreleased] link was not already handled above +links_section && first_version_link == "" && !unreleased_link_handled && /^\[[0-9]+\.[0-9]+\.[0-9]+(-(beta-)?java(-preview)?\.[0-9]+)?\]:/ { + match($0, /\[([0-9]+\.[0-9]+\.[0-9]+(-(beta-)?java(-preview)?\.[0-9]+)?)\]:/, arr) + if (arr[1] != "" && repo_url != "") { + first_version_link = arr[1] + # Insert Unreleased and new version links before first version link + print "[Unreleased]: " repo_url "/compare/v" version "...HEAD" + print "[" version "]: " repo_url "/compare/v" arr[1] "...v" version + } +} + # Print all other lines unchanged { print } ' "$CHANGELOG_FILE" > "$TEMP_FILE" diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 1143634f69..29fd1fd119 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -89,6 +89,39 @@ jobs: COPILOT_CLI_PATH: ${{ steps.setup-copilot.outputs.path }} run: mvn verify + - name: Validate reference-impl-sync completeness + if: >- + github.event_name == 'pull_request' && + contains(github.event.pull_request.labels.*.name, 'reference-impl-sync') + run: | + git fetch origin main --depth=1 + CHANGED=$(git diff --name-only origin/main...HEAD) + + # 1. .lastmerge must be updated (proves finish script ran) + if echo "$CHANGED" | grep -q '^\\.lastmerge$'; then + echo "βœ… .lastmerge was updated (finish script ran)" + else + echo "❌ .lastmerge was not updated. The merge-reference-impl-finish.sh script" + echo " must be run before this PR can merge. This script updates .lastmerge," + echo " syncs the CLI version in pom.xml, and syncs scripts/codegen/package.json." + exit 1 + fi + + # 2. If codegen inputs changed, generated output must also have changed + if echo "$CHANGED" | grep -q '^scripts/codegen/'; then + if echo "$CHANGED" | grep -q '^src/generated/java/'; then + echo "βœ… Codegen inputs changed and generated files were regenerated" + else + echo "❌ scripts/codegen/ was updated but src/generated/java/ has no changes." + echo " The Codegen Check workflow should regenerate these files automatically." + echo " If it hasn't run yet, wait for it to complete and push regenerated files." + echo " Or run manually: cd scripts/codegen && npm ci && npm run generate" + exit 1 + fi + else + echo "βœ… No codegen input changes β€” regeneration not needed" + fi + - name: Upload test results for site generation if: success() && github.ref == 'refs/heads/main' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 @@ -97,6 +130,7 @@ jobs: path: | target/jacoco-test-results/sdk-tests.exec target/surefire-reports/ + target/surefire-reports-isolated/ retention-days: 1 - name: Generate JaCoCo badge diff --git a/.github/workflows/codegen-agentic-fix.lock.yml b/.github/workflows/codegen-agentic-fix.lock.yml index b44681eb74..f5ab2030d5 100644 --- a/.github/workflows/codegen-agentic-fix.lock.yml +++ b/.github/workflows/codegen-agentic-fix.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"e9707f96f0d1dc7a2c4bbd2c193b917ff5793901b14d055a1fef0da62dd2b928","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"70880e5fb9ec78342cf56974b2414d8cf0a516533e201e437835164543ef58c0","compiler_version":"v0.71.5","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"b8068426813005612b960b5ab0b8bd2c27142323","version":"v0.71.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.40","digest":"sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40","digest":"sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.40","digest":"sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +14,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.68.3). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.71.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -36,16 +36,18 @@ # - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 +# - github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.20 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.20 -# - ghcr.io/github/gh-aw-mcpg:v0.2.19 -# - ghcr.io/github/github-mcp-server:v0.32.0 -# - node:lts-alpine +# - ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51 +# - ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c +# - ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 +# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f name: "Codegen Agentic Fix" "on": @@ -85,6 +87,7 @@ jobs: outputs: comment_id: "" comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} @@ -93,30 +96,34 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Codegen Agentic Fix" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/codegen-agentic-fix.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" - 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 || 'auto' }} - GH_AW_INFO_VERSION: "1.0.21" - GH_AW_INFO_AGENT_VERSION: "1.0.21" - GH_AW_INFO_CLI_VERSION: "v0.68.3" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_AGENT_VERSION: "1.0.40" + GH_AW_INFO_CLI_VERSION: "v0.71.5" GH_AW_INFO_WORKFLOW_NAME: "Codegen Agentic Fix" 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.25.20" + GH_AW_INFO_AWF_VERSION: "v0.25.40" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -135,11 +142,23 @@ jobs: sparse-checkout: | .github .agents + .claude + .codex + .crush + .gemini + .opencode + .pi sparse-checkout-cone-mode: true fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_WORKFLOW_FILE: "codegen-agentic-fix.lock.yml" GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" @@ -150,9 +169,9 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); await main(); - name: Check compile-agentic version - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.68.3" + GH_AW_COMPILED_VERSION: "v0.71.5" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -178,20 +197,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_89262d074598e425_EOF' + cat << 'GH_AW_PROMPT_d94a435f1fbdc38f_EOF' - GH_AW_PROMPT_89262d074598e425_EOF + GH_AW_PROMPT_d94a435f1fbdc38f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_89262d074598e425_EOF' + cat << 'GH_AW_PROMPT_d94a435f1fbdc38f_EOF' - Tools: add_comment(max:5), push_to_pull_request_branch(max:3), missing_tool, missing_data, noop - GH_AW_PROMPT_89262d074598e425_EOF + Tools: add_comment(max:5), push_to_pull_request_branch, missing_tool, missing_data, noop + GH_AW_PROMPT_d94a435f1fbdc38f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_89262d074598e425_EOF' + cat << 'GH_AW_PROMPT_d94a435f1fbdc38f_EOF' + GH_AW_PROMPT_d94a435f1fbdc38f_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_d94a435f1fbdc38f_EOF' The following GitHub context information is available for this workflow: {{#if __GH_AW_GITHUB_ACTOR__ }} @@ -220,17 +242,18 @@ jobs: {{/if}} - GH_AW_PROMPT_89262d074598e425_EOF + GH_AW_PROMPT_d94a435f1fbdc38f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_89262d074598e425_EOF' + cat << 'GH_AW_PROMPT_d94a435f1fbdc38f_EOF' {{#runtime-import .github/workflows/codegen-agentic-fix.md}} - GH_AW_PROMPT_89262d074598e425_EOF + GH_AW_PROMPT_d94a435f1fbdc38f_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" GH_AW_INPUTS_BRANCH: ${{ inputs.branch }} GH_AW_INPUTS_ERROR_SUMMARY: ${{ inputs.error_summary }} GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} @@ -241,7 +264,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); await main(); - name: Substitute placeholders - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_GITHUB_ACTOR: ${{ github.actor }} @@ -255,6 +278,7 @@ jobs: GH_AW_INPUTS_BRANCH: ${{ inputs.branch }} GH_AW_INPUTS_ERROR_SUMMARY: ${{ inputs.error_summary }} GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` β€” run `safeoutputs --help` to see available tools' with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -276,7 +300,8 @@ jobs: GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, GH_AW_INPUTS_BRANCH: process.env.GH_AW_INPUTS_BRANCH, GH_AW_INPUTS_ERROR_SUMMARY: process.env.GH_AW_INPUTS_ERROR_SUMMARY, - GH_AW_INPUTS_PR_NUMBER: process.env.GH_AW_INPUTS_PR_NUMBER + GH_AW_INPUTS_PR_NUMBER: process.env.GH_AW_INPUTS_PR_NUMBER, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST } }); - name: Validate prompt placeholders @@ -294,10 +319,12 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation + include-hidden-files: true path: | /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base if-no-files-found: ignore retention-days: 1 @@ -329,11 +356,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Codegen Agentic Fix" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/codegen-agentic-fix.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" - name: Set runtime paths id: set-runtime-paths run: | @@ -369,7 +400,7 @@ jobs: id: checkout-pr if: | github.event.pull_request || github.event.issue.pull_request - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: @@ -380,11 +411,11 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.40 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.40 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -395,23 +426,33 @@ jobs: script: | const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 ghcr.io/github/gh-aw-firewall/squid:0.25.20 ghcr.io/github/gh-aw-mcpg:v0.2.19 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - - name: Write Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280 ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51 ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_e118843832e795e2_EOF' - {"add_comment":{"max":5,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_to_pull_request_branch":{"if_no_changes":"warn","max":3,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"]},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_e118843832e795e2_EOF - - name: Write Safe Outputs Tools + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_b6e7bb90508de076_EOF' + {"add_comment":{"max":5,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_to_pull_request_branch":{"if_no_changes":"warn","labels":["dependencies"],"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"target":"*"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_b6e7bb90508de076_EOF + - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { - "add_comment": " CONSTRAINTS: Maximum 5 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", - "push_to_pull_request_branch": " CONSTRAINTS: Maximum 3 push(es) can be made." + "add_comment": " CONSTRAINTS: Maximum 5 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading." }, "repo_params": {}, "dynamic_tools": [] @@ -534,7 +575,7 @@ jobs: } } } - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -592,11 +633,12 @@ jobs: GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | set -eo pipefail - mkdir -p /tmp/gh-aw/mcp-config + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') echo "::add-mask::${MCP_GATEWAY_API_KEY}" export MCP_GATEWAY_API_KEY @@ -606,15 +648,19 @@ jobs: 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 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_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -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.2.19' + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -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_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -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.3.6' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_1f06674d94d6899b_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_f8f308e27510535f_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v0.32.0", + "container": "ghcr.io/github/github-mcp-server:v1.0.3", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", @@ -650,37 +696,55 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_1f06674d94d6899b_EOF - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + GH_AW_MCP_CONFIG_f8f308e27510535f_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - name: activation - path: /tmp/gh-aw - - name: Clean git credentials + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials continue-on-error: true run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 + timeout-minutes: 60 run: | set -o pipefail touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.40/awf-config.schema.json","network":{"allowDomains":["*.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","docs.github.com","github-cloud.githubusercontent.com","github-cloud.s3.amazonaws.com","github.blog","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","www.googleapis.com"]},"apiProxy":{"enabled":true,"models":{"auto":["large"],"deep-research":["copilot/deep-research*","google/deep-research*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash"],"opus":["copilot/*opus*","anthropic/*opus*"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"]}},"container":{"imageTag":"0.25.40,squid=sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51,agent=sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504,api-proxy=sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280,cli-proxy=sha256:3e7152911d4b4b7b97beef9d3d7d924ff7902227e86001ef3838fb728d5d514c"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 - sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,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,www.googleapis.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.20 --skip-pull --enable-api-proxy \ - -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_API_KEY: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.68.3 + GH_AW_VERSION: v0.71.5 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows 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 }} @@ -725,7 +789,7 @@ jobs: bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" - name: Redact secrets in logs if: always() - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -751,7 +815,7 @@ jobs: - name: Ingest agent output id: collect_output if: always() - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} 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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,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,www.googleapis.com" @@ -765,7 +829,7 @@ jobs: await main(); - name: Parse agent logs for step summary if: always() - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ with: @@ -777,7 +841,7 @@ jobs: - name: Parse MCP Gateway logs for step summary if: always() id: parse-mcp-gateway - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -790,9 +854,9 @@ jobs: env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs run: | - # Fix permissions on firewall logs so they can be uploaded as artifacts + # Fix permissions on firewall logs/audit dirs 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 + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall 2>/dev/null || true # 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" @@ -802,13 +866,23 @@ jobs: - name: Parse token usage for step summary if: always() continue-on-error: true - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); - name: Write agent output placeholder if missing if: always() run: | @@ -828,14 +902,17 @@ jobs: /tmp/gh-aw/mcp-logs/ /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json /tmp/gh-aw/sandbox/firewall/logs/ /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json if-no-files-found: ignore conclusion: @@ -864,11 +941,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Codegen Agentic Fix" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/codegen-agentic-fix.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -885,7 +966,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Process no-op messages id: noop - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: "1" @@ -902,7 +983,7 @@ jobs: await main(); - name: Log detection run id: detection_runs - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Codegen Agentic Fix" @@ -918,7 +999,7 @@ jobs: await main(); - name: Record missing tool id: missing_tool - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" @@ -932,7 +1013,7 @@ jobs: await main(); - name: Record incomplete id: report_incomplete - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" @@ -947,13 +1028,14 @@ jobs: - name: Handle agent failure id: handle_agent_failure if: always() - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Codegen Agentic Fix" 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: "codegen-agentic-fix" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} @@ -961,13 +1043,16 @@ jobs: GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" - GH_AW_TIMEOUT_MINUTES: "20" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "60" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -992,11 +1077,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Codegen Agentic Fix" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/codegen-agentic-fix.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1022,7 +1111,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 ghcr.io/github/gh-aw-firewall/squid:0.25.20 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280 ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51 - name: Check if detection needed id: detection_guard if: always() @@ -1037,10 +1126,10 @@ jobs: echo "run_detection=false" >> "$GITHUB_OUTPUT" echo "Detection skipped: no agent outputs or patches to analyze" fi - - name: Clear MCP configuration for detection + - name: Clear MCP Config for detection if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f "${RUNNER_TEMP}/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 @@ -1059,7 +1148,7 @@ jobs: 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@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: WORKFLOW_NAME: "Codegen Agentic Fix" WORKFLOW_DESCRIPTION: "Agentic fix for codegen-related build/test failures. Invoked when\nmvn verify fails after code generation changes." @@ -1075,33 +1164,44 @@ jobs: run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.40 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.40 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true id: detection_agentic_execution # Copilot CLI tool arguments (sorted): timeout-minutes: 20 run: | set -o pipefail touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.40/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.40,squid=sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51,agent=sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504,api-proxy=sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280,cli-proxy=sha256:3e7152911d4b4b7b97beef9d3d7d924ff7902227e86001ef3838fb728d5d514c"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 - sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --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,telemetry.enterprise.githubcopilot.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.20 --skip-pull --enable-api-proxy \ - -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_API_KEY: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.68.3 + GH_AW_VERSION: v0.71.5 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows GITHUB_HEAD_REF: ${{ github.head_ref }} GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_SERVER_URL: ${{ github.server_url }} @@ -1122,16 +1222,33 @@ jobs: - name: Parse and conclude threat detection id: detection_conclusion if: always() - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } safe_outputs: needs: @@ -1153,6 +1270,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_VERSION: "1.0.40" GH_AW_WORKFLOW_ID: "codegen-agentic-fix" GH_AW_WORKFLOW_NAME: "Codegen Agentic Fix" outputs: @@ -1169,11 +1287,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Codegen Agentic Fix" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/codegen-agentic-fix.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1227,13 +1349,13 @@ jobs: echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - name: Process Safe Outputs id: process_safe_outputs - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,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,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":5,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":3,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"]},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":5,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"labels\":[\"dependencies\"],\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"target\":\"*\"},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codegen-agentic-fix.md b/.github/workflows/codegen-agentic-fix.md index eb93e1095d..b4aa0a8f20 100644 --- a/.github/workflows/codegen-agentic-fix.md +++ b/.github/workflows/codegen-agentic-fix.md @@ -23,6 +23,8 @@ permissions: contents: read actions: read +timeout-minutes: 60 + network: allowed: - defaults @@ -33,9 +35,10 @@ tools: toolsets: [context, repos] safe-outputs: - push_to_pull_request_branch: - max: 3 - add_comment: + push-to-pull-request-branch: + target: "*" + labels: [dependencies] + add-comment: target: "*" max: 5 noop: @@ -204,7 +207,7 @@ For each attempt: ### Step 5: Push fixes -After `mvn verify` passes, commit all changes and use the `push_to_pull_request_branch` safe-output tool to push to PR #${{ inputs.pr_number }}: +After `mvn verify` passes, commit all changes and use the `push-to-pull-request-branch` safe-output tool to push to PR #${{ inputs.pr_number }}: ```bash git add -A @@ -213,13 +216,13 @@ git commit -m "Fix codegen and build failures after @github/copilot update Automated fix applied by codegen-agentic-fix workflow." ``` -Then call the `push_to_pull_request_branch` tool to push your commits to the PR branch. +Then call the `push-to-pull-request-branch` tool to push your commits to the PR branch. ### Step 6: Failure handling If all 3 attempts fail: -1. Call the `add_comment` tool on PR #${{ inputs.pr_number }} explaining: +1. Call the `add-comment` tool on PR #${{ inputs.pr_number }} explaining: - What errors remain - What fixes were attempted - Whether the issue is in the code generator or handwritten code diff --git a/.github/workflows/codegen-check.yml b/.github/workflows/codegen-check.yml index b6f161d852..95e9365782 100644 --- a/.github/workflows/codegen-check.yml +++ b/.github/workflows/codegen-check.yml @@ -128,6 +128,9 @@ jobs: run: | ERROR_SUMMARY=$(cat /tmp/error-summary.txt) + # Ensure PR has dependencies label (required by codegen-agentic-fix safe-output) + gh pr edit "$PR_NUMBER" --add-label dependencies + gh workflow run codegen-agentic-fix.lock.yml \ -f branch="$BRANCH" \ -f pr_number="$PR_NUMBER" \ diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 5bc3f56182..9522a64923 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -24,9 +24,9 @@ jobs: # Install GitHub CLI and gh-aw extension for Copilot Agent interaction - name: Install gh-aw extension - uses: github/gh-aw/actions/setup-cli@ce1794953e0ec42adc41b6fca05e02ab49ee21c3 # v0.68.3 + uses: github/gh-aw/actions/setup-cli@4d44d0e89851a877f4ddc0cb6c0197e42b1016c5 # v0.73.0 with: - version: v0.42.17 + version: v0.68.3 # Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml index 99e61ac18f..cc612389b7 100644 --- a/.github/workflows/publish-maven.yml +++ b/.github/workflows/publish-maven.yml @@ -87,8 +87,8 @@ jobs: else # Split version: supports "0.1.32", "0.1.32-java.0", and "0.1.32-java-preview.0" formats # Validate RELEASE_VERSION format explicitly to provide clear errors - if ! echo "$RELEASE_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-java(-preview)?\.[0-9]+)?$'; then - echo "Error: RELEASE_VERSION '$RELEASE_VERSION' is invalid. Expected format: M.M.P, M.M.P-java.N, or M.M.P-java-preview.N (e.g., 1.2.3, 1.2.3-java.0, or 1.2.3-java-preview.0)." >&2 + if ! echo "$RELEASE_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-(beta-)?java(-preview)?\.[0-9]+)?$'; then + echo "Error: RELEASE_VERSION '$RELEASE_VERSION' is invalid. Expected format: M.M.P, M.M.P-java.N, M.M.P-java-preview.N, M.M.P-beta-java.N, or M.M.P-beta-java-preview.N (e.g., 1.2.3, 1.2.3-java.0, 1.2.3-java-preview.0, 1.2.3-beta-java.0, or 1.2.3-beta-java-preview.0)." >&2 exit 1 fi # Extract the base M.M.P portion (before any qualifier) @@ -121,21 +121,21 @@ jobs: # Update CHANGELOG.md with release version and Reference implementation sync hash ./.github/scripts/release/update-changelog.sh "${VERSION}" "${REFERENCE_IMPL_HASH}" - # Update version in README.md (supports versions like 1.0.0, 0.1.32-java.0, and 0.3.0-java-preview.0) - sed -i "s|[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-java\(-preview\)\{0,1\}\.[0-9][0-9]*\)\{0,1\}|${VERSION}|g" README.md - sed -i "s|copilot-sdk-java:[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-java\(-preview\)\{0,1\}\.[0-9][0-9]*\)\{0,1\}|copilot-sdk-java:${VERSION}|g" README.md + # Update version in README.md (supports any version qualifier like -java.N, -java-preview.N, -beta-java.N) + sed -i "s|[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-[a-z][a-z0-9-]*\.[0-9][0-9]*\)*|${VERSION}|g" README.md + sed -i "s|copilot-sdk-java:[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-[a-z][a-z0-9-]*\.[0-9][0-9]*\)*|copilot-sdk-java:${VERSION}|g" README.md # Update snapshot version in README.md DEV_VERSION="${{ steps.versions.outputs.dev_version }}" - sed -i "s|[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-java\(-preview\)\{0,1\}\.[0-9][0-9]*\)\{0,1\}-SNAPSHOT|${DEV_VERSION}|g" README.md + sed -i "s|[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-[a-z][a-z0-9-]*\.[0-9][0-9]*\)*-SNAPSHOT|${DEV_VERSION}|g" README.md # Update version in jbang-example.java - sed -i "s|copilot-sdk-java:[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-java\(-preview\)\{0,1\}\.[0-9][0-9]*\)\{0,1\}|copilot-sdk-java:${VERSION}|g" jbang-example.java + sed -i "s|copilot-sdk-java:[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-[a-z][a-z0-9-]*\.[0-9][0-9]*\)*|copilot-sdk-java:${VERSION}|g" jbang-example.java sed -i 's|copilot-sdk-java:${project\.version}|copilot-sdk-java:'"${VERSION}"'|g' jbang-example.java # Update version in cookbook files (hardcoded for direct GitHub browsing and JBang usage) find src/site/markdown/cookbook -name "*.md" -type f -exec \ - sed -i "s|copilot-sdk-java:[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-java\(-preview\)\{0,1\}\.[0-9][0-9]*\)\{0,1\}|copilot-sdk-java:${VERSION}|g" {} \; + sed -i "s|copilot-sdk-java:[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-[a-z][a-z0-9-]*\.[0-9][0-9]*\)*|copilot-sdk-java:${VERSION}|g" {} \; # Commit the documentation changes before release:prepare (requires clean working directory) git add CHANGELOG.md README.md jbang-example.java src/site/markdown/cookbook/ diff --git a/.github/workflows/weekly-reference-impl-sync.lock.yml b/.github/workflows/reference-impl-sync.lock.yml similarity index 69% rename from .github/workflows/weekly-reference-impl-sync.lock.yml rename to .github/workflows/reference-impl-sync.lock.yml index 49bfd3ac0f..72ae7105b7 100644 --- a/.github/workflows/weekly-reference-impl-sync.lock.yml +++ b/.github/workflows/reference-impl-sync.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"54f733578d3011b148c7aff09a0e72085d787a777a996488f08aeffdf52ec93b","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_AGENT_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"54b1896209b363ceadbcb123b6c46dbbcc0118d9140313360c341b09d03bbb96","compiler_version":"v0.71.5","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_AGENT_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"b8068426813005612b960b5ab0b8bd2c27142323","version":"v0.71.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.40","digest":"sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40","digest":"sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.40","digest":"sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +14,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.68.3). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.71.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -22,7 +22,7 @@ # # For more information: https://github.github.com/gh-aw/introduction/overview/ # -# Weekly reference implementation sync workflow. Checks for new commits in the official +# Reference implementation sync workflow. Checks for new commits in the official # Copilot SDK (github/copilot-sdk) and assigns to Copilot to port changes. # # Secrets used: @@ -36,22 +36,24 @@ # - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 +# - github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.20 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.20 -# - ghcr.io/github/gh-aw-mcpg:v0.2.19 -# - ghcr.io/github/github-mcp-server:v0.32.0 -# - node:lts-alpine +# - ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51 +# - ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c +# - ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 +# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f -name: "Weekly Reference Implementation Sync" +name: "Reference Implementation Sync" "on": schedule: - - cron: "40 11 * * 0" - # Friendly format: weekly (scattered) + - cron: "10 16 * * *" + # Friendly format: daily (scattered) workflow_dispatch: inputs: aw_context: @@ -65,7 +67,7 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}" -run-name: "Weekly Reference Implementation Sync" +run-name: "Reference Implementation Sync" jobs: activation: @@ -76,6 +78,7 @@ jobs: outputs: comment_id: "" comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} @@ -84,30 +87,34 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Reference Implementation Sync" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/reference-impl-sync.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" - 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 || 'auto' }} - GH_AW_INFO_VERSION: "1.0.21" - GH_AW_INFO_AGENT_VERSION: "1.0.21" - GH_AW_INFO_CLI_VERSION: "v0.68.3" - GH_AW_INFO_WORKFLOW_NAME: "Weekly Reference Implementation Sync" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_AGENT_VERSION: "1.0.40" + GH_AW_INFO_CLI_VERSION: "v0.71.5" + GH_AW_INFO_WORKFLOW_NAME: "Reference Implementation Sync" 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.25.20" + GH_AW_INFO_AWF_VERSION: "v0.25.40" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -126,13 +133,25 @@ jobs: sparse-checkout: | .github .agents + .claude + .codex + .crush + .gemini + .opencode + .pi sparse-checkout-cone-mode: true fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_WORKFLOW_FILE: "weekly-reference-impl-sync.lock.yml" + GH_AW_WORKFLOW_FILE: "reference-impl-sync.lock.yml" GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | @@ -141,9 +160,9 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); await main(); - name: Check compile-agentic version - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.68.3" + GH_AW_COMPILED_VERSION: "v0.71.5" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -166,17 +185,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_9a049ffd9b204d97_EOF' + cat << 'GH_AW_PROMPT_c9d6e485c3f68bea_EOF' - GH_AW_PROMPT_9a049ffd9b204d97_EOF + GH_AW_PROMPT_c9d6e485c3f68bea_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_9a049ffd9b204d97_EOF' + cat << 'GH_AW_PROMPT_c9d6e485c3f68bea_EOF' - Tools: add_comment(max:10), create_issue, close_issue(max:10), assign_to_agent, missing_tool, missing_data, noop + Tools: add_comment(max:10), create_issue, close_issue(max:10), close_pull_request(max:10), assign_to_agent, missing_tool, missing_data, noop + GH_AW_PROMPT_c9d6e485c3f68bea_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_c9d6e485c3f68bea_EOF' The following GitHub context information is available for this workflow: {{#if __GH_AW_GITHUB_ACTOR__ }} @@ -205,17 +227,18 @@ jobs: {{/if}} - GH_AW_PROMPT_9a049ffd9b204d97_EOF + GH_AW_PROMPT_c9d6e485c3f68bea_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_9a049ffd9b204d97_EOF' + cat << 'GH_AW_PROMPT_c9d6e485c3f68bea_EOF' - {{#runtime-import .github/workflows/weekly-reference-impl-sync.md}} - GH_AW_PROMPT_9a049ffd9b204d97_EOF + {{#runtime-import .github/workflows/reference-impl-sync.md}} + GH_AW_PROMPT_c9d6e485c3f68bea_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -223,7 +246,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); await main(); - name: Substitute placeholders - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_GITHUB_ACTOR: ${{ github.actor }} @@ -234,6 +257,7 @@ jobs: GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` β€” run `safeoutputs --help` to see available tools' with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -252,7 +276,8 @@ jobs: 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 + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST } }); - name: Validate prompt placeholders @@ -270,10 +295,12 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation + include-hidden-files: true path: | /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base if-no-files-found: ignore retention-days: 1 @@ -284,6 +311,7 @@ jobs: actions: read contents: read issues: read + pull-requests: read concurrency: group: "gh-aw-copilot-${{ github.workflow }}" env: @@ -292,7 +320,7 @@ jobs: GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - GH_AW_WORKFLOW_ID_SANITIZED: weeklyreferenceimplsync + GH_AW_WORKFLOW_ID_SANITIZED: referenceimplsync outputs: agentic_engine_timeout: ${{ steps.detect-copilot-errors.outputs.agentic_engine_timeout || 'false' }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} @@ -308,11 +336,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Reference Implementation Sync" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/reference-impl-sync.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" - name: Set runtime paths id: set-runtime-paths run: | @@ -348,7 +380,7 @@ jobs: id: checkout-pr if: | github.event.pull_request || github.event.issue.pull_request - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: @@ -359,11 +391,11 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.40 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.40 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -374,17 +406,28 @@ jobs: script: | const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 ghcr.io/github/gh-aw-firewall/squid:0.25.20 ghcr.io/github/gh-aw-mcpg:v0.2.19 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - - name: Write Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280 ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51 ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_2a317c5680ea1925_EOF' - {"add_comment":{"max":10,"target":"*"},"assign_to_agent":{"max":1,"model":"claude-opus-4.6","name":"copilot","target":"*"},"close_issue":{"max":10,"required_labels":["reference-impl-sync"],"target":"*"},"create_issue":{"assignees":["copilot-swe-agent"],"expires":144,"labels":["reference-impl-sync"],"max":1,"title_prefix":"[reference-impl-sync] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_2a317c5680ea1925_EOF - - name: Write Safe Outputs Tools + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_06a3a494957bf1f1_EOF' + {"add_comment":{"max":10,"target":"*"},"assign_to_agent":{"max":1,"model":"claude-opus-4.6","name":"copilot","target":"*"},"close_issue":{"max":10,"required_labels":["reference-impl-sync"],"target":"*"},"close_pull_request":{"max":10,"target":"*"},"create_issue":{"expires":144,"labels":["reference-impl-sync"],"max":1,"title_prefix":"[reference-impl-sync] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_06a3a494957bf1f1_EOF + - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { @@ -392,7 +435,8 @@ jobs: "add_comment": " CONSTRAINTS: Maximum 10 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", "assign_to_agent": " CONSTRAINTS: Maximum 1 issue(s) can be assigned to agent.", "close_issue": " CONSTRAINTS: Maximum 10 issue(s) can be closed. Target: *.", - "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[reference-impl-sync] \". Labels [\"reference-impl-sync\"] will be automatically added. Assignees [\"copilot-swe-agent\"] will be automatically assigned." + "close_pull_request": " CONSTRAINTS: Maximum 10 pull request(s) can be closed. Target: *.", + "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[reference-impl-sync] \". Labels [\"reference-impl-sync\"] will be automatically added." }, "repo_params": {}, "dynamic_tools": [] @@ -464,6 +508,24 @@ jobs: } } }, + "close_pull_request": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "pull_request_number": { + "optionalPositiveInteger": true + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, "create_issue": { "defaultMax": 1, "fields": { @@ -571,7 +633,7 @@ jobs: } } } - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -629,11 +691,12 @@ jobs: GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | set -eo pipefail - mkdir -p /tmp/gh-aw/mcp-config + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') echo "::add-mask::${MCP_GATEWAY_API_KEY}" export MCP_GATEWAY_API_KEY @@ -643,20 +706,24 @@ jobs: 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 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_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -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.2.19' + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -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_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -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.3.6' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_ee30822602a32ffa_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_03f014fbed5a3560_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v0.32.0", + "container": "ghcr.io/github/github-mcp-server:v1.0.3", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "context,repos,issues" + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" }, "guard-policies": { "allow-only": { @@ -687,15 +754,28 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_ee30822602a32ffa_EOF - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + GH_AW_MCP_CONFIG_03f014fbed5a3560_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - name: activation - path: /tmp/gh-aw - - name: Clean git credentials + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials continue-on-error: true run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): @@ -703,21 +783,26 @@ jobs: run: | set -o pipefail touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.40/awf-config.schema.json","network":{"allowDomains":["*.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","docs.github.com","github-cloud.githubusercontent.com","github-cloud.s3.amazonaws.com","github.blog","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","www.googleapis.com"]},"apiProxy":{"enabled":true,"models":{"auto":["large"],"deep-research":["copilot/deep-research*","google/deep-research*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash"],"opus":["copilot/*opus*","anthropic/*opus*"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"]}},"container":{"imageTag":"0.25.40,squid=sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51,agent=sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504,api-proxy=sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280,cli-proxy=sha256:3e7152911d4b4b7b97beef9d3d7d924ff7902227e86001ef3838fb728d5d514c"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 - sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,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,www.googleapis.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.20 --skip-pull --enable-api-proxy \ - -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_API_KEY: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.68.3 + GH_AW_VERSION: v0.71.5 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows 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 }} @@ -762,7 +847,7 @@ jobs: bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" - name: Redact secrets in logs if: always() - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -788,7 +873,7 @@ jobs: - name: Ingest agent output id: collect_output if: always() - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} 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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,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,www.googleapis.com" @@ -802,7 +887,7 @@ jobs: await main(); - name: Parse agent logs for step summary if: always() - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ with: @@ -814,7 +899,7 @@ jobs: - name: Parse MCP Gateway logs for step summary if: always() id: parse-mcp-gateway - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -827,9 +912,9 @@ jobs: env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs run: | - # Fix permissions on firewall logs so they can be uploaded as artifacts + # Fix permissions on firewall logs/audit dirs 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 + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall 2>/dev/null || true # 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" @@ -839,13 +924,23 @@ jobs: - name: Parse token usage for step summary if: always() continue-on-error: true - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); - name: Write agent output placeholder if missing if: always() run: | @@ -865,14 +960,17 @@ jobs: /tmp/gh-aw/mcp-logs/ /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json /tmp/gh-aw/sandbox/firewall/logs/ /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json if-no-files-found: ignore conclusion: @@ -891,7 +989,7 @@ jobs: issues: write pull-requests: write concurrency: - group: "gh-aw-conclusion-weekly-reference-impl-sync" + group: "gh-aw-conclusion-reference-impl-sync" cancel-in-progress: false outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} @@ -901,11 +999,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Reference Implementation Sync" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/reference-impl-sync.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -922,11 +1024,11 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Process no-op messages id: noop - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: "1" - GH_AW_WORKFLOW_NAME: "Weekly Reference Implementation Sync" + GH_AW_WORKFLOW_NAME: "Reference Implementation Sync" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_NOOP_REPORT_AS_ISSUE: "false" @@ -939,10 +1041,10 @@ jobs: await main(); - name: Log detection run id: detection_runs - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Weekly Reference Implementation Sync" + GH_AW_WORKFLOW_NAME: "Reference Implementation Sync" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} @@ -955,11 +1057,11 @@ jobs: await main(); - name: Record missing tool id: missing_tool - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" - GH_AW_WORKFLOW_NAME: "Weekly Reference Implementation Sync" + GH_AW_WORKFLOW_NAME: "Reference Implementation Sync" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -969,11 +1071,11 @@ jobs: await main(); - name: Record incomplete id: report_incomplete - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" - GH_AW_WORKFLOW_NAME: "Weekly Reference Implementation Sync" + GH_AW_WORKFLOW_NAME: "Reference Implementation Sync" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -984,13 +1086,14 @@ jobs: - name: Handle agent failure id: handle_agent_failure if: always() - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Weekly Reference Implementation Sync" + GH_AW_WORKFLOW_NAME: "Reference Implementation Sync" 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-reference-impl-sync" + GH_AW_WORKFLOW_ID: "reference-impl-sync" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} @@ -998,12 +1101,15 @@ jobs: GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" 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_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "20" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1029,11 +1135,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Reference Implementation Sync" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/reference-impl-sync.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1059,7 +1169,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 ghcr.io/github/gh-aw-firewall/squid:0.25.20 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280 ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51 - name: Check if detection needed id: detection_guard if: always() @@ -1074,10 +1184,10 @@ jobs: echo "run_detection=false" >> "$GITHUB_OUTPUT" echo "Detection skipped: no agent outputs or patches to analyze" fi - - name: Clear MCP configuration for detection + - name: Clear MCP Config for detection if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f "${RUNNER_TEMP}/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 @@ -1096,10 +1206,10 @@ jobs: 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@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - WORKFLOW_NAME: "Weekly Reference Implementation Sync" - WORKFLOW_DESCRIPTION: "Weekly reference implementation sync workflow. Checks for new commits in the official\nCopilot SDK (github/copilot-sdk) and assigns to Copilot to port changes." + WORKFLOW_NAME: "Reference Implementation Sync" + WORKFLOW_DESCRIPTION: "Reference implementation 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: | @@ -1112,33 +1222,44 @@ jobs: run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.40 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.40 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true id: detection_agentic_execution # Copilot CLI tool arguments (sorted): timeout-minutes: 20 run: | set -o pipefail touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.40/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.40,squid=sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51,agent=sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504,api-proxy=sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280,cli-proxy=sha256:3e7152911d4b4b7b97beef9d3d7d924ff7902227e86001ef3838fb728d5d514c"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 - sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --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,telemetry.enterprise.githubcopilot.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.20 --skip-pull --enable-api-proxy \ - -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_API_KEY: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.68.3 + GH_AW_VERSION: v0.71.5 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows GITHUB_HEAD_REF: ${{ github.head_ref }} GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_SERVER_URL: ${{ github.server_url }} @@ -1159,16 +1280,33 @@ jobs: - name: Parse and conclude threat detection id: detection_conclusion if: always() - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } safe_outputs: needs: @@ -1184,14 +1322,15 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/weekly-reference-impl-sync" + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/reference-impl-sync" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_WORKFLOW_ID: "weekly-reference-impl-sync" - GH_AW_WORKFLOW_NAME: "Weekly Reference Implementation Sync" + GH_AW_ENGINE_VERSION: "1.0.40" + GH_AW_WORKFLOW_ID: "reference-impl-sync" + GH_AW_WORKFLOW_NAME: "Reference Implementation Sync" outputs: assign_to_agent_assigned: ${{ steps.process_safe_outputs.outputs.assign_to_agent_assigned }} assign_to_agent_assignment_error_count: ${{ steps.process_safe_outputs.outputs.assign_to_agent_assignment_error_count }} @@ -1209,11 +1348,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Reference Implementation Sync" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/reference-impl-sync.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1239,13 +1382,13 @@ jobs: echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - name: Process Safe Outputs id: process_safe_outputs - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,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,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"assign_to_agent\":{\"max\":1,\"model\":\"claude-opus-4.6\",\"name\":\"copilot\",\"target\":\"*\"},\"close_issue\":{\"max\":10,\"required_labels\":[\"reference-impl-sync\"],\"target\":\"*\"},\"create_issue\":{\"assignees\":[\"copilot-swe-agent\"],\"expires\":144,\"labels\":[\"reference-impl-sync\"],\"max\":1,\"title_prefix\":\"[reference-impl-sync] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"assign_to_agent\":{\"max\":1,\"model\":\"claude-opus-4.6\",\"name\":\"copilot\",\"target\":\"*\"},\"close_issue\":{\"max\":10,\"required_labels\":[\"reference-impl-sync\"],\"target\":\"*\"},\"close_pull_request\":{\"max\":10,\"target\":\"*\"},\"create_issue\":{\"expires\":144,\"labels\":[\"reference-impl-sync\"],\"max\":1,\"title_prefix\":\"[reference-impl-sync] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" GH_AW_ASSIGN_TO_AGENT_TOKEN: ${{ secrets.GH_AW_AGENT_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/weekly-reference-impl-sync.md b/.github/workflows/reference-impl-sync.md similarity index 70% rename from .github/workflows/weekly-reference-impl-sync.md rename to .github/workflows/reference-impl-sync.md index 1d1f9a4fd5..20cad89e14 100644 --- a/.github/workflows/weekly-reference-impl-sync.md +++ b/.github/workflows/reference-impl-sync.md @@ -1,16 +1,17 @@ --- description: | - Weekly reference implementation sync workflow. Checks for new commits in the official + Reference implementation sync workflow. Checks for new commits in the official Copilot SDK (github/copilot-sdk) and assigns to Copilot to port changes. on: - schedule: weekly + schedule: daily workflow_dispatch: permissions: contents: read actions: read issues: read + pull-requests: read network: allowed: @@ -19,12 +20,11 @@ network: tools: github: - toolsets: [context, repos, issues] + toolsets: [context, repos, issues, pull_requests] safe-outputs: create-issue: title-prefix: "[reference-impl-sync] " - assignees: [copilot-swe-agent] labels: [reference-impl-sync] expires: 6 close-issue: @@ -38,10 +38,13 @@ safe-outputs: name: "copilot" model: "claude-opus-4.6" target: "*" + close-pull-request: + target: "*" + max: 10 noop: report-as-issue: false --- -# Weekly Reference Implementation Sync +# Reference Implementation Sync You are an automation agent that detects new reference implementation changes and creates GitHub issues. You do **NOT** perform any code merges, edits, or pushes. Do **NOT** invoke any skills (especially `agentic-merge-reference-impl`). Your only job is to check for changes and use safe-output tools to create or close issues. @@ -79,14 +82,16 @@ Go to Step 3b. 1. Search for any open issues with the `reference-impl-sync` label using the GitHub MCP tools. 2. If there are open `reference-impl-sync` labeled issues, close each one using the `close_issue` safe-output tool with a comment: "No new reference implementation changes detected. The Java SDK is up to date. Closing." -3. Call the `noop` safe-output tool with message: "No new reference implementation changes since last merge ()." -4. **Stop here.** Do not proceed further. +3. Search for any open pull requests whose head branch starts with `copilot/reference-impl`. Close each one using the `close_pull_request` safe-output tool with a comment: "Superseded β€” the Java SDK is up to date with the reference implementation. Closing stale sync PR." +4. Call the `noop` safe-output tool with message: "No new reference implementation changes since last merge ()." +5. **Stop here.** Do not proceed further. ### Step 3b: Changes detected 1. Search for any open issues with the `reference-impl-sync` label using the GitHub MCP tools. 2. Close each existing open `reference-impl-sync` issue using the `close_issue` safe-output tool with a comment: "Superseded by a newer reference implementation sync check." -3. Create a new issue using the `create_issue` safe-output tool with: +3. Search for any open pull requests whose head branch starts with `copilot/reference-impl`. Close each one using the `close_pull_request` safe-output tool with a comment: "Superseded by a newer reference implementation sync issue. Closing stale sync PR." +4. Create a new issue using the `create_issue` safe-output tool with: - **Title:** `Reference Implementation sync: new commits ()` - **Body:** Include the following information: ``` @@ -105,9 +110,15 @@ Go to Step 3b. ### Instructions - Follow the [agentic-merge-reference-impl](.github/prompts/agentic-merge-reference-impl.prompt.md) prompt to port these changes to the Java SDK. + **You MUST follow these steps in order. Do NOT improvise or skip scripts.** + + 1. βœ…βœ…Read [.github/prompts/agentic-merge-reference-impl.prompt.md](.github/prompts/agentic-merge-reference-impl.prompt.md) in full before startingβœ…βœ… + + ❌❌Do NOT clone the reference implementation manually β€” the start script does this.❌❌ + ❌❌Do NOT update .lastmerge manually β€” the finish script does this.❌❌ + ❌❌Do NOT skip the finish script β€” it syncs codegen versions and updates .lastmerge.❌❌ ``` -4. After creating the issue, use the `assign_to_agent` safe-output tool to assign Copilot to the newly created issue. +5. After creating the issue, use the `assign_to_agent` safe-output tool to assign Copilot to the newly created issue. ## Important constraints diff --git a/.github/workflows/update-copilot-dependency.yml b/.github/workflows/update-copilot-dependency.yml index f7faee073a..d29da6acb7 100644 --- a/.github/workflows/update-copilot-dependency.yml +++ b/.github/workflows/update-copilot-dependency.yml @@ -147,6 +147,7 @@ jobs: if gh pr view "$BRANCH" >/dev/null 2>&1; then echo "Pull request for branch '$BRANCH' already exists; updated branch only." PR_NUMBER=$(gh pr view "$BRANCH" --json number --jq .number) + gh pr edit "$PR_NUMBER" --add-label dependencies else PR_NUMBER=$(gh pr create \ --title "Update @github/copilot to $VERSION" \ @@ -157,6 +158,7 @@ jobs: - Re-ran Java code generator (\`scripts/codegen\`) > Created by the **Update @github/copilot Dependency** workflow." \ + --label dependencies \ --base main \ --head "$BRANCH" \ | grep -oE '[0-9]+$' || gh pr view "$BRANCH" --json number --jq .number) diff --git a/.github/workflows/weekly-reference-impl-sync.yml b/.github/workflows/weekly-reference-impl-sync.yml deleted file mode 100644 index aa3fc971ac..0000000000 --- a/.github/workflows/weekly-reference-impl-sync.yml +++ /dev/null @@ -1,181 +0,0 @@ -name: "Weekly Reference Implementation Sync" - -on: - schedule: - # Every Monday at 10:00 UTC (5:00 AM EST / 6:00 AM EDT) - - cron: '0 10 * * 1' - workflow_dispatch: - -permissions: - contents: write - issues: write - pull-requests: write - -env: - # Used for `gh` CLI operations to create/close/comment issues. Must be a PAT with repo permissions because the default - GH_AW_AGENT_TOKEN: ${{ secrets.GH_AW_AGENT_TOKEN }} - GH_TOKEN: ${{ secrets.GH_AW_AGENT_TOKEN }} - -jobs: - - check-and-sync: - name: "Check reference implementation & trigger Copilot merge" - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Check for reference implementation changes - id: check - run: | - LAST_MERGE=$(cat .lastmerge) - echo "Last merged commit: $LAST_MERGE" - - git clone --quiet https://github.com/github/copilot-sdk.git /tmp/reference-impl - cd /tmp/reference-impl - - REFERENCE_IMPL_HEAD=$(git rev-parse HEAD) - echo "Reference implementation HEAD: $REFERENCE_IMPL_HEAD" - - echo "last_merge=$LAST_MERGE" >> "$GITHUB_OUTPUT" - - if [ "$LAST_MERGE" = "$REFERENCE_IMPL_HEAD" ]; then - echo "No new reference implementation changes since last merge." - echo "has_changes=false" >> "$GITHUB_OUTPUT" - else - COMMIT_COUNT=$(git rev-list --count "$LAST_MERGE".."$REFERENCE_IMPL_HEAD") - echo "Found $COMMIT_COUNT new reference implementation commits." - echo "has_changes=true" >> "$GITHUB_OUTPUT" - echo "commit_count=$COMMIT_COUNT" >> "$GITHUB_OUTPUT" - echo "reference_impl_head=$REFERENCE_IMPL_HEAD" >> "$GITHUB_OUTPUT" - - # Generate a short summary of changes - SUMMARY=$(git log --oneline "$LAST_MERGE".."$REFERENCE_IMPL_HEAD" | head -20) - { - echo "summary<> "$GITHUB_OUTPUT" - fi - - - name: Close previous reference-impl-sync issues - if: steps.check.outputs.has_changes == 'true' - run: | - # Find all open issues with the reference-impl-sync label - OPEN_ISSUES=$(gh issue list \ - --repo "${{ github.repository }}" \ - --label "reference-impl-sync" \ - --state open \ - --json number \ - --jq '.[].number') - - for ISSUE_NUM in $OPEN_ISSUES; do - echo "Closing superseded issue #${ISSUE_NUM}" - gh issue comment "$ISSUE_NUM" \ - --repo "${{ github.repository }}" \ - --body "Superseded by a newer reference implementation sync issue. Closing this one." - gh issue close "$ISSUE_NUM" \ - --repo "${{ github.repository }}" \ - --reason "not planned" - done - - - name: Close stale reference-impl-sync issues (no changes) - if: steps.check.outputs.has_changes == 'false' - run: | - OPEN_ISSUES=$(gh issue list \ - --repo "${{ github.repository }}" \ - --label "reference-impl-sync" \ - --state open \ - --json number \ - --jq '.[].number') - - for ISSUE_NUM in $OPEN_ISSUES; do - echo "Closing stale issue #${ISSUE_NUM} β€” reference implementation is up to date" - gh issue comment "$ISSUE_NUM" \ - --repo "${{ github.repository }}" \ - --body "No new reference implementation changes detected. The Java SDK is up to date. Closing." - gh issue close "$ISSUE_NUM" \ - --repo "${{ github.repository }}" \ - --reason "completed" - done - - - name: Create issue and assign to Copilot - id: create-issue - if: steps.check.outputs.has_changes == 'true' - env: - SUMMARY: ${{ steps.check.outputs.summary }} - run: | - COMMIT_COUNT="${{ steps.check.outputs.commit_count }}" - LAST_MERGE="${{ steps.check.outputs.last_merge }}" - REFERENCE_IMPL_HEAD="${{ steps.check.outputs.reference_impl_head }}" - DATE=$(date -u +"%Y-%m-%d") - REPO="${{ github.repository }}" - - BODY="## Automated Reference Implementation Sync - - There are **${COMMIT_COUNT}** new commits in the [official Copilot SDK](https://github.com/github/copilot-sdk) since the last merge. - - - **Last merged commit:** [\`${LAST_MERGE}\`](https://github.com/github/copilot-sdk/commit/${LAST_MERGE}) - - **Reference implementation HEAD:** [\`${REFERENCE_IMPL_HEAD}\`](https://github.com/github/copilot-sdk/commit/${REFERENCE_IMPL_HEAD}) - - ### Recent reference implementation commits - - \`\`\` - ${SUMMARY} - \`\`\` - - ### Instructions - - Follow the [agentic-merge-reference-impl](.github/prompts/agentic-merge-reference-impl.prompt.md) prompt to port these changes to the Java SDK." - - # Create the issue and assign to Copilot coding agent - ISSUE_URL=$(gh issue create \ - --repo "$REPO" \ - --title "Reference implementation sync: ${COMMIT_COUNT} new commits (${DATE})" \ - --body "$BODY" \ - --label "reference-impl-sync" \ - --assignee "copilot-swe-agent") - - echo "issue_url=$ISSUE_URL" >> "$GITHUB_OUTPUT" - echo "βœ… Issue created and assigned to Copilot coding agent: $ISSUE_URL" - - - name: Summary - if: always() - env: - SUMMARY: ${{ steps.check.outputs.summary }} - run: | - HAS_CHANGES="${{ steps.check.outputs.has_changes }}" - COMMIT_COUNT="${{ steps.check.outputs.commit_count }}" - LAST_MERGE="${{ steps.check.outputs.last_merge }}" - REFERENCE_IMPL_HEAD="${{ steps.check.outputs.reference_impl_head }}" - ISSUE_URL="${{ steps.create-issue.outputs.issue_url }}" - - { - echo "## Weekly Reference Implementation Sync" - echo "" - if [ "$HAS_CHANGES" = "true" ]; then - echo "### βœ… New reference implementation changes detected" - echo "" - echo "| | |" - echo "|---|---|" - echo "| **New commits** | ${COMMIT_COUNT} |" - echo "| **Last merged** | \`${LAST_MERGE:0:12}\` |" - echo "| **Reference implementation HEAD** | \`${REFERENCE_IMPL_HEAD:0:12}\` |" - echo "" - echo "An issue has been created and assigned to the Copilot coding agent: " - echo " -> ${ISSUE_URL}" - echo "" - echo "### Recent reference implementation commits" - echo "" - echo '```' - echo "$SUMMARY" - echo '```' - else - echo "### ⏭️ No changes" - echo "" - echo "The Java SDK is already up to date with the reference implementation Copilot SDK." - echo "" - echo "**Last merged commit:** \`${LAST_MERGE:0:12}\`" - fi - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index 35ea546f09..654d3278f8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ changebundle.txt* .settings scripts/codegen/node_modules/ *~ +*.sln diff --git a/.lastmerge b/.lastmerge index caffac28be..600b63a547 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -dd2dcbc439256acfb9feb2cff07c0b9c820091b8 +e20f5bef125860accb30c60d1b35109371a77f16 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d198ee218..8e174dd216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] -> **Reference implementation sync:** [`github/copilot-sdk@dd2dcbc`](https://github.com/github/copilot-sdk/commit/dd2dcbc439256acfb9feb2cff07c0b9c820091b8) +> **Reference implementation sync:** [`github/copilot-sdk@e20f5be`](https://github.com/github/copilot-sdk/commit/e20f5bef125860accb30c60d1b35109371a77f16) + +## [1.0.0-beta-java.4] - 2026-05-16 + +> **Reference implementation sync:** [`github/copilot-sdk@e20f5be`](https://github.com/github/copilot-sdk/commit/e20f5bef125860accb30c60d1b35109371a77f16) +## [1.0.0-beta-java.3] - 2026-05-11 +> **Reference implementation sync:** [`github/copilot-sdk@4a0437b`](https://github.com/github/copilot-sdk/commit/4a0437bb03a0b60a1867f14ae8e3faf053afa5aa) +## [1.0.0-beta-java.2] - 2026-05-08 + +> **Reference implementation sync:** [`github/copilot-sdk@066a69c`](https://github.com/github/copilot-sdk/commit/066a69c1e849adf1bd98564ab1b52316ec471182) +## [1.0.0-beta-java.1] - 2026-05-05 + +> **Reference implementation sync:** [`github/copilot-sdk@c063458`](https://github.com/github/copilot-sdk/commit/c063458ecc3d606766f04cf203b11b08de672cc8) ## [0.3.0-java.2] - 2026-04-26 > **Reference implementation sync:** [`github/copilot-sdk@dd2dcbc`](https://github.com/github/copilot-sdk/commit/dd2dcbc439256acfb9feb2cff07c0b9c820091b8) @@ -497,31 +509,13 @@ New types: `GetForegroundSessionResponse`, `SetForegroundSessionResponse` - Pre-commit hook for Spotless code formatting - Comprehensive API documentation -[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.3.0-java-preview.0...HEAD -[0.3.0-java-preview.0]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java-preview.0 -[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.3.0-java-preview.0...HEAD -[0.3.0-java-preview.0]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java-preview.0 -[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.3.0-java-preview.1...HEAD -[0.3.0-java-preview.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java-preview.1 -[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.3.0-java.2...HEAD +[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v1.0.0-beta-java.4...HEAD +[1.0.0-beta-java.4]: https://github.com/github/copilot-sdk-java/compare/v1.0.0-beta-java.3...v1.0.0-beta-java.4 +[1.0.0-beta-java.3]: https://github.com/github/copilot-sdk-java/compare/v1.0.0-beta-java.2...v1.0.0-beta-java.3 +[1.0.0-beta-java.2]: https://github.com/github/copilot-sdk-java/compare/v1.0.0-beta-java.1...v1.0.0-beta-java.2 +[1.0.0-beta-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.3.0-java.2...v1.0.0-beta-java.1 [0.3.0-java.2]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java.2 -[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1 -[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.3.0-java-preview.0...HEAD -[0.3.0-java-preview.0]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java-preview.0 -[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1 -[0.2.1-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...v0.2.1-java.1 -[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.3.0-java-preview.0...HEAD -[0.3.0-java-preview.0]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java-preview.0 -[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1 -[0.2.1-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...v0.2.1-java.1 -[0.2.1-java.0]: https://github.com/github/copilot-sdk-java/compare/v0.1.32-java.0...v0.2.1-java.0 -[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.3.0-java-preview.0...HEAD -[0.3.0-java-preview.0]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java-preview.0 -[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1 -[0.2.1-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...v0.2.1-java.1 -[0.2.1-java.0]: https://github.com/github/copilot-sdk-java/compare/v0.1.32-java.0...v0.2.1-java.0 -[0.1.32-java.0]: https://github.com/github/copilot-sdk-java/compare/v1.0.11...v0.1.32-java.0 -[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.3.0-java-preview.0...HEAD +[0.3.0-java-preview.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java-preview.1 [0.3.0-java-preview.0]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java-preview.0 [0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1 [0.2.1-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...v0.2.1-java.1 @@ -539,4 +533,3 @@ New types: `GetForegroundSessionResponse`, `SetForegroundSessionResponse` [1.0.2]: https://github.com/github/copilot-sdk-java/compare/v1.0.1...v1.0.2 [1.0.1]: https://github.com/github/copilot-sdk-java/compare/1.0.0...v1.0.1 [1.0.0]: https://github.com/github/copilot-sdk-java/releases/tag/1.0.0 - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f86976c01f..38f2def77c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,9 @@ If you have ideas for entirely new features, please post an issue or start a dis 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 +### Running locally, including tests and linters + +The POM has logic to ensure a known correct installation of Copilot CLI is used when executing the tests. If you want to test against a different installation of Copilot CLI, set the value of the `copilot.cli.path` maven property to the fully qualified path to the Copilot CLI. ```bash # Build and run all tests @@ -54,6 +56,22 @@ mvn spotless:apply mvn spotless:check ``` +## Running the known correct Copilot CLI installation + +### POSIX + +```bash +export COPILOT_CLI_PATH="$(find "$PWD/target" -type f -path '*/nodejs/node_modules/@github/copilot/index.js' | head -n 1)" +node ${COPILOT_CLI_PATH} +``` + +### PowerShell + +```PowerShell +$env:COPILOT_CLI_PATH = (Get-ChildItem -Path "$PWD\target" -Recurse -Filter 'index.js' -File | Where-Object { $_.FullName -match '[\\/]nodejs[\\/]node_modules[\\/]@github[\\/]copilot[\\/]index\.js$' } | Select-Object -First 1 -ExpandProperty FullName) +node $env:COPILOT_CLI_PATH +``` + Here are a few things you can do that will increase the likelihood of your pull request being accepted: - Write tests. diff --git a/README.md b/README.md index f5658f3d6f..f2b342dfc3 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A com.github copilot-sdk-java - 0.3.0-java.2 + 1.0.0-beta-java.4 ``` @@ -53,14 +53,14 @@ Snapshot builds of the next development version are published to Maven Central S com.github copilot-sdk-java - 0.3.1-java.1-SNAPSHOT + 1.0.0-beta-java.5-SNAPSHOT ``` ### Gradle ```groovy -implementation 'com.github:copilot-sdk-java:0.3.0-java.2' +implementation 'com.github:copilot-sdk-java:1.0.0-beta-java.4' ``` ## Quick Start @@ -165,7 +165,7 @@ Contributions are welcome! Please see the [Contributing Guide](CONTRIBUTING.md) This SDK tracks the official [Copilot SDK](https://github.com/github/copilot-sdk) (.NET reference implementation) and ports changes to Java. The reference implementation merge process is automated with AI assistance: -**Weekly automated sync** β€” A [scheduled GitHub Actions workflow](.github/workflows/weekly-reference-impl-sync.yml) runs every Monday at 5 AM ET. It checks for new reference implementation commits since the last merge (tracked in [`.lastmerge`](.lastmerge)), and if changes are found, creates an issue labeled `reference-impl-sync` and assigns it to the GitHub Copilot coding agent. Any previously open `reference-impl-sync` issues are automatically closed. +**Automated sync** β€” A [scheduled GitHub Actions workflow](.github/workflows/reference-impl-sync.yml) runs on the schedule specified in that file. It checks for new reference implementation commits since the last merge (tracked in [`.lastmerge`](.lastmerge)), and if changes are found, creates an issue labeled `reference-impl-sync` and assigns it to the GitHub Copilot coding agent. Any previously open `reference-impl-sync` issues are automatically closed. The sync also updates the `@github/copilot` version in both `pom.xml` and `scripts/codegen/package.json` to keep schemas and test CLI in lockstep. **Reusable prompt** β€” The merge workflow is defined in [`agentic-merge-reference-impl.prompt.md`](.github/prompts/agentic-merge-reference-impl.prompt.md). It can be triggered manually from: - **VS Code Copilot Chat** β€” type `/agentic-merge-reference-impl` diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 9a6f3761b5..fcde1f6c97 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -11,9 +11,9 @@ - + - + diff --git a/docs/WORKFLOWS.md b/docs/WORKFLOWS.md index 3df254890c..9dd3833d1b 100644 --- a/docs/WORKFLOWS.md +++ b/docs/WORKFLOWS.md @@ -6,11 +6,11 @@ |----------|-------------|----------|----------| | [Build & Test](workflows/build-test.yml) | Builds, lints, and tests the Java SDK | `push` (main), `pull_request`, `merge_group`, `workflow_dispatch` | Sundays at 00:00 UTC | | [Codegen Check](workflows/codegen-check.yml) | Verifies that generated Java files are up-to-date with the JSON schemas | `push` (main), `pull_request`, `workflow_dispatch` | β€” | -| [Update @github/copilot Dependency](workflows/update-copilot-dependency.yml) | Updates the `@github/copilot` npm package, re-runs code generation, and opens a PR | `workflow_dispatch` | β€” | +| [Update @github/copilot Dependency](workflows/update-copilot-dependency.yml) | _(Codegen-only)_ Updates the `@github/copilot` npm package in `scripts/codegen`, re-runs code generation, and opens a PR. Does **not** update `pom.xml`; use the Reference Implementation Sync for full version alignment. | `workflow_dispatch` | β€” | | [Deploy Documentation](workflows/deploy-site.yml) | Generates and deploys versioned docs to GitHub Pages | `workflow_run` (after Build & Test), `release`, `workflow_dispatch` | β€” | | [Publish to Maven Central](workflows/publish-maven.yml) | Releases the SDK to Maven Central and creates a GitHub Release | `workflow_dispatch` | β€” | -| [Weekly Reference Implementation Sync](workflows/weekly-reference-impl-sync.yml) | Checks for new reference implementation commits and creates an issue for Copilot to merge | `workflow_dispatch` | Mondays at 10:00 UTC | -| [Weekly Reference Implementation Sync (Agentic)](workflows/weekly-reference-impl-sync.lock.yml) | Compiled agentic workflow that executes the reference implementation sync via `gh-aw` | `workflow_dispatch` | Tuesdays at 08:39 UTC (scattered) | +| [Reference Implementation Sync](workflows/reference-impl-sync.yml) | Checks for new reference implementation commits and creates an issue for Copilot to merge | `workflow_dispatch` | [See workflow](workflows/reference-impl-sync.yml) | +| [Reference Implementation Sync (Agentic)](workflows/reference-impl-sync.lock.yml) | Compiled agentic workflow that executes the reference implementation sync via `gh-aw` | `workflow_dispatch` | [See workflow](workflows/reference-impl-sync.lock.yml) | | [Copilot Setup Steps](workflows/copilot-setup-steps.yml) | Configures the environment for the GitHub Copilot coding agent | `push` (on self-change), `workflow_dispatch` | β€” | --- @@ -64,11 +64,11 @@ Manual-only workflow that performs a full release: --- -## Weekly Reference Implementation Sync +## Reference Implementation Sync -**File:** [`weekly-reference-impl-sync.yml`](workflows/weekly-reference-impl-sync.yml) +**File:** [`reference-impl-sync.yml`](workflows/reference-impl-sync.yml) -Runs every Monday at 10:00 UTC. Clones the official `github/copilot-sdk` repository and compares `HEAD` against the commit hash stored in `.lastmerge`. +Runs on the schedule specified in [`.github/workflows/reference-impl-sync.yml`](workflows/reference-impl-sync.yml). Clones the official `github/copilot-sdk` repository and compares `HEAD` against the commit hash stored in `.lastmerge`. If new commits are found: 1. Closes any previously open `reference-impl-sync` issues @@ -79,9 +79,9 @@ If no changes are found, any stale open `reference-impl-sync` issues are closed. --- -## Weekly Reference Implementation Sync (Agentic Workflow: Experimental) +## Reference Implementation Sync (Agentic Workflow: Experimental) -**File:** [`weekly-reference-impl-sync.lock.yml`](workflows/weekly-reference-impl-sync.lock.yml) +**File:** [`reference-impl-sync.lock.yml`](workflows/reference-impl-sync.lock.yml) Auto-generated compiled workflow produced by `gh aw compile` from the corresponding `.md` source. This is the agentic counterpart that actually executes the reference implementation merge using the `gh-aw` MCP server and Copilot coding agent. @@ -113,6 +113,8 @@ If changes appear, commit the updated generated files. **File:** [`update-copilot-dependency.yml`](workflows/update-copilot-dependency.yml) +> **Note:** This workflow is for codegen-only updates to `scripts/codegen`. It does **not** update `pom.xml`. For full version alignment (both CLI and codegen in lockstep), use the **Reference Implementation Sync** workflow instead. + Manual workflow triggered when a new version of the `@github/copilot` npm package is published. Accepts a `version` input (e.g. `1.0.25`). Steps: diff --git a/docs/adr/adr-002-maven-version-and-reference-implementation-tracking.md b/docs/adr/adr-002-maven-version-and-reference-implementation-tracking.md index 6a0b54f225..248a024c23 100644 --- a/docs/adr/adr-002-maven-version-and-reference-implementation-tracking.md +++ b/docs/adr/adr-002-maven-version-and-reference-implementation-tracking.md @@ -8,7 +8,7 @@ Releases of this implementation track releases of the reference implementation. - Simple number qualifier (0.1.32-0, 0.1.32-1, ...) fails on a subtle but important point: 0.1.32-0 is treated identically to 0.1.32 by Maven (trailing zeros are normalized away), and bare numeric qualifiers are pre-release semantics. Your "first release" would sort before the reference implementation bare version. -- Java and number in the qualifier (0.1.32-java.N +- Java and number in the qualifier (0.1.32-java.N) - java is an unknown qualifier that sorts correctly and accurately describes what it is β€” the Java-ecosystem release of this version. diff --git a/jbang-example.java b/jbang-example.java index 4b711ba735..1c41679cdf 100644 --- a/jbang-example.java +++ b/jbang-example.java @@ -1,5 +1,5 @@ -! -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.generated.SessionUsageInfoEvent; diff --git a/pom.xml b/pom.xml index 5abdefcbff..a925a81cfd 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java - 0.3.0-java.2 + 1.0.0-beta-java.4 jar GitHub Copilot SDK :: Java @@ -33,7 +33,7 @@ scm:git:https://github.com/github/copilot-sdk-java.git scm:git:https://github.com/github/copilot-sdk-java.git https://github.com/github/copilot-sdk-java - v0.3.0-java.2 + v1.0.0-beta-java.4 @@ -49,10 +49,53 @@ ${project.build.directory}/copilot-sdk ${copilot.sdk.clone.dir}/test + + ${copilot.sdk.clone.dir}/nodejs/node_modules/@github/copilot/index.js false + + ${skip.test.harness} + + ^1.0.48 + @@ -60,7 +103,7 @@ com.fasterxml.jackson.core jackson-databind - 2.21.2 + 2.21.3 com.fasterxml.jackson.core @@ -70,7 +113,7 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 - 2.21.2 + 2.21.3 @@ -85,7 +128,7 @@ org.junit.jupiter junit-jupiter - 5.14.3 + 5.14.4 test @@ -129,13 +172,6 @@ org.apache.maven.plugins maven-jar-plugin 3.5.0 - - - - com.github.copilot.sdk.java - - - @@ -240,6 +276,35 @@ + + + install-nodejs-cli-dependencies + generate-test-resources + + exec + + + ${skip.cli.install} + npm + ${copilot.sdk.clone.dir}/nodejs + + ci + --ignore-scripts + + + @@ -247,13 +312,61 @@ maven-surefire-plugin 3.5.5 + alphabetical ${testExecutionAgentArgs} ${surefire.jvm.args} + + 2 ${copilot.tests.dir} ${copilot.sdk.clone.dir} + + + ${copilot.cli.path} + + + + + isolated-resume-tests + test + + test + + + isolated-resume + + ${project.build.directory}/surefire-reports-isolated + + + + + default-test + + isolated-resume + + + @@ -587,6 +700,39 @@ true + + + skip-cli-install-when-tests-skipped + + + skipTests + true + + + + true + + + + + skip-cli-install-when-maven-test-skip + + + maven.test.skip + true + + + + true + + debug diff --git a/scripts/codegen/java.ts b/scripts/codegen/java.ts index 909fe296c6..0a96ab9f14 100644 --- a/scripts/codegen/java.ts +++ b/scripts/codegen/java.ts @@ -43,7 +43,7 @@ function toCamelCase(name: string): string { } function toEnumConstant(value: string): string { - return value.toUpperCase().replace(/[-. ]/g, "_"); + return value.toUpperCase().replace(/[-. /:]/g, "_").replace(/^_+/, "").replace(/_+/g, "_"); } // ── Schema path resolution ─────────────────────────────────────────────────── diff --git a/scripts/codegen/package-lock.json b/scripts/codegen/package-lock.json index 682b30bc66..36d7689a71 100644 --- a/scripts/codegen/package-lock.json +++ b/scripts/codegen/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.36", + "@github/copilot": "^1.0.48", "json-schema": "^0.4.0", "tsx": "^4.20.6" } @@ -428,26 +428,26 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.36.tgz", - "integrity": "sha512-x0N5wLzw+tANzb+vCFYLHn3BV3qii2oyn14wC20RO7SsS8/YeBH8olvwlDLJ4PB0mL17QOiytNCdkvjvprm28w==", + "version": "1.0.48", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.48.tgz", + "integrity": "sha512-U5SzyTEq376UU9A4Sd3TEKz+Y2nRUd90cLO4Hc1otaB8yFSy9Ur2UVGcI2/wCoodL3a39k6WbdgNzFxr0gWFRQ==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.36", - "@github/copilot-darwin-x64": "1.0.36", - "@github/copilot-linux-arm64": "1.0.36", - "@github/copilot-linux-x64": "1.0.36", - "@github/copilot-win32-arm64": "1.0.36", - "@github/copilot-win32-x64": "1.0.36" + "@github/copilot-darwin-arm64": "1.0.48", + "@github/copilot-darwin-x64": "1.0.48", + "@github/copilot-linux-arm64": "1.0.48", + "@github/copilot-linux-x64": "1.0.48", + "@github/copilot-win32-arm64": "1.0.48", + "@github/copilot-win32-x64": "1.0.48" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.36.tgz", - "integrity": "sha512-5qkb7frTS4K/LdTDLrzKo78VR4aw/EZ6JzLz4KfmaW4UYyPiNirExDFXa/By22X0o8YMfOp4MCA2KSCAxKdgTg==", + "version": "1.0.48", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.48.tgz", + "integrity": "sha512-82MLoMQwPVVFM8EYssihFxSEPUYtZADE8rMzQ3jG9HgRg2qjQSfnHQS1mKe64dlXswZUK/onw6/8kjnW5I4pPg==", "cpu": [ "arm64" ], @@ -461,9 +461,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.36.tgz", - "integrity": "sha512-AdsM8QtM5QSzMLpavLREh8HALO5G+VWzGNQqIHu4f0YQC/s1cGoiwo3wsgkpxRcLGBykFc+bDX3yK3MDQ8XvSw==", + "version": "1.0.48", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.48.tgz", + "integrity": "sha512-1VQ5r5F0h8GwboXmZTcutqcJT+iCpPXAF27QqodmpKEvW9aYfG8g9X2kFJOzDZoX+SA3Uaka9qXdYKF2xT6Uog==", "cpu": [ "x64" ], @@ -477,9 +477,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.36.tgz", - "integrity": "sha512-n7K1I6r0ggOJ4A9uAMS11USTvn6BKtAwvrOkzEaeRK89VNUJzpTe6p0mE13ItzRe5eot9WLBQOxvXLtL9f6E+g==", + "version": "1.0.48", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.48.tgz", + "integrity": "sha512-PmsGnb0DZlI+Bf53l9HM1PAHHkUcMyB4y8v/7tnC/jDOV5dGF124n0HnDNfJLOLiJGiQGodthIif6QtPaAxpeA==", "cpu": [ "arm64" ], @@ -493,9 +493,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.36.tgz", - "integrity": "sha512-wBtCdR3ITZcq07BJbkwHfwI6ayiwbH5pF1ex+Ycl4UI+Lf1vP9eQD6wJppPgsrjwFcdeWRThaYTPCRTkSGHv5g==", + "version": "1.0.48", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.48.tgz", + "integrity": "sha512-b2cc4euSlke9fYHXXsS2EL9UYbctN0h4lZvtAcKUDY+RCnpYAQOVBZK+c1R9dQrtsT6Z/yUv7PuFPSs8qdtc2Q==", "cpu": [ "x64" ], @@ -509,9 +509,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.36.tgz", - "integrity": "sha512-0GzZUZQn07alI8BgbzK0NlR5+ta/Rd0sWmd8kbRCns7oybAIkSALy6BKVwJmVHtXUi6h4iUE8oiFhkn0spymvw==", + "version": "1.0.48", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.48.tgz", + "integrity": "sha512-VEEOwddtpJ3DTbXGhnK6K8im4ofl9m08q1m/K++sNvWV8wkkOSOQBTiPdyUsuU/TXAoFhb8tZMIJv+6NnMBtMw==", "cpu": [ "arm64" ], @@ -525,9 +525,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.36.tgz", - "integrity": "sha512-UBX9qj0McCK/SLq93XIr1i80fj3b3XmE3befVFrzxQuTeOoxLURN35vi7W+4x+4ZfsDHQpRTlJNjZw9w0fPr+Q==", + "version": "1.0.48", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.48.tgz", + "integrity": "sha512-93BzvXLPHTyy1gWBXQY/IWIHor4IAwZuuo7/obG80/Qa6U0WeaN9slz/FBJvrsgVNrrRfEID5Xm3At+S6Kj67Q==", "cpu": [ "x64" ], diff --git a/scripts/codegen/package.json b/scripts/codegen/package.json index 1b540376e1..82454e774e 100644 --- a/scripts/codegen/package.json +++ b/scripts/codegen/package.json @@ -7,7 +7,7 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.36", + "@github/copilot": "^1.0.48", "json-schema": "^0.4.0", "tsx": "^4.20.6" } diff --git a/src/generated/java/com/github/copilot/sdk/generated/AbortEvent.java b/src/generated/java/com/github/copilot/sdk/generated/AbortEvent.java index d236f04f0b..16cfabc0e6 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/AbortEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/AbortEvent.java @@ -35,8 +35,8 @@ public final class AbortEvent extends SessionEvent { @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public record AbortEventData( - /** Reason the current turn was aborted (e.g., "user initiated") */ - @JsonProperty("reason") String reason + /** Finite reason code describing why the current turn was aborted */ + @JsonProperty("reason") AbortReason reason ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/AbortReason.java b/src/generated/java/com/github/copilot/sdk/generated/AbortReason.java new file mode 100644 index 0000000000..2bb93b8865 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/AbortReason.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Finite reason code describing why the current turn was aborted + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AbortReason { + /** The {@code user_initiated} variant. */ + USER_INITIATED("user_initiated"), + /** The {@code remote_command} variant. */ + REMOTE_COMMAND("remote_command"), + /** The {@code user_abort} variant. */ + USER_ABORT("user_abort"); + + private final String value; + AbortReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AbortReason fromValue(String value) { + for (AbortReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AbortReason value: " + value); + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java index a6d75a8f65..3ac0b9780a 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java @@ -38,6 +38,8 @@ public final class AssistantMessageEvent extends SessionEvent { public record AssistantMessageEventData( /** Unique identifier for this assistant message */ @JsonProperty("messageId") String messageId, + /** Model that produced this assistant message, if known */ + @JsonProperty("model") String model, /** The assistant's text response content */ @JsonProperty("content") String content, /** Tool invocations requested by the assistant in this message */ @@ -56,6 +58,12 @@ public record AssistantMessageEventData( @JsonProperty("interactionId") String interactionId, /** GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs */ @JsonProperty("requestId") String requestId, + /** Raw Anthropic content array with advisor blocks (server_tool_use, advisor_tool_result) for verbatim round-tripping */ + @JsonProperty("anthropicAdvisorBlocks") List anthropicAdvisorBlocks, + /** Anthropic advisor model ID used for this response, for timeline display on replay */ + @JsonProperty("anthropicAdvisorModel") String anthropicAdvisorModel, + /** Identifier for the agent loop turn that produced this message, matching the corresponding assistant.turn_start event */ + @JsonProperty("turnId") String turnId, /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId ) { diff --git a/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageStartEvent.java b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageStartEvent.java new file mode 100644 index 0000000000..8cf1a9c8e7 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageStartEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The {@code assistant.message_start} session event. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantMessageStartEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.message_start"; } + + @JsonProperty("data") + private AssistantMessageStartEventData data; + + public AssistantMessageStartEventData getData() { return data; } + public void setData(AssistantMessageStartEventData data) { this.data = data; } + + /** Data payload for {@link AssistantMessageStartEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantMessageStartEventData( + /** Message ID this start event belongs to, matching subsequent deltas and assistant.message */ + @JsonProperty("messageId") String messageId, + /** Generation phase this message belongs to for phased-output models */ + @JsonProperty("phase") String phase + ) { + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageToolRequest.java b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageToolRequest.java index d45803b66a..e185a01fa8 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageToolRequest.java +++ b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageToolRequest.java @@ -33,6 +33,8 @@ public record AssistantMessageToolRequest( @JsonProperty("toolTitle") String toolTitle, /** Name of the MCP server hosting this tool, when the tool is an MCP tool */ @JsonProperty("mcpServerName") String mcpServerName, + /** Original tool name on the MCP server, when the tool is an MCP tool */ + @JsonProperty("mcpToolName") String mcpToolName, /** Resolved intention summary describing what this specific call does */ @JsonProperty("intentionSummary") String intentionSummary ) { diff --git a/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageApiEndpoint.java b/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageApiEndpoint.java new file mode 100644 index 0000000000..e69e4ef868 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageApiEndpoint.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * API endpoint used for this model call, matching CAPI supported_endpoints vocabulary + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AssistantUsageApiEndpoint { + /** The {@code /chat/completions} variant. */ + CHAT_COMPLETIONS("/chat/completions"), + /** The {@code /v1/messages} variant. */ + V1_MESSAGES("/v1/messages"), + /** The {@code /responses} variant. */ + RESPONSES("/responses"), + /** The {@code ws:/responses} variant. */ + WS_RESPONSES("ws:/responses"); + + private final String value; + AssistantUsageApiEndpoint(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AssistantUsageApiEndpoint fromValue(String value) { + for (AssistantUsageApiEndpoint v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AssistantUsageApiEndpoint value: " + value); + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsage.java b/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsage.java index 4bb8aa92c6..ee3e9f9cfb 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsage.java +++ b/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsage.java @@ -24,7 +24,7 @@ public record AssistantUsageCopilotUsage( /** Itemized token usage breakdown */ @JsonProperty("tokenDetails") List tokenDetails, - /** Total cost in nano-AIU (AI Units) for this request */ + /** Total cost in nano-AI units for this request */ @JsonProperty("totalNanoAiu") Double totalNanoAiu ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageEvent.java b/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageEvent.java index 4613d520e7..ba97e03958 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageEvent.java @@ -62,6 +62,8 @@ public record AssistantUsageEventData( @JsonProperty("apiCallId") String apiCallId, /** GitHub request tracing ID (x-github-request-id header) for server-side log correlation */ @JsonProperty("providerCallId") String providerCallId, + /** API endpoint used for this model call, matching CAPI supported_endpoints vocabulary */ + @JsonProperty("apiEndpoint") AssistantUsageApiEndpoint apiEndpoint, /** Parent tool call ID when this usage originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId, /** Per-quota resource usage snapshots, keyed by quota identifier */ diff --git a/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchRequestedEvent.java b/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchRequestedEvent.java index 8ad1a2da82..e234ccd287 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchRequestedEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchRequestedEvent.java @@ -38,7 +38,9 @@ public record AutoModeSwitchRequestedEventData( /** Unique identifier for this request; used to respond via session.respondToAutoModeSwitch() */ @JsonProperty("requestId") String requestId, /** The rate limit error code that triggered this request */ - @JsonProperty("errorCode") String errorCode + @JsonProperty("errorCode") String errorCode, + /** Seconds until the rate limit resets, when known. Lets clients render a humanized reset time alongside the prompt. */ + @JsonProperty("retryAfterSeconds") Double retryAfterSeconds ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsage.java b/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsage.java index 7e02e4f778..76e5a0ed8f 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsage.java +++ b/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsage.java @@ -24,7 +24,7 @@ public record CompactionCompleteCompactionTokensUsedCopilotUsage( /** Itemized token usage breakdown */ @JsonProperty("tokenDetails") List tokenDetails, - /** Total cost in nano-AIU (AI Units) for this request */ + /** Total cost in nano-AI units for this request */ @JsonProperty("totalNanoAiu") Double totalNanoAiu ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/CustomAgentsUpdatedAgent.java b/src/generated/java/com/github/copilot/sdk/generated/CustomAgentsUpdatedAgent.java index 74637aaa82..0d9dbc295b 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/CustomAgentsUpdatedAgent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/CustomAgentsUpdatedAgent.java @@ -27,7 +27,7 @@ public record CustomAgentsUpdatedAgent( @JsonProperty("description") String description, /** Source location: user, project, inherited, remote, or plugin */ @JsonProperty("source") String source, - /** List of tool names available to this agent */ + /** List of tool names available to this agent, or null when all tools are available */ @JsonProperty("tools") List tools, /** Whether the agent can be selected by the user */ @JsonProperty("userInvocable") Boolean userInvocable, diff --git a/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredStaticClientConfig.java b/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredStaticClientConfig.java index b8c0dd93d5..b037b82ac5 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredStaticClientConfig.java +++ b/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredStaticClientConfig.java @@ -24,6 +24,8 @@ public record McpOauthRequiredStaticClientConfig( /** OAuth client ID for the server */ @JsonProperty("clientId") String clientId, /** Whether this is a public OAuth client */ - @JsonProperty("publicClient") Boolean publicClient + @JsonProperty("publicClient") Boolean publicClient, + /** Optional non-default OAuth grant type. When set to 'client_credentials', the OAuth flow runs headlessly using the client_id + keychain-stored secret (no browser, no callback server). */ + @JsonProperty("grantType") String grantType ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureEvent.java b/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureEvent.java new file mode 100644 index 0000000000..939e49ef55 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureEvent.java @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The {@code model.call_failure} session event. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ModelCallFailureEvent extends SessionEvent { + + @Override + public String getType() { return "model.call_failure"; } + + @JsonProperty("data") + private ModelCallFailureEventData data; + + public ModelCallFailureEventData getData() { return data; } + public void setData(ModelCallFailureEventData data) { this.data = data; } + + /** Data payload for {@link ModelCallFailureEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ModelCallFailureEventData( + /** Model identifier used for the failed API call */ + @JsonProperty("model") String model, + /** What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls */ + @JsonProperty("initiator") String initiator, + /** Completion ID from the model provider (e.g., chatcmpl-abc123) */ + @JsonProperty("apiCallId") String apiCallId, + /** GitHub request tracing ID (x-github-request-id header) for server-side log correlation */ + @JsonProperty("providerCallId") String providerCallId, + /** HTTP status code from the failed request */ + @JsonProperty("statusCode") Long statusCode, + /** Duration of the failed API call in milliseconds */ + @JsonProperty("durationMs") Double durationMs, + /** Where the failed model call originated */ + @JsonProperty("source") ModelCallFailureSource source, + /** Raw provider/runtime error message for restricted telemetry */ + @JsonProperty("errorMessage") String errorMessage + ) { + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureSource.java b/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureSource.java new file mode 100644 index 0000000000..469adaab43 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureSource.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Where the failed model call originated + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelCallFailureSource { + /** The {@code top_level} variant. */ + TOP_LEVEL("top_level"), + /** The {@code subagent} variant. */ + SUBAGENT("subagent"), + /** The {@code mcp_sampling} variant. */ + MCP_SAMPLING("mcp_sampling"); + + private final String value; + ModelCallFailureSource(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelCallFailureSource fromValue(String value) { + for (ModelCallFailureSource v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelCallFailureSource value: " + value); + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedEvent.java b/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedEvent.java index 0045393c1a..0b8da105e2 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedEvent.java @@ -40,7 +40,7 @@ public record PermissionCompletedEventData( /** Optional tool call ID associated with this permission prompt; clients may use it to correlate UI created from tool-scoped prompts */ @JsonProperty("toolCallId") String toolCallId, /** The result of the permission request */ - @JsonProperty("result") PermissionCompletedResult result + @JsonProperty("result") Object result ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionCustomNotificationEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionCustomNotificationEvent.java new file mode 100644 index 0000000000..8d3e3b68ac --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionCustomNotificationEvent.java @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * The {@code session.custom_notification} session event. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionCustomNotificationEvent extends SessionEvent { + + @Override + public String getType() { return "session.custom_notification"; } + + @JsonProperty("data") + private SessionCustomNotificationEventData data; + + public SessionCustomNotificationEventData getData() { return data; } + public void setData(SessionCustomNotificationEventData data) { this.data = data; } + + /** Data payload for {@link SessionCustomNotificationEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionCustomNotificationEventData( + /** Namespace for the custom notification producer */ + @JsonProperty("source") String source, + /** Source-defined custom notification name */ + @JsonProperty("name") String name, + /** Optional source-defined payload schema version */ + @JsonProperty("version") Long version, + /** Optional source-defined string identifiers describing the payload subject */ + @JsonProperty("subject") Map subject, + /** Source-defined JSON payload for the custom notification */ + @JsonProperty("payload") Object payload + ) { + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionErrorEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionErrorEvent.java index 33dc688348..5c174c4357 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionErrorEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionErrorEvent.java @@ -37,6 +37,10 @@ public final class SessionErrorEvent extends SessionEvent { public record SessionErrorEventData( /** Category of error (e.g., "authentication", "authorization", "quota", "rate_limit", "context_limit", "query") */ @JsonProperty("errorType") String errorType, + /** Fine-grained error code from the upstream provider, when available. For `errorType: "rate_limit"`, this is one of the `RateLimitErrorCode` values (e.g., `"user_weekly_rate_limited"`, `"user_global_rate_limited"`, `"rate_limited"`, `"user_model_rate_limited"`, `"integration_rate_limited"`). For `errorType: "quota"`, this is the CAPI quota error code (e.g., `"quota_exceeded"`, `"session_quota_exceeded"`, `"billing_not_configured"`). */ + @JsonProperty("errorCode") String errorCode, + /** Only set on `errorType: "rate_limit"`. When `true`, the runtime will follow this error with an `auto_mode_switch.requested` event (or silently switch if `continueOnAutoMode` is enabled). UI clients can use this flag to suppress duplicate rendering of the rate-limit error when they show their own auto-mode-switch prompt. */ + @JsonProperty("eligibleForAutoSwitch") Boolean eligibleForAutoSwitch, /** Human-readable error message */ @JsonProperty("message") String message, /** Error stack trace, when available */ diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java index 85c62a2418..2181be1972 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java @@ -31,6 +31,8 @@ @JsonSubTypes.Type(value = SessionErrorEvent.class, name = "session.error"), @JsonSubTypes.Type(value = SessionIdleEvent.class, name = "session.idle"), @JsonSubTypes.Type(value = SessionTitleChangedEvent.class, name = "session.title_changed"), + @JsonSubTypes.Type(value = SessionScheduleCreatedEvent.class, name = "session.schedule_created"), + @JsonSubTypes.Type(value = SessionScheduleCancelledEvent.class, name = "session.schedule_cancelled"), @JsonSubTypes.Type(value = SessionInfoEvent.class, name = "session.info"), @JsonSubTypes.Type(value = SessionWarningEvent.class, name = "session.warning"), @JsonSubTypes.Type(value = SessionModelChangeEvent.class, name = "session.model_change"), @@ -54,9 +56,11 @@ @JsonSubTypes.Type(value = AssistantReasoningDeltaEvent.class, name = "assistant.reasoning_delta"), @JsonSubTypes.Type(value = AssistantStreamingDeltaEvent.class, name = "assistant.streaming_delta"), @JsonSubTypes.Type(value = AssistantMessageEvent.class, name = "assistant.message"), + @JsonSubTypes.Type(value = AssistantMessageStartEvent.class, name = "assistant.message_start"), @JsonSubTypes.Type(value = AssistantMessageDeltaEvent.class, name = "assistant.message_delta"), @JsonSubTypes.Type(value = AssistantTurnEndEvent.class, name = "assistant.turn_end"), @JsonSubTypes.Type(value = AssistantUsageEvent.class, name = "assistant.usage"), + @JsonSubTypes.Type(value = ModelCallFailureEvent.class, name = "model.call_failure"), @JsonSubTypes.Type(value = AbortEvent.class, name = "abort"), @JsonSubTypes.Type(value = ToolUserRequestedEvent.class, name = "tool.user_requested"), @JsonSubTypes.Type(value = ToolExecutionStartEvent.class, name = "tool.execution_start"), @@ -83,6 +87,7 @@ @JsonSubTypes.Type(value = SamplingCompletedEvent.class, name = "sampling.completed"), @JsonSubTypes.Type(value = McpOauthRequiredEvent.class, name = "mcp.oauth_required"), @JsonSubTypes.Type(value = McpOauthCompletedEvent.class, name = "mcp.oauth_completed"), + @JsonSubTypes.Type(value = SessionCustomNotificationEvent.class, name = "session.custom_notification"), @JsonSubTypes.Type(value = ExternalToolRequestedEvent.class, name = "external_tool.requested"), @JsonSubTypes.Type(value = ExternalToolCompletedEvent.class, name = "external_tool.completed"), @JsonSubTypes.Type(value = CommandQueuedEvent.class, name = "command.queued"), @@ -110,6 +115,8 @@ public abstract sealed class SessionEvent permits SessionErrorEvent, SessionIdleEvent, SessionTitleChangedEvent, + SessionScheduleCreatedEvent, + SessionScheduleCancelledEvent, SessionInfoEvent, SessionWarningEvent, SessionModelChangeEvent, @@ -133,9 +140,11 @@ public abstract sealed class SessionEvent permits AssistantReasoningDeltaEvent, AssistantStreamingDeltaEvent, AssistantMessageEvent, + AssistantMessageStartEvent, AssistantMessageDeltaEvent, AssistantTurnEndEvent, AssistantUsageEvent, + ModelCallFailureEvent, AbortEvent, ToolUserRequestedEvent, ToolExecutionStartEvent, @@ -162,6 +171,7 @@ public abstract sealed class SessionEvent permits SamplingCompletedEvent, McpOauthRequiredEvent, McpOauthCompletedEvent, + SessionCustomNotificationEvent, ExternalToolRequestedEvent, ExternalToolCompletedEvent, CommandQueuedEvent, diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionInfoEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionInfoEvent.java index 4dee36ba5c..01c1e0efbe 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionInfoEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionInfoEvent.java @@ -40,7 +40,9 @@ public record SessionInfoEventData( /** Human-readable informational message for display in the timeline */ @JsonProperty("message") String message, /** Optional URL associated with this message that the user can open in a browser */ - @JsonProperty("url") String url + @JsonProperty("url") String url, + /** Optional actionable tip displayed with this message */ + @JsonProperty("tip") String tip ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionModelChangeEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionModelChangeEvent.java index c23c8a5f50..812cff0e81 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionModelChangeEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionModelChangeEvent.java @@ -42,7 +42,9 @@ public record SessionModelChangeEventData( /** Reasoning effort level before the model change, if applicable */ @JsonProperty("previousReasoningEffort") String previousReasoningEffort, /** Reasoning effort level after the model change, if applicable */ - @JsonProperty("reasoningEffort") String reasoningEffort + @JsonProperty("reasoningEffort") String reasoningEffort, + /** Reason the change happened, when not user-initiated. Currently `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path. UI clients can use this to render contextual copy. */ + @JsonProperty("cause") String cause ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java index 7973e7d17c..31bd3b4c36 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java @@ -48,8 +48,12 @@ public record SessionResumeEventData( @JsonProperty("context") WorkingDirectoryContext context, /** Whether the session was already in use by another client at resume time */ @JsonProperty("alreadyInUse") Boolean alreadyInUse, + /** True when this resume attached to a session that the runtime already had running in-memory (for example, an extension joining a session another client was actively driving). False (or omitted) for cold resumes β€” the runtime had to reconstitute the session from its persisted event log. */ + @JsonProperty("sessionWasActive") Boolean sessionWasActive, /** Whether this session supports remote steering via Mission Control */ - @JsonProperty("remoteSteerable") Boolean remoteSteerable + @JsonProperty("remoteSteerable") Boolean remoteSteerable, + /** When true, tool calls and permission requests left in flight by the previous session lifetime remain pending after resume and the agentic loop awaits their results. User sends are queued behind the pending work until all such requests reach a terminal state. When false (the default), any such tool calls and permission requests are immediately marked as interrupted on resume. */ + @JsonProperty("continuePendingWork") Boolean continuePendingWork ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCancelledEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCancelledEvent.java new file mode 100644 index 0000000000..01cd6e4619 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCancelledEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The {@code session.schedule_cancelled} session event. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionScheduleCancelledEvent extends SessionEvent { + + @Override + public String getType() { return "session.schedule_cancelled"; } + + @JsonProperty("data") + private SessionScheduleCancelledEventData data; + + public SessionScheduleCancelledEventData getData() { return data; } + public void setData(SessionScheduleCancelledEventData data) { this.data = data; } + + /** Data payload for {@link SessionScheduleCancelledEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionScheduleCancelledEventData( + /** Id of the scheduled prompt that was cancelled */ + @JsonProperty("id") Long id + ) { + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCreatedEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCreatedEvent.java new file mode 100644 index 0000000000..bf0be67caf --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCreatedEvent.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The {@code session.schedule_created} session event. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionScheduleCreatedEvent extends SessionEvent { + + @Override + public String getType() { return "session.schedule_created"; } + + @JsonProperty("data") + private SessionScheduleCreatedEventData data; + + public SessionScheduleCreatedEventData getData() { return data; } + public void setData(SessionScheduleCreatedEventData data) { this.data = data; } + + /** Data payload for {@link SessionScheduleCreatedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionScheduleCreatedEventData( + /** Sequential id assigned to the scheduled prompt within the session */ + @JsonProperty("id") Long id, + /** Interval between ticks in milliseconds */ + @JsonProperty("intervalMs") Long intervalMs, + /** Prompt text that gets enqueued on every tick */ + @JsonProperty("prompt") String prompt, + /** Whether the schedule re-arms after each tick (`/every`) or fires once (`/after`) */ + @JsonProperty("recurring") Boolean recurring + ) { + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java index 39b6cfcfab..97d5060d32 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java @@ -42,6 +42,10 @@ public record SessionShutdownEventData( @JsonProperty("errorReason") String errorReason, /** Total number of premium API requests used during the session */ @JsonProperty("totalPremiumRequests") Double totalPremiumRequests, + /** Session-wide accumulated nano-AI units cost */ + @JsonProperty("totalNanoAiu") Double totalNanoAiu, + /** Session-wide per-token-type accumulated token counts */ + @JsonProperty("tokenDetails") Map tokenDetails, /** Cumulative time spent in API calls during the session, in milliseconds */ @JsonProperty("totalApiDurationMs") Double totalApiDurationMs, /** Unix timestamp (milliseconds) when the session started */ diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionStartEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionStartEvent.java index 6d83575e85..f5d6378fcd 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionStartEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionStartEvent.java @@ -55,7 +55,9 @@ public record SessionStartEventData( /** Whether the session was already in use by another client at start time */ @JsonProperty("alreadyInUse") Boolean alreadyInUse, /** Whether this session supports remote steering via Mission Control */ - @JsonProperty("remoteSteerable") Boolean remoteSteerable + @JsonProperty("remoteSteerable") Boolean remoteSteerable, + /** When set, identifies a parent session whose context this session continues β€” e.g., a detached headless rem-agent run launched on the parent's interactive shutdown. Telemetry from this session is reported under the parent's session_id. */ + @JsonProperty("detachedFromSpawningParentSessionId") String detachedFromSpawningParentSessionId ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetric.java b/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetric.java index e5cae14764..e7a831ca67 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetric.java +++ b/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetric.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; import javax.annotation.processing.Generated; @javax.annotation.processing.Generated("copilot-sdk-codegen") @@ -19,6 +20,10 @@ public record ShutdownModelMetric( /** Request count and cost metrics */ @JsonProperty("requests") ShutdownModelMetricRequests requests, /** Token usage breakdown */ - @JsonProperty("usage") ShutdownModelMetricUsage usage + @JsonProperty("usage") ShutdownModelMetricUsage usage, + /** Accumulated nano-AI units cost for this model */ + @JsonProperty("totalNanoAiu") Double totalNanoAiu, + /** Token count details per type */ + @JsonProperty("tokenDetails") Map tokenDetails ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java b/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java new file mode 100644 index 0000000000..9f7cde6301 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ShutdownModelMetricTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Double tokenCount +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java b/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java new file mode 100644 index 0000000000..ec51804886 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ShutdownTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Double tokenCount +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/SubagentStartedEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SubagentStartedEvent.java index 6e9926bd4e..4bc945c9c0 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SubagentStartedEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SubagentStartedEvent.java @@ -42,7 +42,9 @@ public record SubagentStartedEventData( /** Human-readable display name of the sub-agent */ @JsonProperty("agentDisplayName") String agentDisplayName, /** Description of what the sub-agent does */ - @JsonProperty("agentDescription") String agentDescription + @JsonProperty("agentDescription") String agentDescription, + /** Model the sub-agent will run with, when known at start. Surfaced in the timeline for auto-selected sub-agents (e.g. rubber-duck). */ + @JsonProperty("model") String model ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java b/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java index 6481374d1b..a1deeeab56 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java @@ -52,6 +52,8 @@ public record ToolExecutionCompleteEventData( @JsonProperty("error") ToolExecutionCompleteError error, /** Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts) */ @JsonProperty("toolTelemetry") Map toolTelemetry, + /** Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event */ + @JsonProperty("turnId") String turnId, /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId ) { diff --git a/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionStartEvent.java b/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionStartEvent.java index 7da7572864..5a48cdd977 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionStartEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionStartEvent.java @@ -45,6 +45,8 @@ public record ToolExecutionStartEventData( @JsonProperty("mcpServerName") String mcpServerName, /** Original tool name on the MCP server, when the tool is an MCP tool */ @JsonProperty("mcpToolName") String mcpToolName, + /** Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event */ + @JsonProperty("turnId") String turnId, /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId ) { diff --git a/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java b/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java index 5d156082db..fa680d3e64 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java @@ -50,8 +50,12 @@ public record UserMessageEventData( @JsonProperty("source") String source, /** The agent mode that was active when this message was sent */ @JsonProperty("agentMode") UserMessageAgentMode agentMode, + /** True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. */ + @JsonProperty("isAutopilotContinuation") Boolean isAutopilotContinuation, /** CAPI interaction ID for correlating this user message with its turn */ - @JsonProperty("interactionId") String interactionId + @JsonProperty("interactionId") String interactionId, + /** Parent agent task ID for background telemetry correlated to this user turn */ + @JsonProperty("parentAgentTaskId") String parentAgentTaskId ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfo.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfo.java index edd34eb947..4695ff9fb9 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfo.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfo.java @@ -21,6 +21,8 @@ public record AgentInfo( /** Human-readable display name */ @JsonProperty("displayName") String displayName, /** Description of the agent's purpose */ - @JsonProperty("description") String description + @JsonProperty("description") String description, + /** Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. */ + @JsonProperty("path") String path ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectParams.java new file mode 100644 index 0000000000..665cd9694c --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code connect} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectParams( + /** Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN */ + @JsonProperty("token") String token +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectResult.java new file mode 100644 index 0000000000..56666b2523 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectResult.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code connect} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectResult( + /** Always true on success */ + @JsonProperty("ok") Boolean ok, + /** Server protocol version number */ + @JsonProperty("protocolVersion") Long protocolVersion, + /** Server package version */ + @JsonProperty("version") String version +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/Model.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/Model.java index 9ba457cecb..9bda6a3007 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/Model.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/Model.java @@ -30,6 +30,10 @@ public record Model( /** Supported reasoning effort levels (only present if model supports reasoning effort) */ @JsonProperty("supportedReasoningEfforts") List supportedReasoningEfforts, /** Default reasoning effort level (only present if model supports reasoning effort) */ - @JsonProperty("defaultReasoningEffort") String defaultReasoningEffort + @JsonProperty("defaultReasoningEffort") String defaultReasoningEffort, + /** Model capability category for grouping in the model picker */ + @JsonProperty("modelPickerCategory") ModelPickerCategory modelPickerCategory, + /** Relative cost tier for token-based billing users */ + @JsonProperty("modelPickerPriceCategory") ModelPickerPriceCategory modelPickerPriceCategory ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelBilling.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelBilling.java index 656f5383d5..9e634bb79a 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelBilling.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelBilling.java @@ -22,6 +22,8 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record ModelBilling( /** Billing cost multiplier relative to the base rate */ - @JsonProperty("multiplier") Double multiplier + @JsonProperty("multiplier") Double multiplier, + /** Token-level pricing information for this model */ + @JsonProperty("tokenPrices") ModelBillingTokenPrices tokenPrices ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelBillingTokenPrices.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelBillingTokenPrices.java new file mode 100644 index 0000000000..34005daf1b --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelBillingTokenPrices.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Token-level pricing information for this model + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelBillingTokenPrices( + /** Price per billing batch of input tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) */ + @JsonProperty("inputPrice") Long inputPrice, + /** Price per billing batch of output tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) */ + @JsonProperty("outputPrice") Long outputPrice, + /** Price per billing batch of cached tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) */ + @JsonProperty("cachePrice") Long cachePrice, + /** Number of tokens per standard billing batch */ + @JsonProperty("batchSize") Long batchSize +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPickerCategory.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPickerCategory.java new file mode 100644 index 0000000000..ba0bdddfd1 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPickerCategory.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Model capability category for grouping in the model picker + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelPickerCategory { + /** The {@code lightweight} variant. */ + LIGHTWEIGHT("lightweight"), + /** The {@code versatile} variant. */ + VERSATILE("versatile"), + /** The {@code powerful} variant. */ + POWERFUL("powerful"); + + private final String value; + ModelPickerCategory(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelPickerCategory fromValue(String value) { + for (ModelPickerCategory v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelPickerCategory value: " + value); + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPickerPriceCategory.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPickerPriceCategory.java new file mode 100644 index 0000000000..cf722e4968 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPickerPriceCategory.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Relative cost tier for token-based billing users + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelPickerPriceCategory { + /** The {@code low} variant. */ + LOW("low"), + /** The {@code medium} variant. */ + MEDIUM("medium"), + /** The {@code high} variant. */ + HIGH("high"), + /** The {@code very_high} variant. */ + VERY_HIGH("very_high"); + + private final String value; + ModelPickerPriceCategory(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelPickerPriceCategory fromValue(String value) { + for (ModelPickerPriceCategory v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelPickerPriceCategory value: " + value); + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/RemoteSessionMode.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/RemoteSessionMode.java new file mode 100644 index 0000000000..4f659bb6b6 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/RemoteSessionMode.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Per-session remote mode. "off" disables remote, "export" exports session events to Mission Control without enabling remote steering, "on" enables both export and remote steering. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum RemoteSessionMode { + /** The {@code off} variant. */ + OFF("off"), + /** The {@code export} variant. */ + EXPORT("export"), + /** The {@code on} variant. */ + ON("on"); + + private final String value; + RemoteSessionMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static RemoteSessionMode fromValue(String value) { + for (RemoteSessionMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown RemoteSessionMode value: " + value); + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java index d2e3161000..b82c49ec23 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java @@ -63,4 +63,12 @@ public CompletableFuture ping(PingParams params) { return caller.invoke("ping", params, PingResult.class); } + /** + * Invokes {@code connect}. + * @since 1.0.0 + */ + public CompletableFuture connect(ConnectParams params) { + return caller.invoke("connect", params, ConnectResult.class); + } + } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsApi.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsApi.java index 351a2d14cd..6366af794a 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsApi.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsApi.java @@ -29,6 +29,27 @@ public final class SessionCommandsApi { this.sessionId = sessionId; } + /** + * Invokes {@code session.commands.list}. + * @since 1.0.0 + */ + public CompletableFuture list() { + return caller.invoke("session.commands.list", java.util.Map.of("sessionId", this.sessionId), SessionCommandsListResult.class); + } + + /** + * Invokes {@code session.commands.invoke}. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture invoke(SessionCommandsInvokeParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.commands.invoke", _p, Void.class); + } + /** * Invokes {@code session.commands.handlePendingCommand}. *

@@ -42,4 +63,17 @@ public CompletableFuture handlePendin return caller.invoke("session.commands.handlePendingCommand", _p, SessionCommandsHandlePendingCommandResult.class); } + /** + * Invokes {@code session.commands.respondToQueuedCommand}. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture respondToQueuedCommand(SessionCommandsRespondToQueuedCommandParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.commands.respondToQueuedCommand", _p, SessionCommandsRespondToQueuedCommandResult.class); + } + } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsInvokeParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsInvokeParams.java new file mode 100644 index 0000000000..141ec9524d --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsInvokeParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.commands.invoke} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionCommandsInvokeParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Command name. Leading slashes are stripped and the name is matched case-insensitively. */ + @JsonProperty("name") String name, + /** Raw input after the command name */ + @JsonProperty("input") String input +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsListParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsListParams.java new file mode 100644 index 0000000000..dd51f0e768 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsListParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.commands.list} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionCommandsListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsListResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsListResult.java new file mode 100644 index 0000000000..e819ba64c1 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsListResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.commands.list} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionCommandsListResult( + /** Commands available in this session */ + @JsonProperty("commands") List commands +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandParams.java new file mode 100644 index 0000000000..46796b728c --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.commands.respondToQueuedCommand} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionCommandsRespondToQueuedCommandParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Request ID from the queued command event */ + @JsonProperty("requestId") String requestId, + /** Result of the queued command execution */ + @JsonProperty("result") Object result +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandResult.java new file mode 100644 index 0000000000..272849411a --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.commands.respondToQueuedCommand} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionCommandsRespondToQueuedCommandResult( + /** Whether the response was accepted (false if the requestId was not found or already resolved) */ + @JsonProperty("success") Boolean success +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameGetResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameGetResult.java index 9e516b45aa..82ba816531 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameGetResult.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameGetResult.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record SessionNameGetResult( - /** The session name, falling back to the auto-generated summary, or null if neither exists */ + /** The session name (user-set or auto-generated), or null if not yet set */ @JsonProperty("name") String name ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteApi.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteApi.java new file mode 100644 index 0000000000..790e1a7255 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteApi.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code remote} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionRemoteApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionRemoteApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Invokes {@code session.remote.enable}. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture enable(SessionRemoteEnableParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.remote.enable", _p, SessionRemoteEnableResult.class); + } + + /** + * Invokes {@code session.remote.disable}. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture disable() { + return caller.invoke("session.remote.disable", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteDisableParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteDisableParams.java new file mode 100644 index 0000000000..2de2190ecc --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteDisableParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.remote.disable} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionRemoteDisableParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableParams.java new file mode 100644 index 0000000000..aa1fff7a23 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.remote.enable} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionRemoteEnableParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Per-session remote mode. "off" disables remote, "export" exports session events to Mission Control without enabling remote steering, "on" enables both export and remote steering. */ + @JsonProperty("mode") RemoteSessionMode mode +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableResult.java new file mode 100644 index 0000000000..bd8ccc48b5 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableResult.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.remote.enable} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionRemoteEnableResult( + /** Mission Control frontend URL for this session */ + @JsonProperty("url") String url, + /** Whether remote steering is enabled */ + @JsonProperty("remoteSteerable") Boolean remoteSteerable +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java index 39ab121b6d..60a741224c 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java @@ -46,6 +46,8 @@ public final class SessionRpc { public final SessionFleetApi fleet; /** API methods for the {@code agent} namespace. */ public final SessionAgentApi agent; + /** API methods for the {@code tasks} namespace. */ + public final SessionTasksApi tasks; /** API methods for the {@code skills} namespace. */ public final SessionSkillsApi skills; /** API methods for the {@code mcp} namespace. */ @@ -68,6 +70,8 @@ public final class SessionRpc { public final SessionHistoryApi history; /** API methods for the {@code usage} namespace. */ public final SessionUsageApi usage; + /** API methods for the {@code remote} namespace. */ + public final SessionRemoteApi remote; /** * Creates a new session RPC client. @@ -87,6 +91,7 @@ public SessionRpc(RpcCaller caller, String sessionId) { this.instructions = new SessionInstructionsApi(caller, sessionId); this.fleet = new SessionFleetApi(caller, sessionId); this.agent = new SessionAgentApi(caller, sessionId); + this.tasks = new SessionTasksApi(caller, sessionId); this.skills = new SessionSkillsApi(caller, sessionId); this.mcp = new SessionMcpApi(caller, sessionId); this.plugins = new SessionPluginsApi(caller, sessionId); @@ -98,6 +103,15 @@ public SessionRpc(RpcCaller caller, String sessionId) { this.shell = new SessionShellApi(caller, sessionId); this.history = new SessionHistoryApi(caller, sessionId); this.usage = new SessionUsageApi(caller, sessionId); + this.remote = new SessionRemoteApi(caller, sessionId); + } + + /** + * Invokes {@code session.suspend}. + * @since 1.0.0 + */ + public CompletableFuture suspend() { + return caller.invoke("session.suspend", java.util.Map.of("sessionId", this.sessionId), Void.class); } /** diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsApi.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsApi.java index b32419a3e6..6f46d19d75 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsApi.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsApi.java @@ -75,8 +75,8 @@ public CompletableFuture disable(SessionSkillsDisableParams params) { * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ - public CompletableFuture reload() { - return caller.invoke("session.skills.reload", java.util.Map.of("sessionId", this.sessionId), Void.class); + public CompletableFuture reload() { + return caller.invoke("session.skills.reload", java.util.Map.of("sessionId", this.sessionId), SessionSkillsReloadResult.class); } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsReloadResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsReloadResult.java index 1bfca46e1a..1333d57cc1 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsReloadResult.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsReloadResult.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import javax.annotation.processing.Generated; /** @@ -20,5 +21,10 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionSkillsReloadResult() { +public record SessionSkillsReloadResult( + /** Warnings emitted while loading skills (e.g. skills that loaded but had issues) */ + @JsonProperty("warnings") List warnings, + /** Errors emitted while loading skills (e.g. skills that failed to load entirely) */ + @JsonProperty("errors") List errors +) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSuspendParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSuspendParams.java new file mode 100644 index 0000000000..300b1e4cba --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSuspendParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.suspend} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSuspendParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksApi.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksApi.java new file mode 100644 index 0000000000..6ee8ed2052 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksApi.java @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code tasks} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionTasksApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionTasksApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Invokes {@code session.tasks.startAgent}. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture startAgent(SessionTasksStartAgentParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.startAgent", _p, SessionTasksStartAgentResult.class); + } + + /** + * Invokes {@code session.tasks.list}. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture list() { + return caller.invoke("session.tasks.list", java.util.Map.of("sessionId", this.sessionId), SessionTasksListResult.class); + } + + /** + * Invokes {@code session.tasks.promoteToBackground}. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture promoteToBackground(SessionTasksPromoteToBackgroundParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.promoteToBackground", _p, SessionTasksPromoteToBackgroundResult.class); + } + + /** + * Invokes {@code session.tasks.cancel}. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture cancel(SessionTasksCancelParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.cancel", _p, SessionTasksCancelResult.class); + } + + /** + * Invokes {@code session.tasks.remove}. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture remove(SessionTasksRemoveParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.remove", _p, SessionTasksRemoveResult.class); + } + + /** + * Invokes {@code session.tasks.sendMessage}. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture sendMessage(SessionTasksSendMessageParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.sendMessage", _p, SessionTasksSendMessageResult.class); + } + +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelParams.java new file mode 100644 index 0000000000..7f3ba8a5a8 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.tasks.cancel} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksCancelParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Task identifier */ + @JsonProperty("id") String id +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelResult.java new file mode 100644 index 0000000000..976e75d646 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.tasks.cancel} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksCancelResult( + /** Whether the task was successfully cancelled */ + @JsonProperty("cancelled") Boolean cancelled +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListParams.java new file mode 100644 index 0000000000..379221deb1 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.tasks.list} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListResult.java new file mode 100644 index 0000000000..47c5b1bec1 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.tasks.list} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksListResult( + /** Currently tracked tasks */ + @JsonProperty("tasks") List tasks +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundParams.java new file mode 100644 index 0000000000..14aca99e38 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.tasks.promoteToBackground} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksPromoteToBackgroundParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Task identifier */ + @JsonProperty("id") String id +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundResult.java new file mode 100644 index 0000000000..87f09638f6 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.tasks.promoteToBackground} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksPromoteToBackgroundResult( + /** Whether the task was successfully promoted to background mode */ + @JsonProperty("promoted") Boolean promoted +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveParams.java new file mode 100644 index 0000000000..16e3bbf413 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.tasks.remove} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksRemoveParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Task identifier */ + @JsonProperty("id") String id +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveResult.java new file mode 100644 index 0000000000..c2f10222d1 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.tasks.remove} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksRemoveResult( + /** Whether the task was removed. Returns false if the task does not exist or is still running/idle (cancel it first). */ + @JsonProperty("removed") Boolean removed +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksSendMessageParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksSendMessageParams.java new file mode 100644 index 0000000000..70c70eed75 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksSendMessageParams.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.tasks.sendMessage} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksSendMessageParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Agent task identifier */ + @JsonProperty("id") String id, + /** Message content to send to the agent */ + @JsonProperty("message") String message, + /** Agent ID of the sender, if sent on behalf of another agent */ + @JsonProperty("fromAgentId") String fromAgentId +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksSendMessageResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksSendMessageResult.java new file mode 100644 index 0000000000..f4aabb1710 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksSendMessageResult.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.tasks.sendMessage} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksSendMessageResult( + /** Whether the message was successfully delivered or steered */ + @JsonProperty("sent") Boolean sent, + /** Error message if delivery failed */ + @JsonProperty("error") String error +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentParams.java new file mode 100644 index 0000000000..74a2ba3097 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentParams.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.tasks.startAgent} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksStartAgentParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Type of agent to start (e.g., 'explore', 'task', 'general-purpose') */ + @JsonProperty("agentType") String agentType, + /** Task prompt for the agent */ + @JsonProperty("prompt") String prompt, + /** Short name for the agent, used to generate a human-readable ID */ + @JsonProperty("name") String name, + /** Short description of the task */ + @JsonProperty("description") String description, + /** Optional model override */ + @JsonProperty("model") String model +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentResult.java new file mode 100644 index 0000000000..34a5dd4921 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.tasks.startAgent} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksStartAgentResult( + /** Generated agent ID for the background task */ + @JsonProperty("agentId") String agentId +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java index 9cfdbcfc8d..cb1897d279 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java @@ -26,6 +26,10 @@ public record SessionUsageGetMetricsResult( @JsonProperty("totalPremiumRequestCost") Double totalPremiumRequestCost, /** Raw count of user-initiated API requests */ @JsonProperty("totalUserRequests") Long totalUserRequests, + /** Session-wide accumulated nano-AI units cost */ + @JsonProperty("totalNanoAiu") Long totalNanoAiu, + /** Session-wide per-token-type accumulated token counts */ + @JsonProperty("tokenDetails") Map tokenDetails, /** Total time spent in model API calls (milliseconds) */ @JsonProperty("totalApiDurationMs") Double totalApiDurationMs, /** Session start timestamp (epoch milliseconds) */ diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceResult.java index 2a2260db8a..6474c29822 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceResult.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceResult.java @@ -36,8 +36,8 @@ public record SessionWorkspacesGetWorkspaceResultWorkspace( @JsonProperty("repository") String repository, @JsonProperty("host_type") SessionWorkspacesGetWorkspaceResultWorkspaceHostType hostType, @JsonProperty("branch") String branch, - @JsonProperty("summary") String summary, @JsonProperty("name") String name, + @JsonProperty("user_named") Boolean userNamed, @JsonProperty("summary_count") Long summaryCount, @JsonProperty("created_at") OffsetDateTime createdAt, @JsonProperty("updated_at") OffsetDateTime updatedAt, @@ -45,7 +45,6 @@ public record SessionWorkspacesGetWorkspaceResultWorkspace( @JsonProperty("mc_task_id") String mcTaskId, @JsonProperty("mc_session_id") String mcSessionId, @JsonProperty("mc_last_event_id") String mcLastEventId, - @JsonProperty("session_sync_level") SessionWorkspacesGetWorkspaceResultWorkspaceSessionSyncLevel sessionSyncLevel, @JsonProperty("chronicle_sync_dismissed") Boolean chronicleSyncDismissed ) { @@ -67,26 +66,5 @@ public static SessionWorkspacesGetWorkspaceResultWorkspaceHostType fromValue(Str throw new IllegalArgumentException("Unknown SessionWorkspacesGetWorkspaceResultWorkspaceHostType value: " + value); } } - - public enum SessionWorkspacesGetWorkspaceResultWorkspaceSessionSyncLevel { - /** The {@code local} variant. */ - LOCAL("local"), - /** The {@code user} variant. */ - USER("user"), - /** The {@code repo_and_user} variant. */ - REPO_AND_USER("repo_and_user"); - - private final String value; - SessionWorkspacesGetWorkspaceResultWorkspaceSessionSyncLevel(String value) { this.value = value; } - @com.fasterxml.jackson.annotation.JsonValue - public String getValue() { return value; } - @com.fasterxml.jackson.annotation.JsonCreator - public static SessionWorkspacesGetWorkspaceResultWorkspaceSessionSyncLevel fromValue(String value) { - for (SessionWorkspacesGetWorkspaceResultWorkspaceSessionSyncLevel v : values()) { - if (v.value.equals(value)) return v; - } - throw new IllegalArgumentException("Unknown SessionWorkspacesGetWorkspaceResultWorkspaceSessionSyncLevel value: " + value); - } - } } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsForkParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsForkParams.java index 06ea46c932..644be05ee0 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsForkParams.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsForkParams.java @@ -24,6 +24,8 @@ public record SessionsForkParams( /** Source session ID to fork from */ @JsonProperty("sessionId") String sessionId, /** Optional event ID boundary. When provided, the fork includes only events before this ID (exclusive). When omitted, all events are included. */ - @JsonProperty("toEventId") String toEventId + @JsonProperty("toEventId") String toEventId, + /** Optional friendly name to assign to the forked session. */ + @JsonProperty("name") String name ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsForkResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsForkResult.java index 911574f568..77543916ec 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsForkResult.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsForkResult.java @@ -22,6 +22,8 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record SessionsForkResult( /** The new forked session's ID */ - @JsonProperty("sessionId") String sessionId + @JsonProperty("sessionId") String sessionId, + /** Friendly name assigned to the forked session, if any. */ + @JsonProperty("name") String name ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInfo.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInfo.java new file mode 100644 index 0000000000..e494f4894c --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInfo.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SlashCommandInfo( + /** Canonical command name without a leading slash */ + @JsonProperty("name") String name, + /** Canonical aliases without leading slashes */ + @JsonProperty("aliases") List aliases, + /** Human-readable command description */ + @JsonProperty("description") String description, + /** Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command */ + @JsonProperty("kind") SlashCommandKind kind, + /** Optional unstructured input hint */ + @JsonProperty("input") SlashCommandInput input, + /** Whether the command may run while an agent turn is active */ + @JsonProperty("allowDuringAgentExecution") Boolean allowDuringAgentExecution, + /** Whether the command is experimental */ + @JsonProperty("experimental") Boolean experimental +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInput.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInput.java new file mode 100644 index 0000000000..186dec5a81 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInput.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional unstructured input hint + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SlashCommandInput( + /** Hint to display when command input has not been provided */ + @JsonProperty("hint") String hint, + /** When true, the command requires non-empty input; clients should render the input hint as required */ + @JsonProperty("required") Boolean required, + /** Optional completion hint for the input (e.g. 'directory' for filesystem path completion) */ + @JsonProperty("completion") SlashCommandInputCompletion completion, + /** When true, clients should pass the full text after the command name as a single argument rather than splitting on whitespace */ + @JsonProperty("preserveMultilineInput") Boolean preserveMultilineInput +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInputCompletion.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInputCompletion.java new file mode 100644 index 0000000000..c192fa9c0f --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInputCompletion.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Optional completion hint for the input (e.g. 'directory' for filesystem path completion) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SlashCommandInputCompletion { + /** The {@code directory} variant. */ + DIRECTORY("directory"); + + private final String value; + SlashCommandInputCompletion(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SlashCommandInputCompletion fromValue(String value) { + for (SlashCommandInputCompletion v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SlashCommandInputCompletion value: " + value); + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandKind.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandKind.java new file mode 100644 index 0000000000..1f08c4efcd --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandKind.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SlashCommandKind { + /** The {@code builtin} variant. */ + BUILTIN("builtin"), + /** The {@code skill} variant. */ + SKILL("skill"), + /** The {@code client} variant. */ + CLIENT("client"); + + private final String value; + SlashCommandKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SlashCommandKind fromValue(String value) { + for (SlashCommandKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SlashCommandKind value: " + value); + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java index 8334872cbb..844d4a1bf8 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; import javax.annotation.processing.Generated; @javax.annotation.processing.Generated("copilot-sdk-codegen") @@ -19,6 +20,10 @@ public record UsageMetricsModelMetric( /** Request count and cost metrics for this model */ @JsonProperty("requests") UsageMetricsModelMetricRequests requests, /** Token usage metrics for this model */ - @JsonProperty("usage") UsageMetricsModelMetricUsage usage + @JsonProperty("usage") UsageMetricsModelMetricUsage usage, + /** Accumulated nano-AI units cost for this model */ + @JsonProperty("totalNanoAiu") Long totalNanoAiu, + /** Token count details per type */ + @JsonProperty("tokenDetails") Map tokenDetails ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricTokenDetail.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricTokenDetail.java new file mode 100644 index 0000000000..51ef4f3ee2 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricTokenDetail.java @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsModelMetricTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Long tokenCount +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsTokenDetail.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsTokenDetail.java new file mode 100644 index 0000000000..1f763f0281 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsTokenDetail.java @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Long tokenCount +) { +} diff --git a/src/main/java/com/github/copilot/sdk/CliServerManager.java b/src/main/java/com/github/copilot/sdk/CliServerManager.java index 07b5eefecc..bd4effe5a8 100644 --- a/src/main/java/com/github/copilot/sdk/CliServerManager.java +++ b/src/main/java/com/github/copilot/sdk/CliServerManager.java @@ -30,14 +30,28 @@ final class CliServerManager { private static final Logger LOG = Logger.getLogger(CliServerManager.class.getName()); + private static final int STDERR_READER_JOIN_TIMEOUT_MS = 5000; private final CopilotClientOptions options; private final StringBuilder stderrBuffer = new StringBuilder(); + private volatile Thread stderrThread; + private String connectionToken; CliServerManager(CopilotClientOptions options) { this.options = options; } + /** + * Sets the connection token to pass to the CLI process via environment + * variable. + * + * @param connectionToken + * the token, or {@code null} if not applicable + */ + void setConnectionToken(String connectionToken) { + this.connectionToken = connectionToken; + } + /** * Starts the CLI server process. * @@ -76,16 +90,20 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { } // Default UseLoggedInUser to false when GitHubToken is provided - boolean useLoggedInUser = options.getUseLoggedInUser() != null - ? options.getUseLoggedInUser() - : (options.getGitHubToken() == null || options.getGitHubToken().isEmpty()); + boolean useLoggedInUser = options.getUseLoggedInUser() + .orElse(options.getGitHubToken() == null || options.getGitHubToken().isEmpty()); if (!useLoggedInUser) { args.add("--no-auto-login"); } - if (options.getSessionIdleTimeoutSeconds() != null && options.getSessionIdleTimeoutSeconds() > 0) { + if (options.getSessionIdleTimeoutSeconds().isPresent() + && options.getSessionIdleTimeoutSeconds().getAsInt() > 0) { args.add("--session-idle-timeout"); - args.add(String.valueOf(options.getSessionIdleTimeoutSeconds())); + args.add(String.valueOf(options.getSessionIdleTimeoutSeconds().getAsInt())); + } + + if (options.isRemote()) { + args.add("--remote"); } List command = resolveCliCommand(cliPath, args); @@ -115,6 +133,16 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGitHubToken()); } + // Set Copilot home directory if configured + if (options.getCopilotHome() != null && !options.getCopilotHome().isEmpty()) { + pb.environment().put("COPILOT_HOME", options.getCopilotHome()); + } + + // Set connection token for TCP mode + if (connectionToken != null && !connectionToken.isEmpty()) { + pb.environment().put("COPILOT_CONNECTION_TOKEN", connectionToken); + } + // Set telemetry environment variables if configured if (options.getTelemetry() != null) { var telemetry = options.getTelemetry(); @@ -131,9 +159,9 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { if (telemetry.getSourceName() != null) { pb.environment().put("COPILOT_OTEL_SOURCE_NAME", telemetry.getSourceName()); } - if (telemetry.getCaptureContent() != null) { + if (telemetry.getCaptureContent().isPresent()) { pb.environment().put("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", - telemetry.getCaptureContent() ? "true" : "false"); + telemetry.getCaptureContent().get() ? "true" : "false"); } } @@ -177,7 +205,7 @@ JsonRpcClient connectToServer(Process process, String tcpHost, Integer tcpPort) } private void startStderrReader(Process process) { - var stderrThread = new Thread(() -> { + var thread = new Thread(() -> { try (BufferedReader reader = new BufferedReader( new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) { String line; @@ -191,8 +219,9 @@ private void startStderrReader(Process process) { LOG.log(Level.FINE, "Error reading stderr", e); } }, "cli-stderr-reader"); - stderrThread.setDaemon(true); - stderrThread.start(); + thread.setDaemon(true); + thread.start(); + this.stderrThread = thread; } private Integer waitForPortAnnouncement(Process process) throws IOException { @@ -204,11 +233,9 @@ private Integer waitForPortAnnouncement(Process process) throws IOException { while (System.currentTimeMillis() < deadline) { String line = reader.readLine(); if (line == null) { + awaitStderrReader(); String stderr = getStderrOutput(); - if (!stderr.isEmpty()) { - throw new IOException("CLI process exited unexpectedly. stderr: " + stderr); - } - throw new IOException("CLI process exited unexpectedly"); + throw new IOException(formatCliExitedMessage("CLI process exited unexpectedly.", stderr)); } Matcher matcher = portPattern.matcher(line); @@ -228,12 +255,30 @@ String getStderrOutput() { } } + private void awaitStderrReader() { + Thread t = this.stderrThread; + if (t != null) { + try { + t.join(STDERR_READER_JOIN_TIMEOUT_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + private void clearStderrBuffer() { synchronized (stderrBuffer) { stderrBuffer.setLength(0); } } + static String formatCliExitedMessage(String message, String stderrOutput) { + if (stderrOutput == null || stderrOutput.isEmpty()) { + return message; + } + return message + "\nstderr: " + stderrOutput; + } + 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 4b3bb0904f..4d0770319a 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -21,6 +21,7 @@ import com.github.copilot.sdk.json.CopilotClientOptions; import com.github.copilot.sdk.json.CreateSessionResponse; +import com.github.copilot.sdk.generated.rpc.ConnectParams; import com.github.copilot.sdk.generated.rpc.ServerRpc; import com.github.copilot.sdk.json.DeleteSessionResponse; import com.github.copilot.sdk.json.GetAuthStatusResponse; @@ -75,6 +76,7 @@ public final class CopilotClient implements AutoCloseable { * shutdown via {@link #stop()}. */ public static final int AUTOCLOSEABLE_TIMEOUT_SECONDS = 10; + private static final int FORCE_KILL_TIMEOUT_SECONDS = 10; private final CopilotClientOptions options; private final CliServerManager serverManager; private final LifecycleEventManager lifecycleManager = new LifecycleEventManager(); @@ -83,6 +85,7 @@ public final class CopilotClient implements AutoCloseable { private volatile boolean disposed = false; private final String optionsHost; private final Integer optionsPort; + private final String effectiveConnectionToken; private volatile List modelsCache; private final Object modelsCacheLock = new Object(); @@ -117,11 +120,29 @@ 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().isPresent())) { throw new IllegalArgumentException( "GitHubToken and UseLoggedInUser cannot be used with CliUrl (external server manages its own auth)"); } + // Validate tcpConnectionToken + if (this.options.getTcpConnectionToken() != null) { + if (this.options.getTcpConnectionToken().isEmpty()) { + throw new IllegalArgumentException("TcpConnectionToken must be a non-empty string"); + } + if (this.options.isUseStdio()) { + throw new IllegalArgumentException("TcpConnectionToken cannot be used with UseStdio = true"); + } + } + + // Compute effective connection token: use provided, or auto-generate for + // SDK-spawned TCP mode, or null for stdio/external server + boolean sdkSpawnsCli = !this.options.isUseStdio() + && (this.options.getCliUrl() == null || this.options.getCliUrl().isEmpty()); + this.effectiveConnectionToken = this.options.getTcpConnectionToken() != null + ? this.options.getTcpConnectionToken() + : (sdkSpawnsCli ? java.util.UUID.randomUUID().toString() : null); + // Parse CliUrl if provided if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty()) { URI uri = CliServerManager.parseCliUrl(this.options.getCliUrl()); @@ -133,6 +154,7 @@ public CopilotClient(CopilotClientOptions options) { } this.serverManager = new CliServerManager(this.options); + this.serverManager.setConnectionToken(this.effectiveConnectionToken); } /** @@ -165,9 +187,10 @@ private CompletableFuture startCore() { } private Connection startCoreBody() { + Process process = null; + long startNanos = System.nanoTime(); try { JsonRpcClient rpc; - Process process = null; if (optionsHost != null && optionsPort != null) { // External server (TCP) @@ -180,6 +203,9 @@ private Connection startCoreBody() { processInfo.port()); } + LoggingHelpers.logTiming(LOG, Level.FINE, "CopilotClient.start transport setup complete. Elapsed={Elapsed}", + startNanos); + Connection connection = new Connection(rpc, process, new ServerRpc(rpc::invoke)); // Register handlers for server-to-client calls @@ -189,40 +215,81 @@ private Connection startCoreBody() { // Verify protocol version verifyProtocolVersion(connection); + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.start protocol verification complete. Elapsed={Elapsed}", startNanos); - LOG.info("Copilot client connected"); + LoggingHelpers.logTiming(LOG, Level.FINE, "CopilotClient.start complete. Elapsed={Elapsed}", startNanos); return connection; } catch (Exception e) { + if (!(e instanceof java.util.concurrent.CancellationException)) { + LoggingHelpers.logTiming(LOG, Level.WARNING, e, "CopilotClient.start failed. Elapsed={Elapsed}", + startNanos); + } + // Clean up the spawned process if connection setup failed + if (process != null) { + cleanupCliProcess(process); + } String stderr = serverManager.getStderrOutput(); if (!stderr.isEmpty()) { - throw new CompletionException(new IOException("CLI process exited unexpectedly. stderr: " + stderr, e)); + throw new CompletionException(new IOException( + CliServerManager.formatCliExitedMessage("CLI process exited unexpectedly.", stderr), e)); } throw new CompletionException(e); } } private static final int MIN_PROTOCOL_VERSION = 2; + private static final int METHOD_NOT_FOUND_ERROR_CODE = -32601; private void verifyProtocolVersion(Connection connection) throws Exception { int expectedVersion = SdkProtocolVersion.get(); - var params = new HashMap(); - params.put("message", null); - PingResponse pingResponse = connection.rpc.invoke("ping", params, PingResponse.class).get(30, TimeUnit.SECONDS); + Integer serverVersion; - if (pingResponse.protocolVersion() == null) { - throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion - + ", but server does not report a protocol version. " + try { + // Try the new 'connect' RPC which supports connection tokens + var connectParams = new ConnectParams(effectiveConnectionToken); + var connectResponse = connection.rpc + .invoke("connect", connectParams, com.github.copilot.sdk.generated.rpc.ConnectResult.class) + .get(30, TimeUnit.SECONDS); + serverVersion = connectResponse.protocolVersion() != null + ? connectResponse.protocolVersion().intValue() + : null; + } catch (Exception e) { + // Unwrap CompletionException/ExecutionException to check inner cause + Throwable cause = e; + while (cause instanceof java.util.concurrent.ExecutionException || cause instanceof CompletionException) { + cause = cause.getCause(); + } + if (cause instanceof JsonRpcException rpcEx && isUnsupportedConnectMethod(rpcEx)) { + // Legacy server without 'connect'; fall back to 'ping'. + // A token, if any, is silently dropped β€” the legacy server can't enforce one. + var params = new HashMap(); + params.put("message", null); + PingResponse pingResponse = connection.rpc.invoke("ping", params, PingResponse.class).get(30, + TimeUnit.SECONDS); + serverVersion = pingResponse.protocolVersion(); + } else { + throw e; + } + } + + if (serverVersion == null) { + throw new RuntimeException("SDK protocol version mismatch: SDK supports versions " + MIN_PROTOCOL_VERSION + + "-" + expectedVersion + ", but server does not report a protocol version. " + "Please update your server to ensure compatibility."); } - int serverVersion = pingResponse.protocolVersion(); if (serverVersion < MIN_PROTOCOL_VERSION || serverVersion > expectedVersion) { - throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion - + " (minimum " + MIN_PROTOCOL_VERSION + "), but server reports version " + serverVersion + ". " + throw new RuntimeException("SDK protocol version mismatch: SDK supports versions " + MIN_PROTOCOL_VERSION + + "-" + expectedVersion + ", but server reports version " + serverVersion + ". " + "Please update your SDK or server to ensure compatibility."); } } + private static boolean isUnsupportedConnectMethod(JsonRpcException ex) { + return ex.getCode() == METHOD_NOT_FOUND_ERROR_CODE || "Unhandled method connect".equals(ex.getMessage()); + } + /** * Disconnects from the Copilot server and closes all active sessions. *

@@ -299,15 +366,28 @@ private CompletableFuture cleanupConnection() { } if (connection.process != null) { - try { - if (connection.process.isAlive()) { - connection.process.destroyForcibly(); - } - } catch (Exception e) { - LOG.log(Level.FINE, "Error killing process", e); + cleanupCliProcess(connection.process); + } + }).exceptionally(ex -> { + LOG.log(Level.FINE, "Ignoring failed Copilot client startup during cleanup", ex); + return null; + }); + } + + private static void cleanupCliProcess(Process process) { + try { + if (process.isAlive()) { + Process destroyedProcess = process.destroyForcibly(); + if (!destroyedProcess.waitFor(FORCE_KILL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + LOG.fine("Process did not terminate within force kill timeout"); } } - }).exceptionally(ex -> null); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.log(Level.FINE, "Interrupted while killing process", e); + } catch (Exception e) { + LOG.log(Level.FINE, "Error killing process", e); + } } /** @@ -347,18 +427,23 @@ public CompletableFuture createSession(SessionConfig config) { + "new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)")); } return ensureConnected().thenCompose(connection -> { + long totalNanos = System.nanoTime(); // 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(); + long setupNanos = System.nanoTime(); var session = new CopilotSession(sessionId, connection.rpc); if (options.getExecutor() != null) { session.setExecutor(options.getExecutor()); } SessionRequestBuilder.configureSession(session, config); sessions.put(sessionId, session); + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.createSession local setup complete. Elapsed={Elapsed}, SessionId=" + sessionId, + setupNanos); // Extract transform callbacks from the system message config. // Callbacks are registered with the session; a wire-safe copy of the @@ -374,7 +459,12 @@ public CompletableFuture createSession(SessionConfig config) { request.setSystemMessage(extracted.wireSystemMessage()); } + long rpcNanos = System.nanoTime(); return connection.rpc.invoke("session.create", request, CreateSessionResponse.class).thenApply(response -> { + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.createSession session creation request completed. Elapsed={Elapsed}, SessionId=" + + sessionId, + rpcNanos); session.setWorkspacePath(response.workspacePath()); session.setCapabilities(response.capabilities()); // If the server returned a different sessionId (e.g. a v2 CLI that ignores @@ -385,9 +475,13 @@ public CompletableFuture createSession(SessionConfig config) { session.setActiveSessionId(returnedId); sessions.put(returnedId, session); } + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.createSession complete. Elapsed={Elapsed}, SessionId=" + sessionId, totalNanos); return session; }).exceptionally(ex -> { sessions.remove(sessionId); + LoggingHelpers.logTiming(LOG, Level.WARNING, ex, + "CopilotClient.createSession failed. Elapsed={Elapsed}, SessionId=" + sessionId, totalNanos); throw ex instanceof RuntimeException re ? re : new RuntimeException(ex); }); }); @@ -426,13 +520,18 @@ public CompletableFuture resumeSession(String sessionId, ResumeS + "new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)")); } return ensureConnected().thenCompose(connection -> { + long totalNanos = System.nanoTime(); // Register the session before the RPC call to avoid missing early events. + long setupNanos = System.nanoTime(); var session = new CopilotSession(sessionId, connection.rpc); if (options.getExecutor() != null) { session.setExecutor(options.getExecutor()); } SessionRequestBuilder.configureSession(session, config); sessions.put(sessionId, session); + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.resumeSession local setup complete. Elapsed={Elapsed}, SessionId=" + sessionId, + setupNanos); // Extract transform callbacks from the system message config. var extracted = SessionRequestBuilder.extractTransformCallbacks(config.getSystemMessage()); @@ -445,7 +544,12 @@ public CompletableFuture resumeSession(String sessionId, ResumeS request.setSystemMessage(extracted.wireSystemMessage()); } + long rpcNanos = System.nanoTime(); return connection.rpc.invoke("session.resume", request, ResumeSessionResponse.class).thenApply(response -> { + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.resumeSession session resume request completed. Elapsed={Elapsed}, SessionId=" + + sessionId, + rpcNanos); session.setWorkspacePath(response.workspacePath()); session.setCapabilities(response.capabilities()); // If the server returned a different sessionId than what was requested, re-key. @@ -455,9 +559,13 @@ public CompletableFuture resumeSession(String sessionId, ResumeS session.setActiveSessionId(returnedId); sessions.put(returnedId, session); } + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.resumeSession complete. Elapsed={Elapsed}, SessionId=" + sessionId, totalNanos); return session; }).exceptionally(ex -> { sessions.remove(sessionId); + LoggingHelpers.logTiming(LOG, Level.WARNING, ex, + "CopilotClient.resumeSession failed. Elapsed={Elapsed}, SessionId=" + sessionId, totalNanos); throw ex instanceof RuntimeException re ? re : new RuntimeException(ex); }); }); @@ -744,18 +852,15 @@ public CompletableFuture getForegroundSessionId() { * if the operation fails */ public CompletableFuture setForegroundSessionId(String sessionId) { - return ensureConnected() - .thenCompose( - connection -> connection.rpc - .invoke("session.setForeground", Map.of("sessionId", sessionId), - com.github.copilot.sdk.json.SetForegroundSessionResponse.class) - .thenAccept(response -> { - if (!response.success()) { - throw new RuntimeException(response.error() != null - ? response.error() - : "Failed to set foreground session"); - } - })); + return ensureConnected().thenCompose(connection -> connection.rpc + .invoke("session.setForeground", new com.github.copilot.sdk.json.SetForegroundSessionRequest(sessionId), + com.github.copilot.sdk.json.SetForegroundSessionResponse.class) + .thenAccept(response -> { + if (!response.success()) { + throw new RuntimeException( + response.error() != null ? response.error() : "Failed to set foreground session"); + } + })); } /** diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index c1374320cd..79a7f343b7 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -54,6 +54,10 @@ import com.github.copilot.sdk.generated.SessionEvent; import com.github.copilot.sdk.generated.SessionIdleEvent; import com.github.copilot.sdk.json.AgentInfo; +import com.github.copilot.sdk.json.AutoModeSwitchHandler; +import com.github.copilot.sdk.json.AutoModeSwitchInvocation; +import com.github.copilot.sdk.json.AutoModeSwitchRequest; +import com.github.copilot.sdk.json.AutoModeSwitchResponse; import com.github.copilot.sdk.json.CommandContext; import com.github.copilot.sdk.json.CommandDefinition; import com.github.copilot.sdk.json.CommandHandler; @@ -62,6 +66,10 @@ import com.github.copilot.sdk.json.ElicitationParams; import com.github.copilot.sdk.json.ElicitationResult; import com.github.copilot.sdk.json.ElicitationResultAction; +import com.github.copilot.sdk.json.ExitPlanModeHandler; +import com.github.copilot.sdk.json.ExitPlanModeInvocation; +import com.github.copilot.sdk.json.ExitPlanModeRequest; +import com.github.copilot.sdk.json.ExitPlanModeResult; import com.github.copilot.sdk.json.ElicitationSchema; import com.github.copilot.sdk.json.GetMessagesResponse; import com.github.copilot.sdk.json.HookInvocation; @@ -156,6 +164,8 @@ public final class CopilotSession implements AutoCloseable { private final AtomicReference permissionHandler = new AtomicReference<>(); private final AtomicReference userInputHandler = new AtomicReference<>(); private final AtomicReference elicitationHandler = new AtomicReference<>(); + private final AtomicReference exitPlanModeHandler = new AtomicReference<>(); + private final AtomicReference autoModeSwitchHandler = new AtomicReference<>(); private final AtomicReference hooksHandler = new AtomicReference<>(); private volatile EventErrorHandler eventErrorHandler; private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS; @@ -497,13 +507,24 @@ public CompletableFuture send(MessageOptions options) { */ public CompletableFuture sendAndWait(MessageOptions options, long timeoutMs) { ensureNotTerminated(); + long totalNanos = System.nanoTime(); var future = new CompletableFuture(); var lastAssistantMessage = new AtomicReference(); + var firstAssistantMessageLogged = new java.util.concurrent.atomic.AtomicBoolean(false); Consumer handler = evt -> { if (evt instanceof AssistantMessageEvent msg) { lastAssistantMessage.set(msg); + if (firstAssistantMessageLogged.compareAndSet(false, true)) { + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotSession.sendAndWait first assistant message. Elapsed={Elapsed}, SessionId=" + + sessionId, + totalNanos); + } } else if (evt instanceof SessionIdleEvent) { + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotSession.sendAndWait idle received. Elapsed={Elapsed}, SessionId=" + sessionId, + totalNanos); future.complete(lastAssistantMessage.get()); } else if (evt instanceof SessionErrorEvent errorEvent) { String message = errorEvent.getData() != null ? errorEvent.getData().message() : "session error"; @@ -568,8 +589,23 @@ public CompletableFuture sendAndWait(MessageOptions optio } if (!result.isDone()) { if (ex != null) { + if (ex instanceof TimeoutException) { + LoggingHelpers.logTiming(LOG, Level.WARNING, ex, + "CopilotSession.sendAndWait failed. Elapsed={Elapsed}, SessionId=" + sessionId + + ", CompletedBy=timeout", + totalNanos); + } else if (!(ex instanceof java.util.concurrent.CancellationException)) { + LoggingHelpers.logTiming(LOG, Level.WARNING, ex, + "CopilotSession.sendAndWait failed. Elapsed={Elapsed}, SessionId=" + sessionId + + ", CompletedBy=error", + totalNanos); + } result.completeExceptionally(ex); } else { + LoggingHelpers.logTiming( + LOG, Level.FINE, "CopilotSession.sendAndWait complete. Elapsed={Elapsed}, SessionId=" + + sessionId + ", CompletedBy=idle, AssistantMessageReceived=" + (r != null), + totalNanos); result.complete(r); } } @@ -1082,9 +1118,9 @@ private void handleElicitationRequestAsync(ElicitationContext context, String re */ private void assertElicitation() { SessionCapabilities caps = capabilities; - if (caps == null || caps.getUi() == null || !Boolean.TRUE.equals(caps.getUi().getElicitation())) { + if (caps == null || caps.getUi() == null || !caps.getUi().getElicitation().orElse(false)) { throw new IllegalStateException("Elicitation is not supported by the host. " - + "Check session.getCapabilities().getUi()?.getElicitation() before calling UI methods."); + + "Check session.getCapabilities().getUi().getElicitation().orElse(false) before calling UI methods."); } } @@ -1165,10 +1201,10 @@ public CompletableFuture input(String message, InputOptions options) { field.put("title", options.getTitle()); if (options.getDescription() != null) field.put("description", options.getDescription()); - if (options.getMinLength() != null) - field.put("minLength", options.getMinLength()); - if (options.getMaxLength() != null) - field.put("maxLength", options.getMaxLength()); + if (options.getMinLength().isPresent()) + field.put("minLength", options.getMinLength().getAsInt()); + if (options.getMaxLength().isPresent()) + field.put("maxLength", options.getMaxLength().getAsInt()); if (options.getFormat() != null) field.put("format", options.getFormat()); if (options.getDefaultValue() != null) @@ -1291,6 +1327,32 @@ void registerElicitationHandler(ElicitationHandler handler) { elicitationHandler.set(handler); } + /** + * Registers an exit-plan-mode handler for this session. + *

+ * Called internally when creating or resuming a session with an exit-plan-mode + * handler. + * + * @param handler + * the handler to invoke when an exit-plan-mode request is received + */ + void registerExitPlanModeHandler(ExitPlanModeHandler handler) { + exitPlanModeHandler.set(handler); + } + + /** + * Registers an auto-mode-switch handler for this session. + *

+ * Called internally when creating or resuming a session with an + * auto-mode-switch handler. + * + * @param handler + * the handler to invoke when an auto-mode-switch request is received + */ + void registerAutoModeSwitchHandler(AutoModeSwitchHandler handler) { + autoModeSwitchHandler.set(handler); + } + /** * Sets the capabilities reported by the host for this session. *

@@ -1330,6 +1392,60 @@ CompletableFuture handleUserInputRequest(UserInputRequest req } } + /** + * Handles an exit-plan-mode request from the Copilot CLI. + *

+ * Called internally when the server sends an {@code exitPlanMode.request}. + * + * @param request + * the exit-plan-mode request + * @return a future that resolves with the user's decision + */ + CompletableFuture handleExitPlanModeRequest(ExitPlanModeRequest request) { + ExitPlanModeHandler handler = exitPlanModeHandler.get(); + if (handler == null) { + return CompletableFuture.completedFuture(new ExitPlanModeResult().setApproved(true)); + } + + try { + var invocation = new ExitPlanModeInvocation().setSessionId(sessionId); + return handler.handle(request, invocation).exceptionally(ex -> { + LOG.log(Level.SEVERE, "Exit plan mode handler threw an exception", ex); + throw new RuntimeException("Exit plan mode handler error", ex); + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed to process exit plan mode request", e); + return CompletableFuture.failedFuture(e); + } + } + + /** + * Handles an auto-mode-switch request from the Copilot CLI. + *

+ * Called internally when the server sends an {@code autoModeSwitch.request}. + * + * @param request + * the auto-mode-switch request + * @return a future that resolves with the user's decision + */ + CompletableFuture handleAutoModeSwitchRequest(AutoModeSwitchRequest request) { + AutoModeSwitchHandler handler = autoModeSwitchHandler.get(); + if (handler == null) { + return CompletableFuture.completedFuture(AutoModeSwitchResponse.NO); + } + + try { + var invocation = new AutoModeSwitchInvocation().setSessionId(sessionId); + return handler.handle(request, invocation).exceptionally(ex -> { + LOG.log(Level.SEVERE, "Auto mode switch handler threw an exception", ex); + throw new RuntimeException("Auto mode switch handler error", ex); + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed to process auto mode switch request", e); + return CompletableFuture.failedFuture(e); + } + } + /** * Registers hook handlers for this session. *

@@ -1579,7 +1695,8 @@ public CompletableFuture setModel(String model, String reasoningEffort, ModelCapabilitiesOverrideSupports supports = null; if (modelCapabilities.getSupports() != null) { var s = modelCapabilities.getSupports(); - supports = new ModelCapabilitiesOverrideSupports(s.getVision(), s.getReasoningEffort()); + supports = new ModelCapabilitiesOverrideSupports(s.getVision().orElse(null), + s.getReasoningEffort().orElse(null)); } ModelCapabilitiesOverrideLimits limits = null; if (modelCapabilities.getLimits() != null) { @@ -1824,6 +1941,8 @@ public void close() { permissionHandler.set(null); userInputHandler.set(null); elicitationHandler.set(null); + exitPlanModeHandler.set(null); + autoModeSwitchHandler.set(null); hooksHandler.set(null); } diff --git a/src/main/java/com/github/copilot/sdk/JsonRpcClient.java b/src/main/java/com/github/copilot/sdk/JsonRpcClient.java index 66ab1726df..73db478ca9 100644 --- a/src/main/java/com/github/copilot/sdk/JsonRpcClient.java +++ b/src/main/java/com/github/copilot/sdk/JsonRpcClient.java @@ -104,6 +104,7 @@ public void registerMethodHandler(String method, BiConsumer ha * Sends a JSON-RPC request and waits for the response. */ public CompletableFuture invoke(String method, Object params, Class responseType) { + long timingNanos = System.nanoTime(); long id = requestIdCounter.incrementAndGet(); var future = new CompletableFuture(); pendingRequests.put(id, future); @@ -123,13 +124,24 @@ public CompletableFuture invoke(String method, Object params, Class re return future.thenApply(result -> { try { - if (responseType == Void.class || responseType == void.class) { - return null; + T value = null; + if (responseType != Void.class && responseType != void.class) { + value = MAPPER.treeToValue(result, responseType); } - return MAPPER.treeToValue(result, responseType); + LoggingHelpers.logTiming(LOG, Level.FINE, + "JsonRpc.invoke JSON-RPC request finished. Elapsed={Elapsed}, Method=" + method + ", RequestId=" + + id + ", Status=Succeeded", + timingNanos); + return value; } catch (JsonProcessingException e) { throw new CompletionException(e); } + }).exceptionally(ex -> { + LoggingHelpers.logTiming(LOG, Level.WARNING, ex, + "JsonRpc.invoke JSON-RPC request finished. Elapsed={Elapsed}, Method=" + method + ", RequestId=" + + id + ", Status=Failed", + timingNanos); + throw ex instanceof RuntimeException re ? re : new RuntimeException(ex); }); } diff --git a/src/main/java/com/github/copilot/sdk/LoggingHelpers.java b/src/main/java/com/github/copilot/sdk/LoggingHelpers.java new file mode 100644 index 0000000000..654494ef13 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/LoggingHelpers.java @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Internal helper for timing-based diagnostic logging. + */ +final class LoggingHelpers { + + private LoggingHelpers() { + // Utility class + } + + /** + * Formats elapsed time as a human-readable duration string. + * + * @param startNanos + * the start time from {@link System#nanoTime()} + * @return formatted duration (e.g. "PT0.123S") + */ + static String formatElapsed(long startNanos) { + long elapsedNanos = System.nanoTime() - startNanos; + long millis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos); + return String.format("PT%d.%03dS", millis / 1000, millis % 1000); + } + + /** + * Logs a timing message at the given level if the logger accepts it. + * + * @param logger + * the logger to use + * @param level + * the log level + * @param message + * the message template + * @param startNanos + * the start time from {@link System#nanoTime()} + */ + static void logTiming(Logger logger, Level level, String message, long startNanos) { + if (!logger.isLoggable(level)) { + return; + } + logger.log(level, message.replace("{Elapsed}", formatElapsed(startNanos))); + } + + /** + * Logs a timing message at the given level with an exception. + * + * @param logger + * the logger to use + * @param level + * the log level + * @param exception + * the exception, may be {@code null} + * @param message + * the message template + * @param startNanos + * the start time from {@link System#nanoTime()} + */ + static void logTiming(Logger logger, Level level, Throwable exception, String message, long startNanos) { + if (!logger.isLoggable(level)) { + return; + } + String formatted = message.replace("{Elapsed}", formatElapsed(startNanos)); + if (exception != null) { + logger.log(level, formatted, exception); + } else { + logger.log(level, formatted); + } + } +} diff --git a/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java b/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java index d085f7fcec..1d76d8b88a 100644 --- a/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java +++ b/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java @@ -17,6 +17,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.json.AutoModeSwitchRequest; +import com.github.copilot.sdk.json.ExitPlanModeRequest; import com.github.copilot.sdk.json.PermissionRequestResult; import com.github.copilot.sdk.json.PermissionRequestResultKind; import com.github.copilot.sdk.json.SessionLifecycleEvent; @@ -79,6 +81,10 @@ void registerHandlers(JsonRpcClient rpc) { (requestId, params) -> handlePermissionRequest(rpc, requestId, params)); rpc.registerMethodHandler("userInput.request", (requestId, params) -> handleUserInputRequest(rpc, requestId, params)); + rpc.registerMethodHandler("exitPlanMode.request", + (requestId, params) -> handleExitPlanModeRequest(rpc, requestId, params)); + rpc.registerMethodHandler("autoModeSwitch.request", + (requestId, params) -> handleAutoModeSwitchRequest(rpc, requestId, params)); rpc.registerMethodHandler("hooks.invoke", (requestId, params) -> handleHooksInvoke(rpc, requestId, params)); rpc.registerMethodHandler("systemMessage.transform", (requestId, params) -> handleSystemMessageTransform(rpc, requestId, params)); @@ -125,6 +131,10 @@ private void handleLifecycleEvent(JsonNode params) { private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params) { runAsync(() -> { + final long requestIdLong = parseRequestId(requestId, "tool.call"); + if (requestIdLong == -1) { + return; + } try { String sessionId = params.get("sessionId").asText(); String toolCallId = params.get("toolCallId").asText(); @@ -133,7 +143,7 @@ private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params CopilotSession session = sessions.get(sessionId); if (session == null) { - rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session " + sessionId); + rpc.sendErrorResponse(requestIdLong, -32602, "Unknown session " + sessionId); return; } @@ -141,7 +151,7 @@ private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params if (tool == null || tool.handler() == null) { var result = ToolResultObject.failure("Tool '" + toolName + "' is not supported.", "tool '" + toolName + "' not supported"); - rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result)); + rpc.sendResponse(requestIdLong, Map.of("result", result)); return; } @@ -157,7 +167,7 @@ private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params toolResult = ToolResultObject .success(result instanceof String s ? s : MAPPER.writeValueAsString(result)); } - rpc.sendResponse(Long.parseLong(requestId), Map.of("result", toolResult)); + rpc.sendResponse(requestIdLong, Map.of("result", toolResult)); } catch (Exception e) { LOG.log(Level.SEVERE, "Error sending tool result", e); } @@ -166,7 +176,7 @@ private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params var result = ToolResultObject.failure( "Invoking this tool produced an error. Detailed information is not available.", ex.getMessage()); - rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result)); + rpc.sendResponse(requestIdLong, Map.of("result", result)); } catch (Exception e) { LOG.log(Level.SEVERE, "Error sending tool error", e); } @@ -175,7 +185,7 @@ private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params } catch (Exception e) { LOG.log(Level.SEVERE, "Error handling tool call", e); try { - rpc.sendErrorResponse(Long.parseLong(requestId), -32603, e.getMessage()); + rpc.sendErrorResponse(requestIdLong, -32603, e.getMessage()); } catch (IOException ioe) { LOG.log(Level.SEVERE, "Failed to send error response", ioe); } @@ -185,6 +195,10 @@ private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params private void handlePermissionRequest(JsonRpcClient rpc, String requestId, JsonNode params) { runAsync(() -> { + final long requestIdLong = parseRequestId(requestId, "permission.request"); + if (requestIdLong == -1) { + return; + } try { String sessionId = params.get("sessionId").asText(); JsonNode permissionRequest = params.get("permissionRequest"); @@ -193,7 +207,7 @@ private void handlePermissionRequest(JsonRpcClient rpc, String requestId, JsonNo if (session == null) { var result = new PermissionRequestResult() .setKind(PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER); - rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result)); + rpc.sendResponse(requestIdLong, Map.of("result", result)); return; } @@ -206,7 +220,7 @@ private void handlePermissionRequest(JsonRpcClient rpc, String requestId, JsonNo throw new IllegalStateException( "Permission handlers cannot return 'no-result' when connected to a protocol v2 server."); } - rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result)); + rpc.sendResponse(requestIdLong, Map.of("result", result)); } catch (IOException e) { LOG.log(Level.SEVERE, "Error sending permission result", e); } @@ -214,7 +228,7 @@ private void handlePermissionRequest(JsonRpcClient rpc, String requestId, JsonNo try { var result = new PermissionRequestResult() .setKind(PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER); - rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result)); + rpc.sendResponse(requestIdLong, Map.of("result", result)); } catch (IOException e) { LOG.log(Level.SEVERE, "Error sending permission denied", e); } @@ -229,6 +243,10 @@ private void handlePermissionRequest(JsonRpcClient rpc, String requestId, JsonNo private void handleUserInputRequest(JsonRpcClient rpc, String requestId, JsonNode params) { LOG.fine("Received userInput.request: " + params); runAsync(() -> { + final long requestIdLong = parseRequestId(requestId, "userInput.request"); + if (requestIdLong == -1) { + return; + } try { String sessionId = params.get("sessionId").asText(); String question = params.get("question").asText(); @@ -240,7 +258,7 @@ private void handleUserInputRequest(JsonRpcClient rpc, String requestId, JsonNod LOG.fine("Found session: " + (session != null)); if (session == null) { LOG.fine("Session not found, sending error"); - rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session " + sessionId); + rpc.sendErrorResponse(requestIdLong, -32602, "Unknown session " + sessionId); return; } @@ -262,7 +280,7 @@ private void handleUserInputRequest(JsonRpcClient rpc, String requestId, JsonNod String answer = response.getAnswer() != null ? response.getAnswer() : ""; LOG.fine("Sending userInput response: answer=" + answer + ", wasFreeform=" + response.isWasFreeform()); - rpc.sendResponse(Long.parseLong(requestId), + rpc.sendResponse(requestIdLong, Map.of("answer", answer, "wasFreeform", response.isWasFreeform())); } catch (IOException e) { LOG.log(Level.SEVERE, "Error sending user input response", e); @@ -270,8 +288,7 @@ private void handleUserInputRequest(JsonRpcClient rpc, String requestId, JsonNod }).exceptionally(ex -> { LOG.log(Level.WARNING, "User input handler exception", ex); try { - rpc.sendErrorResponse(Long.parseLong(requestId), -32603, - "User input handler error: " + ex.getMessage()); + rpc.sendErrorResponse(requestIdLong, -32603, "User input handler error: " + ex.getMessage()); } catch (IOException e) { LOG.log(Level.SEVERE, "Error sending user input error", e); } @@ -283,8 +300,110 @@ private void handleUserInputRequest(JsonRpcClient rpc, String requestId, JsonNod }); } + private void handleExitPlanModeRequest(JsonRpcClient rpc, String requestId, JsonNode params) { + runAsync(() -> { + final long requestIdLong = parseRequestId(requestId, "exitPlanMode.request"); + if (requestIdLong == -1) { + return; + } + try { + String sessionId = params.get("sessionId").asText(); + + CopilotSession session = sessions.get(sessionId); + if (session == null) { + rpc.sendErrorResponse(requestIdLong, -32602, "Unknown session " + sessionId); + return; + } + + var request = new ExitPlanModeRequest(); + if (params.has("summary")) { + request.setSummary(params.get("summary").asText()); + } + if (params.has("planContent") && !params.get("planContent").isNull()) { + request.setPlanContent(params.get("planContent").asText()); + } + if (params.has("actions") && params.get("actions").isArray()) { + var actions = new ArrayList(); + for (JsonNode action : params.get("actions")) { + actions.add(action.asText()); + } + request.setActions(actions); + } + if (params.has("recommendedAction") && !params.get("recommendedAction").isNull()) { + request.setRecommendedAction(params.get("recommendedAction").asText()); + } + + session.handleExitPlanModeRequest(request).thenAccept(result -> { + try { + rpc.sendResponse(requestIdLong, MAPPER.valueToTree(result)); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending exit plan mode response", e); + } + }).exceptionally(ex -> { + try { + rpc.sendErrorResponse(requestIdLong, -32603, + "Exit plan mode handler error: " + ex.getMessage()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending exit plan mode error", e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling exit plan mode request", e); + } + }); + } + + private void handleAutoModeSwitchRequest(JsonRpcClient rpc, String requestId, JsonNode params) { + runAsync(() -> { + final long requestIdLong = parseRequestId(requestId, "autoModeSwitch.request"); + if (requestIdLong == -1) { + return; + } + try { + String sessionId = params.get("sessionId").asText(); + + CopilotSession session = sessions.get(sessionId); + if (session == null) { + rpc.sendErrorResponse(requestIdLong, -32602, "Unknown session " + sessionId); + return; + } + + var request = new AutoModeSwitchRequest(); + if (params.has("errorCode") && !params.get("errorCode").isNull()) { + request.setErrorCode(params.get("errorCode").asText()); + } + if (params.has("retryAfterSeconds") && !params.get("retryAfterSeconds").isNull()) { + request.setRetryAfterSeconds(params.get("retryAfterSeconds").asDouble()); + } + + session.handleAutoModeSwitchRequest(request).thenAccept(response -> { + try { + rpc.sendResponse(requestIdLong, Map.of("response", response)); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending auto mode switch response", e); + } + }).exceptionally(ex -> { + try { + rpc.sendErrorResponse(requestIdLong, -32603, + "Auto mode switch handler error: " + ex.getMessage()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending auto mode switch error", e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling auto mode switch request", e); + } + }); + } + private void handleHooksInvoke(JsonRpcClient rpc, String requestId, JsonNode params) { runAsync(() -> { + final long requestIdLong = parseRequestId(requestId, "hooks.invoke"); + if (requestIdLong == -1) { + return; + } try { String sessionId = params.get("sessionId").asText(); String hookType = params.get("hookType").asText(); @@ -292,20 +411,19 @@ private void handleHooksInvoke(JsonRpcClient rpc, String requestId, JsonNode par CopilotSession session = sessions.get(sessionId); if (session == null) { - rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session " + sessionId); + rpc.sendErrorResponse(requestIdLong, -32602, "Unknown session " + sessionId); return; } session.handleHooksInvoke(hookType, input).thenAccept(output -> { try { - rpc.sendResponse(Long.parseLong(requestId), Collections.singletonMap("output", output)); + rpc.sendResponse(requestIdLong, Collections.singletonMap("output", output)); } catch (IOException e) { LOG.log(Level.SEVERE, "Error sending hooks response", e); } }).exceptionally(ex -> { try { - rpc.sendErrorResponse(Long.parseLong(requestId), -32603, - "Hooks handler error: " + ex.getMessage()); + rpc.sendErrorResponse(requestIdLong, -32603, "Hooks handler error: " + ex.getMessage()); } catch (IOException e) { LOG.log(Level.SEVERE, "Error sending hooks error", e); } @@ -328,15 +446,11 @@ interface LifecycleEventDispatcher { private void handleSystemMessageTransform(JsonRpcClient rpc, String requestId, JsonNode params) { runAsync(() -> { + final long requestIdLong = parseRequestId(requestId, "systemMessage.transform"); + if (requestIdLong == -1) { + return; + } try { - final long requestIdLong; - try { - requestIdLong = Long.parseLong(requestId); - } catch (NumberFormatException nfe) { - LOG.log(Level.SEVERE, "Invalid requestId for systemMessage.transform: " + requestId, nfe); - return; - } - String sessionId = params.has("sessionId") ? params.get("sessionId").asText() : null; JsonNode sections = params.get("sections"); @@ -366,6 +480,25 @@ private void handleSystemMessageTransform(JsonRpcClient rpc, String requestId, J }); } + /** + * Parses a JSON-RPC request ID string into a {@code long}. + * + * @param requestId + * the request ID string received from the JSON-RPC layer + * @param methodName + * the RPC method name, used in the log message on failure + * @return the parsed request ID, or {@code -1} if the string is not a valid + * long + */ + private static long parseRequestId(String requestId, String methodName) { + try { + return Long.parseLong(requestId); + } catch (NumberFormatException nfe) { + LOG.log(Level.SEVERE, "Invalid requestId for " + methodName + ": " + requestId, nfe); + return -1; + } + } + private void runAsync(Runnable task) { try { if (executor != null) { diff --git a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java index de6977ea8d..52bfb3337f 100644 --- a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java +++ b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java @@ -111,20 +111,28 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setAvailableTools(config.getAvailableTools()); request.setExcludedTools(config.getExcludedTools()); request.setProvider(config.getProvider()); - request.setRequestUserInput(config.getOnUserInputRequest() != null ? true : null); - request.setHooks(config.getHooks() != null && config.getHooks().hasHooks() ? true : null); + config.getEnableSessionTelemetry().ifPresent(request::setEnableSessionTelemetry); + if (config.getOnUserInputRequest() != null) { + request.setRequestUserInput(true); + } + if (config.getHooks() != null && config.getHooks().hasHooks()) { + request.setHooks(true); + } request.setWorkingDirectory(config.getWorkingDirectory()); - request.setStreaming(config.isStreaming() ? true : null); - request.setIncludeSubAgentStreamingEvents(config.getIncludeSubAgentStreamingEvents()); + if (config.isStreaming()) { + request.setStreaming(true); + } + config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents); request.setMcpServers(config.getMcpServers()); request.setCustomAgents(config.getCustomAgents()); request.setDefaultAgent(config.getDefaultAgent()); request.setAgent(config.getAgent()); request.setInfiniteSessions(config.getInfiniteSessions()); request.setSkillDirectories(config.getSkillDirectories()); + request.setInstructionDirectories(config.getInstructionDirectories()); request.setDisabledSkills(config.getDisabledSkills()); request.setConfigDir(config.getConfigDir()); - request.setEnableConfigDiscovery(config.getEnableConfigDiscovery()); + config.getEnableConfigDiscovery().ifPresent(request::setEnableConfigDiscovery); request.setModelCapabilities(config.getModelCapabilities()); if (config.getCommands() != null && !config.getCommands().isEmpty()) { @@ -136,7 +144,14 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess if (config.getOnElicitationRequest() != null) { request.setRequestElicitation(true); } + if (config.getOnExitPlanMode() != null) { + request.setRequestExitPlanMode(true); + } + if (config.getOnAutoModeSwitch() != null) { + request.setRequestAutoModeSwitch(true); + } request.setGitHubToken(config.getGitHubToken()); + request.setRemoteSession(config.getRemoteSession()); return request; } @@ -186,19 +201,29 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setAvailableTools(config.getAvailableTools()); request.setExcludedTools(config.getExcludedTools()); request.setProvider(config.getProvider()); - request.setRequestUserInput(config.getOnUserInputRequest() != null ? true : null); - request.setHooks(config.getHooks() != null && config.getHooks().hasHooks() ? true : null); + config.getEnableSessionTelemetry().ifPresent(request::setEnableSessionTelemetry); + if (config.getOnUserInputRequest() != null) { + request.setRequestUserInput(true); + } + if (config.getHooks() != null && config.getHooks().hasHooks()) { + request.setHooks(true); + } request.setWorkingDirectory(config.getWorkingDirectory()); request.setConfigDir(config.getConfigDir()); - request.setEnableConfigDiscovery(config.getEnableConfigDiscovery()); - request.setDisableResume(config.isDisableResume() ? true : null); - request.setStreaming(config.isStreaming() ? true : null); - request.setIncludeSubAgentStreamingEvents(config.getIncludeSubAgentStreamingEvents()); + config.getEnableConfigDiscovery().ifPresent(request::setEnableConfigDiscovery); + if (config.isDisableResume()) { + request.setDisableResume(true); + } + if (config.isStreaming()) { + request.setStreaming(true); + } + config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents); request.setMcpServers(config.getMcpServers()); request.setCustomAgents(config.getCustomAgents()); request.setDefaultAgent(config.getDefaultAgent()); request.setAgent(config.getAgent()); request.setSkillDirectories(config.getSkillDirectories()); + request.setInstructionDirectories(config.getInstructionDirectories()); request.setDisabledSkills(config.getDisabledSkills()); request.setInfiniteSessions(config.getInfiniteSessions()); request.setModelCapabilities(config.getModelCapabilities()); @@ -212,7 +237,14 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo if (config.getOnElicitationRequest() != null) { request.setRequestElicitation(true); } + if (config.getOnExitPlanMode() != null) { + request.setRequestExitPlanMode(true); + } + if (config.getOnAutoModeSwitch() != null) { + request.setRequestAutoModeSwitch(true); + } request.setGitHubToken(config.getGitHubToken()); + request.setRemoteSession(config.getRemoteSession()); return request; } @@ -248,6 +280,12 @@ static void configureSession(CopilotSession session, SessionConfig config) { if (config.getOnElicitationRequest() != null) { session.registerElicitationHandler(config.getOnElicitationRequest()); } + if (config.getOnExitPlanMode() != null) { + session.registerExitPlanModeHandler(config.getOnExitPlanMode()); + } + if (config.getOnAutoModeSwitch() != null) { + session.registerAutoModeSwitchHandler(config.getOnAutoModeSwitch()); + } if (config.getOnEvent() != null) { session.on(config.getOnEvent()); } @@ -284,6 +322,12 @@ static void configureSession(CopilotSession session, ResumeSessionConfig config) if (config.getOnElicitationRequest() != null) { session.registerElicitationHandler(config.getOnElicitationRequest()); } + if (config.getOnExitPlanMode() != null) { + session.registerExitPlanModeHandler(config.getOnExitPlanMode()); + } + if (config.getOnAutoModeSwitch() != null) { + session.registerAutoModeSwitchHandler(config.getOnAutoModeSwitch()); + } if (config.getOnEvent() != null) { session.on(config.getOnEvent()); } diff --git a/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchHandler.java b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchHandler.java new file mode 100644 index 0000000000..5fa19f4b1f --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchHandler.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler for auto-mode-switch requests from the agent. + *

+ * Register an auto-mode-switch handler via + * {@link SessionConfig#setOnAutoModeSwitch(AutoModeSwitchHandler)} or + * {@link ResumeSessionConfig#setOnAutoModeSwitch(AutoModeSwitchHandler)}. When + * provided, the server routes {@code autoModeSwitch.request} callbacks to this + * handler. + * + *

Example Usage

+ * + *
{@code
+ * AutoModeSwitchHandler handler = (request, invocation) -> {
+ * 	System.out.println("Rate limited: " + request.getErrorCode());
+ * 	return CompletableFuture.completedFuture(AutoModeSwitchResponse.YES);
+ * };
+ *
+ * var session = client.createSession(new SessionConfig().setOnAutoModeSwitch(handler)).get();
+ * }
+ * + * @see AutoModeSwitchRequest + * @see AutoModeSwitchResponse + * @since 1.0.8 + */ +@FunctionalInterface +public interface AutoModeSwitchHandler { + + /** + * Handles an auto-mode-switch request from the agent. + * + * @param request + * the auto-mode-switch request containing the error code and + * retry-after seconds + * @param invocation + * context information about the invocation + * @return a future that resolves with the user's decision + */ + CompletableFuture handle(AutoModeSwitchRequest request, + AutoModeSwitchInvocation invocation); +} diff --git a/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchInvocation.java b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchInvocation.java new file mode 100644 index 0000000000..278d10ccfd --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchInvocation.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Context for an auto-mode-switch request invocation. + * + * @since 1.0.8 + */ +public class AutoModeSwitchInvocation { + + private String sessionId; + + /** + * Gets the session ID. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the session ID. + * + * @param sessionId + * the session ID + * @return this instance for method chaining + */ + public AutoModeSwitchInvocation setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchRequest.java b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchRequest.java new file mode 100644 index 0000000000..0ef784c02a --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchRequest.java @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Request to switch to auto mode after an eligible rate limit. + *

+ * This is sent by the server when the agent encounters a rate limit and wants + * to switch to an alternative model automatically. + * + * @since 1.0.8 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AutoModeSwitchRequest { + + @JsonProperty("errorCode") + private String errorCode; + + @JsonProperty("retryAfterSeconds") + private Double retryAfterSeconds; + + /** + * Gets the rate-limit error code that triggered the request. + * + * @return the error code, or {@code null} + */ + public String getErrorCode() { + return errorCode; + } + + /** + * Sets the rate-limit error code. + * + * @param errorCode + * the error code + * @return this instance for method chaining + */ + public AutoModeSwitchRequest setErrorCode(String errorCode) { + this.errorCode = errorCode; + return this; + } + + /** + * Gets the seconds until the rate limit resets, when known. + * + * @return the retry-after seconds, or {@code null} + */ + public Double getRetryAfterSeconds() { + return retryAfterSeconds; + } + + /** + * Sets the seconds until the rate limit resets. + * + * @param retryAfterSeconds + * the retry-after seconds + * @return this instance for method chaining + */ + public AutoModeSwitchRequest setRetryAfterSeconds(Double retryAfterSeconds) { + this.retryAfterSeconds = retryAfterSeconds; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchResponse.java b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchResponse.java new file mode 100644 index 0000000000..c072b73e2b --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchResponse.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Response to an auto-mode-switch request. + * + * @since 1.0.8 + */ +public enum AutoModeSwitchResponse { + + /** Approve the switch for this rate-limit cycle. */ + YES("yes"), + + /** Approve and remember the choice for this session. */ + YES_ALWAYS("yes_always"), + + /** Decline the switch. */ + NO("no"); + + private final String value; + + AutoModeSwitchResponse(String value) { + this.value = value; + } + + /** + * Gets the wire value of this response. + * + * @return the string value + */ + @JsonValue + public String getValue() { + return value; + } +} 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 3a14d17335..e4605ffe10 100644 --- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -14,6 +14,9 @@ import java.util.function.Supplier; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Optional; +import java.util.OptionalInt; /** * Configuration options for creating a @@ -44,6 +47,7 @@ public class CopilotClientOptions { private String[] cliArgs; private String cliPath; private String cliUrl; + private String copilotHome; private String cwd; private Map environment; private Executor executor; @@ -53,6 +57,8 @@ public class CopilotClientOptions { private int port; private TelemetryConfig telemetry; private Integer sessionIdleTimeoutSeconds; + private boolean remote; + private String tcpConnectionToken; private Boolean useLoggedInUser; private boolean useStdio = true; @@ -191,6 +197,37 @@ public CopilotClientOptions setCliUrl(String cliUrl) { return this; } + /** + * Gets the base directory for Copilot data (session state, config, etc.). + * + * @return the Copilot home directory path, or {@code null} to use the CLI + * default ({@code ~/.copilot}) + */ + public String getCopilotHome() { + return copilotHome; + } + + /** + * Sets the base directory for Copilot data (session state, config, etc.). + *

+ * Sets the {@code COPILOT_HOME} environment variable on the spawned CLI + * process. When {@code null}, the {@code COPILOT_HOME} env var is not set on + * the spawned process, so the CLI falls back to its default + * ({@code ~/.copilot}). + *

+ * This option is only used when the SDK spawns the CLI process; it is ignored + * when connecting to an external server via {@link #setCliUrl(String)}. + * + * @param copilotHome + * the Copilot home directory path, or {@code null} to use the CLI + * default + * @return this options instance for method chaining + */ + public CopilotClientOptions setCopilotHome(String copilotHome) { + this.copilotHome = copilotHome; + return this; + } + /** * Gets the working directory for the CLI process. * @@ -405,6 +442,37 @@ public CopilotClientOptions setPort(int port) { return this; } + /** + * Returns whether remote session support (Mission Control integration) is + * enabled. + *

+ * When {@code true}, sessions in a GitHub repository working directory are + * accessible from GitHub web and mobile. + * + * @return {@code true} if remote sessions are enabled + */ + public boolean isRemote() { + return remote; + } + + /** + * Enables remote session support (Mission Control integration). + *

+ * When {@code true}, sessions in a GitHub repository working directory are + * accessible from GitHub web and mobile. + *

+ * This option is only used when the SDK spawns the CLI process; it is ignored + * when connecting to an external server via {@link #setCliUrl(String)}. + * + * @param remote + * {@code true} to enable remote sessions + * @return this options instance for method chaining + */ + public CopilotClientOptions setRemote(boolean remote) { + this.remote = remote; + return this; + } + /** * Gets the OpenTelemetry configuration for the CLI server. * @@ -434,42 +502,80 @@ public CopilotClientOptions setTelemetry(TelemetryConfig telemetry) { /** * Gets the server-wide idle timeout for sessions in seconds. * - * @return the session idle timeout in seconds, or {@code null} to disable - * (sessions live indefinitely) + * @return an {@link OptionalInt} containing the session idle timeout in + * seconds, or {@link java.util.OptionalInt#empty()} if not set. Use + * {@link #clearSessionIdleTimeoutSeconds()} to revert to the default. * @since 1.3.0 */ - public Integer getSessionIdleTimeoutSeconds() { - return sessionIdleTimeoutSeconds; + @JsonIgnore + public OptionalInt getSessionIdleTimeoutSeconds() { + return sessionIdleTimeoutSeconds == null ? OptionalInt.empty() : OptionalInt.of(sessionIdleTimeoutSeconds); } /** * Sets the server-wide idle timeout for sessions in seconds. *

* Sessions without activity for this duration are automatically cleaned up. Set - * to {@code 0} or leave as {@code null} to disable (sessions live - * indefinitely). + * to {@code 0} to disable (sessions live indefinitely). Use + * {@link #clearSessionIdleTimeoutSeconds()} to revert to the default. *

* This option is only used when the SDK spawns the CLI process; it is ignored * when connecting to an external server via {@link #setCliUrl(String)}. * * @param sessionIdleTimeoutSeconds - * the idle timeout in seconds, or {@code null} to disable + * the idle timeout in seconds * @return this options instance for method chaining * @since 1.3.0 */ - public CopilotClientOptions setSessionIdleTimeoutSeconds(Integer sessionIdleTimeoutSeconds) { + public CopilotClientOptions setSessionIdleTimeoutSeconds(int sessionIdleTimeoutSeconds) { this.sessionIdleTimeoutSeconds = sessionIdleTimeoutSeconds; return this; } + /** + * Clears the sessionIdleTimeoutSeconds setting, reverting to the default + * behavior. + * + * @return this instance for method chaining + */ + public CopilotClientOptions clearSessionIdleTimeoutSeconds() { + this.sessionIdleTimeoutSeconds = null; + return this; + } + + /** + * Gets the connection token for the headless CLI server (TCP only). + * + * @return the connection token, or {@code null} if not set + */ + public String getTcpConnectionToken() { + return tcpConnectionToken; + } + + /** + * Sets the connection token for the headless CLI server (TCP only). + *

+ * When the SDK spawns its own CLI in TCP mode and this is omitted, a UUID is + * generated automatically so the loopback listener is safe by default. Cannot + * be combined with {@link #setUseStdio(boolean)} = {@code true}. + * + * @param tcpConnectionToken + * the connection token (must not be {@code null} or empty) + * @return this options instance for method chaining + */ + public CopilotClientOptions setTcpConnectionToken(String tcpConnectionToken) { + this.tcpConnectionToken = Objects.requireNonNull(tcpConnectionToken, "tcpConnectionToken must not be null"); + return this; + } + /** * Returns whether to use the logged-in user for authentication. * - * @return {@code true} to use logged-in user auth, {@code false} to use only - * explicit tokens, or {@code null} to use default behavior + * @return an {@link Optional} containing the boolean value, or empty if not set */ - public Boolean getUseLoggedInUser() { - return useLoggedInUser; + @JsonIgnore + public Optional getUseLoggedInUser() { + return Optional.ofNullable(useLoggedInUser); } /** @@ -479,15 +585,23 @@ public Boolean getUseLoggedInUser() { * auth. When false, only explicit tokens (gitHubToken or environment variables) * are used. Default: true (but defaults to false when gitHubToken is provided). *

- * Passing {@code null} is equivalent to passing {@link Boolean#FALSE}. * * @param useLoggedInUser - * {@code true} to use logged-in user auth, {@code false} or - * {@code null} otherwise + * {@code true} to use logged-in user auth, {@code false} otherwise * @return this options instance for method chaining */ - public CopilotClientOptions setUseLoggedInUser(Boolean useLoggedInUser) { - this.useLoggedInUser = useLoggedInUser != null ? useLoggedInUser : Boolean.FALSE; + public CopilotClientOptions setUseLoggedInUser(boolean useLoggedInUser) { + this.useLoggedInUser = useLoggedInUser; + return this; + } + + /** + * Clears the useLoggedInUser setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public CopilotClientOptions clearUseLoggedInUser() { + this.useLoggedInUser = null; return this; } @@ -533,6 +647,7 @@ public CopilotClientOptions clone() { copy.cliArgs = this.cliArgs != null ? this.cliArgs.clone() : null; copy.cliPath = this.cliPath; copy.cliUrl = this.cliUrl; + copy.copilotHome = this.copilotHome; copy.cwd = this.cwd; copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null; copy.executor = this.executor; @@ -540,7 +655,9 @@ public CopilotClientOptions clone() { copy.logLevel = this.logLevel; copy.onListModels = this.onListModels; copy.port = this.port; + copy.remote = this.remote; copy.sessionIdleTimeoutSeconds = this.sessionIdleTimeoutSeconds; + copy.tcpConnectionToken = this.tcpConnectionToken; copy.telemetry = this.telemetry; copy.useLoggedInUser = this.useLoggedInUser; copy.useStdio = this.useStdio; 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 ef8d5fda2d..d6bcc7b2b7 100644 --- a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java @@ -52,6 +52,9 @@ public final class CreateSessionRequest { @JsonProperty("provider") private ProviderConfig provider; + @JsonProperty("enableSessionTelemetry") + private Boolean enableSessionTelemetry; + @JsonProperty("requestPermission") private Boolean requestPermission; @@ -91,6 +94,9 @@ public final class CreateSessionRequest { @JsonProperty("skillDirectories") private List skillDirectories; + @JsonProperty("instructionDirectories") + private List instructionDirectories; + @JsonProperty("disabledSkills") private List disabledSkills; @@ -106,12 +112,21 @@ public final class CreateSessionRequest { @JsonProperty("requestElicitation") private Boolean requestElicitation; + @JsonProperty("requestExitPlanMode") + private Boolean requestExitPlanMode; + + @JsonProperty("requestAutoModeSwitch") + private Boolean requestAutoModeSwitch; + @JsonProperty("modelCapabilities") private ModelCapabilitiesOverride modelCapabilities; @JsonProperty("gitHubToken") private String gitHubToken; + @JsonProperty("remoteSession") + private String remoteSession; + /** Gets the model name. @return the model */ public String getModel() { return model; @@ -204,36 +219,76 @@ public void setProvider(ProviderConfig provider) { this.provider = provider; } + /** Gets enable session telemetry flag. @return the flag */ + public Boolean getEnableSessionTelemetry() { + return enableSessionTelemetry; + } + + /** + * Sets enable session telemetry flag. @param enableSessionTelemetry the flag + */ + public void setEnableSessionTelemetry(boolean enableSessionTelemetry) { + this.enableSessionTelemetry = enableSessionTelemetry; + } + + /** + * Clears the enableSessionTelemetry setting, reverting to the default behavior. + */ + public void clearEnableSessionTelemetry() { + this.enableSessionTelemetry = null; + } + /** Gets request permission flag. @return the flag */ public Boolean getRequestPermission() { return requestPermission; } /** Sets request permission flag. @param requestPermission the flag */ - public void setRequestPermission(Boolean requestPermission) { + public void setRequestPermission(boolean requestPermission) { this.requestPermission = requestPermission; } + /** + * Clears the requestPermission setting, reverting to the default behavior. + */ + public void clearRequestPermission() { + this.requestPermission = null; + } + /** Gets request user input flag. @return the flag */ public Boolean getRequestUserInput() { return requestUserInput; } /** Sets request user input flag. @param requestUserInput the flag */ - public void setRequestUserInput(Boolean requestUserInput) { + public void setRequestUserInput(boolean requestUserInput) { this.requestUserInput = requestUserInput; } + /** + * Clears the requestUserInput setting, reverting to the default behavior. + */ + public void clearRequestUserInput() { + this.requestUserInput = null; + } + /** Gets hooks flag. @return the flag */ public Boolean getHooks() { return hooks; } /** Sets hooks flag. @param hooks the flag */ - public void setHooks(Boolean hooks) { + public void setHooks(boolean hooks) { this.hooks = hooks; } + /** + * Clears the hooks setting, reverting to the default behavior. + */ + public void clearHooks() { + this.hooks = null; + } + /** Gets working directory. @return the working directory */ public String getWorkingDirectory() { return workingDirectory; @@ -250,10 +305,17 @@ public Boolean getStreaming() { } /** Sets streaming flag. @param streaming the flag */ - public void setStreaming(Boolean streaming) { + public void setStreaming(boolean streaming) { this.streaming = streaming; } + /** + * Clears the streaming setting, reverting to the default behavior. + */ + public void clearStreaming() { + this.streaming = null; + } + /** Gets MCP servers. @return the servers map */ public Map getMcpServers() { return mcpServers == null ? null : Collections.unmodifiableMap(mcpServers); @@ -326,6 +388,18 @@ public void setSkillDirectories(List skillDirectories) { this.skillDirectories = skillDirectories; } + /** Gets instruction directories. @return the instruction directories */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets instruction directories. @param instructionDirectories the directories + */ + public void setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + } + /** Gets disabled skills. @return the disabled skill names */ public List getDisabledSkills() { return disabledSkills == null ? null : Collections.unmodifiableList(disabledSkills); @@ -352,10 +426,17 @@ public Boolean getEnableConfigDiscovery() { } /** Sets enable config discovery flag. @param enableConfigDiscovery the flag */ - public void setEnableConfigDiscovery(Boolean enableConfigDiscovery) { + public void setEnableConfigDiscovery(boolean enableConfigDiscovery) { this.enableConfigDiscovery = enableConfigDiscovery; } + /** + * Clears the enableConfigDiscovery setting, reverting to the default behavior. + */ + public void clearEnableConfigDiscovery() { + this.enableConfigDiscovery = null; + } + /** Gets include sub-agent streaming events flag. @return the flag */ public Boolean getIncludeSubAgentStreamingEvents() { return includeSubAgentStreamingEvents; @@ -365,10 +446,18 @@ public Boolean getIncludeSubAgentStreamingEvents() { * Sets include sub-agent streaming events flag. @param * includeSubAgentStreamingEvents the flag */ - public void setIncludeSubAgentStreamingEvents(Boolean includeSubAgentStreamingEvents) { + public void setIncludeSubAgentStreamingEvents(boolean includeSubAgentStreamingEvents) { this.includeSubAgentStreamingEvents = includeSubAgentStreamingEvents; } + /** + * Clears the includeSubAgentStreamingEvents setting, reverting to the default + * behavior. + */ + public void clearIncludeSubAgentStreamingEvents() { + this.includeSubAgentStreamingEvents = null; + } + /** Gets the commands wire definitions. @return the commands */ public List getCommands() { return commands == null ? null : Collections.unmodifiableList(commands); @@ -385,10 +474,39 @@ public Boolean getRequestElicitation() { } /** Sets the requestElicitation flag. @param requestElicitation the flag */ - public void setRequestElicitation(Boolean requestElicitation) { + public void setRequestElicitation(boolean requestElicitation) { this.requestElicitation = requestElicitation; } + /** + * Clears the requestElicitation setting, reverting to the default behavior. + */ + public void clearRequestElicitation() { + this.requestElicitation = null; + } + + /** Gets the requestExitPlanMode flag. @return the flag */ + public Boolean getRequestExitPlanMode() { + return requestExitPlanMode; + } + + /** Sets the requestExitPlanMode flag. @param requestExitPlanMode the flag */ + public void setRequestExitPlanMode(Boolean requestExitPlanMode) { + this.requestExitPlanMode = requestExitPlanMode; + } + + /** Gets the requestAutoModeSwitch flag. @return the flag */ + public Boolean getRequestAutoModeSwitch() { + return requestAutoModeSwitch; + } + + /** + * Sets the requestAutoModeSwitch flag. @param requestAutoModeSwitch the flag + */ + public void setRequestAutoModeSwitch(Boolean requestAutoModeSwitch) { + this.requestAutoModeSwitch = requestAutoModeSwitch; + } + /** Gets the model capabilities override. @return the override */ public ModelCapabilitiesOverride getModelCapabilities() { return modelCapabilities; @@ -413,4 +531,16 @@ public String getGitHubToken() { public void setGitHubToken(String gitHubToken) { this.gitHubToken = gitHubToken; } + + /** Gets the remote session mode. @return the remote session mode */ + public String getRemoteSession() { + return remoteSession; + } + + /** + * Sets the remote session mode. @param remoteSession the remote session mode + */ + public void setRemoteSession(String remoteSession) { + this.remoteSession = remoteSession; + } } diff --git a/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java b/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java index 1421db603f..bb9520055e 100644 --- a/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java @@ -10,6 +10,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Optional; /** * Configuration for a custom agent in a Copilot session. @@ -197,10 +199,12 @@ public CustomAgentConfig setMcpServers(Map mcpServers) /** * Gets whether inference mode is enabled. * - * @return the infer flag, or {@code null} if not set + * @return an {@link java.util.Optional} containing the infer flag, or + * {@link java.util.Optional#empty()} if not set */ - public Boolean getInfer() { - return infer; + @JsonIgnore + public Optional getInfer() { + return Optional.ofNullable(infer); } /** @@ -210,11 +214,21 @@ public Boolean getInfer() { * {@code true} to enable inference mode * @return this config for method chaining */ - public CustomAgentConfig setInfer(Boolean infer) { + public CustomAgentConfig setInfer(boolean infer) { this.infer = infer; return this; } + /** + * Clears the infer setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public CustomAgentConfig clearInfer() { + this.infer = null; + return this; + } + /** * Gets the list of skill names to preload into this agent's context. * diff --git a/src/main/java/com/github/copilot/sdk/json/ExitPlanModeHandler.java b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeHandler.java new file mode 100644 index 0000000000..13ecbe075c --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeHandler.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler for exit-plan-mode requests from the agent. + *

+ * Register an exit-plan-mode handler via + * {@link SessionConfig#setOnExitPlanMode(ExitPlanModeHandler)} or + * {@link ResumeSessionConfig#setOnExitPlanMode(ExitPlanModeHandler)}. When + * provided, the server routes {@code exitPlanMode.request} callbacks to this + * handler. + * + *

Example Usage

+ * + *
{@code
+ * ExitPlanModeHandler handler = (request, invocation) -> {
+ * 	// Review the plan and decide whether to approve
+ * 	return CompletableFuture
+ * 			.completedFuture(new ExitPlanModeResult().setApproved(true).setSelectedAction("interactive"));
+ * };
+ *
+ * var session = client.createSession(new SessionConfig().setOnExitPlanMode(handler)).get();
+ * }
+ * + * @see ExitPlanModeRequest + * @see ExitPlanModeResult + * @since 1.0.8 + */ +@FunctionalInterface +public interface ExitPlanModeHandler { + + /** + * Handles an exit-plan-mode request from the agent. + * + * @param request + * the exit-plan-mode request containing the summary, plan content, + * and available actions + * @param invocation + * context information about the invocation + * @return a future that resolves with the user's decision + */ + CompletableFuture handle(ExitPlanModeRequest request, ExitPlanModeInvocation invocation); +} diff --git a/src/main/java/com/github/copilot/sdk/json/ExitPlanModeInvocation.java b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeInvocation.java new file mode 100644 index 0000000000..6fd0231266 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeInvocation.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Context for an exit-plan-mode request invocation. + * + * @since 1.0.8 + */ +public class ExitPlanModeInvocation { + + private String sessionId; + + /** + * Gets the session ID. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the session ID. + * + * @param sessionId + * the session ID + * @return this instance for method chaining + */ + public ExitPlanModeInvocation setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ExitPlanModeRequest.java b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeRequest.java new file mode 100644 index 0000000000..be09350ef3 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeRequest.java @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Request to exit plan mode and continue with a selected action. + *

+ * This is sent by the server when the agent wants to exit plan mode and + * requests user confirmation. + * + * @since 1.0.8 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ExitPlanModeRequest { + + @JsonProperty("summary") + private String summary = ""; + + @JsonProperty("planContent") + private String planContent; + + @JsonProperty("actions") + private List actions; + + @JsonProperty("recommendedAction") + private String recommendedAction = "autopilot"; + + /** + * Gets the summary of the plan or proposed next step. + * + * @return the summary + */ + public String getSummary() { + return summary; + } + + /** + * Sets the summary of the plan or proposed next step. + * + * @param summary + * the summary + * @return this instance for method chaining + */ + public ExitPlanModeRequest setSummary(String summary) { + this.summary = summary; + return this; + } + + /** + * Gets the full plan content, when available. + * + * @return the plan content, or {@code null} if not available + */ + public String getPlanContent() { + return planContent; + } + + /** + * Sets the full plan content. + * + * @param planContent + * the plan content + * @return this instance for method chaining + */ + public ExitPlanModeRequest setPlanContent(String planContent) { + this.planContent = planContent; + return this; + } + + /** + * Gets the available actions the user can select. + * + * @return the list of actions, or {@code null} if not specified + */ + public List getActions() { + return actions == null ? null : Collections.unmodifiableList(actions); + } + + /** + * Sets the available actions the user can select. + * + * @param actions + * the list of actions + * @return this instance for method chaining + */ + public ExitPlanModeRequest setActions(List actions) { + this.actions = actions; + return this; + } + + /** + * Gets the action recommended by the runtime. + * + * @return the recommended action + */ + public String getRecommendedAction() { + return recommendedAction; + } + + /** + * Sets the action recommended by the runtime. + * + * @param recommendedAction + * the recommended action + * @return this instance for method chaining + */ + public ExitPlanModeRequest setRecommendedAction(String recommendedAction) { + this.recommendedAction = recommendedAction; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ExitPlanModeResult.java b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeResult.java new file mode 100644 index 0000000000..876e750b46 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeResult.java @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response to an exit-plan-mode request. + * + * @since 1.0.8 + */ +public class ExitPlanModeResult { + + @JsonProperty("approved") + private boolean approved = true; + + @JsonProperty("selectedAction") + private String selectedAction; + + @JsonProperty("feedback") + private String feedback; + + /** + * Returns whether the user approved exiting plan mode. + * + * @return {@code true} if approved + */ + public boolean isApproved() { + return approved; + } + + /** + * Sets whether the user approved exiting plan mode. + * + * @param approved + * {@code true} if approved + * @return this instance for method chaining + */ + public ExitPlanModeResult setApproved(boolean approved) { + this.approved = approved; + return this; + } + + /** + * Gets the selected action, if the user chose one. + * + * @return the selected action, or {@code null} + */ + public String getSelectedAction() { + return selectedAction; + } + + /** + * Sets the selected action. + * + * @param selectedAction + * the selected action + * @return this instance for method chaining + */ + public ExitPlanModeResult setSelectedAction(String selectedAction) { + this.selectedAction = selectedAction; + return this; + } + + /** + * Gets optional feedback provided by the user. + * + * @return the feedback, or {@code null} + */ + public String getFeedback() { + return feedback; + } + + /** + * Sets feedback from the user. + * + * @param feedback + * the feedback text + * @return this instance for method chaining + */ + public ExitPlanModeResult setFeedback(String feedback) { + this.feedback = feedback; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/InfiniteSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/InfiniteSessionConfig.java index d45851f8a1..561796ede7 100644 --- a/src/main/java/com/github/copilot/sdk/json/InfiniteSessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/InfiniteSessionConfig.java @@ -6,6 +6,9 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Optional; +import java.util.OptionalDouble; /** * Configuration for infinite sessions with automatic context compaction and @@ -43,10 +46,12 @@ public class InfiniteSessionConfig { /** * Gets whether infinite sessions are enabled. * - * @return {@code true} if enabled, {@code null} to use default (true) + * @return an {@link Optional} containing the boolean value, or empty to use + * default (true) */ - public Boolean getEnabled() { - return enabled; + @JsonIgnore + public Optional getEnabled() { + return Optional.ofNullable(enabled); } /** @@ -58,18 +63,32 @@ public Boolean getEnabled() { * {@code true} to enable infinite sessions * @return this config instance for method chaining */ - public InfiniteSessionConfig setEnabled(Boolean enabled) { + public InfiniteSessionConfig setEnabled(boolean enabled) { this.enabled = enabled; return this; } + /** + * Clears the enabled setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public InfiniteSessionConfig clearEnabled() { + this.enabled = null; + return this; + } + /** * Gets the background compaction threshold. * - * @return the threshold (0.0-1.0), or {@code null} to use default + * @return an {@link OptionalDouble} containing the threshold (0.0-1.0), or + * empty to use default */ - public Double getBackgroundCompactionThreshold() { - return backgroundCompactionThreshold; + @JsonIgnore + public OptionalDouble getBackgroundCompactionThreshold() { + return backgroundCompactionThreshold == null + ? OptionalDouble.empty() + : OptionalDouble.of(backgroundCompactionThreshold); } /** @@ -82,18 +101,33 @@ public Double getBackgroundCompactionThreshold() { * the threshold (0.0-1.0) * @return this config instance for method chaining */ - public InfiniteSessionConfig setBackgroundCompactionThreshold(Double backgroundCompactionThreshold) { + public InfiniteSessionConfig setBackgroundCompactionThreshold(double backgroundCompactionThreshold) { this.backgroundCompactionThreshold = backgroundCompactionThreshold; return this; } + /** + * Clears the backgroundCompactionThreshold setting, reverting to the default + * behavior. + * + * @return this instance for method chaining + */ + public InfiniteSessionConfig clearBackgroundCompactionThreshold() { + this.backgroundCompactionThreshold = null; + return this; + } + /** * Gets the buffer exhaustion threshold. * - * @return the threshold (0.0-1.0), or {@code null} to use default + * @return an {@link OptionalDouble} containing the threshold (0.0-1.0), or + * empty to use default */ - public Double getBufferExhaustionThreshold() { - return bufferExhaustionThreshold; + @JsonIgnore + public OptionalDouble getBufferExhaustionThreshold() { + return bufferExhaustionThreshold == null + ? OptionalDouble.empty() + : OptionalDouble.of(bufferExhaustionThreshold); } /** @@ -107,8 +141,20 @@ public Double getBufferExhaustionThreshold() { * the threshold (0.0-1.0) * @return this config instance for method chaining */ - public InfiniteSessionConfig setBufferExhaustionThreshold(Double bufferExhaustionThreshold) { + public InfiniteSessionConfig setBufferExhaustionThreshold(double bufferExhaustionThreshold) { this.bufferExhaustionThreshold = bufferExhaustionThreshold; return this; } + + /** + * Clears the bufferExhaustionThreshold setting, reverting to the default + * behavior. + * + * @return this instance for method chaining + */ + public InfiniteSessionConfig clearBufferExhaustionThreshold() { + this.bufferExhaustionThreshold = null; + return this; + } + } diff --git a/src/main/java/com/github/copilot/sdk/json/InputOptions.java b/src/main/java/com/github/copilot/sdk/json/InputOptions.java index 9b0b6c8dd8..ca1cc5d385 100644 --- a/src/main/java/com/github/copilot/sdk/json/InputOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/InputOptions.java @@ -4,17 +4,25 @@ package com.github.copilot.sdk.json; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.OptionalInt; + /** * Options for the {@link SessionUiApi#input(String, InputOptions)} convenience * method. * * @since 1.0.0 */ +@JsonInclude(JsonInclude.Include.NON_NULL) public class InputOptions { private String title; private String description; + @JsonProperty("minLength") private Integer minLength; + @JsonProperty("maxLength") private Integer maxLength; private String format; private String defaultValue; @@ -46,34 +54,66 @@ public InputOptions setDescription(String description) { return this; } - /** Gets the minimum character length. @return the min length */ - public Integer getMinLength() { - return minLength; + /** + * Gets the minimum character length. + * + * @return an {@link java.util.OptionalInt} containing the min length, or + * {@link java.util.OptionalInt#empty()} if not set + */ + @JsonIgnore + public OptionalInt getMinLength() { + return minLength == null ? OptionalInt.empty() : OptionalInt.of(minLength); } /** * Sets the minimum character length. @param minLength the min length @return * this */ - public InputOptions setMinLength(Integer minLength) { + public InputOptions setMinLength(int minLength) { this.minLength = minLength; return this; } - /** Gets the maximum character length. @return the max length */ - public Integer getMaxLength() { - return maxLength; + /** + * Clears the minLength setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public InputOptions clearMinLength() { + this.minLength = null; + return this; + } + + /** + * Gets the maximum character length. + * + * @return an {@link java.util.OptionalInt} containing the max length, or + * {@link java.util.OptionalInt#empty()} if not set + */ + @JsonIgnore + public OptionalInt getMaxLength() { + return maxLength == null ? OptionalInt.empty() : OptionalInt.of(maxLength); } /** * Sets the maximum character length. @param maxLength the max length @return * this */ - public InputOptions setMaxLength(Integer maxLength) { + public InputOptions setMaxLength(int maxLength) { this.maxLength = maxLength; return this; } + /** + * Clears the maxLength setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public InputOptions clearMaxLength() { + this.maxLength = null; + return this; + } + /** * Gets the semantic format hint (e.g., {@code "email"}, {@code "uri"}, * {@code "date"}, {@code "date-time"}). diff --git a/src/main/java/com/github/copilot/sdk/json/ModelCapabilitiesOverride.java b/src/main/java/com/github/copilot/sdk/json/ModelCapabilitiesOverride.java index 18701ad671..02727d4b5c 100644 --- a/src/main/java/com/github/copilot/sdk/json/ModelCapabilitiesOverride.java +++ b/src/main/java/com/github/copilot/sdk/json/ModelCapabilitiesOverride.java @@ -7,6 +7,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Optional; +import java.util.OptionalInt; /** * Per-property overrides for model capabilities, deep-merged over runtime @@ -106,48 +109,73 @@ public static class Supports { /** * Gets the vision override. * - * @return {@code true} to enable vision, {@code false} to disable, or - * {@code null} to use the runtime default + * @return an {@link java.util.Optional} containing {@code true} to enable + * vision or {@code false} to disable, or + * {@link java.util.Optional#empty()} to use the runtime default */ - public Boolean getVision() { - return vision; + @JsonIgnore + public Optional getVision() { + return Optional.ofNullable(vision); } /** - * Sets whether vision (image input) is enabled. + * Sets whether vision (image input) is enabled. Use {@link #clearVision()} to + * revert to the runtime default. * * @param vision - * {@code true} to enable, {@code false} to disable, or {@code null} - * to use the runtime default + * {@code true} to enable, {@code false} to disable * @return this instance for method chaining */ - public Supports setVision(Boolean vision) { + public Supports setVision(boolean vision) { this.vision = vision; return this; } + /** + * Clears the vision setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public Supports clearVision() { + this.vision = null; + return this; + } + /** * Gets the reasoning effort override. * - * @return {@code true} to enable reasoning effort, {@code false} to disable, or - * {@code null} to use the runtime default + * @return an {@link java.util.Optional} containing {@code true} to enable + * reasoning effort or {@code false} to disable, or + * {@link java.util.Optional#empty()} to use the runtime default */ - public Boolean getReasoningEffort() { - return reasoningEffort; + @JsonIgnore + public Optional getReasoningEffort() { + return Optional.ofNullable(reasoningEffort); } /** - * Sets whether reasoning effort configuration is enabled. + * Sets whether reasoning effort configuration is enabled. Use + * {@link #clearReasoningEffort()} to revert to the runtime default. * * @param reasoningEffort - * {@code true} to enable, {@code false} to disable, or {@code null} - * to use the runtime default + * {@code true} to enable, {@code false} to disable * @return this instance for method chaining */ - public Supports setReasoningEffort(Boolean reasoningEffort) { + public Supports setReasoningEffort(boolean reasoningEffort) { this.reasoningEffort = reasoningEffort; return this; } + + /** + * Clears the reasoningEffort setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public Supports clearReasoningEffort() { + this.reasoningEffort = null; + return this; + } + } /** @@ -174,8 +202,9 @@ public static class Limits { * * @return the override value, or {@code null} to use the runtime default */ - public Integer getMaxPromptTokens() { - return maxPromptTokens; + @JsonIgnore + public OptionalInt getMaxPromptTokens() { + return maxPromptTokens == null ? OptionalInt.empty() : OptionalInt.of(maxPromptTokens); } /** @@ -185,18 +214,29 @@ public Integer getMaxPromptTokens() { * the override value, or {@code null} to use the runtime default * @return this instance for method chaining */ - public Limits setMaxPromptTokens(Integer maxPromptTokens) { + public Limits setMaxPromptTokens(int maxPromptTokens) { this.maxPromptTokens = maxPromptTokens; return this; } + /** + * Clears the maxPromptTokens setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public Limits clearMaxPromptTokens() { + this.maxPromptTokens = null; + return this; + } + /** * Gets the maximum output tokens override. * * @return the override value, or {@code null} to use the runtime default */ - public Integer getMaxOutputTokens() { - return maxOutputTokens; + @JsonIgnore + public OptionalInt getMaxOutputTokens() { + return maxOutputTokens == null ? OptionalInt.empty() : OptionalInt.of(maxOutputTokens); } /** @@ -206,18 +246,29 @@ public Integer getMaxOutputTokens() { * the override value, or {@code null} to use the runtime default * @return this instance for method chaining */ - public Limits setMaxOutputTokens(Integer maxOutputTokens) { + public Limits setMaxOutputTokens(int maxOutputTokens) { this.maxOutputTokens = maxOutputTokens; return this; } + /** + * Clears the maxOutputTokens setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public Limits clearMaxOutputTokens() { + this.maxOutputTokens = null; + return this; + } + /** * Gets the maximum context window tokens override. * * @return the override value, or {@code null} to use the runtime default */ - public Integer getMaxContextWindowTokens() { - return maxContextWindowTokens; + @JsonIgnore + public OptionalInt getMaxContextWindowTokens() { + return maxContextWindowTokens == null ? OptionalInt.empty() : OptionalInt.of(maxContextWindowTokens); } /** @@ -227,9 +278,20 @@ public Integer getMaxContextWindowTokens() { * the override value, or {@code null} to use the runtime default * @return this instance for method chaining */ - public Limits setMaxContextWindowTokens(Integer maxContextWindowTokens) { + public Limits setMaxContextWindowTokens(int maxContextWindowTokens) { this.maxContextWindowTokens = maxContextWindowTokens; return this; } + + /** + * Clears the maxContextWindowTokens setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public Limits clearMaxContextWindowTokens() { + this.maxContextWindowTokens = null; + return this; + } + } } diff --git a/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java b/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java index 3b29956814..1c5e6fcc7f 100644 --- a/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java @@ -9,6 +9,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.OptionalInt; /** * Configuration for a custom API provider (BYOK - Bring Your Own Key). @@ -57,6 +59,18 @@ public class ProviderConfig { @JsonProperty("headers") private Map headers; + @JsonProperty("modelId") + private String modelId; + + @JsonProperty("wireModel") + private String wireModel; + + @JsonProperty("maxPromptTokens") + private Integer maxPromptTokens; + + @JsonProperty("maxOutputTokens") + private Integer maxOutputTokens; + /** * Gets the provider type. * @@ -225,4 +239,134 @@ public ProviderConfig setHeaders(Map headers) { this.headers = headers; return this; } + + /** + * Gets the well-known model name used by the runtime. + *

+ * Used to look up agent configuration (tools, prompts, reasoning behavior) and + * default token limits. Also used as the wire model when + * {@link #getWireModel()} is not set. + * + * @return the model ID, or {@code null} if not set + */ + public String getModelId() { + return modelId; + } + + /** + * Sets the well-known model name used by the runtime. + *

+ * Used to look up agent configuration (tools, prompts, reasoning behavior) and + * default token limits. Also used as the wire model when + * {@link #getWireModel()} is not set. Falls back to + * {@link SessionConfig#getModel()}. + * + * @param modelId + * the model ID + * @return this config for method chaining + */ + public ProviderConfig setModelId(String modelId) { + this.modelId = modelId; + return this; + } + + /** + * Gets the model name sent to the provider API for inference. + * + * @return the wire model name, or {@code null} if not set + */ + public String getWireModel() { + return wireModel; + } + + /** + * Sets the model name sent to the provider API for inference. + *

+ * Use this when the provider's model name (e.g. an Azure deployment name or a + * custom fine-tune name) differs from {@link #getModelId()}. Falls back to + * {@link #getModelId()}, then {@link SessionConfig#getModel()}. + * + * @param wireModel + * the wire model name + * @return this config for method chaining + */ + public ProviderConfig setWireModel(String wireModel) { + this.wireModel = wireModel; + return this; + } + + /** + * Gets the maximum prompt token override. + * + * @return an {@link java.util.OptionalInt} containing the max prompt tokens, or + * {@link java.util.OptionalInt#empty()} if not set + */ + @JsonIgnore + public OptionalInt getMaxPromptTokens() { + return maxPromptTokens == null ? OptionalInt.empty() : OptionalInt.of(maxPromptTokens); + } + + /** + * Sets the maximum prompt tokens override. + *

+ * Overrides the resolved model's default max prompt tokens. The runtime + * triggers conversation compaction before sending a request when the prompt + * (system message, history, tool definitions, user message) would exceed this + * limit. + * + * @param maxPromptTokens + * the max prompt tokens + * @return this config for method chaining + */ + public ProviderConfig setMaxPromptTokens(int maxPromptTokens) { + this.maxPromptTokens = maxPromptTokens; + return this; + } + + /** + * Clears the maxPromptTokens setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public ProviderConfig clearMaxPromptTokens() { + this.maxPromptTokens = null; + return this; + } + + /** + * Gets the maximum output token override. + * + * @return an {@link java.util.OptionalInt} containing the max output tokens, or + * {@link java.util.OptionalInt#empty()} if not set + */ + @JsonIgnore + public OptionalInt getMaxOutputTokens() { + return maxOutputTokens == null ? OptionalInt.empty() : OptionalInt.of(maxOutputTokens); + } + + /** + * Sets the maximum output tokens override. + *

+ * Overrides the resolved model's default max output tokens. When hit, the model + * stops generating and returns a truncated response. + * + * @param maxOutputTokens + * the max output tokens + * @return this config for method chaining + */ + public ProviderConfig setMaxOutputTokens(int maxOutputTokens) { + this.maxOutputTokens = maxOutputTokens; + return this; + } + + /** + * Clears the maxOutputTokens setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public ProviderConfig clearMaxOutputTokens() { + this.maxOutputTokens = null; + return this; + } + } diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java index b4dacf3702..72c9f6f47a 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java @@ -11,8 +11,10 @@ import java.util.function.Consumer; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.github.copilot.sdk.generated.SessionEvent; +import java.util.Optional; /** * Configuration for resuming an existing Copilot session. @@ -43,6 +45,7 @@ public class ResumeSessionConfig { private List availableTools; private List excludedTools; private ProviderConfig provider; + private Boolean enableSessionTelemetry; private String reasoningEffort; private ModelCapabilitiesOverride modelCapabilities; private PermissionHandler onPermissionRequest; @@ -59,12 +62,16 @@ public class ResumeSessionConfig { private DefaultAgentConfig defaultAgent; private String agent; private List skillDirectories; + private List instructionDirectories; private List disabledSkills; private InfiniteSessionConfig infiniteSessions; private Consumer onEvent; private List commands; private ElicitationHandler onElicitationRequest; + private ExitPlanModeHandler onExitPlanMode; + private AutoModeSwitchHandler onAutoModeSwitch; private String gitHubToken; + private String remoteSession; /** * Gets the AI model to use. @@ -228,6 +235,50 @@ public ResumeSessionConfig setProvider(ProviderConfig provider) { return this; } + /** + * Enables or disables internal session telemetry for this session. When + * {@code false}, disables session telemetry. When unset (the default) or + * {@code true}, telemetry is enabled for GitHub-authenticated sessions. When a + * custom {@link ProviderConfig} (BYOK) is configured, session telemetry is + * always disabled regardless of this setting. This is independent of + * {@link com.github.copilot.sdk.json.CopilotClientOptions#getTelemetry() + * CopilotClientOptions.TelemetryConfig}, which configures OpenTelemetry export + * for observability. + * + * @return an {@link java.util.Optional} containing whether session telemetry is + * enabled, or {@link java.util.Optional#empty()} for the default + */ + @JsonIgnore + public Optional getEnableSessionTelemetry() { + return Optional.ofNullable(enableSessionTelemetry); + } + + /** + * Enables or disables internal session telemetry for this session. When + * {@code false}, disables session telemetry. When unset (the default) or + * {@code true}, telemetry is enabled for GitHub-authenticated sessions. When a + * custom {@link ProviderConfig} (BYOK) is configured, session telemetry is + * always disabled regardless of this setting. + * + * @param enableSessionTelemetry + * whether to enable session telemetry + * @return this config for method chaining + */ + public ResumeSessionConfig setEnableSessionTelemetry(boolean enableSessionTelemetry) { + this.enableSessionTelemetry = enableSessionTelemetry; + return this; + } + + /** + * Clears the enableSessionTelemetry setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public ResumeSessionConfig clearEnableSessionTelemetry() { + this.enableSessionTelemetry = null; + return this; + } + /** * Gets the reasoning effort level. * @@ -367,8 +418,9 @@ public ResumeSessionConfig setConfigDir(String configDir) { * @return {@code true} to enable discovery, {@code false} to disable, or * {@code null} to use the runtime default */ - public Boolean getEnableConfigDiscovery() { - return enableConfigDiscovery; + @JsonIgnore + public Optional getEnableConfigDiscovery() { + return Optional.ofNullable(enableConfigDiscovery); } /** @@ -384,19 +436,30 @@ public Boolean getEnableConfigDiscovery() { * {@code null} to use the runtime default * @return this config for method chaining */ - public ResumeSessionConfig setEnableConfigDiscovery(Boolean enableConfigDiscovery) { + public ResumeSessionConfig setEnableConfigDiscovery(boolean enableConfigDiscovery) { this.enableConfigDiscovery = enableConfigDiscovery; return this; } + /** + * Clears the enableConfigDiscovery setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public ResumeSessionConfig clearEnableConfigDiscovery() { + this.enableConfigDiscovery = null; + return this; + } + /** * Gets whether sub-agent streaming events are included. * * @return {@code true} to include sub-agent streaming events, {@code false} to * suppress them, or {@code null} to use the runtime default */ - public Boolean getIncludeSubAgentStreamingEvents() { - return includeSubAgentStreamingEvents; + @JsonIgnore + public Optional getIncludeSubAgentStreamingEvents() { + return Optional.ofNullable(includeSubAgentStreamingEvents); } /** @@ -407,11 +470,22 @@ public Boolean getIncludeSubAgentStreamingEvents() { * suppress * @return this config for method chaining */ - public ResumeSessionConfig setIncludeSubAgentStreamingEvents(Boolean includeSubAgentStreamingEvents) { + public ResumeSessionConfig setIncludeSubAgentStreamingEvents(boolean includeSubAgentStreamingEvents) { this.includeSubAgentStreamingEvents = includeSubAgentStreamingEvents; return this; } + /** + * Clears the includeSubAgentStreamingEvents setting, reverting to the default + * behavior. + * + * @return this instance for method chaining + */ + public ResumeSessionConfig clearIncludeSubAgentStreamingEvents() { + this.includeSubAgentStreamingEvents = null; + return this; + } + /** * Gets the model capabilities override. * @@ -591,6 +665,27 @@ public ResumeSessionConfig setSkillDirectories(List skillDirectories) { return this; } + /** + * Gets the additional directories to search for custom instruction files. + * + * @return the list of instruction directory paths + */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets additional directories to search for custom instruction files. + * + * @param instructionDirectories + * the list of instruction directory paths + * @return this config for method chaining + */ + public ResumeSessionConfig setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + return this; + } + /** * Gets the disabled skills. * @@ -711,6 +806,60 @@ public ResumeSessionConfig setOnElicitationRequest(ElicitationHandler onElicitat return this; } + /** + * Gets the exit-plan-mode request handler. + * + * @return the exit-plan-mode handler, or {@code null} + * @since 1.0.8 + */ + public ExitPlanModeHandler getOnExitPlanMode() { + return onExitPlanMode; + } + + /** + * Sets a handler for exit-plan-mode requests from the server. + *

+ * When provided, the server will route {@code exitPlanMode.request} callbacks + * to this handler. + * + * @param onExitPlanMode + * the exit-plan-mode handler + * @return this config for method chaining + * @see ExitPlanModeHandler + * @since 1.0.8 + */ + public ResumeSessionConfig setOnExitPlanMode(ExitPlanModeHandler onExitPlanMode) { + this.onExitPlanMode = onExitPlanMode; + return this; + } + + /** + * Gets the auto-mode-switch request handler. + * + * @return the auto-mode-switch handler, or {@code null} + * @since 1.0.8 + */ + public AutoModeSwitchHandler getOnAutoModeSwitch() { + return onAutoModeSwitch; + } + + /** + * Sets a handler for auto-mode-switch requests from the server. + *

+ * When provided, the server will route {@code autoModeSwitch.request} callbacks + * to this handler. + * + * @param onAutoModeSwitch + * the auto-mode-switch handler + * @return this config for method chaining + * @see AutoModeSwitchHandler + * @since 1.0.8 + */ + public ResumeSessionConfig setOnAutoModeSwitch(AutoModeSwitchHandler onAutoModeSwitch) { + this.onAutoModeSwitch = onAutoModeSwitch; + return this; + } + /** * Gets the GitHub token for per-session authentication. * @@ -738,6 +887,34 @@ public ResumeSessionConfig setGitHubToken(String gitHubToken) { return this; } + /** + * Gets the per-session remote behavior control. + *

+ * See {@link SessionConfig#getRemoteSession()} for details on possible values. + * + * @return the remote session mode, or {@code null} if not set + * @since 1.4.0 + */ + public String getRemoteSession() { + return remoteSession; + } + + /** + * Sets the per-session remote behavior control. + *

+ * See {@link SessionConfig#setRemoteSession(String)} for details on possible + * values. + * + * @param remoteSession + * the remote session mode + * @return this config for method chaining + * @since 1.4.0 + */ + public ResumeSessionConfig setRemoteSession(String remoteSession) { + this.remoteSession = remoteSession; + return this; + } + /** * Creates a shallow clone of this {@code ResumeSessionConfig} instance. *

@@ -759,6 +936,7 @@ public ResumeSessionConfig clone() { 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.enableSessionTelemetry = this.enableSessionTelemetry; copy.reasoningEffort = this.reasoningEffort; copy.modelCapabilities = this.modelCapabilities; copy.onPermissionRequest = this.onPermissionRequest; @@ -775,12 +953,18 @@ public ResumeSessionConfig clone() { copy.defaultAgent = this.defaultAgent; copy.agent = this.agent; copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null; + copy.instructionDirectories = this.instructionDirectories != null + ? new ArrayList<>(this.instructionDirectories) + : null; copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; copy.infiniteSessions = this.infiniteSessions; copy.onEvent = this.onEvent; copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; copy.onElicitationRequest = this.onElicitationRequest; + copy.onExitPlanMode = this.onExitPlanMode; + copy.onAutoModeSwitch = this.onAutoModeSwitch; copy.gitHubToken = this.gitHubToken; + copy.remoteSession = this.remoteSession; 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 e36e90b675..8aca77b7d2 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java @@ -53,6 +53,9 @@ public final class ResumeSessionRequest { @JsonProperty("provider") private ProviderConfig provider; + @JsonProperty("enableSessionTelemetry") + private Boolean enableSessionTelemetry; + @JsonProperty("requestPermission") private Boolean requestPermission; @@ -98,6 +101,9 @@ public final class ResumeSessionRequest { @JsonProperty("skillDirectories") private List skillDirectories; + @JsonProperty("instructionDirectories") + private List instructionDirectories; + @JsonProperty("disabledSkills") private List disabledSkills; @@ -110,12 +116,21 @@ public final class ResumeSessionRequest { @JsonProperty("requestElicitation") private Boolean requestElicitation; + @JsonProperty("requestExitPlanMode") + private Boolean requestExitPlanMode; + + @JsonProperty("requestAutoModeSwitch") + private Boolean requestAutoModeSwitch; + @JsonProperty("modelCapabilities") private ModelCapabilitiesOverride modelCapabilities; @JsonProperty("gitHubToken") private String gitHubToken; + @JsonProperty("remoteSession") + private String remoteSession; + /** Gets the session ID. @return the session ID */ public String getSessionId() { return sessionId; @@ -211,36 +226,76 @@ public void setProvider(ProviderConfig provider) { this.provider = provider; } + /** Gets enable session telemetry flag. @return the flag */ + public Boolean getEnableSessionTelemetry() { + return enableSessionTelemetry; + } + + /** + * Sets enable session telemetry flag. @param enableSessionTelemetry the flag + */ + public void setEnableSessionTelemetry(boolean enableSessionTelemetry) { + this.enableSessionTelemetry = enableSessionTelemetry; + } + + /** + * Clears the enableSessionTelemetry setting, reverting to the default behavior. + */ + public void clearEnableSessionTelemetry() { + this.enableSessionTelemetry = null; + } + /** Gets request permission flag. @return the flag */ public Boolean getRequestPermission() { return requestPermission; } /** Sets request permission flag. @param requestPermission the flag */ - public void setRequestPermission(Boolean requestPermission) { + public void setRequestPermission(boolean requestPermission) { this.requestPermission = requestPermission; } + /** + * Clears the requestPermission setting, reverting to the default behavior. + */ + public void clearRequestPermission() { + this.requestPermission = null; + } + /** Gets request user input flag. @return the flag */ public Boolean getRequestUserInput() { return requestUserInput; } /** Sets request user input flag. @param requestUserInput the flag */ - public void setRequestUserInput(Boolean requestUserInput) { + public void setRequestUserInput(boolean requestUserInput) { this.requestUserInput = requestUserInput; } + /** + * Clears the requestUserInput setting, reverting to the default behavior. + */ + public void clearRequestUserInput() { + this.requestUserInput = null; + } + /** Gets hooks flag. @return the flag */ public Boolean getHooks() { return hooks; } /** Sets hooks flag. @param hooks the flag */ - public void setHooks(Boolean hooks) { + public void setHooks(boolean hooks) { this.hooks = hooks; } + /** + * Clears the hooks setting, reverting to the default behavior. + */ + public void clearHooks() { + this.hooks = null; + } + /** Gets working directory. @return the working directory */ public String getWorkingDirectory() { return workingDirectory; @@ -267,30 +322,51 @@ public Boolean getEnableConfigDiscovery() { } /** Sets enable config discovery flag. @param enableConfigDiscovery the flag */ - public void setEnableConfigDiscovery(Boolean enableConfigDiscovery) { + public void setEnableConfigDiscovery(boolean enableConfigDiscovery) { this.enableConfigDiscovery = enableConfigDiscovery; } + /** + * Clears the enableConfigDiscovery setting, reverting to the default behavior. + */ + public void clearEnableConfigDiscovery() { + this.enableConfigDiscovery = null; + } + /** Gets disable resume flag. @return the flag */ public Boolean getDisableResume() { return disableResume; } /** Sets disable resume flag. @param disableResume the flag */ - public void setDisableResume(Boolean disableResume) { + public void setDisableResume(boolean disableResume) { this.disableResume = disableResume; } + /** + * Clears the disableResume setting, reverting to the default behavior. + */ + public void clearDisableResume() { + this.disableResume = null; + } + /** Gets streaming flag. @return the flag */ public Boolean getStreaming() { return streaming; } /** Sets streaming flag. @param streaming the flag */ - public void setStreaming(Boolean streaming) { + public void setStreaming(boolean streaming) { this.streaming = streaming; } + /** + * Clears the streaming setting, reverting to the default behavior. + */ + public void clearStreaming() { + this.streaming = null; + } + /** Gets include sub-agent streaming events flag. @return the flag */ public Boolean getIncludeSubAgentStreamingEvents() { return includeSubAgentStreamingEvents; @@ -300,10 +376,18 @@ public Boolean getIncludeSubAgentStreamingEvents() { * Sets include sub-agent streaming events flag. @param * includeSubAgentStreamingEvents the flag */ - public void setIncludeSubAgentStreamingEvents(Boolean includeSubAgentStreamingEvents) { + public void setIncludeSubAgentStreamingEvents(boolean includeSubAgentStreamingEvents) { this.includeSubAgentStreamingEvents = includeSubAgentStreamingEvents; } + /** + * Clears the includeSubAgentStreamingEvents setting, reverting to the default + * behavior. + */ + public void clearIncludeSubAgentStreamingEvents() { + this.includeSubAgentStreamingEvents = null; + } + /** Gets MCP servers. @return the servers map */ public Map getMcpServers() { return mcpServers == null ? null : Collections.unmodifiableMap(mcpServers); @@ -366,6 +450,18 @@ public void setSkillDirectories(List skillDirectories) { this.skillDirectories = skillDirectories; } + /** Gets instruction directories. @return the instruction directories */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets instruction directories. @param instructionDirectories the directories + */ + public void setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + } + /** Gets disabled skills. @return the disabled skill names */ public List getDisabledSkills() { return disabledSkills == null ? null : Collections.unmodifiableList(disabledSkills); @@ -405,10 +501,39 @@ public Boolean getRequestElicitation() { } /** Sets the requestElicitation flag. @param requestElicitation the flag */ - public void setRequestElicitation(Boolean requestElicitation) { + public void setRequestElicitation(boolean requestElicitation) { this.requestElicitation = requestElicitation; } + /** + * Clears the requestElicitation setting, reverting to the default behavior. + */ + public void clearRequestElicitation() { + this.requestElicitation = null; + } + + /** Gets the requestExitPlanMode flag. @return the flag */ + public Boolean getRequestExitPlanMode() { + return requestExitPlanMode; + } + + /** Sets the requestExitPlanMode flag. @param requestExitPlanMode the flag */ + public void setRequestExitPlanMode(Boolean requestExitPlanMode) { + this.requestExitPlanMode = requestExitPlanMode; + } + + /** Gets the requestAutoModeSwitch flag. @return the flag */ + public Boolean getRequestAutoModeSwitch() { + return requestAutoModeSwitch; + } + + /** + * Sets the requestAutoModeSwitch flag. @param requestAutoModeSwitch the flag + */ + public void setRequestAutoModeSwitch(Boolean requestAutoModeSwitch) { + this.requestAutoModeSwitch = requestAutoModeSwitch; + } + /** Gets the model capabilities override. @return the override */ public ModelCapabilitiesOverride getModelCapabilities() { return modelCapabilities; @@ -433,4 +558,16 @@ public String getGitHubToken() { public void setGitHubToken(String gitHubToken) { this.gitHubToken = gitHubToken; } + + /** Gets the remote session mode. @return the remote session mode */ + public String getRemoteSession() { + return remoteSession; + } + + /** + * Sets the remote session mode. @param remoteSession the remote session mode + */ + public void setRemoteSession(String remoteSession) { + this.remoteSession = remoteSession; + } } 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 09661346a2..f1a383402f 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java @@ -11,8 +11,10 @@ import java.util.function.Consumer; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.github.copilot.sdk.generated.SessionEvent; +import java.util.Optional; /** * Configuration for creating a new Copilot session. @@ -45,6 +47,7 @@ public class SessionConfig { private List availableTools; private List excludedTools; private ProviderConfig provider; + private Boolean enableSessionTelemetry; private PermissionHandler onPermissionRequest; private UserInputHandler onUserInputRequest; private SessionHooks hooks; @@ -57,6 +60,7 @@ public class SessionConfig { private String agent; private InfiniteSessionConfig infiniteSessions; private List skillDirectories; + private List instructionDirectories; private List disabledSkills; private String configDir; private Boolean enableConfigDiscovery; @@ -64,7 +68,10 @@ public class SessionConfig { private Consumer onEvent; private List commands; private ElicitationHandler onElicitationRequest; + private ExitPlanModeHandler onExitPlanMode; + private AutoModeSwitchHandler onAutoModeSwitch; private String gitHubToken; + private String remoteSession; /** * Gets the custom session ID. @@ -282,6 +289,50 @@ public SessionConfig setProvider(ProviderConfig provider) { return this; } + /** + * Enables or disables internal session telemetry for this session. When + * {@code false}, disables session telemetry. When unset (the default) or + * {@code true}, telemetry is enabled for GitHub-authenticated sessions. When a + * custom {@link ProviderConfig} (BYOK) is configured, session telemetry is + * always disabled regardless of this setting. This is independent of + * {@link com.github.copilot.sdk.json.CopilotClientOptions#getTelemetry() + * CopilotClientOptions.TelemetryConfig}, which configures OpenTelemetry export + * for observability. + * + * @return an {@link java.util.Optional} containing whether session telemetry is + * enabled, or {@link java.util.Optional#empty()} for the default + */ + @JsonIgnore + public Optional getEnableSessionTelemetry() { + return Optional.ofNullable(enableSessionTelemetry); + } + + /** + * Enables or disables internal session telemetry for this session. When + * {@code false}, disables session telemetry. When unset (the default) or + * {@code true}, telemetry is enabled for GitHub-authenticated sessions. When a + * custom {@link ProviderConfig} (BYOK) is configured, session telemetry is + * always disabled regardless of this setting. + * + * @param enableSessionTelemetry + * whether to enable session telemetry + * @return this config instance for method chaining + */ + public SessionConfig setEnableSessionTelemetry(boolean enableSessionTelemetry) { + this.enableSessionTelemetry = enableSessionTelemetry; + return this; + } + + /** + * Clears the enableSessionTelemetry setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public SessionConfig clearEnableSessionTelemetry() { + this.enableSessionTelemetry = null; + return this; + } + /** * Gets the permission request handler. * @@ -550,6 +601,27 @@ public SessionConfig setSkillDirectories(List skillDirectories) { return this; } + /** + * Gets the additional directories to search for custom instruction files. + * + * @return the list of instruction directory paths + */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets additional directories to search for custom instruction files. + * + * @param instructionDirectories + * the list of instruction directory paths + * @return this config instance for method chaining + */ + public SessionConfig setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + return this; + } + /** * Gets the disabled skill names. * @@ -601,11 +673,13 @@ public SessionConfig setConfigDir(String configDir) { /** * Gets whether automatic configuration discovery is enabled. * - * @return {@code true} to enable discovery, {@code false} to disable, or - * {@code null} to use the runtime default + * @return an {@link java.util.Optional} containing {@code true} to enable + * discovery or {@code false} to disable, or + * {@link java.util.Optional#empty()} to use the runtime default */ - public Boolean getEnableConfigDiscovery() { - return enableConfigDiscovery; + @JsonIgnore + public Optional getEnableConfigDiscovery() { + return Optional.ofNullable(enableConfigDiscovery); } /** @@ -619,23 +693,34 @@ public Boolean getEnableConfigDiscovery() { * name collision. * * @param enableConfigDiscovery - * {@code true} to enable discovery, {@code false} to disable, or - * {@code null} to use the runtime default + * {@code true} to enable discovery, {@code false} to disable * @return this config instance for method chaining */ - public SessionConfig setEnableConfigDiscovery(Boolean enableConfigDiscovery) { + public SessionConfig setEnableConfigDiscovery(boolean enableConfigDiscovery) { this.enableConfigDiscovery = enableConfigDiscovery; return this; } + /** + * Clears the enableConfigDiscovery setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public SessionConfig clearEnableConfigDiscovery() { + this.enableConfigDiscovery = null; + return this; + } + /** * Gets whether sub-agent streaming events are included. * - * @return {@code true} to include sub-agent streaming events, {@code false} to - * suppress them, or {@code null} to use the runtime default + * @return an {@link java.util.Optional} containing {@code true} to include + * sub-agent streaming events or {@code false} to suppress them, or + * {@link java.util.Optional#empty()} to use the runtime default */ - public Boolean getIncludeSubAgentStreamingEvents() { - return includeSubAgentStreamingEvents; + @JsonIgnore + public Optional getIncludeSubAgentStreamingEvents() { + return Optional.ofNullable(includeSubAgentStreamingEvents); } /** @@ -652,11 +737,22 @@ public Boolean getIncludeSubAgentStreamingEvents() { * suppress * @return this config instance for method chaining */ - public SessionConfig setIncludeSubAgentStreamingEvents(Boolean includeSubAgentStreamingEvents) { + public SessionConfig setIncludeSubAgentStreamingEvents(boolean includeSubAgentStreamingEvents) { this.includeSubAgentStreamingEvents = includeSubAgentStreamingEvents; return this; } + /** + * Clears the includeSubAgentStreamingEvents setting, reverting to the default + * behavior. + * + * @return this instance for method chaining + */ + public SessionConfig clearIncludeSubAgentStreamingEvents() { + this.includeSubAgentStreamingEvents = null; + return this; + } + /** * Gets the model capabilities override. * @@ -763,6 +859,60 @@ public SessionConfig setOnElicitationRequest(ElicitationHandler onElicitationReq return this; } + /** + * Gets the exit-plan-mode request handler. + * + * @return the exit-plan-mode handler, or {@code null} + * @since 1.0.8 + */ + public ExitPlanModeHandler getOnExitPlanMode() { + return onExitPlanMode; + } + + /** + * Sets a handler for exit-plan-mode requests from the server. + *

+ * When provided, the server will route {@code exitPlanMode.request} callbacks + * to this handler. + * + * @param onExitPlanMode + * the exit-plan-mode handler + * @return this config instance for method chaining + * @see ExitPlanModeHandler + * @since 1.0.8 + */ + public SessionConfig setOnExitPlanMode(ExitPlanModeHandler onExitPlanMode) { + this.onExitPlanMode = onExitPlanMode; + return this; + } + + /** + * Gets the auto-mode-switch request handler. + * + * @return the auto-mode-switch handler, or {@code null} + * @since 1.0.8 + */ + public AutoModeSwitchHandler getOnAutoModeSwitch() { + return onAutoModeSwitch; + } + + /** + * Sets a handler for auto-mode-switch requests from the server. + *

+ * When provided, the server will route {@code autoModeSwitch.request} callbacks + * to this handler. + * + * @param onAutoModeSwitch + * the auto-mode-switch handler + * @return this config instance for method chaining + * @see AutoModeSwitchHandler + * @since 1.0.8 + */ + public SessionConfig setOnAutoModeSwitch(AutoModeSwitchHandler onAutoModeSwitch) { + this.onAutoModeSwitch = onAutoModeSwitch; + return this; + } + /** * Gets the GitHub token for per-session authentication. * @@ -790,6 +940,45 @@ public SessionConfig setGitHubToken(String gitHubToken) { return this; } + /** + * Gets the per-session remote behavior control. + *

+ * Possible values: + *

    + *
  • {@code "off"} β€” local only, no remote export (default)
  • + *
  • {@code "export"} β€” export session events to GitHub without enabling + * remote steering
  • + *
  • {@code "on"} β€” export to GitHub AND enable remote steering
  • + *
+ * + * @return the remote session mode, or {@code null} if not set + * @since 1.4.0 + */ + public String getRemoteSession() { + return remoteSession; + } + + /** + * Sets the per-session remote behavior control. + *

+ * Possible values: + *

    + *
  • {@code "off"} β€” local only, no remote export (default)
  • + *
  • {@code "export"} β€” export session events to GitHub without enabling + * remote steering
  • + *
  • {@code "on"} β€” export to GitHub AND enable remote steering
  • + *
+ * + * @param remoteSession + * the remote session mode + * @return this config instance for method chaining + * @since 1.4.0 + */ + public SessionConfig setRemoteSession(String remoteSession) { + this.remoteSession = remoteSession; + return this; + } + /** * Creates a shallow clone of this {@code SessionConfig} instance. *

@@ -813,6 +1002,7 @@ public SessionConfig clone() { 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.enableSessionTelemetry = this.enableSessionTelemetry; copy.onPermissionRequest = this.onPermissionRequest; copy.onUserInputRequest = this.onUserInputRequest; copy.hooks = this.hooks; @@ -825,6 +1015,9 @@ public SessionConfig clone() { copy.agent = this.agent; copy.infiniteSessions = this.infiniteSessions; copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null; + copy.instructionDirectories = this.instructionDirectories != null + ? new ArrayList<>(this.instructionDirectories) + : null; copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; copy.configDir = this.configDir; copy.enableConfigDiscovery = this.enableConfigDiscovery; @@ -832,7 +1025,10 @@ public SessionConfig clone() { copy.onEvent = this.onEvent; copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; copy.onElicitationRequest = this.onElicitationRequest; + copy.onExitPlanMode = this.onExitPlanMode; + copy.onAutoModeSwitch = this.onAutoModeSwitch; copy.gitHubToken = this.gitHubToken; + copy.remoteSession = this.remoteSession; return copy; } } diff --git a/src/main/java/com/github/copilot/sdk/json/SessionUiCapabilities.java b/src/main/java/com/github/copilot/sdk/json/SessionUiCapabilities.java index 9b8e0b5872..d19d531eef 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionUiCapabilities.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionUiCapabilities.java @@ -4,23 +4,31 @@ package com.github.copilot.sdk.json; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + /** * UI-specific capability flags for a session. * * @since 1.0.0 */ +@JsonInclude(JsonInclude.Include.NON_NULL) public class SessionUiCapabilities { + @JsonProperty("elicitation") private Boolean elicitation; /** * Returns whether the host supports interactive elicitation dialogs. * - * @return {@code true} if elicitation is supported, {@code false} or - * {@code null} otherwise + * @return an {@link Optional} containing the boolean value, or empty if not set */ - public Boolean getElicitation() { - return elicitation; + @JsonIgnore + public Optional getElicitation() { + return Optional.ofNullable(elicitation); } /** @@ -30,8 +38,19 @@ public Boolean getElicitation() { * {@code true} if elicitation is supported * @return this instance for method chaining */ - public SessionUiCapabilities setElicitation(Boolean elicitation) { + public SessionUiCapabilities setElicitation(boolean elicitation) { this.elicitation = elicitation; return this; } + + /** + * Clears the elicitation setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public SessionUiCapabilities clearElicitation() { + this.elicitation = null; + return this; + } + } diff --git a/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionRequest.java new file mode 100644 index 0000000000..d3944871a0 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionRequest.java @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Request body for session.setForeground RPC call. + *

+ * Using an explicit record type (rather than an ad-hoc map) ensures correct + * JSON serialization in all execution environments. + * + * @since 1.0.0 + */ +public record SetForegroundSessionRequest( + /** The session ID to bring to the foreground. */ + @JsonProperty("sessionId") String sessionId) { +} diff --git a/src/main/java/com/github/copilot/sdk/json/TelemetryConfig.java b/src/main/java/com/github/copilot/sdk/json/TelemetryConfig.java index 8407ab6098..7272c98841 100644 --- a/src/main/java/com/github/copilot/sdk/json/TelemetryConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/TelemetryConfig.java @@ -4,6 +4,11 @@ package com.github.copilot.sdk.json; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + /** * OpenTelemetry configuration for the Copilot CLI server. *

@@ -21,6 +26,7 @@ * @see CopilotClientOptions#setTelemetry(TelemetryConfig) * @since 1.2.0 */ +@JsonInclude(JsonInclude.Include.NON_NULL) public class TelemetryConfig { private String otlpEndpoint; @@ -128,11 +134,13 @@ public TelemetryConfig setSourceName(String sourceName) { * Maps to the {@code OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT} * environment variable. * - * @return {@code true} to capture content, {@code false} to suppress it, or - * {@code null} to use the default + * @return an {@link java.util.Optional} containing {@code true} to capture + * content or {@code false} to suppress it, or + * {@link java.util.Optional#empty()} to use the default */ - public Boolean getCaptureContent() { - return captureContent; + @JsonIgnore + public Optional getCaptureContent() { + return Optional.ofNullable(captureContent); } /** @@ -142,8 +150,19 @@ public Boolean getCaptureContent() { * {@code true} to capture content, {@code false} to suppress it * @return this config for method chaining */ - public TelemetryConfig setCaptureContent(Boolean captureContent) { + public TelemetryConfig setCaptureContent(boolean captureContent) { this.captureContent = captureContent; return this; } + + /** + * Clears the captureContent setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public TelemetryConfig clearCaptureContent() { + this.captureContent = null; + return this; + } + } diff --git a/src/main/java/com/github/copilot/sdk/json/UserInputRequest.java b/src/main/java/com/github/copilot/sdk/json/UserInputRequest.java index 9b466f866c..23b0d88123 100644 --- a/src/main/java/com/github/copilot/sdk/json/UserInputRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/UserInputRequest.java @@ -8,7 +8,10 @@ import java.util.List; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Optional; /** * Request for user input from the agent. @@ -19,6 +22,7 @@ * @since 1.0.6 */ @JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) public class UserInputRequest { @JsonProperty("question") @@ -75,11 +79,13 @@ public UserInputRequest setChoices(List choices) { /** * Returns whether freeform text input is allowed. * - * @return {@code true} if freeform input is allowed, {@code null} if not + * @return an {@link java.util.Optional} containing {@code true} if freeform + * input is allowed, or {@link java.util.Optional#empty()} if not * specified */ - public Boolean getAllowFreeform() { - return allowFreeform; + @JsonIgnore + public Optional getAllowFreeform() { + return Optional.ofNullable(allowFreeform); } /** @@ -89,8 +95,19 @@ public Boolean getAllowFreeform() { * {@code true} to allow freeform input * @return this instance for method chaining */ - public UserInputRequest setAllowFreeform(Boolean allowFreeform) { + public UserInputRequest setAllowFreeform(boolean allowFreeform) { this.allowFreeform = allowFreeform; return this; } + + /** + * Clears the allowFreeform setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public UserInputRequest clearAllowFreeform() { + this.allowFreeform = null; + return this; + } + } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000000..d912fb420f --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** + * GitHub Copilot SDK for Java. + */ +module com.github.copilot.sdk.java { + requires transitive com.fasterxml.jackson.annotation; + requires com.fasterxml.jackson.core; + requires transitive com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.datatype.jsr310; + requires static com.github.spotbugs.annotations; + requires static java.compiler; + requires static java.net.http; + requires java.logging; + + exports com.github.copilot.sdk; + exports com.github.copilot.sdk.generated; + exports com.github.copilot.sdk.generated.rpc; + exports com.github.copilot.sdk.json; + + opens com.github.copilot.sdk to com.fasterxml.jackson.databind; + opens com.github.copilot.sdk.generated to com.fasterxml.jackson.databind; + opens com.github.copilot.sdk.json to com.fasterxml.jackson.databind; +} diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index a4c4d830ad..9b5d00c5ac 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -53,6 +53,9 @@ This guide covers advanced scenarios for extending and customizing your Copilot - [Incoming Elicitation Handler](#Incoming_Elicitation_Handler) - [Session Capabilities](#Session_Capabilities) - [Outgoing Elicitation via session.getUi()](#Outgoing_Elicitation_via_session.getUi) +- [Mode Handlers](#Mode_Handlers) + - [Exit Plan Mode](#Exit_Plan_Mode) + - [Auto Mode Switch](#Auto_Mode_Switch) - [Getting Session Metadata by ID](#Getting_Session_Metadata_by_ID) --- @@ -383,6 +386,29 @@ 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. +### Model Overrides + +Use `modelId` and `wireModel` to control model resolution and the model name on the wire: + +```java +var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setProvider(new ProviderConfig() + .setType("openai") + .setBaseUrl("https://api.openai.com/v1") + .setApiKey("sk-...") + .setModelId("gpt-4o") // Runtime config lookup + .setWireModel("my-finetune-v3") // Sent to the provider API + .setMaxPromptTokens(100_000) // Override max prompt tokens + .setMaxOutputTokens(4096)) // Override max output tokens +).get(); +``` + +- **`modelId`** β€” Well-known model name used by the runtime to look up agent configuration (tools, prompts, reasoning behavior) and default token limits. Also used as the wire model when `wireModel` is not set. +- **`wireModel`** β€” Model name sent to the provider API for inference. Use when the provider's model name (e.g., an Azure deployment name or a custom fine-tune name) differs from `modelId`. +- **`maxPromptTokens`** β€” Overrides the resolved model's default max prompt tokens. The runtime triggers conversation compaction when the prompt would exceed this limit. +- **`maxOutputTokens`** β€” Overrides the resolved model's default max output tokens. + ### 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: @@ -626,6 +652,31 @@ var session = client.createSession( --- +## Instruction Directories + +Provide additional directories containing custom instruction files. These instructions are automatically included in the system message for all conversations in the session. + +```java +var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setInstructionDirectories(List.of("/path/to/instructions")) +).get(); +``` + +Instruction files are discovered from `.github/instructions/` subdirectories within each specified path and should use the `.instructions.md` extension. + +This is also supported on session resume: + +```java +var session = client.resumeSession(sessionId, + new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setInstructionDirectories(List.of("/path/to/instructions")) +); +``` + +--- + ## Custom Configuration Directory Use a custom configuration directory for session settings: @@ -1219,6 +1270,66 @@ All `getUi()` methods throw `IllegalStateException` if the host does not support --- +## Mode Handlers + +Mode handlers let your application respond to mode transitions requested by the Copilot CLI. + +### Exit Plan Mode + +When the model finishes creating a plan and wants to transition out of plan mode, it invokes the `exitPlanMode` handler. Register the handler via `SessionConfig.setOnExitPlanMode()`: + +```java +var session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnExitPlanMode((request, invocation) -> { + System.out.println("Plan summary: " + request.getSummary()); + System.out.println("Available actions: " + request.getActions()); + System.out.println("Recommended: " + request.getRecommendedAction()); + + return CompletableFuture.completedFuture( + new ExitPlanModeResult() + .setApproved(true) + .setSelectedAction("interactive") + .setFeedback("Looks good, proceed!")); + })).get(); +``` + +When no handler is registered, the SDK automatically approves the plan (`approved=true`). The handler receives an `ExitPlanModeRequest` with: + +| Field | Description | +|---------------------|-------------------------------------------------| +| `summary` | Summary of the plan that was created | +| `planContent` | Full content of the plan file | +| `actions` | Available actions (e.g., interactive, autopilot) | +| `recommendedAction` | The recommended action for the user | + +### Auto Mode Switch + +When the model encounters a rate limit or similar constraint, it may request to switch modes automatically. Register the handler via `SessionConfig.setOnAutoModeSwitch()`: + +```java +var session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnAutoModeSwitch((request, invocation) -> { + System.out.println("Error: " + request.getErrorCode()); + System.out.println("Retry after: " + request.getRetryAfterSeconds() + "s"); + + return CompletableFuture.completedFuture(AutoModeSwitchResponse.YES); + })).get(); +``` + +When no handler is registered, the SDK returns `NO` (declining the mode switch). The response options are: + +| Response | Description | +|---------------------------------|------------------------------------------| +| `AutoModeSwitchResponse.YES` | Allow the mode switch this time | +| `AutoModeSwitchResponse.YES_ALWAYS`| Always allow automatic mode switches | +| `AutoModeSwitchResponse.NO` | Decline the mode switch | + +Both handlers are also available on `ResumeSessionConfig` for resumed sessions. + +--- + ## Getting Session Metadata by ID Retrieve metadata for a specific session without listing all sessions: @@ -1237,6 +1348,39 @@ This is more efficient than `listSessions()` when you already know the session I --- +## Remote Sessions + +Remote sessions enable Mission Control integration, making sessions accessible from GitHub web and mobile. When enabled, sessions in a GitHub repository working directory receive a remote URL. + +### Enabling Remote Sessions + +Set `remote(true)` on the client options to enable remote session support for all sessions: + +```java +var options = new CopilotClientOptions() + .setRemote(true) + .setCwd("/path/to/github-repo"); + +try (var client = new CopilotClient(options)) { + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + ).get(); + + // Listen for the remote URL info event + session.on(SessionInfoEvent.class, event -> { + System.out.println("Remote URL: " + event.getData()); + }); +} +``` + +### Prerequisites + +- The user must be authenticated (GitHub token or logged-in user) +- The session's working directory must be a GitHub repository +- This option is only used when the SDK spawns the CLI process; it is ignored when connecting to an external server via `setCliUrl()` + +--- + ## Next Steps - πŸ“– **[Documentation](documentation.html)** - Core concepts, events, streaming, models, tool filtering, reasoning effort diff --git a/src/site/markdown/cookbook/error-handling.md b/src/site/markdown/cookbook/error-handling.md index 4fffd2ba0d..963b8b0933 100644 --- a/src/site/markdown/cookbook/error-handling.md +++ b/src/site/markdown/cookbook/error-handling.md @@ -30,7 +30,7 @@ jbang BasicErrorHandling.java **Code:** ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; @@ -64,7 +64,7 @@ public class BasicErrorHandling { ## Handling specific error types ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import com.github.copilot.sdk.CopilotClient; import java.util.concurrent.ExecutionException; @@ -99,7 +99,7 @@ public class SpecificErrorHandling { ## Timeout handling ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import com.github.copilot.sdk.CopilotSession; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; @@ -130,7 +130,7 @@ public class TimeoutHandling { ## Aborting a request ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import com.github.copilot.sdk.CopilotSession; import com.github.copilot.sdk.json.MessageOptions; import java.util.concurrent.Executors; @@ -162,7 +162,7 @@ public class AbortRequest { ## Graceful shutdown ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import com.github.copilot.sdk.CopilotClient; public class GracefulShutdown { @@ -192,7 +192,7 @@ public class GracefulShutdown { ## Try-with-resources pattern ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; @@ -224,7 +224,7 @@ public class TryWithResources { ## Handling tool errors ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; diff --git a/src/site/markdown/cookbook/managing-local-files.md b/src/site/markdown/cookbook/managing-local-files.md index 723d9b1059..2a7b5540f9 100644 --- a/src/site/markdown/cookbook/managing-local-files.md +++ b/src/site/markdown/cookbook/managing-local-files.md @@ -34,7 +34,7 @@ jbang ManagingLocalFiles.java **Code:** ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.generated.SessionIdleEvent; @@ -161,7 +161,7 @@ session.send(new MessageOptions().setPrompt(prompt)); ## Interactive file organization ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import java.io.BufferedReader; import java.io.InputStreamReader; diff --git a/src/site/markdown/cookbook/multiple-sessions.md b/src/site/markdown/cookbook/multiple-sessions.md index c7ac909f18..84da5a2556 100644 --- a/src/site/markdown/cookbook/multiple-sessions.md +++ b/src/site/markdown/cookbook/multiple-sessions.md @@ -30,7 +30,7 @@ jbang MultipleSessions.java **Code:** ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; @@ -123,7 +123,7 @@ try { ## Managing session lifecycle with CompletableFuture ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import java.util.concurrent.CompletableFuture; import java.util.List; diff --git a/src/site/markdown/cookbook/persisting-sessions.md b/src/site/markdown/cookbook/persisting-sessions.md index de50001fcf..ef80fd3d09 100644 --- a/src/site/markdown/cookbook/persisting-sessions.md +++ b/src/site/markdown/cookbook/persisting-sessions.md @@ -30,7 +30,7 @@ jbang PersistingSessions.java **Code:** ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; @@ -127,7 +127,7 @@ public class DeleteSession { ## Getting session history ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.generated.UserMessageEvent; @@ -162,7 +162,7 @@ public class SessionHistory { ## Complete example with session management ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import java.util.Scanner; public class SessionManager { diff --git a/src/site/markdown/cookbook/pr-visualization.md b/src/site/markdown/cookbook/pr-visualization.md index 40d7cde768..1036bb0f7d 100644 --- a/src/site/markdown/cookbook/pr-visualization.md +++ b/src/site/markdown/cookbook/pr-visualization.md @@ -34,7 +34,7 @@ jbang PRVisualization.java github/copilot-sdk ## Full example: PRVisualization.java ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.generated.ToolExecutionStartEvent; diff --git a/src/site/markdown/setup.md b/src/site/markdown/setup.md index 10c1cb3d80..224fc1948c 100644 --- a/src/site/markdown/setup.md +++ b/src/site/markdown/setup.md @@ -157,12 +157,28 @@ try (var client = new CopilotClient(options)) { Multiple application instances can share a single CLI server: ```java -// In different parts of your application or different containers -var client1 = new CopilotClient(new CopilotClientOptions().setCliUrl("cli-server:4321")); -var client2 = new CopilotClient(new CopilotClientOptions().setCliUrl("cli-server:4321")); +// Use an explicit connection token so all clients can authenticate +var token = "my-shared-secret"; +var client1 = new CopilotClient(new CopilotClientOptions() + .setCliUrl("cli-server:4321").setTcpConnectionToken(token)); +var client2 = new CopilotClient(new CopilotClientOptions() + .setCliUrl("cli-server:4321").setTcpConnectionToken(token)); // Both connect to the same CLI server ``` +### Connection Token (TCP Security) + +When the SDK spawns the CLI in TCP mode, a random connection token is generated automatically +to protect the loopback listener. You can also provide an explicit token: + +```java +var options = new CopilotClientOptions() + .setUseStdio(false) + .setTcpConnectionToken("my-secret-token"); +``` + +> **Note:** `tcpConnectionToken` cannot be used with `useStdio = true`. + ### Deployment Patterns **Container deployment:** @@ -340,10 +356,12 @@ 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` | +| `copilotHome` | String | Base directory for Copilot data | `null` (~/.copilot) | | `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) | +| `tcpConnectionToken` | String | Connection token for TCP mode | `null` (auto-generated) | | `autoStart` | boolean | Auto-start server | `true` | | `autoRestart` | boolean | Auto-restart on crash | `true` | | `logLevel` | String | CLI log level | `"info"` | diff --git a/src/test/java/com/github/copilot/sdk/CapiProxy.java b/src/test/java/com/github/copilot/sdk/CapiProxy.java index 30f8434363..09c4e20161 100644 --- a/src/test/java/com/github/copilot/sdk/CapiProxy.java +++ b/src/test/java/com/github/copilot/sdk/CapiProxy.java @@ -56,10 +56,12 @@ public class CapiProxy implements AutoCloseable { private static final ObjectMapper MAPPER = new ObjectMapper(); - private static final Pattern LISTENING_PATTERN = Pattern.compile("Listening: (http://[^\\s]+)"); + private static final Pattern LISTENING_PATTERN = Pattern.compile("Listening: (http://[^\\s]+)(?:\\s+(\\{.*\\}))?$"); private Process process; private String proxyUrl; + private String connectProxyUrl; + private String caFilePath; private final HttpClient httpClient; private BufferedReader stdoutReader; @@ -96,6 +98,10 @@ public String start() throws IOException, InterruptedException { : new ProcessBuilder("npx", "tsx", "server.ts"); pb.directory(harnessDir.toFile()); pb.redirectErrorStream(false); + // Tell the replaying proxy to fail fast on unmatched requests rather than + // forwarding them to the real API. Without this, unmatched requests hit the + // live API with a fake token and crash the proxy's JSON parser. + pb.environment().put("GITHUB_ACTIONS", "true"); process = pb.start(); @@ -137,7 +143,24 @@ public String start() throws IOException, InterruptedException { throw new IOException("Unexpected proxy output: " + line); } - proxyUrl = matcher.group(1); + String url = matcher.group(1); + + // Parse optional metadata (CONNECT proxy details) + String metadata = matcher.group(2); + if (metadata != null && !metadata.isEmpty()) { + try { + Map meta = MAPPER.readValue(metadata, new TypeReference>() { + }); + connectProxyUrl = meta.get("connectProxyUrl"); + caFilePath = meta.get("caFilePath"); + } catch (Exception e) { + process.destroyForcibly(); + throw new IOException("Failed to parse proxy startup metadata: " + metadata, e); + } + } + + // Only set proxyUrl after all parsing succeeds to avoid inconsistent state + proxyUrl = url; return proxyUrl; } @@ -329,6 +352,8 @@ public void stop(boolean skipWritingCache) throws IOException, InterruptedExcept process = null; proxyUrl = null; + connectProxyUrl = null; + caFilePath = null; } /** @@ -340,6 +365,24 @@ public String getProxyUrl() { return proxyUrl; } + /** + * Gets the CONNECT proxy URL for HTTPS interception. + * + * @return the CONNECT proxy URL, or null if not available + */ + public String getConnectProxyUrl() { + return connectProxyUrl; + } + + /** + * Gets the CA file path for trusting the CONNECT proxy's certificate. + * + * @return the CA file path, or null if not available + */ + public String getCaFilePath() { + return caFilePath; + } + /** * Checks if the proxy process is still alive and responsive. This does both a * process alive check AND an HTTP health check. diff --git a/src/test/java/com/github/copilot/sdk/CompactionTest.java b/src/test/java/com/github/copilot/sdk/CompactionTest.java index da24dabd15..306eeb6c72 100644 --- a/src/test/java/com/github/copilot/sdk/CompactionTest.java +++ b/src/test/java/com/github/copilot/sdk/CompactionTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -52,10 +53,17 @@ static void teardown() throws Exception { /** * Verifies that compaction is triggered with low threshold and emits events. * + *

+ * Disabled due to flakiness β€” compaction timing is non-deterministic and the + * snapshot cannot reliably match across platforms. The reference implementation + * (nodejs) also skips this test. See copilot-sdk#1227. + * * @see Snapshot: * compaction/should_trigger_compaction_with_low_threshold_and_emit_events */ @Test + @Disabled("Flaky: compaction timing varies by platform β€” see https://github.com/github/copilot-sdk/issues/1227") @Timeout(value = 300, unit = TimeUnit.SECONDS) void testShouldTriggerCompactionWithLowThresholdAndEmitEvents() throws Exception { ctx.configureForTest("compaction", "should_trigger_compaction_with_low_threshold_and_emit_events"); diff --git a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java index f4f702f9ee..09bd3ee385 100644 --- a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java +++ b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java @@ -16,8 +16,10 @@ import org.junit.jupiter.api.Test; import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.json.AutoModeSwitchResponse; import com.github.copilot.sdk.json.CopilotClientOptions; import com.github.copilot.sdk.json.DefaultAgentConfig; +import com.github.copilot.sdk.json.ExitPlanModeResult; import com.github.copilot.sdk.json.InfiniteSessionConfig; import com.github.copilot.sdk.json.MessageOptions; import com.github.copilot.sdk.json.ModelInfo; @@ -36,6 +38,11 @@ void copilotClientOptionsCloneBasic() { original.setPort(9000); original.setGitHubToken("ghp_test"); original.setUseLoggedInUser(false); + original.setCopilotHome("/custom/copilot/home"); + original.setRemote(true); + original.setSessionIdleTimeoutSeconds(600); + original.setUseStdio(false); + original.setTcpConnectionToken("my-token-123"); CopilotClientOptions cloned = original.clone(); @@ -44,6 +51,10 @@ void copilotClientOptionsCloneBasic() { assertEquals(original.getPort(), cloned.getPort()); assertEquals(original.getGitHubToken(), cloned.getGitHubToken()); assertEquals(original.getUseLoggedInUser(), cloned.getUseLoggedInUser()); + assertEquals(original.getCopilotHome(), cloned.getCopilotHome()); + assertEquals(original.isRemote(), cloned.isRemote()); + assertEquals(original.getSessionIdleTimeoutSeconds(), cloned.getSessionIdleTimeoutSeconds()); + assertEquals(original.getTcpConnectionToken(), cloned.getTcpConnectionToken()); } @Test @@ -120,6 +131,7 @@ void sessionConfigListIndependence() { toolList.add("grep"); toolList.add("bash"); original.setAvailableTools(toolList); + original.setInstructionDirectories(new ArrayList<>(List.of("/path/a", "/path/b"))); SessionConfig cloned = original.clone(); @@ -129,6 +141,7 @@ void sessionConfigListIndependence() { // The cloned config should be unaffected by mutations to the original list assertEquals(2, cloned.getAvailableTools().size()); assertEquals(3, original.getAvailableTools().size()); + assertEquals(List.of("/path/a", "/path/b"), cloned.getInstructionDirectories()); } @Test @@ -183,6 +196,44 @@ void messageOptionsCloneBasic() { assertEquals(original.getMode(), cloned.getMode()); } + @Test + void sessionConfigEnableSessionTelemetryCopied() { + SessionConfig original = new SessionConfig(); + original.setEnableSessionTelemetry(false); + + SessionConfig cloned = original.clone(); + + assertFalse(cloned.getEnableSessionTelemetry().orElse(true)); + } + + @Test + void sessionConfigEnableSessionTelemetryDefaultIsNull() { + SessionConfig original = new SessionConfig(); + + SessionConfig cloned = original.clone(); + + assertTrue(cloned.getEnableSessionTelemetry().isEmpty()); + } + + @Test + void resumeSessionConfigEnableSessionTelemetryCopied() { + ResumeSessionConfig original = new ResumeSessionConfig(); + original.setEnableSessionTelemetry(false); + + ResumeSessionConfig cloned = original.clone(); + + assertFalse(cloned.getEnableSessionTelemetry().orElse(true)); + } + + @Test + void resumeSessionConfigEnableSessionTelemetryDefaultIsNull() { + ResumeSessionConfig original = new ResumeSessionConfig(); + + ResumeSessionConfig cloned = original.clone(); + + assertTrue(cloned.getEnableSessionTelemetry().isEmpty()); + } + @Test void clonePreservesNullFields() { CopilotClientOptions opts = new CopilotClientOptions(); @@ -250,11 +301,11 @@ void copilotClientOptionsSetTelemetry() { } @Test - void copilotClientOptionsSetUseLoggedInUserNull() { + void copilotClientOptionsClearUseLoggedInUser() { var opts = new CopilotClientOptions(); - opts.setUseLoggedInUser(null); - // null β†’ Boolean.FALSE - assertEquals(Boolean.FALSE, opts.getUseLoggedInUser()); + opts.setUseLoggedInUser(true); + opts.clearUseLoggedInUser(); + assertTrue(opts.getUseLoggedInUser().isEmpty()); } @Test @@ -324,6 +375,34 @@ void copilotClientOptionsSessionIdleTimeoutCloned() { CopilotClientOptions cloned = original.clone(); - assertEquals(600, cloned.getSessionIdleTimeoutSeconds()); + assertEquals(600, cloned.getSessionIdleTimeoutSeconds().getAsInt()); + } + + @Test + void sessionConfigCloneCopiesModeSwitchHandlers() { + SessionConfig original = new SessionConfig(); + original.setOnExitPlanMode( + (request, invocation) -> CompletableFuture.completedFuture(new ExitPlanModeResult())); + original.setOnAutoModeSwitch( + (request, invocation) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + SessionConfig cloned = original.clone(); + + assertSame(original.getOnExitPlanMode(), cloned.getOnExitPlanMode()); + assertSame(original.getOnAutoModeSwitch(), cloned.getOnAutoModeSwitch()); + } + + @Test + void resumeSessionConfigCloneCopiesModeSwitchHandlers() { + ResumeSessionConfig original = new ResumeSessionConfig(); + original.setOnExitPlanMode( + (request, invocation) -> CompletableFuture.completedFuture(new ExitPlanModeResult())); + original.setOnAutoModeSwitch( + (request, invocation) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + ResumeSessionConfig cloned = original.clone(); + + assertSame(original.getOnExitPlanMode(), cloned.getOnExitPlanMode()); + assertSame(original.getOnAutoModeSwitch(), cloned.getOnAutoModeSwitch()); } } diff --git a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java index 1cc067587e..14ed8ca89e 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -20,6 +20,7 @@ import java.util.concurrent.ExecutionException; import static org.junit.jupiter.api.Assertions.*; +import java.util.Optional; /** * Tests for CopilotClient. @@ -153,14 +154,14 @@ void testGitHubTokenOptionAccepted() { void testUseLoggedInUserDefaultsToNull() { var options = new CopilotClientOptions().setCliPath("/path/to/cli"); - assertNull(options.getUseLoggedInUser()); + assertTrue(options.getUseLoggedInUser().isEmpty()); } @Test void testExplicitUseLoggedInUserFalse() { var options = new CopilotClientOptions().setCliPath("/path/to/cli").setUseLoggedInUser(false); - assertEquals(false, options.getUseLoggedInUser()); + assertEquals(Optional.of(false), options.getUseLoggedInUser()); } @Test @@ -168,7 +169,7 @@ void testExplicitUseLoggedInUserTrueWithGitHubToken() { var options = new CopilotClientOptions().setCliPath("/path/to/cli").setGitHubToken("gho_test_token") .setUseLoggedInUser(true); - assertEquals(true, options.getUseLoggedInUser()); + assertEquals(Optional.of(true), options.getUseLoggedInUser()); } @Test @@ -189,14 +190,38 @@ void testUseLoggedInUserWithCliUrlThrows() { void testSessionIdleTimeoutSecondsDefaultsToNull() { var options = new CopilotClientOptions(); - assertNull(options.getSessionIdleTimeoutSeconds()); + assertTrue(options.getSessionIdleTimeoutSeconds().isEmpty()); } @Test void testSessionIdleTimeoutSecondsOptionAccepted() { var options = new CopilotClientOptions().setSessionIdleTimeoutSeconds(600); - assertEquals(600, options.getSessionIdleTimeoutSeconds()); + assertEquals(600, options.getSessionIdleTimeoutSeconds().getAsInt()); + } + + @Test + void testTcpConnectionTokenWithUseStdioThrows() { + var options = new CopilotClientOptions().setUseStdio(true).setTcpConnectionToken("my-token"); + + assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); + } + + @Test + void testTcpConnectionTokenAcceptedInTcpMode() { + var options = new CopilotClientOptions().setUseStdio(false).setTcpConnectionToken("my-token"); + + // Should not throw + try (var client = new CopilotClient(options)) { + assertNotNull(client); + } + } + + @Test + void testCopilotHomeOptionSetOnOptions() { + var options = new CopilotClientOptions().setCopilotHome("/custom/home"); + + assertEquals("/custom/home", options.getCopilotHome()); } // ===== onLifecycle tests ===== diff --git a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java index 8a78c0e4f7..fc44880cf8 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import com.github.copilot.sdk.generated.SessionEvent; @@ -306,6 +307,7 @@ void testShouldResumeSessionUsingTheSameClient() throws Exception { * @see Snapshot: session/should_resume_a_session_using_a_new_client */ @Test + @Tag("isolated-resume") void testShouldResumeSessionUsingNewClient() throws Exception { ctx.configureForTest("session", "should_resume_a_session_using_a_new_client"); diff --git a/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java b/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java index 203f5faed6..2ff3600200 100644 --- a/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java +++ b/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java @@ -11,6 +11,9 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.copilot.sdk.json.GetForegroundSessionResponse; +import com.github.copilot.sdk.json.McpHttpServerConfig; +import com.github.copilot.sdk.json.McpStdioServerConfig; +import com.github.copilot.sdk.json.ModelCapabilitiesOverride; import com.github.copilot.sdk.json.PermissionRequest; import com.github.copilot.sdk.json.PermissionRequestResult; import com.github.copilot.sdk.json.PostToolUseHookInput; @@ -18,6 +21,7 @@ import com.github.copilot.sdk.json.PreToolUseHookInput; import com.github.copilot.sdk.json.PreToolUseHookOutput; import com.github.copilot.sdk.json.SectionOverride; +import com.github.copilot.sdk.json.SetForegroundSessionRequest; import com.github.copilot.sdk.json.SetForegroundSessionResponse; import com.github.copilot.sdk.json.ToolBinaryResult; import com.github.copilot.sdk.json.ToolResultObject; @@ -86,6 +90,14 @@ void getForegroundSessionResponseRecord() { assertEquals("/home/user/project", response.workspacePath()); } + // ===== SetForegroundSessionRequest record ===== + + @Test + void setForegroundSessionRequestRecord() { + var request = new SetForegroundSessionRequest("session-123"); + assertEquals("session-123", request.sessionId()); + } + // ===== SetForegroundSessionResponse record ===== @Test @@ -169,4 +181,52 @@ void permissionRequestResultSetRules() { assertEquals(2, result.getRules().size()); assertEquals("bash:read", result.getRules().get(0)); } + + @Test + void mcpHttpServerConfigCoversGettersAndFluentSetters() { + var headers = java.util.Map.of("Authorization", "Bearer token"); + var tools = java.util.List.of("*", "search"); + + var cfg = new McpHttpServerConfig().setUrl("https://mcp.example.com/sse").setHeaders(headers).setTools(tools) + .setTimeout(45); + + assertEquals("http", cfg.getType()); + assertEquals("https://mcp.example.com/sse", cfg.getUrl()); + assertEquals("Bearer token", cfg.getHeaders().get("Authorization")); + assertEquals(tools, cfg.getTools()); + assertEquals(45, cfg.getTimeout()); + } + + @Test + void mcpStdioServerConfigCoversGettersAndFluentSetters() { + var args = java.util.List.of("-y", "@modelcontextprotocol/server-filesystem"); + var env = java.util.Map.of("DEBUG", "1"); + var tools = java.util.List.of("*"); + + var cfg = new McpStdioServerConfig().setCommand("npx").setArgs(args).setEnv(env).setWorkingDirectory("/tmp") + .setTools(tools).setTimeout(30); + + assertEquals("stdio", cfg.getType()); + assertEquals("npx", cfg.getCommand()); + assertEquals(args, cfg.getArgs()); + assertEquals("1", cfg.getEnv().get("DEBUG")); + assertEquals("/tmp", cfg.getWorkingDirectory()); + assertEquals(tools, cfg.getTools()); + assertEquals(30, cfg.getTimeout()); + } + + @Test + void modelCapabilitiesOverrideCoversNestedSupportsAndLimits() { + var supports = new ModelCapabilitiesOverride.Supports().setVision(true).setReasoningEffort(false); + var limits = new ModelCapabilitiesOverride.Limits().setMaxPromptTokens(2048).setMaxOutputTokens(512) + .setMaxContextWindowTokens(8192); + + var override = new ModelCapabilitiesOverride().setSupports(supports).setLimits(limits); + + assertTrue(override.getSupports().getVision().orElse(false)); + assertFalse(override.getSupports().getReasoningEffort().orElse(true)); + assertEquals(2048, override.getLimits().getMaxPromptTokens().getAsInt()); + assertEquals(512, override.getLimits().getMaxOutputTokens().getAsInt()); + assertEquals(8192, override.getLimits().getMaxContextWindowTokens().getAsInt()); + } } diff --git a/src/test/java/com/github/copilot/sdk/E2ETestContext.java b/src/test/java/com/github/copilot/sdk/E2ETestContext.java index 45fcbc0a5d..9680148ff6 100644 --- a/src/test/java/com/github/copilot/sdk/E2ETestContext.java +++ b/src/test/java/com/github/copilot/sdk/E2ETestContext.java @@ -249,8 +249,33 @@ public List> getExchanges() throws IOException, InterruptedE public Map getEnvironment() { Map env = new HashMap<>(System.getenv()); env.put("COPILOT_API_URL", proxyUrl); + env.put("COPILOT_HOME", homeDir.toString()); + env.put("GH_CONFIG_DIR", homeDir.toString()); env.put("XDG_CONFIG_HOME", homeDir.toString()); env.put("XDG_STATE_HOME", homeDir.toString()); + + // Configure CONNECT proxy for HTTPS interception if available + String connectUrl = proxy.getConnectProxyUrl(); + String caFile = proxy.getCaFilePath(); + if (connectUrl != null && !connectUrl.isEmpty() && caFile != null && !caFile.isEmpty()) { + String noProxy = "127.0.0.1,localhost,::1"; + env.put("HTTP_PROXY", connectUrl); + env.put("HTTPS_PROXY", connectUrl); + env.put("http_proxy", connectUrl); + env.put("https_proxy", connectUrl); + env.put("NO_PROXY", noProxy); + env.put("no_proxy", noProxy); + env.put("NODE_EXTRA_CA_CERTS", caFile); + env.put("SSL_CERT_FILE", caFile); + env.put("REQUESTS_CA_BUNDLE", caFile); + env.put("CURL_CA_BUNDLE", caFile); + env.put("GIT_SSL_CAINFO", caFile); + env.put("GH_TOKEN", "fake-token-for-e2e-tests"); + env.put("GITHUB_TOKEN", "fake-token-for-e2e-tests"); + env.put("GH_ENTERPRISE_TOKEN", ""); + env.put("GITHUB_ENTERPRISE_TOKEN", ""); + } + return env; } @@ -261,13 +286,7 @@ public Map getEnvironment() { */ public CopilotClient createClient() { CopilotClientOptions options = new CopilotClientOptions().setCliPath(cliPath).setCwd(workDir.toString()) - .setEnvironment(getEnvironment()); - - // 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"); - } + .setEnvironment(getEnvironment()).setGitHubToken("fake-token-for-e2e-tests"); return new CopilotClient(options); } @@ -291,10 +310,7 @@ public CopilotClient createClient(CopilotClientOptions options) { if (options.getEnvironment() == null || options.getEnvironment().isEmpty()) { options.setEnvironment(getEnvironment()); } - - // In CI (GitHub Actions), use a fake token to avoid auth issues - String ci = System.getenv("GITHUB_ACTIONS"); - if (ci != null && !ci.isEmpty() && options.getGitHubToken() == null) { + if (options.getGitHubToken() == null) { options.setGitHubToken("fake-token-for-e2e-tests"); } diff --git a/src/test/java/com/github/copilot/sdk/ElicitationTest.java b/src/test/java/com/github/copilot/sdk/ElicitationTest.java index 6b748b4cc2..1f62451276 100644 --- a/src/test/java/com/github/copilot/sdk/ElicitationTest.java +++ b/src/test/java/com/github/copilot/sdk/ElicitationTest.java @@ -40,7 +40,7 @@ void sessionCapabilitiesTypesAreProperlyStructured() { var capabilities = new SessionCapabilities().setUi(new SessionUiCapabilities().setElicitation(true)); assertNotNull(capabilities.getUi()); - assertTrue(capabilities.getUi().getElicitation()); + assertTrue(capabilities.getUi().getElicitation().get()); // Test with null UI var emptyCapabilities = new SessionCapabilities(); @@ -119,8 +119,8 @@ void inputOptionsHasAllFields() { assertEquals("My Title", opts.getTitle()); assertEquals("My Desc", opts.getDescription()); - assertEquals(1, opts.getMinLength()); - assertEquals(100, opts.getMaxLength()); + assertEquals(1, opts.getMinLength().getAsInt()); + assertEquals(100, opts.getMaxLength().getAsInt()); assertEquals("email", opts.getFormat()); assertEquals("default@example.com", opts.getDefaultValue()); } @@ -164,7 +164,7 @@ void buildCreateRequestSetsRequestElicitationWhenHandlerPresent() { var request = SessionRequestBuilder.buildCreateRequest(config); - assertTrue(Boolean.TRUE.equals(request.getRequestElicitation())); + assertEquals(Boolean.TRUE, request.getRequestElicitation()); } @Test @@ -186,6 +186,6 @@ void buildResumeRequestSetsRequestElicitationWhenHandlerPresent() { var request = SessionRequestBuilder.buildResumeRequest("session-1", config); - assertTrue(Boolean.TRUE.equals(request.getRequestElicitation())); + assertEquals(Boolean.TRUE, request.getRequestElicitation()); } } diff --git a/src/test/java/com/github/copilot/sdk/EventFidelityTest.java b/src/test/java/com/github/copilot/sdk/EventFidelityTest.java new file mode 100644 index 0000000000..60b8e13275 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/EventFidelityTest.java @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- + * 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 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.generated.AssistantUsageEvent; +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.generated.SessionUsageInfoEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * E2E tests for event fidelity β€” verifying the shape, ordering, and presence of + * key events emitted from the runtime. + * + *

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

+ */ +public class EventFidelityTest { + + 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 an {@code assistant.usage} event is emitted after the model + * processes a prompt. + * + * @see Snapshot: + * event_fidelity/should_emit_assistant_usage_event_after_model_call + */ + @Test + void testShouldEmitAssistantUsageEventAfterModelCall() throws Exception { + ctx.configureForTest("event_fidelity", "should_emit_assistant_usage_event_after_model_call"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt("What is 5+5? Reply with just the number.")).get(60, + TimeUnit.SECONDS); + + List usageEvents = events.stream().filter(e -> e instanceof AssistantUsageEvent) + .map(e -> (AssistantUsageEvent) e).toList(); + + assertFalse(usageEvents.isEmpty(), "Should have received an assistant.usage event after model call"); + + AssistantUsageEvent lastUsage = usageEvents.get(usageEvents.size() - 1); + assertNotNull(lastUsage.getData().model(), "Usage event should have a model field"); + assertFalse(lastUsage.getData().model().isEmpty(), "Model field should not be empty"); + + session.close(); + } + } + + /** + * Verifies that a {@code session.usage_info} event is emitted after the model + * processes a prompt. + * + * @see Snapshot: + * event_fidelity/should_emit_session_usage_info_event_after_model_call + */ + @Test + void testShouldEmitSessionUsageInfoEventAfterModelCall() throws Exception { + ctx.configureForTest("event_fidelity", "should_emit_session_usage_info_event_after_model_call"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt("What is 5+5? Reply with just the number.")).get(60, + TimeUnit.SECONDS); + + List usageInfoEvents = events.stream() + .filter(e -> e instanceof SessionUsageInfoEvent).map(e -> (SessionUsageInfoEvent) e).toList(); + + assertFalse(usageInfoEvents.isEmpty(), "Should have received a session.usage_info event after model call"); + + session.close(); + } + } +} diff --git a/src/test/java/com/github/copilot/sdk/ExecutorWiringTest.java b/src/test/java/com/github/copilot/sdk/ExecutorWiringTest.java index f836be89a3..a5eb3a62dc 100644 --- a/src/test/java/com/github/copilot/sdk/ExecutorWiringTest.java +++ b/src/test/java/com/github/copilot/sdk/ExecutorWiringTest.java @@ -87,12 +87,8 @@ int getTaskCount() { private CopilotClientOptions createOptionsWithExecutor(TrackingExecutor executor) { CopilotClientOptions options = new CopilotClientOptions().setCliPath(ctx.getCliPath()) - .setCwd(ctx.getWorkDir().toString()).setEnvironment(ctx.getEnvironment()).setExecutor(executor); - - String ci = System.getenv("GITHUB_ACTIONS"); - if (ci != null && !ci.isEmpty()) { - options.setGitHubToken("fake-token-for-e2e-tests"); - } + .setCwd(ctx.getWorkDir().toString()).setEnvironment(ctx.getEnvironment()).setExecutor(executor) + .setGitHubToken("fake-token-for-e2e-tests"); return options; } diff --git a/src/test/java/com/github/copilot/sdk/JsonIncludeNonNullTest.java b/src/test/java/com/github/copilot/sdk/JsonIncludeNonNullTest.java new file mode 100644 index 0000000000..7465507f50 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/JsonIncludeNonNullTest.java @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.CustomAgentConfig; +import com.github.copilot.sdk.json.InfiniteSessionConfig; +import com.github.copilot.sdk.json.InputOptions; +import com.github.copilot.sdk.json.ModelCapabilitiesOverride; +import com.github.copilot.sdk.json.ProviderConfig; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SessionUiCapabilities; +import com.github.copilot.sdk.json.TelemetryConfig; +import com.github.copilot.sdk.json.UserInputRequest; + +/** + * Verifies that public DTO classes in the {@code com.github.copilot.sdk.json} + * package are annotated with {@code @JsonInclude(JsonInclude.Include.NON_NULL)} + * so that null-valued fields are omitted during JSON serialization. + */ +class JsonIncludeNonNullTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + // --- Annotation presence checks --- + + @Test + void copilotClientOptionsHasNonNullAnnotation() { + assertHasNonNullInclude(CopilotClientOptions.class); + } + + @Test + void sessionConfigHasNonNullAnnotation() { + assertHasNonNullInclude(SessionConfig.class); + } + + @Test + void resumeSessionConfigHasNonNullAnnotation() { + assertHasNonNullInclude(ResumeSessionConfig.class); + } + + @Test + void infiniteSessionConfigHasNonNullAnnotation() { + assertHasNonNullInclude(InfiniteSessionConfig.class); + } + + @Test + void inputOptionsHasNonNullAnnotation() { + assertHasNonNullInclude(InputOptions.class); + } + + @Test + void modelCapabilitiesOverrideHasNonNullAnnotation() { + assertHasNonNullInclude(ModelCapabilitiesOverride.class); + } + + @Test + void providerConfigHasNonNullAnnotation() { + assertHasNonNullInclude(ProviderConfig.class); + } + + @Test + void telemetryConfigHasNonNullAnnotation() { + assertHasNonNullInclude(TelemetryConfig.class); + } + + @Test + void sessionUiCapabilitiesHasNonNullAnnotation() { + assertHasNonNullInclude(SessionUiCapabilities.class); + } + + @Test + void customAgentConfigHasNonNullAnnotation() { + assertHasNonNullInclude(CustomAgentConfig.class); + } + + @Test + void userInputRequestHasNonNullAnnotation() { + assertHasNonNullInclude(UserInputRequest.class); + } + + // --- Serialization tests: null fields are omitted --- + + @Test + void inputOptionsOmitsNullFieldsInJson() throws JsonProcessingException { + var opts = new InputOptions(); + String json = MAPPER.writeValueAsString(opts); + assertEquals("{}", json, "All-null InputOptions should serialize to empty JSON"); + } + + @Test + void telemetryConfigOmitsNullFieldsInJson() throws JsonProcessingException { + var config = new TelemetryConfig(); + String json = MAPPER.writeValueAsString(config); + assertEquals("{}", json, "All-null TelemetryConfig should serialize to empty JSON"); + } + + @Test + void sessionUiCapabilitiesOmitsNullFieldsInJson() throws JsonProcessingException { + var caps = new SessionUiCapabilities(); + String json = MAPPER.writeValueAsString(caps); + assertEquals("{}", json, "All-null SessionUiCapabilities should serialize to empty JSON"); + } + + @Test + void userInputRequestOmitsNullFieldsInJson() throws JsonProcessingException { + var req = new UserInputRequest(); + String json = MAPPER.writeValueAsString(req); + assertFalse(json.contains("null"), "UserInputRequest with no fields set should not contain 'null' values"); + } + + @Test + void inputOptionsIncludesSetFieldsInJson() throws JsonProcessingException { + var opts = new InputOptions(); + opts.setMinLength(5); + opts.setMaxLength(100); + String json = MAPPER.writeValueAsString(opts); + assertTrue(json.contains("\"minLength\":5"), "Set minLength should appear in JSON"); + assertTrue(json.contains("\"maxLength\":100"), "Set maxLength should appear in JSON"); + assertFalse(json.contains("\"title\""), "Unset title should be omitted from JSON"); + } + + @Test + void telemetryConfigIncludesSetFieldsInJson() throws JsonProcessingException { + var config = new TelemetryConfig(); + config.setOtlpEndpoint("http://localhost:4318"); + String json = MAPPER.writeValueAsString(config); + assertTrue(json.contains("\"otlpEndpoint\":\"http://localhost:4318\""), + "Set otlpEndpoint should appear in JSON"); + assertFalse(json.contains("\"filePath\""), "Unset filePath should be omitted from JSON"); + } + + @Test + void sessionUiCapabilitiesIncludesSetFieldsInJson() throws JsonProcessingException { + var caps = new SessionUiCapabilities(); + caps.setElicitation(true); + String json = MAPPER.writeValueAsString(caps); + assertTrue(json.contains("\"elicitation\":true"), "Set elicitation should appear in JSON"); + } + + private void assertHasNonNullInclude(Class clazz) { + JsonInclude annotation = clazz.getAnnotation(JsonInclude.class); + assertNotNull(annotation, clazz.getSimpleName() + " should be annotated with @JsonInclude"); + assertEquals(JsonInclude.Include.NON_NULL, annotation.value(), + clazz.getSimpleName() + " @JsonInclude should use Include.NON_NULL"); + } + +} diff --git a/src/test/java/com/github/copilot/sdk/ModeHandlersTest.java b/src/test/java/com/github/copilot/sdk/ModeHandlersTest.java new file mode 100644 index 0000000000..0c84b05736 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/ModeHandlersTest.java @@ -0,0 +1,150 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.ExitPlanModeCompletedEvent; +import com.github.copilot.sdk.generated.ExitPlanModeRequestedEvent; +import com.github.copilot.sdk.json.AutoModeSwitchRequest; +import com.github.copilot.sdk.json.AutoModeSwitchResponse; +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.ExitPlanModeRequest; +import com.github.copilot.sdk.json.ExitPlanModeResult; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * E2E tests for exit-plan-mode and auto-mode-switch handler APIs. + * + *

+ * Ported from {@code ModeHandlersE2ETests.cs} in the reference implementation + * dotnet SDK. + *

+ */ +public class ModeHandlersTest { + + private static final String TOKEN = "mode-handler-token"; + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + private CopilotClient createAuthenticatedClient() { + Map env = new HashMap<>(ctx.getEnvironment()); + env.put("COPILOT_DEBUG_GITHUB_API_URL", ctx.getProxyUrl()); + + return ctx.createClient(new CopilotClientOptions().setEnvironment(env)); + } + + private void configureAuthenticatedUser(String testName) throws Exception { + ctx.configureForTest("mode_handlers", testName); + ctx.setCopilotUserByToken(TOKEN, "mode-handler-user", "individual_pro", ctx.getProxyUrl(), + "https://localhost:1/telemetry", "mode-handler-tracking-id"); + } + + @Test + void shouldInvokeExitPlanModeHandlerWhenModelUsesTool() throws Exception { + final String summary = "Greeting file implementation plan"; + configureAuthenticatedUser("should_invoke_exit_plan_mode_handler_when_model_uses_tool"); + + var handlerCalled = new CompletableFuture(); + + try (var client = createAuthenticatedClient()) { + var session = client.createSession(new SessionConfig().setGitHubToken(TOKEN) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setOnExitPlanMode((request, invocation) -> { + handlerCalled.complete(request); + return CompletableFuture.completedFuture(new ExitPlanModeResult().setApproved(true) + .setSelectedAction("interactive").setFeedback("Approved by the Java E2E test")); + })).get(30, TimeUnit.SECONDS); + + var requestedEvent = new CompletableFuture(); + var completedEvent = new CompletableFuture(); + + session.on(event -> { + if (event instanceof ExitPlanModeRequestedEvent requested + && summary.equals(requested.getData().summary())) { + requestedEvent.complete(requested); + } else if (event instanceof ExitPlanModeCompletedEvent completed + && Boolean.TRUE.equals(completed.getData().approved()) + && "interactive".equals(completed.getData().selectedAction())) { + completedEvent.complete(completed); + } + }); + + var response = session.sendAndWait(new MessageOptions().setPrompt( + "Create a brief implementation plan for adding a greeting.txt file, then request approval with exit_plan_mode.") + .setMode("plan")).get(120, TimeUnit.SECONDS); + + var request = handlerCalled.get(10, TimeUnit.SECONDS); + assertEquals(summary, request.getSummary()); + assertNotNull(request.getActions()); + assertTrue(request.getActions().contains("interactive")); + assertNotNull(request.getPlanContent()); + + var reqEvent = requestedEvent.get(10, TimeUnit.SECONDS); + assertEquals(request.getSummary(), reqEvent.getData().summary()); + + var compEvent = completedEvent.get(10, TimeUnit.SECONDS); + assertTrue(compEvent.getData().approved()); + assertEquals("interactive", compEvent.getData().selectedAction()); + + assertNotNull(response); + + session.close(); + } + } + + @Test + void shouldInvokeAutoModeSwitchHandlerWhenRateLimited() throws Exception { + configureAuthenticatedUser("should_invoke_auto_mode_switch_handler_when_rate_limited"); + + var handlerCalled = new CompletableFuture(); + + try (var client = createAuthenticatedClient()) { + var session = client.createSession( + new SessionConfig().setGitHubToken(TOKEN).setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnAutoModeSwitch((request, invocation) -> { + handlerCalled.complete(request); + return CompletableFuture.completedFuture(AutoModeSwitchResponse.YES); + })) + .get(30, TimeUnit.SECONDS); + + var messageId = session + .send(new MessageOptions() + .setPrompt("Explain that auto mode recovered from a rate limit in one short sentence.")) + .get(30, TimeUnit.SECONDS); + + assertNotNull(messageId); + assertFalse(messageId.isEmpty()); + + var request = handlerCalled.get(30, TimeUnit.SECONDS); + assertEquals("user_weekly_rate_limited", request.getErrorCode()); + assertEquals(1.0, request.getRetryAfterSeconds()); + + session.close(); + } + } +} diff --git a/src/test/java/com/github/copilot/sdk/ModuleDescriptorTest.java b/src/test/java/com/github/copilot/sdk/ModuleDescriptorTest.java new file mode 100644 index 0000000000..36be137345 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/ModuleDescriptorTest.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.module.ModuleDescriptor; +import org.junit.jupiter.api.Test; + +class ModuleDescriptorTest { + + @Test + void sdkHasExplicitModuleDescriptor() { + Module module = CopilotClient.class.getModule(); + assertTrue(module.isNamed()); + assertEquals("com.github.copilot.sdk.java", module.getName()); + + ModuleDescriptor descriptor = module.getDescriptor(); + assertTrue(descriptor.exports().stream().anyMatch(export -> export.source().equals("com.github.copilot.sdk"))); + assertTrue(descriptor.exports().stream() + .anyMatch(export -> export.source().equals("com.github.copilot.sdk.json"))); + assertTrue(descriptor.requires().stream() + .anyMatch(require -> require.name().equals("com.fasterxml.jackson.databind"))); + } +} diff --git a/src/test/java/com/github/copilot/sdk/OptionalApiAndJacksonTest.java b/src/test/java/com/github/copilot/sdk/OptionalApiAndJacksonTest.java new file mode 100644 index 0000000000..2a9770e2e2 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/OptionalApiAndJacksonTest.java @@ -0,0 +1,635 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.CustomAgentConfig; +import com.github.copilot.sdk.json.InfiniteSessionConfig; +import com.github.copilot.sdk.json.InputOptions; +import com.github.copilot.sdk.json.ModelCapabilitiesOverride; +import com.github.copilot.sdk.json.ProviderConfig; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SessionUiCapabilities; +import com.github.copilot.sdk.json.TelemetryConfig; +import com.github.copilot.sdk.json.UserInputRequest; +import org.junit.jupiter.api.Test; + +/** + * Validates that every {@code clearXxx()} method resets its field to absent, + * that Optional-returning getters report the correct state, and that Jackson + * omits cleared fields from serialized output. + */ +class OptionalApiAndJacksonTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + // ── CopilotClientOptions ────────────────────────────────────────── + + @Test + void copilotClientOptions_clearSessionIdleTimeoutSeconds() { + var opts = new CopilotClientOptions(); + opts.setSessionIdleTimeoutSeconds(120); + assertFalse(opts.getSessionIdleTimeoutSeconds().isEmpty()); + + opts.clearSessionIdleTimeoutSeconds(); + assertTrue(opts.getSessionIdleTimeoutSeconds().isEmpty()); + } + + @Test + void copilotClientOptions_clearUseLoggedInUser() { + var opts = new CopilotClientOptions(); + opts.setUseLoggedInUser(true); + assertTrue(opts.getUseLoggedInUser().isPresent()); + + opts.clearUseLoggedInUser(); + assertTrue(opts.getUseLoggedInUser().isEmpty()); + } + + // ── SessionConfig ───────────────────────────────────────────────── + + @Test + void sessionConfig_clearEnableSessionTelemetry() { + var cfg = new SessionConfig(); + cfg.setEnableSessionTelemetry(true); + assertTrue(cfg.getEnableSessionTelemetry().isPresent()); + + cfg.clearEnableSessionTelemetry(); + assertTrue(cfg.getEnableSessionTelemetry().isEmpty()); + } + + @Test + void sessionConfig_clearEnableConfigDiscovery() { + var cfg = new SessionConfig(); + cfg.setEnableConfigDiscovery(false); + assertTrue(cfg.getEnableConfigDiscovery().isPresent()); + + cfg.clearEnableConfigDiscovery(); + assertTrue(cfg.getEnableConfigDiscovery().isEmpty()); + } + + @Test + void sessionConfig_clearIncludeSubAgentStreamingEvents() { + var cfg = new SessionConfig(); + cfg.setIncludeSubAgentStreamingEvents(true); + assertTrue(cfg.getIncludeSubAgentStreamingEvents().isPresent()); + + cfg.clearIncludeSubAgentStreamingEvents(); + assertTrue(cfg.getIncludeSubAgentStreamingEvents().isEmpty()); + } + + // ── ResumeSessionConfig ─────────────────────────────────────────── + + @Test + void resumeSessionConfig_clearEnableSessionTelemetry() { + var cfg = new ResumeSessionConfig(); + cfg.setEnableSessionTelemetry(false); + assertTrue(cfg.getEnableSessionTelemetry().isPresent()); + + cfg.clearEnableSessionTelemetry(); + assertTrue(cfg.getEnableSessionTelemetry().isEmpty()); + } + + @Test + void resumeSessionConfig_clearEnableConfigDiscovery() { + var cfg = new ResumeSessionConfig(); + cfg.setEnableConfigDiscovery(true); + assertTrue(cfg.getEnableConfigDiscovery().isPresent()); + + cfg.clearEnableConfigDiscovery(); + assertTrue(cfg.getEnableConfigDiscovery().isEmpty()); + } + + @Test + void resumeSessionConfig_clearIncludeSubAgentStreamingEvents() { + var cfg = new ResumeSessionConfig(); + cfg.setIncludeSubAgentStreamingEvents(false); + assertTrue(cfg.getIncludeSubAgentStreamingEvents().isPresent()); + + cfg.clearIncludeSubAgentStreamingEvents(); + assertTrue(cfg.getIncludeSubAgentStreamingEvents().isEmpty()); + } + + // ── InfiniteSessionConfig ───────────────────────────────────────── + + @Test + void infiniteSessionConfig_clearEnabled() { + var cfg = new InfiniteSessionConfig(); + cfg.setEnabled(true); + assertTrue(cfg.getEnabled().isPresent()); + + cfg.clearEnabled(); + assertTrue(cfg.getEnabled().isEmpty()); + } + + @Test + void infiniteSessionConfig_clearBackgroundCompactionThreshold() { + var cfg = new InfiniteSessionConfig(); + cfg.setBackgroundCompactionThreshold(0.75); + assertFalse(cfg.getBackgroundCompactionThreshold().isEmpty()); + + cfg.clearBackgroundCompactionThreshold(); + assertTrue(cfg.getBackgroundCompactionThreshold().isEmpty()); + } + + @Test + void infiniteSessionConfig_clearBufferExhaustionThreshold() { + var cfg = new InfiniteSessionConfig(); + cfg.setBufferExhaustionThreshold(0.9); + assertFalse(cfg.getBufferExhaustionThreshold().isEmpty()); + + cfg.clearBufferExhaustionThreshold(); + assertTrue(cfg.getBufferExhaustionThreshold().isEmpty()); + } + + // ── InputOptions ────────────────────────────────────────────────── + + @Test + void inputOptions_clearMinLength() { + var opts = new InputOptions(); + opts.setMinLength(5); + assertFalse(opts.getMinLength().isEmpty()); + + opts.clearMinLength(); + assertTrue(opts.getMinLength().isEmpty()); + } + + @Test + void inputOptions_clearMaxLength() { + var opts = new InputOptions(); + opts.setMaxLength(100); + assertFalse(opts.getMaxLength().isEmpty()); + + opts.clearMaxLength(); + assertTrue(opts.getMaxLength().isEmpty()); + } + + // ── ModelCapabilitiesOverride.Supports ───────────────────────────── + + @Test + void supports_clearVision() { + var s = new ModelCapabilitiesOverride.Supports(); + s.setVision(true); + assertTrue(s.getVision().isPresent()); + + s.clearVision(); + assertTrue(s.getVision().isEmpty()); + } + + @Test + void supports_clearReasoningEffort() { + var s = new ModelCapabilitiesOverride.Supports(); + s.setReasoningEffort(false); + assertTrue(s.getReasoningEffort().isPresent()); + + s.clearReasoningEffort(); + assertTrue(s.getReasoningEffort().isEmpty()); + } + + // ── ModelCapabilitiesOverride.Limits ─────────────────────────────── + + @Test + void limits_clearMaxPromptTokens() { + var l = new ModelCapabilitiesOverride.Limits(); + l.setMaxPromptTokens(4096); + assertFalse(l.getMaxPromptTokens().isEmpty()); + + l.clearMaxPromptTokens(); + assertTrue(l.getMaxPromptTokens().isEmpty()); + } + + @Test + void limits_clearMaxOutputTokens() { + var l = new ModelCapabilitiesOverride.Limits(); + l.setMaxOutputTokens(1024); + assertFalse(l.getMaxOutputTokens().isEmpty()); + + l.clearMaxOutputTokens(); + assertTrue(l.getMaxOutputTokens().isEmpty()); + } + + @Test + void limits_clearMaxContextWindowTokens() { + var l = new ModelCapabilitiesOverride.Limits(); + l.setMaxContextWindowTokens(16384); + assertFalse(l.getMaxContextWindowTokens().isEmpty()); + + l.clearMaxContextWindowTokens(); + assertTrue(l.getMaxContextWindowTokens().isEmpty()); + } + + // ── ProviderConfig ──────────────────────────────────────────────── + + @Test + void providerConfig_clearMaxPromptTokens() { + var cfg = new ProviderConfig(); + cfg.setMaxPromptTokens(2048); + assertFalse(cfg.getMaxPromptTokens().isEmpty()); + + cfg.clearMaxPromptTokens(); + assertTrue(cfg.getMaxPromptTokens().isEmpty()); + } + + @Test + void providerConfig_clearMaxOutputTokens() { + var cfg = new ProviderConfig(); + cfg.setMaxOutputTokens(512); + assertFalse(cfg.getMaxOutputTokens().isEmpty()); + + cfg.clearMaxOutputTokens(); + assertTrue(cfg.getMaxOutputTokens().isEmpty()); + } + + // ── TelemetryConfig ─────────────────────────────────────────────── + + @Test + void telemetryConfig_clearCaptureContent() { + var cfg = new TelemetryConfig(); + cfg.setCaptureContent(true); + assertTrue(cfg.getCaptureContent().isPresent()); + + cfg.clearCaptureContent(); + assertTrue(cfg.getCaptureContent().isEmpty()); + } + + // ── SessionUiCapabilities ───────────────────────────────────────── + + @Test + void sessionUiCapabilities_clearElicitation() { + var caps = new SessionUiCapabilities(); + caps.setElicitation(true); + assertTrue(caps.getElicitation().isPresent()); + + caps.clearElicitation(); + assertTrue(caps.getElicitation().isEmpty()); + } + + // ── CustomAgentConfig ───────────────────────────────────────────── + + @Test + void customAgentConfig_clearInfer() { + var cfg = new CustomAgentConfig(); + cfg.setInfer(true); + assertTrue(cfg.getInfer().isPresent()); + + cfg.clearInfer(); + assertTrue(cfg.getInfer().isEmpty()); + } + + // ── UserInputRequest ────────────────────────────────────────────── + + @Test + void userInputRequest_clearAllowFreeform() { + var req = new UserInputRequest(); + req.setAllowFreeform(false); + assertTrue(req.getAllowFreeform().isPresent()); + + req.clearAllowFreeform(); + assertTrue(req.getAllowFreeform().isEmpty()); + } + + // ── Value retrieval through Optional getters ──────────────────────── + + @Test + void copilotClientOptions_sessionIdleTimeoutSecondsValue() { + var opts = new CopilotClientOptions(); + assertTrue(opts.getSessionIdleTimeoutSeconds().isEmpty()); + + opts.setSessionIdleTimeoutSeconds(300); + assertEquals(300, opts.getSessionIdleTimeoutSeconds().getAsInt()); + + opts.setSessionIdleTimeoutSeconds(0); + assertTrue(opts.getSessionIdleTimeoutSeconds().isPresent()); + assertEquals(0, opts.getSessionIdleTimeoutSeconds().getAsInt()); + } + + @Test + void copilotClientOptions_useLoggedInUserValue() { + var opts = new CopilotClientOptions(); + assertTrue(opts.getUseLoggedInUser().isEmpty()); + + opts.setUseLoggedInUser(true); + assertEquals(Boolean.TRUE, opts.getUseLoggedInUser().get()); + + opts.setUseLoggedInUser(false); + assertEquals(Boolean.FALSE, opts.getUseLoggedInUser().get()); + } + + @Test + void sessionConfig_enableSessionTelemetryValue() { + var cfg = new SessionConfig(); + assertFalse(cfg.getEnableSessionTelemetry().orElse(false)); + + cfg.setEnableSessionTelemetry(true); + assertTrue(cfg.getEnableSessionTelemetry().orElse(false)); + + cfg.setEnableSessionTelemetry(false); + assertFalse(cfg.getEnableSessionTelemetry().orElse(true)); + } + + @Test + void sessionConfig_enableConfigDiscoveryValue() { + var cfg = new SessionConfig(); + assertTrue(cfg.getEnableConfigDiscovery().isEmpty()); + + cfg.setEnableConfigDiscovery(true); + assertTrue(cfg.getEnableConfigDiscovery().get()); + + cfg.setEnableConfigDiscovery(false); + assertFalse(cfg.getEnableConfigDiscovery().get()); + } + + @Test + void sessionConfig_includeSubAgentStreamingEventsValue() { + var cfg = new SessionConfig(); + assertTrue(cfg.getIncludeSubAgentStreamingEvents().isEmpty()); + + cfg.setIncludeSubAgentStreamingEvents(true); + assertTrue(cfg.getIncludeSubAgentStreamingEvents().get()); + } + + @Test + void resumeSessionConfig_enableSessionTelemetryValue() { + var cfg = new ResumeSessionConfig(); + assertTrue(cfg.getEnableSessionTelemetry().isEmpty()); + + cfg.setEnableSessionTelemetry(true); + assertTrue(cfg.getEnableSessionTelemetry().get()); + + cfg.setEnableSessionTelemetry(false); + assertFalse(cfg.getEnableSessionTelemetry().get()); + } + + @Test + void resumeSessionConfig_enableConfigDiscoveryValue() { + var cfg = new ResumeSessionConfig(); + assertTrue(cfg.getEnableConfigDiscovery().isEmpty()); + + cfg.setEnableConfigDiscovery(true); + assertTrue(cfg.getEnableConfigDiscovery().get()); + } + + @Test + void resumeSessionConfig_includeSubAgentStreamingEventsValue() { + var cfg = new ResumeSessionConfig(); + assertTrue(cfg.getIncludeSubAgentStreamingEvents().isEmpty()); + + cfg.setIncludeSubAgentStreamingEvents(false); + assertFalse(cfg.getIncludeSubAgentStreamingEvents().get()); + } + + @Test + void infiniteSessionConfig_thresholdValues() { + var cfg = new InfiniteSessionConfig(); + assertTrue(cfg.getBackgroundCompactionThreshold().isEmpty()); + assertTrue(cfg.getBufferExhaustionThreshold().isEmpty()); + + cfg.setBackgroundCompactionThreshold(0.6); + cfg.setBufferExhaustionThreshold(0.85); + assertEquals(0.6, cfg.getBackgroundCompactionThreshold().getAsDouble(), 0.001); + assertEquals(0.85, cfg.getBufferExhaustionThreshold().getAsDouble(), 0.001); + } + + @Test + void infiniteSessionConfig_enabledValue() { + var cfg = new InfiniteSessionConfig(); + assertTrue(cfg.getEnabled().isEmpty()); + + cfg.setEnabled(true); + assertTrue(cfg.getEnabled().get()); + + cfg.setEnabled(false); + assertFalse(cfg.getEnabled().get()); + } + + @Test + void inputOptions_minAndMaxLengthValues() { + var opts = new InputOptions(); + assertTrue(opts.getMinLength().isEmpty()); + assertTrue(opts.getMaxLength().isEmpty()); + + opts.setMinLength(1); + opts.setMaxLength(255); + assertEquals(1, opts.getMinLength().getAsInt()); + assertEquals(255, opts.getMaxLength().getAsInt()); + } + + @Test + void supports_visionAndReasoningEffortValues() { + var s = new ModelCapabilitiesOverride.Supports(); + assertTrue(s.getVision().isEmpty()); + assertTrue(s.getReasoningEffort().isEmpty()); + + s.setVision(true); + s.setReasoningEffort(false); + assertTrue(s.getVision().get()); + assertFalse(s.getReasoningEffort().get()); + } + + @Test + void limits_tokenValues() { + var l = new ModelCapabilitiesOverride.Limits(); + assertTrue(l.getMaxPromptTokens().isEmpty()); + assertTrue(l.getMaxOutputTokens().isEmpty()); + assertTrue(l.getMaxContextWindowTokens().isEmpty()); + + l.setMaxPromptTokens(4096); + l.setMaxOutputTokens(1024); + l.setMaxContextWindowTokens(16384); + assertEquals(4096, l.getMaxPromptTokens().getAsInt()); + assertEquals(1024, l.getMaxOutputTokens().getAsInt()); + assertEquals(16384, l.getMaxContextWindowTokens().getAsInt()); + } + + @Test + void providerConfig_tokenValues() { + var cfg = new ProviderConfig(); + assertTrue(cfg.getMaxPromptTokens().isEmpty()); + assertTrue(cfg.getMaxOutputTokens().isEmpty()); + + cfg.setMaxPromptTokens(8192); + cfg.setMaxOutputTokens(2048); + assertEquals(8192, cfg.getMaxPromptTokens().getAsInt()); + assertEquals(2048, cfg.getMaxOutputTokens().getAsInt()); + } + + @Test + void telemetryConfig_captureContentValue() { + var cfg = new TelemetryConfig(); + assertTrue(cfg.getCaptureContent().isEmpty()); + + cfg.setCaptureContent(true); + assertTrue(cfg.getCaptureContent().get()); + + cfg.setCaptureContent(false); + assertFalse(cfg.getCaptureContent().get()); + } + + @Test + void sessionUiCapabilities_elicitationValue() { + var caps = new SessionUiCapabilities(); + assertTrue(caps.getElicitation().isEmpty()); + assertFalse(caps.getElicitation().orElse(false)); + + caps.setElicitation(true); + assertTrue(caps.getElicitation().orElse(false)); + } + + @Test + void customAgentConfig_inferValue() { + var cfg = new CustomAgentConfig(); + assertTrue(cfg.getInfer().isEmpty()); + + cfg.setInfer(true); + assertTrue(cfg.getInfer().get()); + + cfg.setInfer(false); + assertFalse(cfg.getInfer().get()); + } + + @Test + void userInputRequest_allowFreeformValue() { + var req = new UserInputRequest(); + assertTrue(req.getAllowFreeform().isEmpty()); + + req.setAllowFreeform(true); + assertTrue(req.getAllowFreeform().get()); + + req.setAllowFreeform(false); + assertFalse(req.getAllowFreeform().get()); + } + + // ── JSON deserialization into Optional-returning classes ─────────── + + @Test + void jackson_deserializeSupportsWithFields() throws Exception { + String json = "{\"vision\":true,\"reasoningEffort\":false}"; + var supports = MAPPER.readValue(json, ModelCapabilitiesOverride.Supports.class); + assertTrue(supports.getVision().get()); + assertFalse(supports.getReasoningEffort().get()); + } + + @Test + void jackson_deserializeSupportsEmpty() throws Exception { + String json = "{}"; + var supports = MAPPER.readValue(json, ModelCapabilitiesOverride.Supports.class); + assertTrue(supports.getVision().isEmpty()); + assertTrue(supports.getReasoningEffort().isEmpty()); + } + + @Test + void jackson_deserializeLimitsWithFields() throws Exception { + String json = "{\"max_prompt_tokens\":4096,\"max_output_tokens\":1024,\"max_context_window_tokens\":16384}"; + var limits = MAPPER.readValue(json, ModelCapabilitiesOverride.Limits.class); + assertEquals(4096, limits.getMaxPromptTokens().getAsInt()); + assertEquals(1024, limits.getMaxOutputTokens().getAsInt()); + assertEquals(16384, limits.getMaxContextWindowTokens().getAsInt()); + } + + @Test + void jackson_deserializeLimitsEmpty() throws Exception { + String json = "{}"; + var limits = MAPPER.readValue(json, ModelCapabilitiesOverride.Limits.class); + assertTrue(limits.getMaxPromptTokens().isEmpty()); + assertTrue(limits.getMaxOutputTokens().isEmpty()); + assertTrue(limits.getMaxContextWindowTokens().isEmpty()); + } + + @Test + void jackson_deserializeInfiniteSessionConfigWithFields() throws Exception { + String json = "{\"enabled\":true,\"backgroundCompactionThreshold\":0.7,\"bufferExhaustionThreshold\":0.9}"; + var cfg = MAPPER.readValue(json, InfiniteSessionConfig.class); + assertTrue(cfg.getEnabled().get()); + assertEquals(0.7, cfg.getBackgroundCompactionThreshold().getAsDouble(), 0.001); + assertEquals(0.9, cfg.getBufferExhaustionThreshold().getAsDouble(), 0.001); + } + + @Test + void jackson_deserializeInfiniteSessionConfigEmpty() throws Exception { + String json = "{}"; + var cfg = MAPPER.readValue(json, InfiniteSessionConfig.class); + assertTrue(cfg.getEnabled().isEmpty()); + assertTrue(cfg.getBackgroundCompactionThreshold().isEmpty()); + assertTrue(cfg.getBufferExhaustionThreshold().isEmpty()); + } + + // ── Jackson serialization roundtrip ─────────────────────────────── + // + // Classes whose fields carry @JsonProperty (InfiniteSessionConfig, + // ModelCapabilitiesOverride inner classes) are serialized via field + // access: Jackson writes the field when set and omits it when cleared. + // + // Classes without @JsonProperty on fields (SessionConfig, + // CopilotClientOptions, TelemetryConfig, ProviderConfig) are not + // directly serialized β€” their values are copied to wire DTOs by + // SessionRequestBuilder. The @JsonIgnore on their Optional-returning + // getters prevents Jackson from attempting to serialize Optional + // wrappers if the class is ever processed. + + @Test + void jackson_infiniteSessionConfigClearedFieldsOmitted() throws Exception { + var cfg = new InfiniteSessionConfig(); + cfg.setEnabled(true); + cfg.setBackgroundCompactionThreshold(0.75); + cfg.setBufferExhaustionThreshold(0.9); + + String withFields = MAPPER.writeValueAsString(cfg); + assertTrue(withFields.contains("enabled")); + assertTrue(withFields.contains("backgroundCompactionThreshold")); + assertTrue(withFields.contains("bufferExhaustionThreshold")); + + cfg.clearEnabled(); + cfg.clearBackgroundCompactionThreshold(); + cfg.clearBufferExhaustionThreshold(); + + String cleared = MAPPER.writeValueAsString(cfg); + assertFalse(cleared.contains("enabled")); + assertFalse(cleared.contains("backgroundCompactionThreshold")); + assertFalse(cleared.contains("bufferExhaustionThreshold")); + } + + @Test + void jackson_modelCapabilitiesOverrideSupportsClearedFieldsOmitted() throws Exception { + var supports = new ModelCapabilitiesOverride.Supports(); + supports.setVision(true); + supports.setReasoningEffort(false); + + String withFields = MAPPER.writeValueAsString(supports); + assertTrue(withFields.contains("vision")); + assertTrue(withFields.contains("reasoningEffort")); + + supports.clearVision(); + supports.clearReasoningEffort(); + + String cleared = MAPPER.writeValueAsString(supports); + assertFalse(cleared.contains("vision")); + assertFalse(cleared.contains("reasoningEffort")); + } + + @Test + void jackson_modelCapabilitiesOverrideLimitsClearedFieldsOmitted() throws Exception { + var limits = new ModelCapabilitiesOverride.Limits(); + limits.setMaxPromptTokens(2048); + limits.setMaxOutputTokens(512); + limits.setMaxContextWindowTokens(16384); + + String withFields = MAPPER.writeValueAsString(limits); + assertTrue(withFields.contains("max_prompt_tokens")); + assertTrue(withFields.contains("max_output_tokens")); + assertTrue(withFields.contains("max_context_window_tokens")); + + limits.clearMaxPromptTokens(); + limits.clearMaxOutputTokens(); + limits.clearMaxContextWindowTokens(); + + String cleared = MAPPER.writeValueAsString(limits); + assertFalse(cleared.contains("max_prompt_tokens")); + assertFalse(cleared.contains("max_output_tokens")); + assertFalse(cleared.contains("max_context_window_tokens")); + } +} diff --git a/src/test/java/com/github/copilot/sdk/PermissionsTest.java b/src/test/java/com/github/copilot/sdk/PermissionsTest.java index b498c1ce76..041d8181cb 100644 --- a/src/test/java/com/github/copilot/sdk/PermissionsTest.java +++ b/src/test/java/com/github/copilot/sdk/PermissionsTest.java @@ -363,4 +363,115 @@ void testShouldDenyToolOperationsWhenHandlerExplicitlyDeniesAfterResume(TestInfo session2.close(); } } + + /** + * Verifies that a permission handler returning {@code noResult} is handled + * correctly β€” the handler is called, and the session can be aborted afterward. + * + * @see Snapshot: permissions/should_deny_permission_with_noresult_kind + */ + @Test + void testShouldDenyPermissionWithNoResultKind() throws Exception { + ctx.configureForTest("permissions", "should_deny_permission_with_noresult_kind"); + + var permissionCalled = new CompletableFuture(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest((request, invocation) -> { + permissionCalled.complete(true); + return CompletableFuture.completedFuture( + new PermissionRequestResult().setKind(PermissionRequestResultKind.NO_RESULT)); + })).get(); + + session.send(new MessageOptions().setPrompt("Run 'node --version'")); + + assertTrue(permissionCalled.get(30, TimeUnit.SECONDS), + "Expected the no-result permission handler to be called."); + + session.abort().get(10, TimeUnit.SECONDS); + session.close(); + } + } + + /** + * Verifies that the runtime short-circuits the permission handler when + * {@code session.permissions.setApproveAll(true)} has been called. + * + * @see Snapshot: + * permissions/should_short_circuit_permission_handler_when_set_approve_all_enabled + */ + @Test + void testShouldShortCircuitPermissionHandlerWhenSetApproveAllEnabled() throws Exception { + ctx.configureForTest("permissions", "should_short_circuit_permission_handler_when_set_approve_all_enabled"); + + var handlerCallCount = new int[]{0}; + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest((request, invocation) -> { + handlerCallCount[0]++; + return CompletableFuture.completedFuture( + new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED)); + })).get(); + + // Set approve-all so the runtime short-circuits + var setResult = session.getRpc().permissions + .setApproveAll(new com.github.copilot.sdk.generated.rpc.SessionPermissionsSetApproveAllParams( + session.getSessionId(), true)) + .get(10, TimeUnit.SECONDS); + assertTrue(setResult.success(), "setApproveAll should succeed"); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Run 'echo test' and tell me what happens")) + .get(60, TimeUnit.SECONDS); + assertNotNull(response); + + // Handler should not have been called since runtime approves all + assertEquals(0, handlerCallCount[0], + "Permission handler should not be called when setApproveAll is enabled"); + + session.close(); + } + } + + /** + * Verifies that the SDK correctly waits for a slow permission handler before + * completing tool execution. + * + * @see Snapshot: permissions/should_wait_for_slow_permission_handler + */ + @Test + void testShouldWaitForSlowPermissionHandler() throws Exception { + ctx.configureForTest("permissions", "should_wait_for_slow_permission_handler"); + + var handlerEntered = new CompletableFuture(); + var releaseHandler = new CompletableFuture(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest((request, invocation) -> { + handlerEntered.complete(null); + return releaseHandler.thenApply( + v -> new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED)); + })).get(); + + // Capture the sendAndWait future before awaiting it so we can interact with the + // handler + CompletableFuture responseFuture = session + .sendAndWait(new MessageOptions().setPrompt("Run 'echo slow_handler_test'")); + + // Wait for permission handler to be entered + handlerEntered.get(30, TimeUnit.SECONDS); + + // Release the handler + releaseHandler.complete(null); + + // Session should complete successfully + AssistantMessageEvent message = responseFuture.get(60, TimeUnit.SECONDS); + assertNotNull(message); + + session.close(); + } + } } diff --git a/src/test/java/com/github/copilot/sdk/ProviderConfigTest.java b/src/test/java/com/github/copilot/sdk/ProviderConfigTest.java index d3e18010bc..6bbb3ae284 100644 --- a/src/test/java/com/github/copilot/sdk/ProviderConfigTest.java +++ b/src/test/java/com/github/copilot/sdk/ProviderConfigTest.java @@ -386,4 +386,52 @@ void testResumeSessionConfigWithoutProviderOmitsField() throws Exception { assertTrue(json.path("provider").isMissingNode(), "provider field should be omitted when null"); } + + // ========================================================================= + // Provider model and token limit overrides + // ========================================================================= + + @Test + void testProviderModelIdAndWireModelSerialization() throws Exception { + var provider = new ProviderConfig().setBaseUrl("https://example.com/provider") + .setHeaders(java.util.Map.of("Authorization", "Bearer provider-token")).setModelId("gpt-4o") + .setWireModel("my-finetune-v3").setMaxPromptTokens(100_000).setMaxOutputTokens(4096); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals("https://example.com/provider", json.get("baseUrl").asText()); + assertEquals("Bearer provider-token", json.get("headers").get("Authorization").asText()); + assertEquals("gpt-4o", json.get("modelId").asText()); + assertEquals("my-finetune-v3", json.get("wireModel").asText()); + assertEquals(100_000, json.get("maxPromptTokens").asInt()); + assertEquals(4096, json.get("maxOutputTokens").asInt()); + + // Round-trip + ProviderConfig deserialized = MAPPER.readValue(MAPPER.writeValueAsString(provider), ProviderConfig.class); + assertEquals("gpt-4o", deserialized.getModelId()); + assertEquals("my-finetune-v3", deserialized.getWireModel()); + assertEquals(100_000, deserialized.getMaxPromptTokens().getAsInt()); + assertEquals(4096, deserialized.getMaxOutputTokens().getAsInt()); + } + + @Test + void testProviderModelFieldsDefaultToNull() { + var provider = new ProviderConfig(); + assertNull(provider.getModelId()); + assertNull(provider.getWireModel()); + assertTrue(provider.getMaxPromptTokens().isEmpty()); + assertTrue(provider.getMaxOutputTokens().isEmpty()); + } + + @Test + void testProviderModelFieldsOmittedWhenNull() throws Exception { + var provider = new ProviderConfig().setType("openai"); + + JsonNode json = MAPPER.valueToTree(provider); + + assertTrue(json.path("modelId").isMissingNode()); + assertTrue(json.path("wireModel").isMissingNode()); + assertTrue(json.path("maxPromptTokens").isMissingNode()); + assertTrue(json.path("maxOutputTokens").isMissingNode()); + } } diff --git a/src/test/java/com/github/copilot/sdk/RemoteSessionTest.java b/src/test/java/com/github/copilot/sdk/RemoteSessionTest.java new file mode 100644 index 0000000000..6e093db6ca --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/RemoteSessionTest.java @@ -0,0 +1,399 @@ +/*--------------------------------------------------------------------------------------------- + * 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 org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.json.CreateSessionRequest; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.ResumeSessionRequest; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * Tests for the {@code remoteSession} feature across all session config types. + *

+ * Validates the complete lifecycle of the remote session mode: + *

    + *
  • Getter/setter and fluent chaining on {@link SessionConfig} and + * {@link ResumeSessionConfig}
  • + *
  • Propagation through {@link SessionRequestBuilder} into + * {@link CreateSessionRequest} and {@link ResumeSessionRequest}
  • + *
  • JSON wire-format serialization: correct key, correct value, omission when + * unset
  • + *
  • Defensive copy via {@code copy()} preserves the value
  • + *
  • All three supported mode values ("off", "export", "on") are transmitted + * correctly
  • + *
+ */ +class RemoteSessionTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + // ========================================================================= + // SessionConfig getter/setter/copy + // ========================================================================= + + @Test + void sessionConfig_remoteSessionDefaultsToNull() { + var cfg = new SessionConfig(); + assertNull(cfg.getRemoteSession(), "remoteSession should be null when not set"); + } + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void sessionConfig_setRemoteSessionReturnsSelf(String mode) { + var cfg = new SessionConfig(); + SessionConfig result = cfg.setRemoteSession(mode); + assertSame(cfg, result, "setRemoteSession should return the same instance for chaining"); + assertEquals(mode, cfg.getRemoteSession()); + } + + @Test + void sessionConfig_copyPreservesRemoteSession() { + var original = new SessionConfig().setRemoteSession("export"); + var copy = original.clone(); + assertEquals("export", copy.getRemoteSession()); + } + + @Test + void sessionConfig_copyPreservesNullRemoteSession() { + var original = new SessionConfig(); + var copy = original.clone(); + assertNull(copy.getRemoteSession()); + } + + @Test + void sessionConfig_setRemoteSessionToNullClearsValue() { + var cfg = new SessionConfig().setRemoteSession("on"); + cfg.setRemoteSession(null); + assertNull(cfg.getRemoteSession()); + } + + // ========================================================================= + // ResumeSessionConfig getter/setter/copy + // ========================================================================= + + @Test + void resumeSessionConfig_remoteSessionDefaultsToNull() { + var cfg = new ResumeSessionConfig(); + assertNull(cfg.getRemoteSession(), "remoteSession should be null when not set"); + } + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void resumeSessionConfig_setRemoteSessionReturnsSelf(String mode) { + var cfg = new ResumeSessionConfig(); + ResumeSessionConfig result = cfg.setRemoteSession(mode); + assertSame(cfg, result, "setRemoteSession should return the same instance for chaining"); + assertEquals(mode, cfg.getRemoteSession()); + } + + @Test + void resumeSessionConfig_copyPreservesRemoteSession() { + var original = new ResumeSessionConfig().setRemoteSession("on"); + var copy = original.clone(); + assertEquals("on", copy.getRemoteSession()); + } + + // ========================================================================= + // SessionRequestBuilder – CreateSessionRequest wiring + // ========================================================================= + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void buildCreateRequest_propagatesRemoteSession(String mode) { + var config = new SessionConfig().setRemoteSession(mode); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + assertEquals(mode, request.getRemoteSession()); + } + + @Test + void buildCreateRequest_nullConfig_remoteSessionIsNull() { + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(null); + assertNull(request.getRemoteSession()); + } + + @Test + void buildCreateRequest_unsetRemoteSession_isNull() { + var config = new SessionConfig(); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + assertNull(request.getRemoteSession()); + } + + // ========================================================================= + // SessionRequestBuilder – ResumeSessionRequest wiring + // ========================================================================= + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void buildResumeRequest_propagatesRemoteSession(String mode) { + var config = new ResumeSessionConfig().setRemoteSession(mode); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + assertEquals(mode, request.getRemoteSession()); + } + + @Test + void buildResumeRequest_nullConfig_remoteSessionIsNull() { + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", null); + assertNull(request.getRemoteSession()); + } + + @Test + void buildResumeRequest_unsetRemoteSession_isNull() { + var config = new ResumeSessionConfig(); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + assertNull(request.getRemoteSession()); + } + + // ========================================================================= + // JSON wire-format: CreateSessionRequest + // ========================================================================= + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void createRequest_serializesRemoteSessionCorrectly(String mode) throws Exception { + var config = new SessionConfig().setRemoteSession(mode); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + String json = MAPPER.writeValueAsString(request); + JsonNode tree = MAPPER.readTree(json); + + assertTrue(tree.has("remoteSession"), "Serialized JSON should contain 'remoteSession' field for mode: " + mode); + assertEquals(mode, tree.get("remoteSession").asText()); + } + + @Test + void createRequest_omitsRemoteSessionWhenNull() throws Exception { + var config = new SessionConfig(); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + String json = MAPPER.writeValueAsString(request); + JsonNode tree = MAPPER.readTree(json); + + assertFalse(tree.has("remoteSession"), "Serialized JSON should omit 'remoteSession' when not set"); + } + + // ========================================================================= + // JSON wire-format: ResumeSessionRequest + // ========================================================================= + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void resumeRequest_serializesRemoteSessionCorrectly(String mode) throws Exception { + var config = new ResumeSessionConfig().setRemoteSession(mode); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + + String json = MAPPER.writeValueAsString(request); + JsonNode tree = MAPPER.readTree(json); + + assertTrue(tree.has("remoteSession"), "Serialized JSON should contain 'remoteSession' field for mode: " + mode); + assertEquals(mode, tree.get("remoteSession").asText()); + } + + @Test + void resumeRequest_omitsRemoteSessionWhenNull() throws Exception { + var config = new ResumeSessionConfig(); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + + String json = MAPPER.writeValueAsString(request); + JsonNode tree = MAPPER.readTree(json); + + assertFalse(tree.has("remoteSession"), "Serialized JSON should omit 'remoteSession' when not set"); + } + + // ========================================================================= + // JSON round-trip: CreateSessionRequest + // ========================================================================= + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void createRequest_roundTripsRemoteSession(String mode) throws Exception { + var config = new SessionConfig().setRemoteSession(mode); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + String json = MAPPER.writeValueAsString(request); + CreateSessionRequest deserialized = MAPPER.readValue(json, CreateSessionRequest.class); + assertEquals(mode, deserialized.getRemoteSession()); + } + + @Test + void createRequest_roundTripsNullRemoteSession() throws Exception { + var config = new SessionConfig(); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + String json = MAPPER.writeValueAsString(request); + CreateSessionRequest deserialized = MAPPER.readValue(json, CreateSessionRequest.class); + assertNull(deserialized.getRemoteSession()); + } + + // ========================================================================= + // JSON round-trip: ResumeSessionRequest + // ========================================================================= + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void resumeRequest_roundTripsRemoteSession(String mode) throws Exception { + var config = new ResumeSessionConfig().setRemoteSession(mode); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + + String json = MAPPER.writeValueAsString(request); + ResumeSessionRequest deserialized = MAPPER.readValue(json, ResumeSessionRequest.class); + assertEquals(mode, deserialized.getRemoteSession()); + } + + // ========================================================================= + // Fluent chaining: remoteSession composes with other config options + // ========================================================================= + + @Test + void sessionConfig_remoteSessionComposesWithOtherFields() { + var config = new SessionConfig().setModel("gpt-4o").setRemoteSession("export").setReasoningEffort("high"); + + assertEquals("gpt-4o", config.getModel()); + assertEquals("export", config.getRemoteSession()); + assertEquals("high", config.getReasoningEffort()); + } + + @Test + void resumeSessionConfig_remoteSessionComposesWithOtherFields() { + var config = new ResumeSessionConfig().setModel("gpt-4o").setRemoteSession("on").setReasoningEffort("medium"); + + assertEquals("gpt-4o", config.getModel()); + assertEquals("on", config.getRemoteSession()); + assertEquals("medium", config.getReasoningEffort()); + } + + @Test + void createRequest_remoteSessionDoesNotAffectOtherFields() throws Exception { + var config = new SessionConfig().setModel("gpt-4o").setRemoteSession("export").setReasoningEffort("high") + .setGitHubToken("ghp_test"); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + String json = MAPPER.writeValueAsString(request); + JsonNode tree = MAPPER.readTree(json); + + assertEquals("export", tree.get("remoteSession").asText()); + assertEquals("gpt-4o", tree.get("model").asText()); + assertEquals("high", tree.get("reasoningEffort").asText()); + assertEquals("ghp_test", tree.get("gitHubToken").asText()); + } + + @Test + void resumeRequest_remoteSessionDoesNotAffectOtherFields() throws Exception { + var config = new ResumeSessionConfig().setModel("gpt-4o").setRemoteSession("on").setReasoningEffort("medium") + .setGitHubToken("ghp_test"); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + String json = MAPPER.writeValueAsString(request); + JsonNode tree = MAPPER.readTree(json); + + assertEquals("on", tree.get("remoteSession").asText()); + assertEquals("gpt-4o", tree.get("model").asText()); + assertEquals("medium", tree.get("reasoningEffort").asText()); + assertEquals("ghp_test", tree.get("gitHubToken").asText()); + } + + // ========================================================================= + // Deserialization from raw JSON (simulates CLI response ingestion) + // ========================================================================= + + @Test + void createRequest_deserializesRemoteSessionFromRawJson() throws Exception { + String json = """ + { + "sessionId": "test-session", + "remoteSession": "export", + "model": "gpt-4o" + } + """; + CreateSessionRequest request = MAPPER.readValue(json, CreateSessionRequest.class); + assertEquals("export", request.getRemoteSession()); + assertEquals("test-session", request.getSessionId()); + } + + @Test + void resumeRequest_deserializesRemoteSessionFromRawJson() throws Exception { + String json = """ + { + "sessionId": "resume-session", + "remoteSession": "on", + "model": "gpt-4o" + } + """; + ResumeSessionRequest request = MAPPER.readValue(json, ResumeSessionRequest.class); + assertEquals("on", request.getRemoteSession()); + assertEquals("resume-session", request.getSessionId()); + } + + @Test + void createRequest_deserializesWithMissingRemoteSession() throws Exception { + String json = """ + { + "sessionId": "test-session", + "model": "gpt-4o" + } + """; + CreateSessionRequest request = MAPPER.readValue(json, CreateSessionRequest.class); + assertNull(request.getRemoteSession()); + } + + // ========================================================================= + // Handoff event with remoteSessionId (remote session lifecycle) + // ========================================================================= + + @Test + void handoffEvent_withRemoteSourceType_containsRemoteSessionId() throws Exception { + String json = """ + { + "type": "session.handoff", + "data": { + "handoffTime": "2025-06-01T12:00:00Z", + "sourceType": "remote", + "remoteSessionId": "remote-sess-42", + "summary": "Session exported for remote execution", + "repository": { + "owner": "test-org", + "name": "test-repo", + "branch": "feature-branch" + } + } + } + """; + + var event = (com.github.copilot.sdk.generated.SessionHandoffEvent) MAPPER.readValue(json, + com.github.copilot.sdk.generated.SessionEvent.class); + assertNotNull(event); + var data = event.getData(); + assertEquals("remote-sess-42", data.remoteSessionId()); + assertEquals(com.github.copilot.sdk.generated.HandoffSourceType.REMOTE, data.sourceType()); + assertEquals("Session exported for remote execution", data.summary()); + assertEquals("test-org", data.repository().owner()); + assertEquals("test-repo", data.repository().name()); + assertEquals("feature-branch", data.repository().branch()); + } + + @Test + void handoffEvent_withoutRemoteSessionId_fieldIsNull() throws Exception { + String json = """ + { + "type": "session.handoff", + "data": { + "targetAgent": "local-agent" + } + } + """; + + var event = (com.github.copilot.sdk.generated.SessionHandoffEvent) MAPPER.readValue(json, + com.github.copilot.sdk.generated.SessionEvent.class); + assertNotNull(event); + assertNull(event.getData().remoteSessionId()); + } +} diff --git a/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java b/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java new file mode 100644 index 0000000000..4c2691d86f --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java @@ -0,0 +1,174 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ProviderConfig; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * E2E tests for session configuration features. + */ +public class SessionConfigE2ETest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + @Test + void testShouldApplyInstructionDirectoriesOnCreate() throws Exception { + ctx.configureForTest("session_config", "should_apply_instructiondirectories_on_create"); + + // Set up instruction directory with a custom instruction file + Path projectDir = ctx.getWorkDir().resolve("instruction-create-project"); + Path instructionDir = ctx.getWorkDir().resolve("extra-create-instructions"); + Path instructionFilesDir = instructionDir.resolve(".github").resolve("instructions"); + String sentinel = "JAVA_CREATE_INSTRUCTION_DIRECTORIES_SENTINEL"; + Files.createDirectories(projectDir); + Files.createDirectories(instructionFilesDir); + Files.writeString(instructionFilesDir.resolve("extra.instructions.md"), "Always include " + sentinel + "."); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setWorkingDirectory(projectDir.toString()) + .setInstructionDirectories(List.of(instructionDir.toString())) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + List> exchanges = ctx.getExchanges(); + assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); + String systemMessage = getSystemMessage(exchanges.get(0)); + assertNotNull(systemMessage, "System message should not be null"); + assertTrue(systemMessage.contains(sentinel), + "System message should contain the instruction sentinel: " + sentinel); + } + } + + @Test + void testShouldApplyInstructionDirectoriesOnResume() throws Exception { + ctx.configureForTest("session_config", "should_apply_instructiondirectories_on_resume"); + + // Set up instruction directory with a custom instruction file + Path projectDir = ctx.getWorkDir().resolve("instruction-resume-project"); + Path instructionDir = ctx.getWorkDir().resolve("extra-resume-instructions"); + Path instructionFilesDir = instructionDir.resolve(".github").resolve("instructions"); + String sentinel = "JAVA_RESUME_INSTRUCTION_DIRECTORIES_SENTINEL"; + Files.createDirectories(projectDir); + Files.createDirectories(instructionFilesDir); + Files.writeString(instructionFilesDir.resolve("extra.instructions.md"), "Always include " + sentinel + "."); + + try (CopilotClient client = ctx.createClient()) { + // Create a session first + CopilotSession session1 = client.createSession(new SessionConfig() + .setWorkingDirectory(projectDir.toString()).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + // Resume with instructionDirectories + CopilotSession session2 = client.resumeSession(session1.getSessionId(), + new ResumeSessionConfig().setWorkingDirectory(projectDir.toString()) + .setInstructionDirectories(List.of(instructionDir.toString())) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + session2.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + List> exchanges = ctx.getExchanges(); + assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); + String systemMessage = getSystemMessage(exchanges.get(0)); + assertNotNull(systemMessage, "System message should not be null"); + assertTrue(systemMessage.contains(sentinel), + "System message should contain the instruction sentinel: " + sentinel); + } + } + + @Test + void testShouldForwardProviderWireModel() throws Exception { + ctx.configureForTest("session_config", "should_forward_provider_wire_model"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setModel("claude-sonnet-4.5") + .setProvider(new ProviderConfig().setType("openai").setBaseUrl(ctx.getProxyUrl()) + .setApiKey("test-provider-key").setWireModel("test-wire-model") + .setMaxOutputTokens(1024)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(30, TimeUnit.SECONDS); + + List> exchanges = ctx.getExchanges(); + assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); + @SuppressWarnings("unchecked") + Map request = (Map) exchanges.get(0).get("request"); + assertEquals("test-wire-model", request.get("model")); + } + } + + @Test + void testShouldUseProviderModelIdAsWireModel() throws Exception { + ctx.configureForTest("session_config", "should_use_provider_model_id_as_wire_model"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setProvider(new ProviderConfig().setType("openai").setBaseUrl(ctx.getProxyUrl()) + .setApiKey("test-provider-key").setModelId("claude-sonnet-4.5")) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(30, TimeUnit.SECONDS); + + List> exchanges = ctx.getExchanges(); + assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); + @SuppressWarnings("unchecked") + Map request = (Map) exchanges.get(0).get("request"); + assertEquals("claude-sonnet-4.5", request.get("model")); + } + } + + @SuppressWarnings("unchecked") + private static String getSystemMessage(Map exchange) { + // The exchange structure is: { request: { messages: [...] }, response: ..., + // requestHeaders: ... } + Object requestObj = exchange.get("request"); + if (!(requestObj instanceof Map request)) { + return null; + } + Object messagesObj = request.get("messages"); + if (messagesObj instanceof List messages) { + for (Object msg : messages) { + if (msg instanceof Map msgMap) { + if ("system".equals(msgMap.get("role"))) { + Object content = msgMap.get("content"); + return content != null ? content.toString() : null; + } + } + } + } + return null; + } +} diff --git a/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java b/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java index 2da7783587..109144e00e 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java @@ -736,7 +736,7 @@ void testParseAbortEvent() throws Exception { { "type": "abort", "data": { - "reason": "user_requested" + "reason": "user_initiated" } } """; @@ -1987,14 +1987,14 @@ void testAbortEventAllFields() throws Exception { { "type": "abort", "data": { - "reason": "user_cancelled" + "reason": "user_abort" } } """; var event = (AbortEvent) parseJson(json); assertNotNull(event); - assertEquals("user_cancelled", event.getData().reason()); + assertEquals(AbortReason.USER_ABORT, event.getData().reason()); } @Test @@ -2275,7 +2275,10 @@ void testParsePermissionCompletedEvent() throws Exception { assertNotNull(event); assertEquals("permission.completed", event.getType()); assertEquals("perm-req-456", event.getData().requestId()); - assertEquals(PermissionCompletedKind.APPROVED, event.getData().result().kind()); + assertNotNull(event.getData().result()); + @SuppressWarnings("unchecked") + var result = (java.util.Map) event.getData().result(); + assertEquals("approved", result.get("kind")); } @Test diff --git a/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java b/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java index 73ed6fffa3..d9f22e7813 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java @@ -180,7 +180,7 @@ void testHandlerReceivesCorrectEventData() { SessionStartEvent startEvent = createSessionStartEvent(); startEvent.setData(new SessionStartEvent.SessionStartEventData("my-session-123", null, null, null, null, null, - null, null, null, null)); + null, null, null, null, null)); dispatchEvent(startEvent); AssistantMessageEvent msgEvent = createAssistantMessageEvent("Test content"); @@ -857,15 +857,15 @@ private SessionStartEvent createSessionStartEvent() { private SessionStartEvent createSessionStartEvent(String sessionId) { var event = new SessionStartEvent(); var data = new SessionStartEvent.SessionStartEventData(sessionId, null, null, null, null, null, null, null, - null, null); + null, null, null); event.setData(data); return event; } private AssistantMessageEvent createAssistantMessageEvent(String content) { var event = new AssistantMessageEvent(); - var data = new AssistantMessageEvent.AssistantMessageEventData(null, content, null, null, null, null, null, - null, null, null, null); + var data = new AssistantMessageEvent.AssistantMessageEventData(null, null, content, null, null, null, null, + null, null, null, null, null, null, null, null); event.setData(data); return event; } diff --git a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java index abb4447f52..43703831a8 100644 --- a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java @@ -12,11 +12,13 @@ import org.junit.jupiter.api.Test; +import com.github.copilot.sdk.json.AutoModeSwitchResponse; import com.github.copilot.sdk.json.CreateSessionRequest; import com.github.copilot.sdk.json.DefaultAgentConfig; import com.github.copilot.sdk.json.ElicitationHandler; import com.github.copilot.sdk.json.ElicitationResult; import com.github.copilot.sdk.json.ElicitationResultAction; +import com.github.copilot.sdk.json.ExitPlanModeResult; import com.github.copilot.sdk.json.ResumeSessionConfig; import com.github.copilot.sdk.json.ResumeSessionRequest; import com.github.copilot.sdk.json.SessionConfig; @@ -86,6 +88,20 @@ void testBuildCreateRequestSetsClientName() { assertEquals("my-app", request.getClientName()); } + @Test + void testBuildCreateRequestForwardsEnableSessionTelemetryWhenFalse() { + var config = new SessionConfig().setEnableSessionTelemetry(false); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + assertFalse(request.getEnableSessionTelemetry()); + } + + @Test + void testBuildCreateRequestOmitsEnableSessionTelemetryWhenNotSet() { + var config = new SessionConfig(); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + assertNull(request.getEnableSessionTelemetry()); + } + // ========================================================================= // buildResumeRequest // ========================================================================= @@ -99,6 +115,20 @@ void testBuildResumeRequestNullConfig() { assertEquals("direct", request.getEnvValueMode(), "envValueMode should be 'direct' even for null config"); } + @Test + void testBuildResumeRequestForwardsEnableSessionTelemetryWhenFalse() { + var config = new ResumeSessionConfig().setEnableSessionTelemetry(false); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + assertFalse(request.getEnableSessionTelemetry()); + } + + @Test + void testBuildResumeRequestOmitsEnableSessionTelemetryWhenNotSet() { + var config = new ResumeSessionConfig(); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + assertNull(request.getEnableSessionTelemetry()); + } + @Test void testBuildResumeRequestWithTools() { var tool = ToolDefinition.create("my_tool", "A tool", Map.of("type", "object"), @@ -441,4 +471,192 @@ void testBuildResumeRequestWithGitHubToken() { assertEquals("ghp_per_session_token", request.getGitHubToken()); } + + // ========================================================================= + // instructionDirectories propagation + // ========================================================================= + + @Test + void testBuildCreateRequestPropagatesInstructionDirectories() { + var dirs = List.of("/path/to/instructions", "/another/path"); + var config = new SessionConfig().setInstructionDirectories(dirs); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertEquals(dirs, request.getInstructionDirectories()); + } + + @Test + void testBuildResumeRequestPropagatesInstructionDirectories() { + var dirs = List.of("/resume/instructions", "/other/dir"); + var config = new ResumeSessionConfig().setInstructionDirectories(dirs); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-inst", config); + + assertEquals(dirs, request.getInstructionDirectories()); + } + + // ========================================================================= + // enableSessionTelemetry serialization + // ========================================================================= + + @Test + void testCreateRequestSerializesEnableSessionTelemetryWhenFalse() throws Exception { + var config = new SessionConfig().setEnableSessionTelemetry(false); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + assertTrue(json.contains("\"enableSessionTelemetry\":false"), + "enableSessionTelemetry should be serialized when set to false"); + } + + @Test + void testCreateRequestOmitsEnableSessionTelemetryWhenNull() throws Exception { + var config = new SessionConfig(); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + assertFalse(json.contains("enableSessionTelemetry"), "enableSessionTelemetry should be omitted when null"); + } + + @Test + void testResumeRequestSerializesEnableSessionTelemetryWhenFalse() throws Exception { + var config = new ResumeSessionConfig().setEnableSessionTelemetry(false); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-tel", config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + assertTrue(json.contains("\"enableSessionTelemetry\":false"), + "enableSessionTelemetry should be serialized when set to false"); + } + + @Test + void testResumeRequestOmitsEnableSessionTelemetryWhenNull() throws Exception { + var config = new ResumeSessionConfig(); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-tel", config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + assertFalse(json.contains("enableSessionTelemetry"), "enableSessionTelemetry should be omitted when null"); + } + + // ========================================================================= + // Mode handler request flags + // ========================================================================= + + @Test + void testBuildCreateRequestWithExitPlanModeHandler() { + var config = new SessionConfig().setOnExitPlanMode( + (request, invocation) -> CompletableFuture.completedFuture(new ExitPlanModeResult())); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertTrue(request.getRequestExitPlanMode()); + } + + @Test + void testBuildCreateRequestWithAutoModeSwitchHandler() { + var config = new SessionConfig().setOnAutoModeSwitch( + (request, invocation) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertTrue(request.getRequestAutoModeSwitch()); + } + + @Test + void testBuildCreateRequestWithoutModeHandlers() { + var config = new SessionConfig(); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertNull(request.getRequestExitPlanMode()); + assertNull(request.getRequestAutoModeSwitch()); + } + + @Test + void testBuildResumeRequestWithExitPlanModeHandler() { + var config = new ResumeSessionConfig().setOnExitPlanMode( + (request, invocation) -> CompletableFuture.completedFuture(new ExitPlanModeResult())); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("session-1", config); + + assertTrue(request.getRequestExitPlanMode()); + } + + @Test + void testBuildResumeRequestWithAutoModeSwitchHandler() { + var config = new ResumeSessionConfig().setOnAutoModeSwitch( + (request, invocation) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("session-1", config); + + assertTrue(request.getRequestAutoModeSwitch()); + } + + @Test + void configureSessionWithExitPlanModeHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new SessionConfig().setOnExitPlanMode( + (request, invocation) -> CompletableFuture.completedFuture(new ExitPlanModeResult())); + + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureSessionWithAutoModeSwitchHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new SessionConfig().setOnAutoModeSwitch( + (request, invocation) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureResumedSessionWithExitPlanModeHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new ResumeSessionConfig().setOnExitPlanMode( + (request, invocation) -> CompletableFuture.completedFuture(new ExitPlanModeResult())); + + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureResumedSessionWithAutoModeSwitchHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new ResumeSessionConfig().setOnAutoModeSwitch( + (request, invocation) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void testCreateRequestSerializesModeFlags() throws Exception { + var config = new SessionConfig() + .setOnExitPlanMode((r, i) -> CompletableFuture.completedFuture(new ExitPlanModeResult())) + .setOnAutoModeSwitch((r, i) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + + assertTrue(json.contains("\"requestExitPlanMode\":true")); + assertTrue(json.contains("\"requestAutoModeSwitch\":true")); + } + + @Test + void testResumeRequestSerializesModeFlags() throws Exception { + var config = new ResumeSessionConfig() + .setOnExitPlanMode((r, i) -> CompletableFuture.completedFuture(new ExitPlanModeResult())) + .setOnAutoModeSwitch((r, i) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("session-1", config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + + assertTrue(json.contains("\"requestExitPlanMode\":true")); + assertTrue(json.contains("\"requestAutoModeSwitch\":true")); + } } diff --git a/src/test/java/com/github/copilot/sdk/StreamingFidelityTest.java b/src/test/java/com/github/copilot/sdk/StreamingFidelityTest.java index f3f5976523..d3df63eb06 100644 --- a/src/test/java/com/github/copilot/sdk/StreamingFidelityTest.java +++ b/src/test/java/com/github/copilot/sdk/StreamingFidelityTest.java @@ -14,6 +14,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import com.github.copilot.sdk.generated.SessionEvent; @@ -139,6 +140,7 @@ void testShouldNotProduceDeltasWhenStreamingIsDisabled() throws Exception { * @see Snapshot: streaming_fidelity/should_produce_deltas_after_session_resume */ @Test + @Tag("isolated-resume") void testShouldProduceDeltasAfterSessionResume() throws Exception { ctx.configureForTest("streaming_fidelity", "should_produce_deltas_after_session_resume"); @@ -182,4 +184,98 @@ void testShouldProduceDeltasAfterSessionResume() throws Exception { } } } + + /** + * Verifies that no delta events are produced after resuming a session with + * streaming disabled (even though it was originally created with streaming + * enabled). + * + * @see Snapshot: + * streaming_fidelity/should_not_produce_deltas_after_session_resume_with_streaming_disabled + */ + @Test + @Tag("isolated-resume") + void testShouldNotProduceDeltasAfterSessionResumeWithStreamingDisabled() throws Exception { + ctx.configureForTest("streaming_fidelity", + "should_not_produce_deltas_after_session_resume_with_streaming_disabled"); + + try (CopilotClient client = ctx.createClient()) { + // Create a streaming session and send an initial message + CopilotSession session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(true)).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 DISABLED + try (CopilotClient newClient = ctx.createClient()) { + CopilotSession session2 = newClient.resumeSession(sessionId, new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(false)).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()); + + // No deltas when streaming is toggled off + List deltaEvents = events.stream() + .filter(e -> e instanceof AssistantMessageDeltaEvent).map(e -> (AssistantMessageDeltaEvent) e) + .toList(); + assertTrue(deltaEvents.isEmpty(), + "Should not receive delta events when streaming is disabled on resume"); + + // 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"); + + session2.close(); + } + } + } + + /** + * Verifies that setting reasoningEffort alongside streaming=true does not break + * the streaming pipeline β€” deltas still arrive and complete successfully. + * + * @see Snapshot: + * streaming_fidelity/should_emit_streaming_deltas_with_reasoning_effort_configured + */ + @Test + void testShouldEmitStreamingDeltasWithReasoningEffortConfigured() throws Exception { + ctx.configureForTest("streaming_fidelity", "should_emit_streaming_deltas_with_reasoning_effort_configured"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setStreaming(true).setReasoningEffort("high")) + .get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt("What is 15 * 17?")).get(60, TimeUnit.SECONDS); + + // With streaming + reasoning effort, we should still get content deltas + List deltaEvents = events.stream() + .filter(e -> e instanceof AssistantMessageDeltaEvent).map(e -> (AssistantMessageDeltaEvent) e) + .toList(); + assertFalse(deltaEvents.isEmpty(), "Should have received delta events with reasoning effort configured"); + + // And a final assistant.message with the answer + List assistantEvents = events.stream() + .filter(e -> e instanceof AssistantMessageEvent).map(e -> (AssistantMessageEvent) e).toList(); + assertFalse(assistantEvents.isEmpty(), "Should have received assistant message events"); + assertTrue(assistantEvents.get(assistantEvents.size() - 1).getData().content().contains("255"), + "Response should contain 255"); + + session.close(); + } + } } diff --git a/src/test/java/com/github/copilot/sdk/TelemetryConfigTest.java b/src/test/java/com/github/copilot/sdk/TelemetryConfigTest.java index 99b360d2db..278777ce13 100644 --- a/src/test/java/com/github/copilot/sdk/TelemetryConfigTest.java +++ b/src/test/java/com/github/copilot/sdk/TelemetryConfigTest.java @@ -22,7 +22,7 @@ void defaultValuesAreNull() { assertNull(config.getFilePath()); assertNull(config.getExporterType()); assertNull(config.getSourceName()); - assertNull(config.getCaptureContent()); + assertTrue(config.getCaptureContent().isEmpty()); } @Test @@ -57,10 +57,10 @@ void sourceNameGetterSetter() { void captureContentGetterSetter() { var config = new TelemetryConfig(); config.setCaptureContent(true); - assertTrue(config.getCaptureContent()); + assertTrue(config.getCaptureContent().get()); config.setCaptureContent(false); - assertFalse(config.getCaptureContent()); + assertFalse(config.getCaptureContent().get()); } @Test @@ -72,6 +72,6 @@ void fluentChainingReturnsThis() { assertEquals("/tmp/spans.json", config.getFilePath()); assertEquals("file", config.getExporterType()); assertEquals("sdk-test", config.getSourceName()); - assertTrue(config.getCaptureContent()); + assertTrue(config.getCaptureContent().get()); } } diff --git a/src/test/java/com/github/copilot/sdk/ToolResultsTest.java b/src/test/java/com/github/copilot/sdk/ToolResultsTest.java new file mode 100644 index 0000000000..31f9d1b07d --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/ToolResultsTest.java @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * 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 java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.generated.ToolExecutionCompleteEvent; +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; +import com.github.copilot.sdk.json.ToolResultObject; + +/** + * E2E tests for tool result types β€” verifying that rejected and denied result + * types are handled correctly by the runtime. + * + *

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

+ */ +public class ToolResultsTest { + + 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 a tool returning a "rejected" resultType is reported as a + * failed tool execution with the correct error code. + * + * @see Snapshot: + * tool_results/should_handle_tool_result_with_rejected_resulttype + */ + @Test + void testShouldHandleToolResultWithRejectedResultType() throws Exception { + ctx.configureForTest("tool_results", "should_handle_tool_result_with_rejected_resulttype"); + + var toolHandlerCalled = new boolean[]{false}; + + Map params = Map.of("type", "object", "properties", Map.of(), "required", List.of()); + + ToolDefinition deployTool = ToolDefinition.create("deploy_service", "Deploys a service", params, + (invocation) -> { + toolHandlerCalled[0] = true; + return CompletableFuture.completedFuture(new ToolResultObject("rejected", + "Deployment rejected: policy violation - production deployments require approval", null, + null, null, null)); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(deployTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt( + "Deploy the service using deploy_service. If it's rejected, tell me it was 'rejected by policy'.")) + .get(60, TimeUnit.SECONDS); + + assertTrue(toolHandlerCalled[0], "Tool handler should have been called"); + + List toolEvents = events.stream() + .filter(e -> e instanceof ToolExecutionCompleteEvent).map(e -> (ToolExecutionCompleteEvent) e) + .toList(); + assertFalse(toolEvents.isEmpty(), "Should have a tool.execution_complete event"); + + ToolExecutionCompleteEvent toolEvt = toolEvents.get(0); + assertFalse(toolEvt.getData().success(), "Tool execution should not be marked as successful"); + assertNotNull(toolEvt.getData().error(), "Should have error details"); + assertEquals("rejected", toolEvt.getData().error().code(), "Error code should be 'rejected'"); + + session.close(); + } + } + + /** + * Verifies that a tool returning a "denied" resultType is reported as a failed + * tool execution with the correct error code. + * + * @see Snapshot: tool_results/should_handle_tool_result_with_denied_resulttype + */ + @Test + void testShouldHandleToolResultWithDeniedResultType() throws Exception { + ctx.configureForTest("tool_results", "should_handle_tool_result_with_denied_resulttype"); + + var toolHandlerCalled = new boolean[]{false}; + + Map params = Map.of("type", "object", "properties", Map.of(), "required", List.of()); + + ToolDefinition accessTool = ToolDefinition.create("access_secret", "Accesses a secret", params, + (invocation) -> { + toolHandlerCalled[0] = true; + return CompletableFuture.completedFuture(new ToolResultObject("denied", + "Access denied: insufficient permissions to read secrets", null, null, null, null)); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(accessTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt( + "Use access_secret to get the API key. If access is denied, tell me it was 'access denied'.")) + .get(60, TimeUnit.SECONDS); + + assertTrue(toolHandlerCalled[0], "Tool handler should have been called"); + + List toolEvents = events.stream() + .filter(e -> e instanceof ToolExecutionCompleteEvent).map(e -> (ToolExecutionCompleteEvent) e) + .toList(); + assertFalse(toolEvents.isEmpty(), "Should have a tool.execution_complete event"); + + ToolExecutionCompleteEvent toolEvt = toolEvents.get(0); + assertFalse(toolEvt.getData().success(), "Tool execution should not be marked as successful"); + assertNotNull(toolEvt.getData().error(), "Should have error details"); + assertEquals("denied", toolEvt.getData().error().code(), "Error code should be 'denied'"); + + session.close(); + } + } +} diff --git a/src/test/java/com/github/copilot/sdk/ToolsTest.java b/src/test/java/com/github/copilot/sdk/ToolsTest.java index d13db36856..6cd0c99bd3 100644 --- a/src/test/java/com/github/copilot/sdk/ToolsTest.java +++ b/src/test/java/com/github/copilot/sdk/ToolsTest.java @@ -360,4 +360,109 @@ void testOverridesBuiltInToolWithCustomTool() throws Exception { session.close(); } } + + /** + * Verifies that the model can call multiple custom tools in parallel within a + * single turn. + * + * @see Snapshot: + * tools/should_execute_multiple_custom_tools_in_parallel_single_turn + */ + @Test + void testShouldExecuteMultipleCustomToolsInParallelSingleTurn() throws Exception { + ctx.configureForTest("tools", "should_execute_multiple_custom_tools_in_parallel_single_turn"); + + var toolACalled = new CompletableFuture(); + var toolBCalled = new CompletableFuture(); + + Map cityParams = Map.of("type", "object", "properties", + Map.of("city", Map.of("type", "string", "description", "City name")), "required", List.of("city")); + Map countryParams = Map.of("type", "object", "properties", + Map.of("country", Map.of("type", "string", "description", "Country name")), "required", + List.of("country")); + + ToolDefinition lookupCity = ToolDefinition.create("lookup_city", "Looks up city information", cityParams, + (invocation) -> { + String city = (String) invocation.getArguments().get("city"); + toolACalled.complete(city); + return CompletableFuture.completedFuture("CITY_" + city.toUpperCase()); + }); + + ToolDefinition lookupCountry = ToolDefinition.create("lookup_country", "Looks up country information", + countryParams, (invocation) -> { + String country = (String) invocation.getArguments().get("country"); + toolBCalled.complete(country); + return CompletableFuture.completedFuture("COUNTRY_" + country.toUpperCase()); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setTools(List.of(lookupCity, lookupCountry)).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt( + "Use lookup_city with 'Paris' and lookup_country with 'France' at the same time, then combine both results in your reply.")) + .get(60, TimeUnit.SECONDS); + + // Both tools should have been called + assertEquals("Paris", toolACalled.get(10, TimeUnit.SECONDS)); + assertEquals("France", toolBCalled.get(10, TimeUnit.SECONDS)); + + assertNotNull(response); + String content = response.getData().content(); + assertTrue(content.contains("CITY_PARIS"), "Response should contain CITY_PARIS: " + content); + assertTrue(content.contains("COUNTRY_FRANCE"), "Response should contain COUNTRY_FRANCE: " + content); + + session.close(); + } + } + + /** + * Verifies that excludedTools are respected even when also listed in + * availableTools. + * + * @see Snapshot: tools/should_respect_availabletools_and_excludedtools_combined + */ + @Test + void testShouldRespectAvailableToolsAndExcludedToolsCombined() throws Exception { + ctx.configureForTest("tools", "should_respect_availabletools_and_excludedtools_combined"); + + var excludedToolCalled = new boolean[]{false}; + + Map inputParams = Map.of("type", "object", "properties", + Map.of("input", Map.of("type", "string", "description", "Input value")), "required", List.of("input")); + + ToolDefinition allowedTool = ToolDefinition.create("allowed_tool", "An allowed tool", inputParams, + (invocation) -> { + String input = (String) invocation.getArguments().get("input"); + return CompletableFuture.completedFuture("ALLOWED_" + input.toUpperCase()); + }); + + ToolDefinition excludedTool = ToolDefinition.create("excluded_tool", "A tool that should be excluded", + inputParams, (invocation) -> { + excludedToolCalled[0] = true; + String input = (String) invocation.getArguments().get("input"); + return CompletableFuture.completedFuture("EXCLUDED_" + input.toUpperCase()); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setTools(List.of(allowedTool, excludedTool)) + .setAvailableTools(List.of("allowed_tool", "excluded_tool")) + .setExcludedTools(List.of("excluded_tool")).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions() + .setPrompt("Use the allowed_tool with input 'test'. Do NOT use excluded_tool.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("ALLOWED_TEST"), + "Response should contain ALLOWED_TEST: " + response.getData().content()); + assertFalse(excludedToolCalled[0], "Excluded tool should not have been called"); + + session.close(); + } + } } diff --git a/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcApiCoverageTest.java b/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcApiCoverageTest.java index 89943e3758..10393abe09 100644 --- a/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcApiCoverageTest.java +++ b/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcApiCoverageTest.java @@ -71,7 +71,7 @@ void serverRpc_sessions_fork_invokes_correct_method() { var stub = new StubCaller(); var server = new ServerRpc(stub); - var params = new SessionsForkParams("parent-session-id", null); + var params = new SessionsForkParams("parent-session-id", null, null); server.sessions.fork(params); assertEquals(1, stub.calls.size()); @@ -657,7 +657,7 @@ void serverRpc_sessionFs_setProvider_params_record() { @Test void sessionsForkParams_record() { - var params = new SessionsForkParams("parent-id", "event-123"); + var params = new SessionsForkParams("parent-id", "event-123", null); assertEquals("parent-id", params.sessionId()); assertEquals("event-123", params.toEventId()); } diff --git a/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcRecordsCoverageTest.java index 8e86bddf22..50e65d3779 100644 --- a/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -67,7 +67,7 @@ void toolsListParams_record() { @Test void sessionsForkParams_record() { - var params = new SessionsForkParams("sess-1", "event-abc"); + var params = new SessionsForkParams("sess-1", "event-abc", null); assertEquals("sess-1", params.sessionId()); assertEquals("event-abc", params.toEventId()); } @@ -483,21 +483,23 @@ void sessionAgentDeselectResult_empty() { @Test void sessionAgentListResult_with_items() { - var item = new AgentInfo("name1", "Name One", "Desc 1"); + var item = new AgentInfo("name1", "Name One", "Desc 1", "/path/to/agent1"); var result = new SessionAgentListResult(List.of(item)); assertEquals(1, result.agents().size()); assertEquals("name1", result.agents().get(0).name()); assertEquals("Name One", result.agents().get(0).displayName()); assertEquals("Desc 1", result.agents().get(0).description()); + assertEquals("/path/to/agent1", result.agents().get(0).path()); } @Test void sessionAgentGetCurrentResult_nested() { - var agent = new AgentInfo("agent-1", "Agent One", "Does things"); + var agent = new AgentInfo("agent-1", "Agent One", "Does things", null); var result = new SessionAgentGetCurrentResult(agent); assertEquals("agent-1", result.agent().name()); assertEquals("Agent One", result.agent().displayName()); assertEquals("Does things", result.agent().description()); + assertNull(result.agent().path()); } @Test @@ -508,7 +510,7 @@ void sessionAgentGetCurrentResult_null_agent() { @Test void sessionAgentReloadResult_with_items() { - var item = new AgentInfo("a", "A", "Desc"); + var item = new AgentInfo("a", "A", "Desc", "/path/to/a"); var result = new SessionAgentReloadResult(List.of(item)); assertEquals(1, result.agents().size()); assertEquals("a", result.agents().get(0).name()); @@ -516,7 +518,7 @@ void sessionAgentReloadResult_with_items() { @Test void sessionAgentSelectResult_nested() { - var agent = new AgentInfo("selected", "Selected", "The selected agent"); + var agent = new AgentInfo("selected", "Selected", "The selected agent", "/path/to/selected"); var result = new SessionAgentSelectResult(agent); assertEquals("selected", result.agent().name()); } @@ -793,7 +795,7 @@ void sessionSkillsListResult_nested() { @Test void sessionSkillsReloadResult_empty() { - assertNotNull(new SessionSkillsReloadResult()); + assertNotNull(new SessionSkillsReloadResult(null, null)); } @Test @@ -830,8 +832,8 @@ void sessionUiHandlePendingElicitationResult_record() { @Test void sessionUsageGetMetricsResult_nested() { var changes = new UsageMetricsCodeChanges(100L, 50L, 5L); - var result = new SessionUsageGetMetricsResult(0.5, 10L, 2000.0, 1700000000000L, changes, null, "gpt-5", 1000L, - 500L); + var result = new SessionUsageGetMetricsResult(0.5, 10L, null, null, 2000.0, 1700000000000L, changes, null, + "gpt-5", 1000L, 500L); assertEquals(0.5, result.totalPremiumRequestCost()); assertEquals(10L, result.totalUserRequests()); assertNotNull(result.codeChanges()); @@ -861,7 +863,7 @@ void sessionWorkspaceReadFileResult_record() { @Test void sessionsForkResult_record() { - var result = new SessionsForkResult("forked-sess-id"); + var result = new SessionsForkResult("forked-sess-id", null); assertEquals("forked-sess-id", result.sessionId()); } @@ -915,8 +917,8 @@ void modelsListResult_nested() { var limits = new ModelCapabilitiesLimits(100000L, 8192L, 128000L, null); var capabilities = new ModelCapabilities(supports, limits); var policy = new ModelPolicy("active", null); - var billing = new ModelBilling(1.0); - var modelItem = new Model("gpt-5", "GPT-5", capabilities, policy, billing, null, null); + var billing = new ModelBilling(1.0, null); + var modelItem = new Model("gpt-5", "GPT-5", capabilities, policy, billing, null, null, null, null); var result = new ModelsListResult(List.of(modelItem)); assertEquals(1, result.models().size());