diff --git a/.github/actions/run-alpine-tests/action.yml b/.github/actions/run-alpine-tests/action.yml index 67a7cf9767..a9842c11be 100644 --- a/.github/actions/run-alpine-tests/action.yml +++ b/.github/actions/run-alpine-tests/action.yml @@ -43,7 +43,7 @@ runs: printf '%s\n' "$ALPINE_TEST_COMMAND" >> "$script_path" chmod +x "$script_path" - docker run --rm \ + docker run --rm --init \ --platform linux/amd64 \ --volume "$GITHUB_WORKSPACE:/workspace" \ --volume "$script_path:/tmp/copilot-sdk-alpine-test.sh:ro" \ diff --git a/.github/workflows/java-publish-snapshot.yml b/.github/workflows/java-publish-snapshot.yml index ce5b8c5f4e..77d5287964 100644 --- a/.github/workflows/java-publish-snapshot.yml +++ b/.github/workflows/java-publish-snapshot.yml @@ -171,6 +171,80 @@ jobs: if-no-files-found: error retention-days: 1 + build-linuxmusl-arm64-classifier: + name: Build Linux musl ARM64 snapshot classifier + needs: resolve-source + runs-on: ubuntu-24.04-arm + permissions: + contents: read + outputs: + version: ${{ steps.build.outputs.version }} + defaults: + run: + shell: bash + working-directory: ./java + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.sha }} + fetch-depth: 1 + persist-credentials: false + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: 22 + + - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: "25" + distribution: "microsoft" + cache: "maven" + + - name: Build and validate linuxmusl-arm64 classifier + id: build + run: | + set -euo pipefail + SOURCE_COMMIT=$(git rev-parse HEAD) + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.source_sha }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved snapshot source." + exit 1 + fi + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + docker run --rm \ + --volume "$GITHUB_WORKSPACE:/workspace" \ + --volume "$HOME/.m2:/root/.m2" \ + --workdir /workspace/java \ + --env HOST_GID="$(id -g)" \ + --env HOST_UID="$(id -u)" \ + "eclipse-temurin:25-jdk-alpine" \ + sh -c "apk add --no-cache git java-cacerts maven nodejs npm && + export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts && + git config --global --add safe.directory /workspace && + node copilot-native/scripts/validate-native-host.mjs linuxmusl-arm64 && + mvn -B -pl copilot-native package -DskipTests -Dcopilot.native.libc=musl && + chown -R \$HOST_UID:\$HOST_GID copilot-native/target" + JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-linuxmusl-arm64.jar" + PRIMARY_JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION.jar" + test -f "$JAR" + node copilot-native/scripts/validate-native-artifact.mjs \ + classifier linuxmusl-arm64 "$JAR" "$(basename "$JAR")" .. + node copilot-native/scripts/validate-native-artifact.mjs placeholder "$PRIMARY_JAR" + MANIFEST="copilot-native/target/linuxmusl-arm64-$VERSION.sha256" + HASH=$(sha256sum "$JAR" | cut -d ' ' -f 1) + printf '%s %s' "$HASH" "$(basename "$JAR")" > "$MANIFEST" + node copilot-native/scripts/validate-native-artifact.mjs \ + checksum "$JAR" "$MANIFEST" "$(basename "$JAR")" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: java-native-linuxmusl-arm64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} + path: | + java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-linuxmusl-arm64.jar + java/copilot-native/target/linuxmusl-arm64-${{ steps.build.outputs.version }}.sha256 + if-no-files-found: error + retention-days: 1 + build-windows-x64-classifier: name: Build Windows x64 snapshot classifier needs: resolve-source @@ -426,6 +500,7 @@ jobs: resolve-source, build-linux-arm64-classifier, build-linuxmusl-x64-classifier, + build-linuxmusl-arm64-classifier, build-windows-x64-classifier, build-windows-arm64-classifier, build-darwin-classifier, @@ -468,6 +543,11 @@ jobs: name: java-native-linuxmusl-x64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/java-native-linuxmusl-x64 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: java-native-linuxmusl-arm64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/java-native-linuxmusl-arm64 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: java-native-win32-x64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} @@ -569,6 +649,31 @@ jobs: echo "linuxmusl_x64_jar=$JAR" >> "$GITHUB_OUTPUT" echo "linuxmusl_x64_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT" + - name: Verify version, source, and Linux musl ARM64 classifier + id: linuxmusl-arm64-artifact + run: | + SOURCE_COMMIT=$(git rev-parse HEAD) + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.source_sha }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved snapshot source." + exit 1 + fi + VERSION="${{ steps.linux-arm64-artifact.outputs.version }}" + if [ "$VERSION" != "${{ needs.build-linuxmusl-arm64-classifier.outputs.version }}" ]; then + echo "::error::Linux musl ARM64 classifier version does not match deploy version." + exit 1 + fi + ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-linuxmusl-arm64" + JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-linuxmusl-arm64.jar" + MANIFEST="$ARTIFACT_DIRECTORY/linuxmusl-arm64-$VERSION.sha256" + test -f "$JAR" + test -f "$MANIFEST" + node "$GITHUB_WORKSPACE/java/copilot-native/scripts/validate-native-artifact.mjs" \ + checksum "$JAR" "$MANIFEST" "$(basename "$JAR")" + node "$GITHUB_WORKSPACE/java/copilot-native/scripts/validate-native-artifact.mjs" \ + classifier linuxmusl-arm64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE" + echo "linuxmusl_arm64_jar=$JAR" >> "$GITHUB_OUTPUT" + echo "linuxmusl_arm64_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT" + - name: Verify version, source, and Darwin classifier id: darwin-artifact run: | @@ -650,6 +755,7 @@ jobs: mvn -B deploy -DskipTests -DskipITs -Dcopilot.native.libc=glibc \ "-Dcopilot.native.external.linux.arm64.classifier.path=${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}" \ "-Dcopilot.native.external.linuxmusl.x64.classifier.path=${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_jar }}" \ + "-Dcopilot.native.external.linuxmusl.arm64.classifier.path=${{ steps.linuxmusl-arm64-artifact.outputs.linuxmusl_arm64_jar }}" \ "-Dcopilot.native.external.win32.classifier.path=${{ steps.windows-artifact.outputs.windows_jar }}" \ "-Dcopilot.native.external.win32.arm64.classifier.path=${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}" \ "-Dcopilot.native.external.darwin.classifier.path=${{ steps.darwin-artifact.outputs.darwin_jar }}" \ @@ -689,6 +795,7 @@ jobs: echo "| \`linux-x64\` | \`ubuntu-latest\` | \`$(basename "$LINUX_JAR")\` | \`$LINUX_SHA\` | Published |" echo "| \`linux-arm64\` | \`ubuntu-24.04-arm\` | \`$(basename "${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}")\` | \`${{ steps.linux-arm64-artifact.outputs.linux_arm64_sha }}\` | Published |" echo "| \`linuxmusl-x64\` | \`Alpine x64\` | \`$(basename "${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_jar }}")\` | \`${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_sha }}\` | Published |" + echo "| \`linuxmusl-arm64\` | \`Alpine ARM64\` | \`$(basename "${{ steps.linuxmusl-arm64-artifact.outputs.linuxmusl_arm64_jar }}")\` | \`${{ steps.linuxmusl-arm64-artifact.outputs.linuxmusl_arm64_sha }}\` | Published |" echo "| \`win32-x64\` | \`windows-latest\` | \`$(basename "${{ steps.windows-artifact.outputs.windows_jar }}")\` | \`${{ steps.windows-artifact.outputs.windows_sha }}\` | Published |" echo "| \`win32-arm64\` | \`windows-11-arm\` | \`$(basename "${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}")\` | \`${{ steps.windows-arm64-artifact.outputs.windows_arm64_sha }}\` | Published |" echo "| \`darwin-x64\` | \`macos-15-intel\` | \`$(basename "${{ steps.darwin-x64-artifact.outputs.darwin_x64_jar }}")\` | \`${{ steps.darwin-x64-artifact.outputs.darwin_x64_sha }}\` | Published |" diff --git a/.github/workflows/java-smoke-test.yml b/.github/workflows/java-smoke-test.yml deleted file mode 100644 index 95f662beb1..0000000000 --- a/.github/workflows/java-smoke-test.yml +++ /dev/null @@ -1,125 +0,0 @@ -name: "Java smoke test" - -on: - workflow_dispatch: - workflow_call: - secrets: - COPILOT_GITHUB_TOKEN: - required: true - -permissions: - contents: read - -jobs: - smoke-test-jdk17: - name: Build SDK and run smoke test (JDK 17) - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' - defaults: - run: - shell: bash - working-directory: ./java - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up JDK 17 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - with: - java-version: "17" - distribution: "microsoft" - cache: "maven" - - - uses: ./.github/actions/setup-copilot - id: setup-copilot - - - name: Build SDK and install to local repo - run: mvn -DskipTests -DskipITs -Pskip-test-harness clean install - - - name: Create and run smoke test via Copilot CLI - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_CLI_JS: ${{ steps.setup-copilot.outputs.javascript-cli-path }} - run: | - cat > /tmp/smoke-test-prompt.txt << 'PROMPT_EOF' - You are running inside the copilot-sdk monorepo, in the java/ subdirectory. - The SDK has already been built and installed into the local Maven repository. - JDK 17 and Maven are already installed and on PATH. - - Execute the prompt at `sdk/src/test/prompts/PROMPT-smoke-test.md` with the following critical overrides: - - **Critical override — disable SNAPSHOT updates (but allow downloads):** The goal of this workflow is to validate the SDK SNAPSHOT that was just built and installed locally, not any newer SNAPSHOT that might exist in a remote repository. To ensure Maven does not download a newer timestamped SNAPSHOT of the SDK while still allowing it to download any missing plugins or dependencies, you must run the smoke-test Maven build without `-U` and with `--no-snapshot-updates`, so that it uses the locally installed SDK artifact. Use `mvn --no-snapshot-updates clean package` instead of `mvn -U clean package` or `mvn -o clean package`. - - **Critical override — do NOT run the jar:** Stop after the `mvn --no-snapshot-updates clean package` build succeeds. Do NOT execute Step 4 (java -jar) or Step 5 (verify exit code) from the prompt. The workflow will run the jar in a separate deterministic step to guarantee the exit code propagates correctly. - - Follow steps 1-3 only: create the `smoke-test/` directory, create `pom.xml` and the Java source file exactly as specified, and build with `mvn --no-snapshot-updates clean package` (no SNAPSHOT updates and without `-U`). - - If any step fails, exit with a non-zero exit code. Do not silently fix errors. - PROMPT_EOF - - node "$COPILOT_CLI_JS" --yolo --prompt "$(cat /tmp/smoke-test-prompt.txt)" - - - name: Run smoke test jar - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - run: | - cd smoke-test - java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar - echo "Smoke test passed (exit code 0)" - - smoke-test-java25: - name: Build SDK and run smoke test (JDK 25) - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' - defaults: - run: - shell: bash - working-directory: ./java - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - with: - java-version: "25" - distribution: "microsoft" - cache: "maven" - - - uses: ./.github/actions/setup-copilot - id: setup-copilot - - - name: Build SDK and install to local repo - run: mvn -DskipTests -DskipITs -Pskip-test-harness clean install - - - name: Create and run smoke test via Copilot CLI - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_CLI_JS: ${{ steps.setup-copilot.outputs.javascript-cli-path }} - run: | - cat > /tmp/smoke-test-prompt.txt << 'PROMPT_EOF' - You are running inside the copilot-sdk monorepo, in the java/ subdirectory. - The SDK has already been built and installed into the local Maven repository. - JDK 25 and Maven are already installed and on PATH. - - Execute the prompt at `sdk/src/test/prompts/PROMPT-smoke-test.md` with the following critical overrides: - - **Critical override — disable SNAPSHOT updates (but allow downloads):** The goal of this workflow is to validate the SDK SNAPSHOT that was just built and installed locally, not any newer SNAPSHOT that might exist in a remote repository. To ensure Maven does not download a newer timestamped SNAPSHOT of the SDK while still allowing it to download any missing plugins or dependencies, you must run the smoke-test Maven build without `-U` and with `--no-snapshot-updates`, so that it uses the locally installed SDK artifact. Use `mvn --no-snapshot-updates clean package` instead of `mvn -U clean package` or `mvn -o clean package`. - - **Critical override — do NOT run the jar:** Stop after the `mvn --no-snapshot-updates clean package` build succeeds. Do NOT execute Step 4 (java -jar) or Step 5 (verify exit code) from the prompt. The workflow will run the jar in a separate deterministic step to guarantee the exit code propagates correctly. - - **Critical override — enable Virtual Threads for JDK 25:** After creating the Java source file from the README "Quick Start" section but BEFORE building, you must modify the source file to enable virtual thread support. The Quick Start code contains inline comments that start with `// JDK 25+:` — these are instructions. Find every such comment and follow what it says (comment out lines it says to comment out, uncomment lines it says to uncomment). Add any imports required by the newly uncommented code (e.g. `java.util.concurrent.Executors`). - Also set `maven.compiler.source` and `maven.compiler.target` to `25` in the `pom.xml`. - - Follow steps 1-3 only: create the `smoke-test/` directory, create `pom.xml` and the Java source file exactly as specified, apply the JDK 25 virtual thread modifications described above, and build with `mvn --no-snapshot-updates clean package` (no SNAPSHOT updates and without `-U`). - - If any step fails, exit with a non-zero exit code. Do not silently fix errors. - PROMPT_EOF - - node "$COPILOT_CLI_JS" --yolo --prompt "$(cat /tmp/smoke-test-prompt.txt)" - - - name: Run smoke test jar - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - run: | - cd smoke-test - java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar - echo "Smoke test passed (exit code 0)" diff --git a/.github/workflows/sdk-dotnet.yml b/.github/workflows/sdk-dotnet.yml index 4e9671c9a9..63b689f94e 100644 --- a/.github/workflows/sdk-dotnet.yml +++ b/.github/workflows/sdk-dotnet.yml @@ -1,6 +1,6 @@ # Invoked by sdk.yml to run the .NET SDK build/test matrix, documentation, # full subprocess CAPI coverage, and focused in-process smoke -# coverage on Linux and x64 musl. +# coverage across standard platforms and x64 musl. name: "SDK .NET" env: @@ -83,6 +83,13 @@ jobs: DOTNET_TEST_RESULTS_DIRECTORY: TestResults/inprocess DOTNET_TEST_FILTER: "FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication" run: ../scripts/ci/run-dotnet-tests.sh + - name: Validate documentation examples + if: github.event_name != 'merge_group' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + run: | + npm ci + npm run extract + npm run validate:cs - if: failure() uses: actions/upload-artifact@v7 with: @@ -92,9 +99,9 @@ jobs: retention-days: 7 dotnet-darwin-arm64: - name: ".NET (macos-latest, default, CAPI)" + name: ".NET (macos-26-xlarge, default, CAPI)" if: inputs.platform == 'all' || inputs.platform == 'darwin-arm64' - runs-on: macos-latest + runs-on: macos-26-xlarge timeout-minutes: 30 defaults: run: @@ -102,6 +109,7 @@ jobs: working-directory: ${{ inputs.sdk-home }}/dotnet env: COPILOT_SDK_E2E_BACKEND: capi + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/subprocess steps: - uses: actions/checkout@v7 timeout-minutes: 4 @@ -123,13 +131,23 @@ jobs: - run: dotnet build --no-restore -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" -p:CopilotSkipCliDownload=true - run: npm ci --ignore-scripts working-directory: ${{ inputs.sdk-home }}/test/harness - - env: + - name: Test out-of-process + id: subprocess + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} run: ../scripts/ci/run-dotnet-tests.sh + - name: Test in-process smoke + if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/inprocess + DOTNET_TEST_FILTER: "FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication" + run: ../scripts/ci/run-dotnet-tests.sh - if: failure() uses: actions/upload-artifact@v7 with: - name: dotnet-test-diagnostics-macos-latest-default-capi-${{ github.run_attempt }} + name: dotnet-test-diagnostics-macos-26-xlarge-default-capi-${{ github.run_attempt }} path: ${{ inputs.sdk-home }}/dotnet/TestResults/ if-no-files-found: warn retention-days: 7 @@ -146,6 +164,7 @@ jobs: env: COPILOT_SDK_E2E_BACKEND: capi DOTNET_TEST_RUNTIME: win-x64 + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/subprocess steps: - uses: actions/checkout@v7 timeout-minutes: 4 @@ -168,9 +187,19 @@ jobs: - run: npm ci --ignore-scripts working-directory: ${{ inputs.sdk-home }}/test/harness - run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - env: + - name: Test out-of-process + id: subprocess + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} run: ../scripts/ci/run-dotnet-tests.sh + - name: Test in-process smoke + if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/inprocess + DOTNET_TEST_FILTER: "FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication" + run: ../scripts/ci/run-dotnet-tests.sh - if: failure() uses: actions/upload-artifact@v7 with: @@ -257,25 +286,3 @@ jobs: path: ${{ inputs.sdk-home }}/dotnet/TestResults/ if-no-files-found: warn retention-days: 7 - - docs-dotnet: - name: "Docs (.NET)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - - uses: actions/setup-dotnet@v6 - with: - dotnet-version: "10.0.x" - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: dotnet restore -p:CopilotSkipCliDownload=true - working-directory: ${{ inputs.sdk-home }}/dotnet - - run: npm run extract && npm run validate:cs - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation diff --git a/.github/workflows/sdk-go.yml b/.github/workflows/sdk-go.yml index 24ef16a35b..e7235998f8 100644 --- a/.github/workflows/sdk-go.yml +++ b/.github/workflows/sdk-go.yml @@ -30,7 +30,7 @@ jobs: strategy: fail-fast: false matrix: - os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} + os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }} runs-on: ${{ matrix.os }} defaults: run: @@ -86,6 +86,17 @@ jobs: run: | go test -v -race -timeout=20m ./internal/e2e -run '^TestInProcessFfiE2E$' go test -v -race -timeout=20m ./internal/e2e -run '^TestClientE2E$/^should_use_configured_github_host_for_authentication$' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' + uses: actions/setup-node@v6 + with: + node-version: 22 + - name: Validate documentation examples + if: github.event_name != 'merge_group' && runner.os == 'Linux' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + run: | + npm ci + npm run extract + npm run validate:go go-musl-x64: name: "Go (Alpine x64)" @@ -138,24 +149,3 @@ jobs: go test -v -race -timeout=20m ./internal/e2e -run '^TestInProcessFfiE2E$' || status=$? go test -v -race -timeout=20m ./internal/e2e -run '^TestClientE2E$/^should_use_configured_github_host_for_authentication$' || status=$? exit "$status" - - docs-go: - name: "Docs (Go)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - - uses: actions/setup-go@v6 - with: - go-version: "1.24" - cache-dependency-path: ${{ inputs.sdk-home }}/go/go.sum - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: npm run extract && npm run validate:go - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation diff --git a/.github/workflows/sdk-java.yml b/.github/workflows/sdk-java.yml index 408298d88c..05579757be 100644 --- a/.github/workflows/sdk-java.yml +++ b/.github/workflows/sdk-java.yml @@ -1,5 +1,5 @@ -# Invoked by sdk.yml to run full subprocess Java SDK coverage on supported JDKs, -# focused in-process smoke coverage across native classifiers, and documentation. +# Invoked by sdk.yml to run full subprocess Java SDK coverage followed by focused +# in-process smoke on standard platforms and x64 musl, plus JDK 17 and docs checks. name: "SDK Java" env: @@ -24,13 +24,19 @@ permissions: jobs: java: - name: "Java (JDK ${{ matrix.test-jdk }})" - if: inputs.platform == 'all' || inputs.platform == 'linux-x64' + name: "Java (${{ matrix.os }}, JDK ${{ matrix.test-jdk }})" + if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) strategy: fail-fast: false matrix: + os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }} test-jdk: ["25", "17"] - runs-on: ubuntu-latest + exclude: + - os: macos-26 + test-jdk: "17" + - os: windows-latest + test-jdk: "17" + runs-on: ${{ matrix.os }} defaults: run: shell: bash @@ -42,7 +48,7 @@ jobs: persist-credentials: false - uses: ./.github/actions/download-sdk-runtime with: - artifact-name: executable-sdk-Linux-X64-gnu + artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -56,13 +62,15 @@ jobs: node-version: 22 # TEMPORARY MERGE QUEUE REDUCTION: pull requests and main pushes retain # static checks; merge groups rerun only SDK compatibility coverage. - - if: github.event_name != 'merge_group' && matrix.test-jdk == '25' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25' run: ./scripts/test-update-documentation-versions.sh - run: ./mvnw test-compile jar:jar - - if: github.event_name != 'merge_group' && matrix.test-jdk == '25' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25' run: ./mvnw javadoc:javadoc - - if: github.event_name != 'merge_group' && matrix.test-jdk == '25' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25' run: ./mvnw spotless:check + - if: runner.os == 'Windows' + run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - if: matrix.test-jdk == '25' id: subprocess env: @@ -94,10 +102,19 @@ jobs: -Dsurefire.failIfNoSpecifiedTests=false \ -Dit.test=InProcessTransportIT,AuthHostE2ETest \ -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" + - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25' + run: ./mvnw install -Dmaven.test.skip=true -Dskip.test.harness=true -Dcopilot.native.skip.download=true + - name: Validate documentation examples + if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + run: | + npm ci + npm run extract + npm run validate:java - if: failure() uses: actions/upload-artifact@v7 with: - name: java-test-results-jdk-${{ matrix.test-jdk }} + name: java-test-results-${{ matrix.os }}-jdk-${{ matrix.test-jdk }} path: | ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports/ ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports-isolated/ @@ -105,18 +122,11 @@ jobs: ${{ inputs.sdk-home }}/java/sdk/target/subprocess-reports/ retention-days: 7 - java-inprocess: - name: "Java (inprocess, ${{ matrix.classifier }})" - if: contains(fromJSON('["all","darwin-arm64","win32-x64"]'), inputs.platform) - strategy: - fail-fast: false - matrix: - include: ${{ fromJSON(inputs.platform == 'darwin-arm64' && '[{"os":"macos-26","classifier":"darwin-arm64"}]' || inputs.platform == 'win32-x64' && '[{"os":"windows-latest","classifier":"win32-x64"}]' || '[{"os":"windows-latest","classifier":"win32-x64"},{"os":"macos-26","classifier":"darwin-arm64"}]') }} - runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash - working-directory: ${{ inputs.sdk-home }}/java + java-musl-x64: + name: "Java (JDK 25, linuxmusl-x64)" + if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' + runs-on: ubuntu-latest + timeout-minutes: 30 steps: - uses: actions/checkout@v7 timeout-minutes: 4 @@ -124,40 +134,59 @@ jobs: persist-credentials: false - uses: ./.github/actions/download-sdk-runtime with: - artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} + artifact-name: executable-sdk-Linux-X64-musl path: ${{ github.workspace }} - - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare + - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . - - uses: actions/setup-java@v5 - with: - java-version: "25" - distribution: microsoft - cache: maven - - uses: actions/setup-node@v6 + - uses: ./.github/actions/run-alpine-tests + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} with: - node-version: 22 - - env: - CI: "true" - run: >- - ./mvnw clean verify -Pinprocess - -Dtest=NoTestsForInProcessSmoke - -Dsurefire.failIfNoSpecifiedTests=false - -Dit.test=InProcessTransportIT,AuthHostE2ETest - -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" + image: eclipse-temurin:25-jdk-alpine + sdk-root: /workspace/${{ inputs.sdk-home }} + workdir: /workspace/${{ inputs.sdk-home }}/java + command: | + apk add --no-cache java-cacerts maven nodejs npm + export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts + export GITHUB_ENV=/tmp/copilot-sdk-runtime.env + export COPILOT_RUNTIME_TARGET=linuxmusl-x64 + export COPILOT_RUNTIME_OUTPUT_DIRECTORY=/workspace/.sdk-runtime/linuxmusl-x64 + node "$COPILOT_SDK_ROOT/scripts/ci/runtime-artifact.mjs" prepare + set -a + . "$GITHUB_ENV" + set +a + node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 + ./mvnw test-compile jar:jar -Dcopilot.native.libc=musl + status=0 + ./mvnw -pl sdk verify -Dskip.test.harness=true -Dcopilot.cli.path="$COPILOT_CLI_PATH" || status=$? + mkdir -p sdk/target/subprocess-reports + for reports in surefire-reports surefire-reports-isolated failsafe-reports; do + if [ -d "sdk/target/$reports" ]; then + mv "sdk/target/$reports" sdk/target/subprocess-reports/ + fi + done + ./mvnw verify -Pinprocess \ + -Dskip.test.harness=true \ + -Dtest=NoTestsForInProcessSmoke \ + -Dsurefire.failIfNoSpecifiedTests=false \ + -Dit.test=InProcessTransportIT,AuthHostE2ETest \ + -Dcopilot.native.libc=musl \ + -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" || status=$? + exit "$status" - if: failure() uses: actions/upload-artifact@v7 with: - name: java-test-results-inprocess-${{ matrix.classifier }} + name: java-test-results-linuxmusl-x64 path: | ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports/ ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports-isolated/ ${{ inputs.sdk-home }}/java/sdk/target/failsafe-reports/ + ${{ inputs.sdk-home }}/java/sdk/target/subprocess-reports/ retention-days: 7 - - java-inprocess-musl-x64: - name: "Java (inprocess, linuxmusl-x64)" - if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' - runs-on: ubuntu-latest + java-musl-arm64: + name: "Java (JDK 25, linuxmusl-arm64)" + if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-arm64' + runs-on: ubuntu-24.04-arm timeout-minutes: 30 steps: - uses: actions/checkout@v7 @@ -166,71 +195,59 @@ jobs: persist-credentials: false - uses: ./.github/actions/download-sdk-runtime with: - artifact-name: executable-sdk-Linux-X64-musl + artifact-name: executable-sdk-Linux-ARM64-musl path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . - - name: Run Java SDK tests (InProcess, musl) + - name: Run Java SDK tests (linuxmusl-arm64) env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} run: | - docker run --rm \ + docker run --rm --init \ --volume "$GITHUB_WORKSPACE:/workspace" \ --workdir "/workspace/$SDK_HOME/java" \ --env CI=true \ --env COPILOT_HMAC_KEY \ --env GITHUB_ACTIONS=true \ --env GITHUB_WORKSPACE=/workspace \ - --env SDK_HOME="/workspace/$SDK_HOME" \ + --env COPILOT_SDK_ROOT="/workspace/$SDK_HOME" \ eclipse-temurin:25-jdk-alpine \ sh -c 'set -eux - apk add --no-cache git java-cacerts maven nodejs npm - export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts + apk add --no-cache bash git java-cacerts maven nodejs npm git config --global --add safe.directory /workspace + export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts export GITHUB_ENV=/tmp/copilot-sdk-runtime.env - export COPILOT_RUNTIME_TARGET=linuxmusl-x64 - export COPILOT_RUNTIME_OUTPUT_DIRECTORY=/workspace/.sdk-runtime/linuxmusl-x64 - node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare + export COPILOT_RUNTIME_TARGET=linuxmusl-arm64 + export COPILOT_RUNTIME_OUTPUT_DIRECTORY=/workspace/.sdk-runtime/linuxmusl-arm64 + node "$COPILOT_SDK_ROOT/scripts/ci/runtime-artifact.mjs" prepare set -a . "$GITHUB_ENV" set +a - node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 - ./mvnw clean verify -Pinprocess \ + node copilot-native/scripts/validate-native-host.mjs linuxmusl-arm64 + ./mvnw test-compile jar:jar -Dcopilot.native.libc=musl + status=0 + ./mvnw -pl sdk verify -Dskip.test.harness=true -Dcopilot.cli.path="$COPILOT_CLI_PATH" || status=$? + mkdir -p sdk/target/subprocess-reports + for reports in surefire-reports surefire-reports-isolated failsafe-reports; do + if [ -d "sdk/target/$reports" ]; then + mv "sdk/target/$reports" sdk/target/subprocess-reports/ + fi + done + ./mvnw verify -Pinprocess \ + -Dskip.test.harness=true \ -Dtest=NoTestsForInProcessSmoke \ -Dsurefire.failIfNoSpecifiedTests=false \ -Dit.test=InProcessTransportIT,AuthHostE2ETest \ -Dcopilot.native.libc=musl \ - -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH"' + -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" || status=$? + exit "$status"' - if: failure() uses: actions/upload-artifact@v7 with: - name: java-test-results-inprocess-linuxmusl-x64 + name: java-test-results-linuxmusl-arm64 path: | ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports/ ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports-isolated/ ${{ inputs.sdk-home }}/java/sdk/target/failsafe-reports/ + ${{ inputs.sdk-home }}/java/sdk/target/subprocess-reports/ retention-days: 7 - - docs-java: - name: "Docs (Java)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - - uses: actions/setup-java@v5 - with: - distribution: microsoft - java-version: "25" - cache: maven - - run: ./mvnw install -Dmaven.test.skip=true -Dskip.test.harness=true -Dcopilot.native.skip.download=true - working-directory: ${{ inputs.sdk-home }}/java - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: npm run extract && npm run validate:java - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation diff --git a/.github/workflows/sdk-nodejs.yml b/.github/workflows/sdk-nodejs.yml index 315331609b..9fbbf97e76 100644 --- a/.github/workflows/sdk-nodejs.yml +++ b/.github/workflows/sdk-nodejs.yml @@ -19,6 +19,11 @@ on: sdk-home: required: true type: string + outputs: + capi-result: + description: "Standard-platform CAPI job result, independent of BYOK jobs" + # Direct result outputs can be empty: https://github.com/actions/runner/issues/2495 + value: ${{ fromJSON(toJSON(jobs.nodejs)).result }} permissions: contents: read @@ -30,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} + os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }} runs-on: ${{ matrix.os }} defaults: run: @@ -89,13 +94,13 @@ jobs: env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} run: | - if [ "$GITHUB_EVENT_NAME" = "merge_group" ]; then + if [ "$GITHUB_EVENT_NAME" = "merge_group" ] || [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then npm test -- --reporter=default --reporter=json --outputFile="$RUNNER_TEMP/sdk-nodejs-results.json" else npm test fi - - name: Upload merge-queue Node.js test results - if: always() && github.event_name == 'merge_group' && runner.os == 'Linux' + - name: Upload Flake Finder Node.js test results + if: always() && (github.event_name == 'merge_group' || github.event_name == 'pull_request') && runner.os == 'Linux' continue-on-error: true uses: actions/upload-artifact@v7 with: @@ -109,6 +114,13 @@ jobs: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} COPILOT_SDK_DEFAULT_CONNECTION: inprocess run: npm test -- test/e2e/inprocess_ffi.e2e.test.ts test/e2e/auth_host.e2e.test.ts + - name: Validate documentation examples + if: github.event_name != 'merge_group' && runner.os == 'Linux' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + run: | + npm ci + npm run extract + npm run validate:ts # Keep each BYOK backend on its own runner; shared runtime E2E coverage belongs # in TypeScript rather than repeating these sweeps across SDK languages. @@ -197,24 +209,3 @@ jobs: npm test || status=$? COPILOT_SDK_DEFAULT_CONNECTION=inprocess npm test -- test/e2e/inprocess_ffi.e2e.test.ts test/e2e/auth_host.e2e.test.ts || status=$? exit "$status" - - docs-typescript: - name: "Docs (TypeScript)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: npm - cache-dependency-path: ${{ inputs.sdk-home }}/nodejs/package-lock.json - - run: npm ci --ignore-scripts - working-directory: ${{ inputs.sdk-home }}/nodejs - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: npm run extract && npm run validate:ts - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation diff --git a/.github/workflows/sdk-platform.yml b/.github/workflows/sdk-platform.yml index b21a9106fc..f1ed4e6ed4 100644 --- a/.github/workflows/sdk-platform.yml +++ b/.github/workflows/sdk-platform.yml @@ -16,6 +16,9 @@ on: description: "Node.js SDK result, independent of other languages" # Direct result outputs can be empty: https://github.com/actions/runner/issues/2495 value: ${{ fromJSON(toJSON(jobs.nodejs)).result }} + nodejs-capi-result: + description: "Standard-platform Node.js CAPI result, independent of BYOK and other languages" + value: ${{ jobs.nodejs.outputs.capi-result }} permissions: contents: read diff --git a/.github/workflows/sdk-python.yml b/.github/workflows/sdk-python.yml index 71bedf2eaa..2abeca3d8a 100644 --- a/.github/workflows/sdk-python.yml +++ b/.github/workflows/sdk-python.yml @@ -30,7 +30,7 @@ jobs: strategy: fail-fast: false matrix: - os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} + os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }} runs-on: ${{ matrix.os }} timeout-minutes: 20 defaults: @@ -83,6 +83,14 @@ jobs: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} COPILOT_SDK_DEFAULT_CONNECTION: inprocess run: uv run --locked pytest -v -s e2e/test_inprocess_ffi_e2e.py e2e/test_auth_host_e2e.py + - name: Validate documentation examples + if: github.event_name != 'merge_group' && runner.os == 'Linux' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + run: | + npm ci + npm run extract + uv run --locked --python 3.12 --project "$GITHUB_WORKSPACE/$SDK_HOME/python" python3 -m mypy --version + uv run --locked --python 3.12 --project "$GITHUB_WORKSPACE/$SDK_HOME/python" npm run validate:py - name: Upload Python test diagnostics if: failure() uses: actions/upload-artifact@v7 @@ -149,29 +157,3 @@ jobs: include-hidden-files: true if-no-files-found: warn retention-days: 7 - - docs-python: - name: "Docs (Python)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - - uses: actions/setup-python@v6 - with: - python-version: "3.12" - - uses: astral-sh/setup-uv@v7 - - run: uv sync --locked - working-directory: ${{ inputs.sdk-home }}/python - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: npm run extract - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: uv run --locked --project "$GITHUB_WORKSPACE/$SDK_HOME/python" python3 -m mypy --version - - run: uv run --locked --project "$GITHUB_WORKSPACE/$SDK_HOME/python" npm run validate:py - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation diff --git a/.github/workflows/sdk-rust.yml b/.github/workflows/sdk-rust.yml index 5ab3134139..abe13e28bb 100644 --- a/.github/workflows/sdk-rust.yml +++ b/.github/workflows/sdk-rust.yml @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: - os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} + os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26-xlarge"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26-xlarge","windows-latest"]') }} runs-on: ${{ matrix.os }} defaults: run: @@ -106,7 +106,7 @@ jobs: - name: Test in-process smoke id: inprocess - if: ${{ !cancelled() && runner.os != 'Windows' && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} COPILOT_SDK_DEFAULT_CONNECTION: inprocess diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index b6314e62f6..56c165db89 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -27,8 +27,150 @@ permissions: contents: read jobs: + detect-sdk-changes: + name: "Detect SDK-impacting changes" + runs-on: ubuntu-slim + outputs: + has_sdk_changes: ${{ steps.full-coverage.outputs.has_sdk_changes || steps.classify.outputs.has_sdk_changes || steps.recover.outputs.has_sdk_changes }} + steps: + - name: Require full coverage outside pull requests + id: full-coverage + if: github.event_name != 'pull_request' + run: echo "has_sdk_changes=true" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v7 + id: checkout + if: github.event_name == 'pull_request' + continue-on-error: true + timeout-minutes: 4 + with: + # Include the synthetic merge commit's base parent without fetching + # repository history. + fetch-depth: 2 + persist-credentials: false + + - name: Detect repository layout + id: repository-layout + if: steps.checkout.outcome == 'success' + continue-on-error: true + run: | + trusted_revision="$(git rev-parse "$GITHUB_SHA^1")" + echo "base-sha=$trusted_revision" >> "$GITHUB_OUTPUT" + if git cat-file -e "${trusted_revision}:src/sdk/package.json" 2>/dev/null && + git cat-file -e "${trusted_revision}:script/sea-build.ts" 2>/dev/null; then + echo "layout=runtime" >> "$GITHUB_OUTPUT" + elif git cat-file -e "${trusted_revision}:package.json" 2>/dev/null && + git cat-file -e "${trusted_revision}:nodejs/package.json" 2>/dev/null; then + echo "layout=standalone" >> "$GITHUB_OUTPUT" + else + echo "::warning::Unable to identify the SDK repository layout; requiring full coverage." + exit 1 + fi + + - name: Find SDK-impacting paths + id: filter + if: steps.repository-layout.outcome == 'success' + continue-on-error: true + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 + with: + base_sha: ${{ steps.repository-layout.outputs.base-sha }} + sha: ${{ github.sha }} + skip_initial_fetch: true + fail_on_initial_diff_error: true + quotepath: false + files_yaml: | + runtime: + - .github/actions/** + - .github/workflows/sdk*.yml + - .github/workflows/sdk*.yaml + - .github/workflows/shared-cli-build.yml + - .github/workflows/publish.yml + - .editorconfig + - .gitattributes + - .nvmrc + - package.json + - package-lock.json + - pnpm-lock.yaml + - sdk-protocol-version.json + - assets/** + - BUILD.bazel + - src/** + - generated/** + - schema/** + - files/** + - third_party/** + - patches/** + - tools/bazel/** + - .cargo/** + - bazel/** + - script/** + - .bazelignore + - .bazelrc + - .bazelversion + - Cargo.lock + - Cargo.toml + - MODULE.bazel + - MODULE.bazel.lock + - pnpm-workspace.yaml + - rust-toolchain.toml + - esbuild.ts + - tsconfig*.json + standalone: + - .github/actions/** + - .github/workflows/sdk*.yml + - .github/workflows/sdk*.yaml + - .github/workflows/shared-cli-build.yml + - .github/workflows/publish.yml + - .editorconfig + - .gitattributes + - .nvmrc + - package.json + - package-lock.json + - pnpm-lock.yaml + - sdk-protocol-version.json + - assets/** + - BUILD.bazel + - nodejs/** + - python/** + - go/** + - dotnet/** + - java/** + - rust/** + - scripts/** + - samples/** + - test/** + - docs/** + - README.md + - vitest.config.ts + + - name: Select layout-specific result + id: classify + if: steps.filter.outcome == 'success' + env: + LAYOUT: ${{ steps.repository-layout.outputs.layout }} + RUNTIME_CHANGED: ${{ steps.filter.outputs.runtime_any_modified }} + STANDALONE_CHANGED: ${{ steps.filter.outputs.standalone_any_modified }} + run: | + if [[ "$LAYOUT" == "runtime" ]]; then + has_sdk_changes="$RUNTIME_CHANGED" + elif [[ "$LAYOUT" == "standalone" ]]; then + has_sdk_changes="$STANDALONE_CHANGED" + else + exit 1 + fi + echo "has_sdk_changes=$has_sdk_changes" >> "$GITHUB_OUTPUT" + + - name: Require full coverage after detection errors + id: recover + if: github.event_name == 'pull_request' && (steps.checkout.outcome != 'success' || steps.repository-layout.outcome != 'success' || steps.filter.outcome != 'success' || steps.classify.outcome != 'success') + run: | + echo "::warning::SDK change detection failed; requiring full coverage." + echo "has_sdk_changes=true" >> "$GITHUB_OUTPUT" + detect-layout: name: "Detect SDK layout" + needs: detect-sdk-changes + if: ${{ !cancelled() && (needs.detect-sdk-changes.result != 'success' || needs.detect-sdk-changes.outputs.has_sdk_changes == 'true') }} runs-on: ubuntu-latest outputs: runtime-source: ${{ steps.detect.outputs.runtime-source }} @@ -233,6 +375,26 @@ jobs: timeout-minutes: 60 secrets: inherit + build-runtime-linuxmusl-arm64: + name: "Build runtime (linuxmusl-arm64)" + needs: detect-layout + if: github.event_name != 'merge_group' + permissions: + actions: read + contents: read + id-token: write + packages: read + uses: ./.github/workflows/sdk-runtime-artifact.yml + with: + runtime-source: ${{ needs.detect-layout.outputs.runtime-source }} + sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} + os: ubuntu-latest-xl + target: linuxmusl-arm64 + artifact: Linux-ARM64-musl + bazel-config: e2e-no-lto + timeout-minutes: 60 + secrets: inherit + build-runtime-darwin-arm64: name: "Build runtime (darwin-arm64)" needs: detect-layout @@ -246,7 +408,7 @@ jobs: with: runtime-source: ${{ needs.detect-layout.outputs.runtime-source }} sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} - os: macos-latest-xlarge + os: macos-26-xlarge target: darwin-arm64 artifact: macOS-ARM64 bazel-config: e2e-no-lto @@ -296,6 +458,16 @@ jobs: sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} secrets: inherit + sdk-linuxmusl-arm64: + name: "Linux musl ARM64 (Java)" + needs: [detect-layout, build-runtime-linuxmusl-arm64] + if: github.event_name != 'merge_group' + uses: ./.github/workflows/sdk-java.yml + with: + platform: linuxmusl-arm64 + sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} + secrets: inherit + sdk-darwin-arm64: name: "macOS ARM64" needs: [detect-layout, build-runtime-darwin-arm64] @@ -317,40 +489,35 @@ jobs: sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} secrets: inherit - sdk-docs: - name: "Documentation" - needs: detect-layout - if: github.event_name != 'merge_group' - uses: ./.github/workflows/sdk-platform.yml - with: - platform: docs - sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} - secrets: inherit - + # Only Linux CAPI gates this rollup; the full SDK aggregate still reports all coverage. sdk-typescript: name: sdk-typescript if: always() - needs: [sdk-linux-x64, sdk-linuxmusl-x64, sdk-darwin-arm64, sdk-win32-x64] + needs: [detect-sdk-changes, sdk-linux-x64] runs-on: ubuntu-slim steps: - - name: Check Node.js SDK results + - name: Check Linux CAPI Node.js SDK result env: + DETECT_RESULT: ${{ needs.detect-sdk-changes.result }} EVENT_NAME: ${{ github.event_name }} + HAS_SDK_CHANGES: ${{ needs.detect-sdk-changes.outputs.has_sdk_changes }} RESULTS: ${{ toJSON(needs) }} run: | - for job in sdk-linux-x64 sdk-linuxmusl-x64 sdk-darwin-arm64 sdk-win32-x64; do - expected=success - result=$(jq -r --arg job "$job" '.[$job].outputs["nodejs-result"]' <<< "$RESULTS") - # TEMPORARY MERGE QUEUE REDUCTION: only Linux x64 runs in merge groups. - if [[ "$EVENT_NAME" == "merge_group" && "$job" != "sdk-linux-x64" ]]; then - expected=skipped - result=$(jq -r --arg job "$job" '.[$job].result' <<< "$RESULTS") - fi - if [[ "$result" != "$expected" ]]; then - echo "::error::Node.js SDK job $job: expected $expected, got $result" + linux_result=$(jq -r '.["sdk-linux-x64"].result' <<< "$RESULTS") + if [[ "$EVENT_NAME" == "pull_request" && "$DETECT_RESULT" == "success" && "$HAS_SDK_CHANGES" == "false" ]]; then + if [[ "$linux_result" != "skipped" ]]; then + echo "::error::Non-SDK pull request did not skip sdk-linux-x64: $linux_result" exit 1 fi - done + echo "No SDK-impacting changes; Linux CAPI Node.js SDK job skipped" + exit 0 + fi + + result=$(jq -r '.["sdk-linux-x64"].outputs["nodejs-capi-result"]' <<< "$RESULTS") + if [[ "$result" != "success" ]]; then + echo "::error::Linux CAPI Node.js SDK job: expected success, got $result" + exit 1 + fi check-freshness: name: "Check schema and SDK freshness" @@ -433,26 +600,45 @@ jobs: name: "SDK" if: always() needs: + - detect-sdk-changes - build-runtime-windows-x64-addons - build-runtime-linux-x64 - build-runtime-linuxmusl-x64 + - build-runtime-linuxmusl-arm64 - build-runtime-darwin-arm64 - build-runtime-win32-x64 - detect-layout - sdk-linux-x64 - sdk-linuxmusl-x64 + - sdk-linuxmusl-arm64 - sdk-darwin-arm64 - sdk-win32-x64 - - sdk-docs - check-freshness runs-on: ubuntu-slim steps: - name: Check SDK results env: + DETECT_RESULT: ${{ needs.detect-sdk-changes.result }} EVENT_NAME: ${{ github.event_name }} + HAS_SDK_CHANGES: ${{ needs.detect-sdk-changes.outputs.has_sdk_changes }} RESULTS: ${{ toJSON(needs) }} RUNTIME_SOURCE: ${{ needs.detect-layout.outputs.runtime-source }} run: | + if [[ "$EVENT_NAME" == "pull_request" && "$DETECT_RESULT" == "success" && "$HAS_SDK_CHANGES" == "false" ]]; then + unexpected=$(jq -r ' + to_entries[] + | select(.key != "detect-sdk-changes" and .value.result != "skipped") + | "\(.key): \(.value.result)" + ' <<< "$RESULTS") + if [[ -n "$unexpected" ]]; then + echo "::error::Non-SDK pull request did not skip all SDK jobs:" + echo "$unexpected" + exit 1 + fi + echo "No SDK-impacting changes; SDK build and test matrix skipped" + exit 0 + fi + # TEMPORARY MERGE QUEUE REDUCTION: keep this branch aligned with the # gated jobs above until the full merge-group matrix is restored. if [[ "$EVENT_NAME" == "merge_group" ]]; then @@ -465,12 +651,13 @@ jobs: skipped_jobs=( build-runtime-windows-x64-addons build-runtime-linuxmusl-x64 + build-runtime-linuxmusl-arm64 build-runtime-darwin-arm64 build-runtime-win32-x64 sdk-linuxmusl-x64 + sdk-linuxmusl-arm64 sdk-darwin-arm64 sdk-win32-x64 - sdk-docs ) for job in "${required_jobs[@]}"; do if [[ "$(jq -r --arg job "$job" '.[$job].result' <<< "$RESULTS")" != "success" ]]; then @@ -501,6 +688,7 @@ jobs: failures=$(jq -r --arg runtime_source "$RUNTIME_SOURCE" ' to_entries[] | select(.value.result != "success") + | select(.key != "detect-sdk-changes") | select( $runtime_source != "published" or .key != "build-runtime-windows-x64-addons" diff --git a/BUILD.bazel b/BUILD.bazel index e25b8ef8e3..4fa953fbf5 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -4,6 +4,7 @@ SHARED_CODEGEN_INPUTS = [ "scripts/codegen/legacy-parameters.ts", "scripts/codegen/package-lock.json", "scripts/codegen/package.json", + "scripts/codegen/extensible-enums.ts", "scripts/codegen/utils.ts", "scripts/runtime-layout.mjs", "scripts/runtime-release.mjs", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e64a0bb04..74519107b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -316,7 +316,12 @@ additional feature/acquisition choices documented in [rust/AGENTS.md](rust/AGENT ### Documentation checks -API snippet validation is separate from SDK tests. From the SDK root: +[SDK CI](.github/workflows/sdk.yml) validates Node.js, Python, Go, .NET, and Java +API snippets after successful tests in their standard Linux jobs (JDK 25 for Java). +These checks run on pull requests, pushes to `main`, and manual workflow runs, +but not merge groups. A documentation failure fails the corresponding language job. + +To validate snippets without running SDK tests, run these commands from the SDK root: ```bash npm --prefix scripts/docs-validation ci @@ -327,8 +332,15 @@ uv run --locked --project python npm run docs:python ``` This extracts and validates snippets for Node.js, Python, Go, .NET, or Java. -Java's current docs validator calls `mvn` directly, -so this task also needs Maven 3.9+ on PATH. There is no `docs:rust` facade; +`docs:java` installs the SDK once through its Maven wrapper, with tests, replay +harness setup, and native downloads disabled, then compiles the snippets using +the same wrapper. CI supplies that installation in its existing post-test step; +the validator does not reinstall it. A separate Maven installation is not required. +Java validation also compiles the exact [README Quick Start](java/README.md#quick-start). +The regular Maven integration suite checks a standalone consumer JAR with a manifest +classpath and runtime dependencies, without a live model, credentials, or native runtime. +These checks replace the former agent-driven Java smoke workflow. +There is no `docs:rust` facade; follow [the Rust SDK workflow](.github/workflows/sdk-rust.yml) for rustdoc. ### Recording and replaying SDK tests @@ -344,12 +356,18 @@ For TypeScript SDK E2Es, use the existing `nodejs/test/e2e/harness/sdkTestContext.ts` fixture. In the runtime repository, also follow the `e2e-test-author` skill's SDK section. -SDK CI runs full subprocess coverage followed by a short in-process smoke -step on the same runner wherever both modes share a platform. Smoke still -runs if the subprocess tests fail, and either failure fails the job. Java's -macOS, Windows, and musl smoke jobs remain separate because they have no -matching subprocess job. Merge groups retain the reduced Linux TypeScript -CAPI subprocess coverage. +All six SDKs run full subprocess coverage followed by a short in-process +smoke step on the same runner on Linux/glibc x64, macOS ARM64, Windows x64, +and Linux/musl x64. Smoke still runs if the subprocess tests fail, and either +failure fails the job. Java uses JDK 25 on all four platforms, plus a +Linux/glibc JDK 17 compatibility job using precompiled classes. +Merge groups retain the reduced Linux TypeScript CAPI subprocess coverage. + +The `sdk-typescript` required rollup checks only the Linux CAPI job, including +its build, packaging, and applicable static checks. Other platforms, BYOK +backends, and languages keep their existing scheduling and failure reporting; +they do not gate this rollup. The full `SDK` aggregate still requires all +scheduled coverage to succeed. The three BYOK backend sweeps run in separate Linux TypeScript jobs, alongside the normal CAPI job; they do not repeat unit tests, packaging, or static @@ -390,6 +408,12 @@ failed-startup cleanup. Keep those lifecycle expectations aligned when changing an SDK transport; test watchdogs must allow all cleanup phases their separate budgets, rather than treating the graceful-exit timeout as a total shutdown cap. +Java also exercises the opt-in `shutdown-error` mode. It rejects +`runtime.shutdown`, writes `.eof` when stdin closes, and holds +cleanup-marker creation and process exit until the test creates +`.release`. This handshake verifies that a failed shutdown RPC +still allows EOF-driven finalization before forced cleanup. + The shared harness records real inference responses under `test/snapshots`. Record new captures with `GITHUB_TOKEN` set and `GITHUB_ACTIONS` unset; never author model responses by hand. Rerun with `GITHUB_ACTIONS=true` and real diff --git a/docs/developer-docs/secrets.md b/docs/developer-docs/secrets.md index 1218f96254..b09ddd213c 100644 --- a/docs/developer-docs/secrets.md +++ b/docs/developer-docs/secrets.md @@ -17,7 +17,7 @@ These secrets are used by the authoritative SDK build/test workflow. These secrets power the GitHub Agentic Workflows (gh-aw) used for issue triage, code generation, and release automation. * **`COPILOT_GITHUB_TOKEN`**: GitHub OAuth token consumed by the Copilot CLI for AI authentication. Required by all agentic workflows when invoking `copilot` for AI inference. - * Workflows: `issue-triage.lock.yml`, `issue-classification.lock.yml`, `handle-bug.lock.yml`, `handle-enhancement.lock.yml`, `handle-question.lock.yml`, `handle-documentation.lock.yml`, `java-codegen-check.yml`, `java-codegen-fix.lock.yml`, `java-smoke-test.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml`, `cross-repo-issue-analysis.lock.yml` + * Workflows: `issue-triage.lock.yml`, `issue-classification.lock.yml`, `handle-bug.lock.yml`, `handle-enhancement.lock.yml`, `handle-question.lock.yml`, `handle-documentation.lock.yml`, `java-codegen-check.yml`, `java-codegen-fix.lock.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml`, `cross-repo-issue-analysis.lock.yml` * **`GH_AW_GITHUB_TOKEN`**: Optional GitHub token override for repository operations (reading code, creating pull requests, and making GitHub API calls). If unset, workflows use the automatic `GITHUB_TOKEN`. * Workflows: `issue-triage.lock.yml`, `issue-classification.lock.yml`, `handle-bug.lock.yml`, `handle-enhancement.lock.yml`, `handle-question.lock.yml`, `handle-documentation.lock.yml`, `java-codegen-fix.lock.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml`, `cross-repo-issue-analysis.lock.yml` diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md index 302844b347..d3826e4f2b 100644 --- a/docs/features/session-persistence.md +++ b/docs/features/session-persistence.md @@ -131,6 +131,34 @@ await session.SendAndWaitAsync(new MessageOptions { Prompt = "Analyze my codebas Later—minutes, hours, or even days—you can resume the session from where you left off. +### Remembered permissions with a session filesystem + +When you configure a custom `sessionFs` provider, runtime-owned durable permission choices use provider storage rather than the runtime host's configuration. The files live under the provider's `sessionStatePath` and retain their existing formats: + +* `permissions.json`: A top-level `locations` map retaining the existing `tool_approvals` and `allowed_directories` fields +* `settings.json`: Top-level `allowedUrls` for `approve-permanently` URL approvals and `sandbox` for saved sandbox preferences +* `config.json`: Top-level `trustedFolders` for remembered folder trust and `sandboxOnboardingShown` for the first-enable sandbox message + +Location keys are normalized provider directory paths, such as `/workspace`, without an added prefix. The runtime resolves them lexically using provider path conventions, without inspecting the host filesystem or discovering host Git repositories. An approval saved for `/workspace` is not automatically selected when the working directory is `/workspace/sub`. + +Provider-backed folder trust also uses lexical paths: a trusted folder covers itself and its descendants, not linked Git worktrees at other paths. Trust those worktree paths explicitly. + +There is no additional session-ID partition inside these files. Sessions that access the same backing files share remembered choices. Use separate provider namespaces when sessions or tenants must not share grants. With a namespace per session, permanent choices survive that session's resumes but do not automatically carry over to new sessions. To restore a session's persisted choices on resume, reconnect it to the same namespace. + +Consent updates are serialized per provider endpoint and file within one runtime process. Separate endpoints do not block each other. If different provider endpoints or runtime processes share backing files, the application must coordinate concurrent updates; the runtime cannot infer that those files are shared. + +These operations never import, update, or fall back to the host's `permissions-config.json`, `settings.json`, or `config.json`, including when provider reads or writes fail. Existing-format files supplied through the provider are read directly. + +As with host configuration, URL approvals are read only from `settings.json`. A legacy `allowedUrls` field in `config.json` is ignored; `config.json` remains the global-state file for remembered folder trust and the sandbox onboarding marker. + +Storage routing does not change permission lifetimes, URL denial precedence, or session policy behavior. Approve-once and approve-for-session do not become permanent grants. + +Saved sandbox preferences are restored on create and resume. Later explicit SDK `options.update` calls can change the live sandbox configuration without rewriting the saved preference. + +Without `sessionFs`, SDK applications in both `empty` and `copilot-cli` modes retain existing host-backed persistence. This includes the existing behavior where permanent URL consent saves host settings and grants access in the live session, but a fresh SDK session does not automatically load the host URL allowlist. + +See [Multi-tenancy and server deployments](../setup/multi-tenancy.md#sessionfs) for provider isolation responsibilities. + ### Transcript recovery The resume option `allowTranscriptRecovery` controls recovery when the runtime loads a @@ -283,7 +311,17 @@ When resuming a session, you can optionally reconfigure many settings. This is u With `model: "auto"`, the optional `capi.autoTier` setting selects an Auto routing preference: `efficiency`, `balance`, `intelligence`, or `fast`. In Python, use `capi={"auto_tier": "balance"}`. This setting applies to V2 Auto routing; V1 Auto requests are unchanged. -`fast` is an integrator-only latency preset, not a first-party GitHub Copilot product preference. The SDK does not decide Fast eligibility, inspect client identity, choose it as a default, or fall back to another tier when a runtime does not support it—an older runtime returns its native error unchanged. +When the runtime's default-off `DYNAMIC_AUTO_TIERS` feature is enabled, the session's `model.list` RPC also returns optional `auto` metadata from the provider's `/meta` endpoint. Use the enabled descriptors with `type: "auto"` to discover additional supported tier identifiers, their display names, descriptions, and ordering. The existing selection methods and JSON fields accept these identifiers. A discovery failure does not fall back to `/models`, and a successful response without `auto` metadata provides no selectable Auto tiers. + +Treat tier identifiers as extensible values, not a closed enumeration. Existing named values remain available; for additional identifiers, use strings in Node.js and Python, `AutoTier("premium-v2")` in Go, `new AutoTier("premium-v2")` in .NET, `AutoTier::Custom` in Rust, or `AutoTier.fromValue` in Java. The provider's `defaultTier` describes its default routing without creating a committed SDK preference. + +If a persisted tier is removed or disabled, resume preserves the explicit preference rather than replacing it with the provider's default. Select an available replacement before activating Auto routing. + +The CLI also preserves settings-derived preferences during startup, even when a tier is unavailable or dynamic discovery is disabled. You can then select a replacement in the model picker. Explicit SDK create and resume preferences are still validated before the operation succeeds, and Auto activation always validates availability against the resolved discovery mode. + +When a create or resume request includes `expAssignments`, the runtime installs those assignments before validating an explicit Auto preference. Validation, discovery, and activation use the same resolved feature decision. + +`fast` is an integrator-only latency preset, not a first-party GitHub Copilot product preference. Fast is not validated against the `/meta` tier catalog when configuring a session. The SDK does not decide Fast eligibility, inspect client identity, choose it as a default, or fall back to another tier when a runtime does not support it—an older runtime returns its native error unchanged. The runtime persists the selected tier, so applications do not need to resend it on every resume: @@ -309,6 +347,8 @@ if (result.status === "pending") { The runtime does not apply the preference immediately. It records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider. A `pending` status therefore confirms that the request was accepted, not that it took effect. Only the most recent request survives: a new request replaces any earlier one that no turn has claimed yet. +If an older request is still validating when a newer preference request or reset arrives, the older call fails with a superseded-request error instead of replacing the newer intent. + Watch for the outcome through these events: * `session.model_change` when the preference commits. diff --git a/docs/features/skills.md b/docs/features/skills.md index 30c5112dbd..df8d319928 100644 --- a/docs/features/skills.md +++ b/docs/features/skills.md @@ -336,6 +336,333 @@ The frontmatter fields: The markdown body contains the instructions that are injected into the session context when the skill is loaded. +## Skill providers (experimental) + +> [!NOTE] +> Skill providers are experimental. The API can change in future SDK releases. + +A skill provider serves skills from your application's own storage, such as a database in a multi-tenant service, instead of `SKILL.md` files on disk. Provider skills join the session's skill catalog alongside file-based skills. The model loads them on demand through the `skill` tool, and users can invoke them like any other skill. + +A provider implements two operations: + +* **List skills**: returns catalog metadata for every skill: a `name`, a `description`, and optionally `userInvocable`, `disableModelInvocation`, and `argumentHint`. The runtime calls it when it loads the session's skills. +* **Read skill**: returns the markdown for one skill, or a not-found result if the skill no longer exists. The runtime calls it only when the skill is loaded. + +Pass the provider when you create or resume a session: + +
+Node.js / TypeScript + +```typescript +import { approveAll, CopilotClient, type SkillProvider } from "@github/copilot-sdk"; + +const releaseSkills = new Map([ + [ + "release-notes", + { + description: "Writes release notes in the team's format.", + markdown: "Group changes by feature area and link each pull request.", + }, + ], +]); + +const skillProvider: SkillProvider = { + listSkills: () => + [...releaseSkills].map(([name, skill]) => ({ name, description: skill.description })), + readSkill: (name) => releaseSkills.get(name)?.markdown ?? null, +}; + +const client = new CopilotClient(); +const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider, +}); +``` + +
+
+Python + +```python +from copilot import CopilotClient, SkillProviderDescriptor +from copilot.session import PermissionHandler + +RELEASE_SKILLS = { + "release-notes": ( + "Writes release notes in the team's format.", + "Group changes by feature area and link each pull request.", + ), +} + + +class ReleaseSkills: + async def list_skills(self) -> list[SkillProviderDescriptor]: + return [ + SkillProviderDescriptor(name=name, description=description) + for name, (description, _) in RELEASE_SKILLS.items() + ] + + async def read_skill(self, name: str) -> str | None: + skill = RELEASE_SKILLS.get(name) + return skill[1] if skill else None + + +async def main(): + client = CopilotClient() + await client.start() + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + skill_provider=ReleaseSkills(), + ) +``` + +
+
+Go + +```go +package main + +import ( + "context" + "fmt" + "log" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" +) + +type releaseSkill struct { + description string + markdown string +} + +type releaseSkills map[string]releaseSkill + +func (s releaseSkills) ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) { + descriptors := make([]rpc.SkillProviderDescriptor, 0, len(s)) + for name, skill := range s { + descriptors = append(descriptors, rpc.SkillProviderDescriptor{Name: name, Description: skill.description}) + } + return descriptors, nil +} + +func (s releaseSkills) ReadSkill(ctx context.Context, name string) (string, error) { + skill, ok := s[name] + if !ok { + return "", fmt.Errorf("%w: %s", copilot.ErrSkillNotFound, name) + } + return skill.markdown, nil +} + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: releaseSkills{ + "release-notes": { + description: "Writes release notes in the team's format.", + markdown: "Group changes by feature area and link each pull request.", + }, + }, + }) + if err != nil { + log.Fatal(err) + } + _ = session +} +``` + +
+
+.NET + +```csharp +#pragma warning disable GHCP001 // Skill providers are experimental. +using GitHub.Copilot; +using GitHub.Copilot.Rpc; + +public sealed class ReleaseSkills : ISkillProvider +{ + private readonly Dictionary _skills = new() + { + ["release-notes"] = ( + "Writes release notes in the team's format.", + "Group changes by feature area and link each pull request."), + }; + + public Task> ListSkillsAsync(CancellationToken cancellationToken) => + Task.FromResult>( + _skills.Select(skill => new SkillProviderDescriptor + { + Name = skill.Key, + Description = skill.Value.Description, + }).ToList()); + + public Task ReadSkillAsync(string name, CancellationToken cancellationToken) => + Task.FromResult(_skills.TryGetValue(name, out var skill) ? skill.Markdown : null); +} + +public static class SkillProviderExample +{ + public static async Task RunAsync(CopilotClient client) + { + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + SkillProvider = new ReleaseSkills(), + }); + } +} +``` + +The .NET skill provider types raise the `GHCP001` experimental diagnostic. Suppress it with `#pragma warning disable GHCP001` or a project-level `GHCP001`. + +
+
+Java + +```java +import com.github.copilot.AllowCopilotExperimental; +import com.github.copilot.CopilotClient; +import com.github.copilot.CopilotSession; +import com.github.copilot.SkillProvider; +import com.github.copilot.SkillProviderDescriptor; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +@AllowCopilotExperimental +class ReleaseSkills implements SkillProvider { + private final Map markdown = Map.of( + "release-notes", "Group changes by feature area and link each pull request."); + + @Override + public CompletableFuture> listSkills() { + return CompletableFuture.completedFuture(List.of(new SkillProviderDescriptor( + "release-notes", "Writes release notes in the team's format.", null, null, null))); + } + + @Override + public CompletableFuture readSkill(String name) { + return CompletableFuture.completedFuture(markdown.get(name)); + } +} + +@AllowCopilotExperimental +class SkillProviderExample { + static CopilotSession createSession(CopilotClient client) throws Exception { + return client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSkillProvider(new ReleaseSkills())) + .get(); + } +} +``` + +The Java skill provider API is `@CopilotExperimental`, so the consuming class or method must opt in with `@AllowCopilotExperimental` (or compile with `-Acopilot.experimental.allowed=true`). See [Using experimental APIs](../../java/README.md#using-experimental-apis). + +
+
+Rust + + +```rust +use std::sync::Arc; + +use async_trait::async_trait; +use github_copilot_sdk::{Error, SessionConfig, SkillProvider, SkillProviderDescriptor}; + +struct ReleaseSkills; + +#[async_trait] +impl SkillProvider for ReleaseSkills { + async fn list_skills(&self) -> Result, Error> { + Ok(vec![SkillProviderDescriptor { + name: "release-notes".into(), + description: "Writes release notes in the team's format.".into(), + ..Default::default() + }]) + } + + async fn read_skill(&self, name: &str) -> Result, Error> { + Ok((name == "release-notes") + .then(|| "Group changes by feature area and link each pull request.".to_string())) + } +} + +let session = client + .create_session( + SessionConfig::default() + .approve_all_permissions() + .with_skill_provider(Arc::new(ReleaseSkills)), + ) + .await?; +``` + +
+ +### Provider skill content + +Read skill returns the skill's `SKILL.md` text. YAML frontmatter is optional: + +* Without frontmatter, the whole text is the skill body, and all metadata comes from the listed skill. +* With frontmatter, an omitted field inherits the listed value. A field that you include must match the listed value, or the skill fails to load. +* `allowed-tools` is read only from frontmatter. +* If the first line of the text is `---`, the runtime parses it as frontmatter, so don't start a body-only skill with a Markdown thematic break. + +Provider skills are text-only. They have no base directory, so they can't reference bundled scripts, templates, or other files. `skills.list` reports them with the source `sdk` and an empty `path`. + +### Serving existing SKILL.md files + +To serve `SKILL.md` files that you already have, such as files stored in a database, parse each file's YAML frontmatter once to build its catalog entry, then return the file unchanged from read skill: + +* Map `name` and `description` to the listed skill's `name` and `description`. Both are required in the catalog; the runtime doesn't fall back to a folder name or the skill body. +* Map `user-invocable`, `disable-model-invocation`, and `argument-hint` to the matching optional fields when the frontmatter sets them. +* Return the original text, frontmatter included, from read skill. Because the listed values came from the same frontmatter, they match, and the runtime still reads `allowed-tools` from it. + +### Trusting provider content + +Treat provider skills like skill directories: their content is trusted input. The model follows a skill's instructions, and the frontmatter controls how the skill is invoked. `allowed-tools` doesn't grant permissions in SDK sessions. The runtime reports it in the `allowedTools` field of the `skill.invoked` event, and your permission handler still decides every tool request. + +Don't serve text that end users or other tenants can edit unless you would let them author a skill file. If you build skills from user input, generate the frontmatter yourself instead of passing user-supplied frontmatter through. + +### Provider limits and errors + +The runtime validates the provider's catalog and content: + +* Names must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$` and be unique, ignoring case. +* A catalog can contain at most 1,024 skills and 1 MiB of metadata. Each description and argument hint can be at most 1,024 characters. +* Each skill's markdown can be at most 1 MiB of UTF-8. +* Each call must complete within 30 seconds. When the runtime stops waiting for a call, because it timed out, the session ended, or a resume replaced the provider, it cancels the request and ignores any later result. Each SDK passes that cancellation to your provider in its usual way: + + | SDK | Cancellation signal | + |---|---| + | Node.js | The `signal` (`AbortSignal`) in the options passed to `listSkills` and `readSkill` is aborted. | + | Python | The provider's `asyncio` task is cancelled with `asyncio.CancelledError`. Synchronous providers run to completion. | + | Go | The call's `context.Context` is cancelled. | + | .NET | The call's `CancellationToken` is cancelled. | + | Java | The returned `CompletableFuture` is cancelled with `cancel(true)`, which doesn't interrupt running work. | + | Rust | The provider future is dropped. | + + These signals cover calls that the runtime cancels. When the connection closes or the client is force-stopped, the .NET, Java, and Rust SDKs also cancel calls that are still running. The Node.js, Python, and Go SDKs don't: a running call continues until it returns, and the SDK discards its result. + +If listing fails or returns an invalid catalog, the runtime reports the problem in the `errors` list returned by `skills.reload`. A failed reload keeps the provider skills from the last successful list. If reading a skill fails, the `skill` tool reports a generic load failure to the model. The SDK never forwards the text of errors your provider throws or returns, so that text can't leak into the conversation. + +### Provider lifecycle + +A provider is bound to one session: + +* A provider enables skills unless you set `enableSkills` to `false`, which keeps the provider bound but never called. In `mode: "empty"`, skills stay disabled until you set `enableSkills` to `true`. +* The provider is never persisted. Pass it again when you resume a session. Resuming without a provider removes it from the session. Extensions that join the session don't change it. +* A provider skill with the same name as a file-based skill replaces the file-based skill, and `skills.reload` reports a warning. +* Sub-agents use their parent session's provider. +* The runtime can call the provider concurrently, for example from the `skill` tool, user invocations, and custom agents that preload skills, so both operations must be safe for concurrent use. +* Cloud sessions don't support skill providers. The SDK rejects the configuration before it creates the session. + ## Configuration options ### SessionConfig skill fields @@ -344,12 +671,18 @@ The markdown body contains the instructions that are injected into the session c |----------|-------|------|-------------| | Node.js | `skillDirectories` | `string[]` | Directories to load skills from | | Node.js | `disabledSkills` | `string[]` | Skills to disable | +| Node.js | `skillProvider` | `SkillProvider` | Experimental provider for SDK-supplied skills | | Python | `skill_directories` | `list[str]` | Directories to load skills from | | Python | `disabled_skills` | `list[str]` | Skills to disable | +| Python | `skill_provider` | `SkillProvider` | Experimental provider for SDK-supplied skills | | Go | `SkillDirectories` | `[]string` | Directories to load skills from | | Go | `DisabledSkills` | `[]string` | Skills to disable | +| Go | `SkillProvider` | `SkillProvider` | Experimental provider for SDK-supplied skills | | .NET | `SkillDirectories` | `List` | Directories to load skills from | | .NET | `DisabledSkills` | `List` | Skills to disable | +| .NET | `SkillProvider` | `ISkillProvider` | Experimental provider for SDK-supplied skills | +| Java | `setSkillProvider` | `SkillProvider` | Experimental provider for SDK-supplied skills | +| Rust | `with_skill_provider` | `Arc` | Experimental provider for SDK-supplied skills | ### Built-in skills and `mode: "empty"` diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 514a8b2b0e..b20339042a 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -734,7 +734,7 @@ The `permissionRequest` is a discriminated union on `kind`: | `kind` | Key Fields | Description | |--------|------------|-------------| | `"shell"` | `fullCommandText`, `intention`, `commands[]`, `possiblePaths[]` | Execute a shell command | -| `"write"` | `fileName`, `diff`, `intention`, `newFileContents?` | Write/modify a file | +| `"write"` | `fileName`, `diff`, `intention`, `newFileContents?`, `fileEdits?` | Write/modify a file | | `"read"` | `path`, `intention` | Read a file or directory | | `"mcp"` | `serverName`, `toolName`, `toolTitle`, `args?`, `readOnly` | Call an MCP tool | | `"url"` | `url`, `intention` | Fetch a URL | @@ -743,6 +743,10 @@ The `permissionRequest` is a discriminated union on `kind`: All `kind` variants also include an optional `toolCallId` linking back to the tool call that triggered the request. +Write requests can also include `fileEdits` in both `permissionRequest` and `promptRequest`. Each entry contains an optional `before` and `after` snapshot, each with an absolute `path` in the session filesystem namespace and complete UTF-8 `content`. A missing `before` means creation; a missing `after` means deletion. An empty `content` string represents an empty file, not an unavailable preview. A move can have different paths on its two sides. When a move overwrites an existing destination, separate entries describe the source deletion and destination replacement. + +These snapshots describe the planned operation, not a later read of the live filesystem. The runtime omits `fileEdits` when it cannot provide a complete text preview, including requests made before reading a sandbox-protected file, non-UTF-8 contents, or an unreadable or non-regular move destination, such as a named pipe. Do not infer a deletion from an empty legacy `newFileContents` value. The existing fields and approval choices remain available to consumers that do not use previews. + ### `permission.completed` A permission request was resolved. @@ -856,7 +860,7 @@ A skill was activated for the current conversation. | `name` | `string` | ✅ | Skill name | | `path` | `string` | ✅ | File path to the SKILL.md definition | | `content` | `string` | ✅ | Full skill content injected into the conversation | -| `allowedTools` | `string[]` | | Tools auto-approved while this skill is active | +| `allowedTools` | `string[]` | | Tools listed in the skill's `allowed-tools` frontmatter. The SDK doesn't approve them automatically; your permission handler still decides. The Copilot CLI auto-approves them while the skill is active. | | `pluginName` | `string` | | Plugin the skill originated from | | `pluginVersion` | `string` | | Plugin version | diff --git a/docs/features/usage-and-billing.md b/docs/features/usage-and-billing.md index ec662b6850..c412cb35c2 100644 --- a/docs/features/usage-and-billing.md +++ b/docs/features/usage-and-billing.md @@ -22,9 +22,21 @@ The table below maps each signal to the API that exposes it. | Accumulated AI credit and token totals | `session.usage.getMetrics` | Session | RPC | | Per-model AI credit pricing | `models.list` | Server | RPC | | Account quota and premium interactions | `account.getQuota` | Server | RPC | +| Session-owned provider quota observations | `session.quota.get` and `session.quota.refresh` | Session | RPC | +| Provider admission-state updates | `session.quota_observation` | Session | Event | + +`session.quota.get` reads local runtime state; `session.quota.refresh` refreshes the provider catalogs and quota sources. Both return `providerQuotas`, keyed by provider identity, quota ID, and observation kind. The runtime keeps one observation store. The legacy `snapshots` field is a derived compatibility view of authoritative budgets, not a second source of quota state. + +Provider cardinality is an emergent property of actual usage and observations supplied to the seam. This API neither knows nor enforces how many providers a user may use: it records and projects zero, one, or many entries identically. Producers supply stable provider/account-scoped references; usage adds model identity, and quota adds quota identity and observation kind. `get()` returns the full known collection, so callers can select an exact key, a subset, or all entries without provider-specific methods. Acquisition uses the sources supplied by the upstream account/model layer; it does not manufacture a union or impose a selected-provider filter on reads. + +Check `observationKind`, `quantityKind`, and `acquisitionStatus` before interpreting a reading. An `authoritative_budget` can supply progress and reset metadata. An `advisory_balance` reports independent signed 64-bit quantities without promising a percentage, reset window, or post-call balance. An `admission_state` reports permission/capacity at request admission and carries no quantities; it does not replace an account balance. Preserve missing values and explicit nulls rather than converting them to zero. A failed or unavailable acquisition is not quota exhaustion. + +For Microsoft 365 Copilot, access and capacity are service verdicts, while quantities are advisory. For example, `allowed`/`available` with both quantities explicitly zero means **0 / 0 AIC reported**, not 100% consumed or inconsistent state. A missing entitlement is different from a zero entitlement. The `-1` entitlement sentinel means unlimited entitlement without changing the reported capacity state. Inference headers can reflect cached admission state; neither those headers nor an on-demand quota refresh establish the charge for the preceding call. + +Usage totals belong to a session, not the active account. In the CLI, switching away from a non-empty Microsoft 365 Copilot session requires starting a new session. The previous session retains its usage and history for resume; the new session starts its own totals. > [!NOTE] -> `session.usage.getMetrics`, `session.metadata.contextInfo`, and `session.metadata.recomputeContextTokens` are marked experimental in the generated RPC surface. In .NET they raise the `GHCP001` experimental diagnostic, which you suppress with `#pragma warning disable GHCP001` or a project-level `GHCP001`. Pin both the SDK and the Copilot CLI runtime if your application depends on them. +> `session.quota`, `session.usage.getMetrics`, `session.metadata.contextInfo`, and `session.metadata.recomputeContextTokens` are marked experimental in the generated RPC surface. In .NET they raise the `GHCP001` experimental diagnostic, which you suppress with `#pragma warning disable GHCP001` or a project-level `GHCP001`. Pin both the SDK and the Copilot CLI runtime if your application depends on them. The field tables below list only the fields used in the examples on this page. The complete, always-current field reference is the generated SDK types plus [Streaming events](./streaming-events.md), which is regenerated from the CLI schema on every dependency bump. Treat those as the source of truth and this page as a task-oriented guide. diff --git a/docs/setup/multi-tenancy.md b/docs/setup/multi-tenancy.md index ebbafe4fb0..7fe893a070 100644 --- a/docs/setup/multi-tenancy.md +++ b/docs/setup/multi-tenancy.md @@ -334,6 +334,24 @@ const client = new CopilotClient({ For languages that expose a provider callback, configure `sessionFs` at the client level and provide a per-session filesystem handler when creating or resuming a session. See [Session Persistence](../features/session-persistence.md) for persistence concepts and storage trade-offs. +Runtime-owned durable permission choices also use the session filesystem. Under the provider's `sessionStatePath`, `permissions.json` retains its top-level `locations` map with `tool_approvals` and `allowed_directories`; `settings.json` stores top-level `allowedUrls` and `sandbox`; and `config.json` stores top-level `trustedFolders`. + +URL approvals are read only from `settings.json`; legacy user-setting fields such as `allowedUrls` in `config.json` are ignored, matching host behavior. + +Location keys are normalized provider directory paths, with no added prefix. Resolution follows the provider's path conventions without inspecting the host filesystem or discovering host Git repositories. `/workspace` and `/workspace/sub` select different location entries. + +The runtime does not add session-ID partitions to these files or change their sharing granularity. Sessions accessing the same files in the same provider namespace share remembered grants. Your application must use distinct namespaces for tenants or sessions that require isolation, authorize access to each namespace, and reconnect resumed sessions to their original namespace. + +With a separate namespace per session, permanent grants survive that session's resumes rather than becoming tenant-wide grants for new sessions. Sharing grants across sessions requires sharing the consent files while keeping other session state separate. The runtime serializes consent updates only within the same provider endpoint and process; applications that share backing files across endpoints or processes must coordinate concurrent updates. + +The runtime blocks direct provider-backed file writes to the lexical approval-metadata paths as defense in depth, not as a tamper-proof storage boundary. Providers must not expose writable aliases (such as symlinks or Windows 8.3 short names) to approval metadata through agent file tools. Shell tools do not run through SessionFS, and their commands are not checked against its virtual metadata paths. If your application separately exposes the provider's backing storage to shell execution or other arbitrary code, your host must protect that storage. + +Session filesystem approval operations never import or fall back to remembered permissions from the host's `permissions-config.json`, URL or sandbox settings from `settings.json`, or folder trust from `config.json`. They do not update those host files, even when the provider has no approvals or reports an error. A provider error is not permission to use host consent. URL deny rules remain authoritative, and folder trust uses provider paths without host Git discovery. + +Approve-once, approve-for-session, rejections, and session policy changes retain their existing lifetimes and event-replay behavior. Storage routing does not turn them into new permanent grants. Protect session transcripts, including durable permission events, with the same tenant isolation as approval metadata. + +This boundary applies only to sessions using a session filesystem provider. All SDK applications without `sessionFs`, in both `empty` and `copilot-cli` modes, and ordinary CLI sessions retain existing host-backed persistence for remembered locations, permanent URL approvals, folder trust, and sandbox settings. + Verified public SDK surfaces: | Language | Client-level config | Per-session provider | diff --git a/dotnet/README.md b/dotnet/README.md index 6bde71b71c..6b077bf8e1 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -178,6 +178,9 @@ Start the CLI server and establish connection. ##### `StopAsync(): Task` Stop the server and close all sessions. Throws if errors are encountered during cleanup. +Closing a session stops subscribed event delivery, drops queued events, and releases +subscriptions without waiting for handlers already admitted for dispatch. Pending `SendAndWaitAsync` +operations fail with `IOException` if the session closes before their final response arrives. For an owned stdio runtime, graceful shutdown closes stdin and waits up to 10 seconds for host cleanup, including telemetry export. This cleanup is best-effort: if the wait times out, the process is terminated and that timeout alone is not reported as a cleanup @@ -186,6 +189,20 @@ error. A successful return does not guarantee that all telemetry was exported. ##### `ForceStopAsync(): Task` Force stop the CLI server without graceful cleanup. Use when `StopAsync()` takes too long. +Stops session event dispatch and releases queued events and subscriptions. A handler already +admitted for dispatch may finish; shutdown does not wait for it. Pending `SendAndWaitAsync` operations fail +with `IOException` if the session closes before their final response arrives. +The SDK releases its references to later handlers' captured objects even if an admitted handler remains blocked. +Local retirement also releases tool, command, hook, auth/input, provider, canvas, +and transform registrations, without changing caller-owned configuration or joining +callbacks already admitted. Registration and callback lookup share a retirement fence; +late accepted tool replacements cannot restore handlers. Ordinary disposal retains +hook callbacks through the detach response and performs local retirement in `finally`. +Both stop methods close session admission before taking the session snapshot. Concurrent +create/resume setup on that connection is rejected and retired locally; restarting the +client opens admission only for the new connection. +Cleanup of a rejected cloud creation stays on its original connection and cannot +restart the client during shutdown. ##### `CreateSessionAsync(SessionConfig? config = null): Task` @@ -207,6 +224,7 @@ Create a new conversation session. - `WorkingDirectory` - Working directory for the session. When not set, the runtime uses its own process working directory. - `EnableSessionStore` - Enables the cross-session store for search and retrieval across sessions. When unset in `CopilotClientMode.CopilotCli`, the runtime default applies (enabled). In `CopilotClientMode.Empty`, defaults to disabled. - `GitHubTokenProvider` - Acquires session-scoped GitHub tokens on demand. Return `GitHubTokenProviderResult.FromToken` with a positive `ExpiresIn` value (production GitHub tokens typically use `8 * 60 * 60` seconds), or `GitHubTokenProviderResult.Cancel()`. Cannot be combined with `GitHubToken`. +- `SkillProvider` - Experimental session-scoped skill provider. See [Skill providers (experimental)](#skill-providers-experimental). - `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `PermissionHandler.ApproveAll` approves requests when managed settings are disabled and throws when `EnableManagedSettings` is true. Custom handlers can inspect `ManagedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. - `OnUserInputRequest` - Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section. - `AskUserVariant` - Selects the model-facing `ask_user` tool shape. Defaults to `AskUserVariant.Legacy`; use `AskUserVariant.Elicitation` with `OnElicitationRequest`. @@ -225,6 +243,7 @@ Resume an existing session. Returns the session with `WorkspacePath` populated i - `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section. - `GitHubTokenProvider` - Replaces the session-scoped token provider when resuming. Cannot be combined with `GitHubToken`. +- `SkillProvider` - Re-supplies the session-scoped skill provider when resuming. - `AskUserVariant` - Re-supplies the model-facing `ask_user` tool shape on cold resume. - `AllowTranscriptRecovery` - Repairs a damaged transcript when true. The default is true in all modes; set false to reject recovery. `session.TranscriptRecovery` contains @@ -254,6 +273,62 @@ await using var session = await client.CreateSessionAsync(new SessionConfig Initial acquisition runs during session creation or resume. Cancellation, provider errors, and invalid token responses reject that operation instead of falling back to ambient authentication. Idle sessions refresh only before their next credential-consuming operation; there is no background refresh timer. +##### Skill providers (experimental) + +Set `SessionConfig.SkillProvider` or `ResumeSessionConfig.SkillProvider` to +serve session-scoped skills from your application. The provider is not persisted: +pass it again on every resume, because resuming without one unbinds it. Skill +providers are only supported for local sessions; `CreateSessionAsync` throws if +`Cloud` and `SkillProvider` are both set. The skill provider types are +experimental and raise the `GHCP001` diagnostic; suppress it with +`#pragma warning disable GHCP001` or `GHCP001`. + +```csharp +#pragma warning disable GHCP001 // Skill providers are experimental. + +public sealed class MySkillProvider : ISkillProvider +{ + public Task> ListSkillsAsync(CancellationToken cancellationToken) => + Task.FromResult>( + [ + new() + { + Name = "project-facts", + Description = "Important facts about this host application", + ArgumentHint = "" + } + ]); + + public Task ReadSkillAsync(string name, CancellationToken cancellationToken) => + Task.FromResult(name == "project-facts" + ? """ + # Project facts + + Use the application's project index before answering. + """ + : null); +} + +await using var client = new CopilotClient(new CopilotClientOptions +{ + Mode = CopilotClientMode.Empty +}); + +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + AvailableTools = [], // Empty mode requires an explicit tool allow-list. + SkillProvider = new MySkillProvider(), + EnableSkills = true // Required in empty mode; otherwise skills default off. +}); +``` + +The runtime may call `ListSkillsAsync` and `ReadSkillAsync` concurrently. Honor +the supplied cancellation token; it is canceled when the runtime abandons the +request (for example, on timeout, session disposal, or a resume that replaces the +provider) or the connection closes. Return `null` from `ReadSkillAsync` when a +skill name is not found. Optional `SkillProviderDescriptor` properties are +omitted from JSON when unset. + ##### `PingAsync(string? message = null): Task` Ping the server to check connectivity. @@ -542,6 +617,24 @@ await session2.DisposeAsync(); --- +## Provider-qualified model selection + +Use the experimental request-object overload to select the `modelId` and `providerId` from a model catalog entry: + +```csharp +using GitHub.Copilot.Rpc; + +#pragma warning disable GHCP001 // Provider-qualified model selection is experimental. +await session.Rpc.Model.SwitchToAsync(new ModelSwitchToRequest +{ + ModelId = modelId, + ProviderId = providerId, +}); +#pragma warning restore GHCP001 +``` + +The SDK supplies the session ID. Existing positional calls, including `session.Rpc.Model.SwitchToAsync("auto", AutoTier.Balance)`, retain their original parameter order and defaults. + ## Auto routing tiers The canonical values are `AutoTier.Efficiency`, `AutoTier.Balance`, `AutoTier.Intelligence`, and `AutoTier.Fast`, which send `efficiency`, `balance`, `intelligence`, and `fast` on the wire. Fast is an integrator-only latency preset, not a fourth first-party GitHub Copilot preference. The SDK forwards the requested value without deciding eligibility or inspecting client identity. An externally supplied older runtime returns its native runtime or JSON-RPC error; the SDK does not downgrade or silently ignore the request. diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 999440992f..7fcfbfef99 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -71,6 +71,8 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable /// that has not been explicitly disposed or removed. /// internal readonly ConcurrentDictionary _sessions = new(); + private readonly object _sessionAdmissionLock = new(); + private bool _sessionAdmissionClosed; private readonly ConcurrentDictionary>> _gitHubTokenProviders = new(); private readonly CopilotClientOptions _options; @@ -365,7 +367,14 @@ private static bool IsFullyQualifiedPath(string path) /// public Task StartAsync(CancellationToken cancellationToken = default) { - return _connectionTask ??= StartCoreAsync(cancellationToken); + lock (_sessionAdmissionLock) + { + if (_connectionTask is null) + { + _sessionAdmissionClosed = false; + } + return _connectionTask ??= StartCoreAsync(cancellationToken); + } async Task StartCoreAsync(CancellationToken ct) { @@ -556,11 +565,12 @@ await InvokeRpcAsync( /// public async Task StopAsync() { + var sessions = CloseSessionAdmission(clearSessions: false); DisconnectAhpHosts(); List errors = []; - CancelPendingExternalTools(); + CancelPendingExternalTools(sessions); - foreach (var session in _sessions.Values.ToArray()) + foreach (var session in sessions) { try { @@ -572,7 +582,6 @@ public async Task StopAsync() } } - _sessions.Clear(); ClearGitHubTokenProviders(); await CleanupConnectionAsync(errors, gracefulRuntimeShutdown: true); @@ -588,6 +597,7 @@ public async Task StopAsync() /// Use this when fails or takes too long. This method: /// /// Clears all sessions immediately without destroying them + /// Stops session event delivery without waiting for running handlers /// Force closes the connection /// Kills the CLI process (if spawned by this client) /// @@ -604,9 +614,13 @@ public async Task StopAsync() /// public async Task ForceStopAsync() { + var sessions = CloseSessionAdmission(clearSessions: true); DisconnectAhpHosts(); - CancelPendingExternalTools(); - _sessions.Clear(); + CancelPendingExternalTools(sessions); + foreach (var session in sessions) + { + session.Retire(); + } ClearGitHubTokenProviders(); var errors = new List(); @@ -614,6 +628,20 @@ public async Task ForceStopAsync() ThrowErrors(errors); } + private CopilotSession[] CloseSessionAdmission(bool clearSessions) + { + lock (_sessionAdmissionLock) + { + _sessionAdmissionClosed = true; + var sessions = _sessions.Values.ToArray(); + if (clearSessions) + { + _sessions.Clear(); + } + return sessions; + } + } + private static void ThrowErrors(List? errors) { if (errors is not null) @@ -632,14 +660,17 @@ private static void ThrowErrors(List? errors) private async Task CleanupConnectionAsync(List? errors, bool gracefulRuntimeShutdown) { - var connectionTask = _connectionTask; + Task? connectionTask; + lock (_sessionAdmissionLock) + { + connectionTask = _connectionTask; + _connectionTask = null; + } if (connectionTask is null) { return; } - _connectionTask = null; - Connection ctx; try { @@ -892,6 +923,7 @@ private CopilotSession InitializeSession( session.RegisterElicitationHandler(config.OnElicitationRequest); session.RegisterExitPlanModeHandler(config.OnExitPlanModeRequest); session.RegisterAutoModeSwitchHandler(config.OnAutoModeSwitchRequest); + session.RegisterSkillProvider(config.SkillProvider); if (config.OnUserInputRequest != null) { session.RegisterUserInputHandler(config.OnUserInputRequest); @@ -911,7 +943,7 @@ private CopilotSession InitializeSession( ConfigureSessionFsHandlers(session, config.CreateSessionFsProvider); session.SetCanvasHandler(config.CanvasHandler); session.RegisterBearerTokenProviders(BuildBearerTokenCallbacks(config)); - RegisterSession(session); + RegisterSession(session, rpc); session.StartProcessingEvents(); LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null, callerName + " local setup complete. Elapsed={Elapsed}, SessionId={SessionId}, Tools={ToolsCount}, Commands={CommandsCount}, Hooks={HasHooks}", @@ -1190,6 +1222,10 @@ public async Task CreateSessionAsync(SessionConfig config, Cance { ArgumentNullException.ThrowIfNull(config); ValidateGitHubTokenConfig(config); + if (config.Cloud is not null && config.SkillProvider is not null) + { + throw new ArgumentException("Skill providers are not supported for cloud sessions."); + } var connection = await EnsureConnectedAsync(cancellationToken); var totalTimestamp = Stopwatch.GetTimestamp(); @@ -1324,10 +1360,12 @@ public async Task CreateSessionAsync(SessionConfig config, Cance FeatureFlags: config.FeatureFlags, ExpAssignments: config.ExpAssignments, EnableManagedSettings: config.EnableManagedSettings, + EnforceManagedModelDefaults: config.EnforceManagedModelDefaults, GitHubMcpToolConfig: config.GitHubMcpToolConfig, ManagedSettings: config.ManagedSettings, EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null, - AdditionalDirectories: config.AdditionalDirectories); + AdditionalDirectories: config.AdditionalDirectories, + HasSkillProvider: config.SkillProvider is not null ? true : null); var rpcTimestamp = Stopwatch.GetTimestamp(); @@ -1401,7 +1439,13 @@ public async Task CreateSessionAsync(SessionConfig config, Cance try { using var cleanupTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - await DeleteSessionAsync(serverAssignedSessionId, cleanupTimeout.Token).ConfigureAwait(false); + var deletion = await InvokeRpcAsync( + connection.Rpc, "session.delete", [new DeleteSessionRequest(serverAssignedSessionId)], + cleanupTimeout.Token).ConfigureAwait(false); + if (!deletion.Success) + { + throw new InvalidOperationException($"Failed to delete session {serverAssignedSessionId}: {deletion.Error}"); + } } catch (Exception cleanupError) when (cleanupError is OperationCanceledException or IOException @@ -1589,11 +1633,13 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes FeatureFlags: config.FeatureFlags, ExpAssignments: config.ExpAssignments, EnableManagedSettings: config.EnableManagedSettings, + EnforceManagedModelDefaults: config.EnforceManagedModelDefaults, GitHubMcpToolConfig: config.GitHubMcpToolConfig, ManagedSettings: config.ManagedSettings, EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null, AdditionalDirectories: config.AdditionalDirectories, - AllowTranscriptRecovery: config.AllowTranscriptRecovery); + AllowTranscriptRecovery: config.AllowTranscriptRecovery, + HasSkillProvider: config.SkillProvider is not null ? true : null); var rpcTimestamp = Stopwatch.GetTimestamp(); var response = await InvokeRpcAsync( @@ -1807,6 +1853,7 @@ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancell if (_sessions.TryRemove(sessionId, out var session)) { + session.ClearSkillProvider(); session.ReleaseGitHubTokenProviderRegistration(); } } @@ -2225,7 +2272,7 @@ private void ConfigureSessionFsHandlers(CopilotSession session, Func ConnectToServerAsync(Process? cliProcess, string? rpc.SetLocalRpcMethod("autoModeSwitch.request", handler.OnAutoModeSwitchRequest); rpc.SetLocalRpcMethod("hooks.invoke", handler.OnHooksInvoke); rpc.SetLocalRpcMethod("systemMessage.transform", handler.OnSystemMessageTransform); + rpc.SetLocalRpcMethod("skillProvider.list", handler.OnSkillProviderList, singleObjectParam: true); + rpc.SetLocalRpcMethod("skillProvider.read", handler.OnSkillProviderRead, singleObjectParam: true); ClientSessionApiRegistration.RegisterClientSessionApiHandlers(rpc, sessionId => { var session = GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); @@ -2856,14 +2905,15 @@ private async Task CancelExternalToolsWhenConnectionClosesAsync(JsonRpc rpc) CancelPendingExternalTools(); } - private void CancelPendingExternalTools() + private void CancelPendingExternalTools(IEnumerable? sessions = null) { if (_clientGlobalApis?.LlmInference is LlmInferenceAdapter llmInferenceAdapter) { llmInferenceAdapter.CancelPending(); } - foreach (var session in _sessions.Values) + foreach (var session in sessions ?? _sessions.Values) { + session.ClearSkillProvider(); session.CancelPendingExternalTools(); } } @@ -2911,12 +2961,29 @@ private static JsonSerializerOptions CreateSerializerOptions() return session; } - private void RegisterSession(CopilotSession session) + private void RegisterSession(CopilotSession session, JsonRpc rpc) { - if (!_sessions.TryAdd(session.SessionId, session)) + Exception error; + lock (_sessionAdmissionLock) { - throw new InvalidOperationException($"Session '{session.SessionId}' is already tracked by this client."); + // A restarted connection must not admit setup that began on the stopped transport. + if (!_sessionAdmissionClosed + && _connectionTask?.Status == System.Threading.Tasks.TaskStatus.RanToCompletion + && ReferenceEquals(_connectionTask.Result.Rpc, rpc)) + { + if (_sessions.TryAdd(session.SessionId, session)) + { + return; + } + error = new InvalidOperationException($"Session '{session.SessionId}' is already tracked by this client."); + } + else + { + error = new IOException("Cannot register a session on a stopped connection."); + } } + session.Retire(); + throw error; } /// @@ -3039,6 +3106,32 @@ public async ValueTask OnSystemMessageTransfo return await session.HandleSystemMessageTransformAsync(sections); } + public async ValueTask OnSkillProviderList(SkillProviderListRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + if (string.IsNullOrEmpty(request.SessionId)) + { + throw new ArgumentException("Skill provider list request is missing a session id."); + } + + var session = client.GetSession(request.SessionId) + ?? throw new InvalidOperationException($"No skill provider for session: {request.SessionId}"); + return await session.HandleSkillProviderListAsync(cancellationToken); + } + + public async ValueTask OnSkillProviderRead(SkillProviderReadRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + if (string.IsNullOrEmpty(request.SessionId) || string.IsNullOrEmpty(request.Name)) + { + throw new ArgumentException("Skill provider read request is missing a session id or skill name."); + } + + var session = client.GetSession(request.SessionId) + ?? throw new InvalidOperationException($"No skill provider for session: {request.SessionId}"); + return await session.HandleSkillProviderReadAsync(request.Name, cancellationToken); + } + } private class Connection( @@ -3194,10 +3287,12 @@ internal record CreateSessionRequest( [property: JsonPropertyName("featureFlags")] IDictionary? FeatureFlags = null, [property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null, [property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null, + [property: JsonPropertyName("enforceManagedModelDefaults")] bool? EnforceManagedModelDefaults = null, [property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null, bool? EnableGitHubTelemetryForwarding = null, [property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null, - IList? AdditionalDirectories = null); + IList? AdditionalDirectories = null, + bool? HasSkillProvider = null); #pragma warning restore GHCP001 internal record ToolDefinition( @@ -3315,11 +3410,13 @@ internal record ResumeSessionRequest( [property: JsonPropertyName("featureFlags")] IDictionary? FeatureFlags = null, [property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null, [property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null, + [property: JsonPropertyName("enforceManagedModelDefaults")] bool? EnforceManagedModelDefaults = null, [property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null, bool? EnableGitHubTelemetryForwarding = null, [property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null, IList? AdditionalDirectories = null, - bool? AllowTranscriptRecovery = null); + bool? AllowTranscriptRecovery = null, + bool? HasSkillProvider = null); #pragma warning restore GHCP001 internal record ResumeSessionResponse( @@ -3407,6 +3504,21 @@ internal record UserInputRequestResponse( string Answer, bool WasFreeform); + internal record SkillProviderListRequest( + string SessionId); + +#pragma warning disable GHCP001 + internal record SkillProviderListResult( + IReadOnlyList Skills); +#pragma warning restore GHCP001 + + internal record SkillProviderReadRequest( + string SessionId, + string Name); + + internal record SkillProviderReadResult( + [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] string? Markdown); + internal record AutoModeSwitchRequestResponse( AutoModeSwitchResponse Response); @@ -3435,6 +3547,12 @@ internal record HooksInvokeResponse( [JsonSerializable(typeof(ExitPlanModeResult))] [JsonSerializable(typeof(GetLastSessionIdResponse))] [JsonSerializable(typeof(HooksInvokeResponse))] + [JsonSerializable(typeof(SkillProviderListRequest))] +#pragma warning disable GHCP001 + [JsonSerializable(typeof(SkillProviderListResult))] +#pragma warning restore GHCP001 + [JsonSerializable(typeof(SkillProviderReadRequest))] + [JsonSerializable(typeof(SkillProviderReadResult))] [JsonSerializable(typeof(ListSessionsRequest))] [JsonSerializable(typeof(ListSessionsResponse))] [JsonSerializable(typeof(GetSessionMetadataRequest))] diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 080abf0116..9a9d8ffea6 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -797,23 +797,6 @@ public sealed class ModelPolicy public string? Terms { get; set; } } -/// A neutral reference to the model provider that produced a model: an opaque id, a human-readable label, and the provider kind. Carried on each enumerated Model so consumers can group by provider without reaching into a provider-shaped internal type. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelProviderRef -{ - /// Opaque, stable id of the provider that produced this model. Matches the enumerated `ModelProviderDescriptor.id`. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// The provider kind. - [JsonPropertyName("kind")] - public ModelProviderKind Kind { get; set; } - - /// Human-readable provider label, owned by the runtime so every consumer renders identical text. - [JsonPropertyName("label")] - public string Label { get; set; } = string.Empty; -} - /// Service-published warning text that hosts should display when presenting a model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class ModelWarningText @@ -879,6 +862,10 @@ public sealed class Model [JsonPropertyName("supportedReasoningEfforts")] public IList? SupportedReasoningEfforts { get; set; } + /// Model vendor as the Copilot API reports it, for example "Anthropic" or "Azure OpenAI". Open vocabulary, passed through unchanged. It can name the vendor that serves the model instead of the one that built it, or a label that is not a vendor, such as "Experimental". Absent when the Copilot API reports no vendor. + [JsonPropertyName("vendor")] + public string? Vendor { get; set; } + /// Warnings the service published for this model, such as a deprecated client version. Present only when the service published at least one warning. The model remains usable; hosts should surface these as advisory rather than blocking. [JsonPropertyName("warningMessages")] public IList? WarningMessages { get; set; } @@ -932,7 +919,7 @@ public sealed class BuiltInModelCatalog [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SandboxHostCapability { - /// The policy feature, as an extensible string: ignore names you do not recognize. Known values: `network` (sandboxed commands can reach the network; on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns), `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`), `denied_paths` (native enforcement of `filesystem.deniedPaths`), `shell` (shell commands inside the sandbox), and `filesystem_enumeration` (enumerate-only filesystem grants; on Windows this needs Process Security Environment 1.1 filesystem enumeration support, and without it sandboxed PowerShell still runs but cannot resolve its current location; other platforms always report it). + /// The policy feature, as an extensible string: ignore names you do not recognize. Known values: `network` (sandboxed commands can reach the network; on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns), `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support or MXC's PSEC 1.0-only proxy-loopback compatibility capability, and a policy that uses it must also set `network.allowLocalNetwork`; compatibility applies only to an explicit identity-less runtime proxy, not general host-loopback access, and other policy restrictions still apply), `denied_paths` (native enforcement of `filesystem.deniedPaths`), `shell` (shell commands inside the sandbox), and `filesystem_enumeration` (enumerate-only filesystem grants; on Windows this needs Process Security Environment 1.1 filesystem enumeration support, and without it sandboxed PowerShell still runs but cannot resolve its current location; other platforms always report it). [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; @@ -1309,6 +1296,7 @@ internal sealed class AccountGetQuotaRequest [JsonDerivedType(typeof(AuthInfoTokenProvider), "token-provider")] [JsonDerivedType(typeof(AuthInfoCopilotApiToken), "copilot-api-token")] [JsonDerivedType(typeof(AuthInfoUser), "user")] +[JsonDerivedType(typeof(AuthInfoAccount), "account")] [JsonDerivedType(typeof(AuthInfoGhCli), "gh-cli")] [JsonDerivedType(typeof(AuthInfoApiKey), "api-key")] public partial class AuthInfo @@ -1805,6 +1793,24 @@ public partial class AuthInfoUser : AuthInfo public required string Login { get; set; } } +/// An interactive account whose model provider owns its credentials. It carries no GitHub credential. +/// The account variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthInfoAccount : AuthInfo +{ + /// + [JsonIgnore] + public override string Type => "account"; + + /// Host coordinate owned by the account's model provider. + [JsonPropertyName("host")] + public required string Host { get; set; } + + /// Login identifying the provider-owned account. + [JsonPropertyName("login")] + public required string Login { get; set; } +} + /// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. /// The gh-cli variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] @@ -6754,6 +6760,128 @@ internal sealed class AgentsGetDiscoveryPathsRequest public IList? ProjectPaths { get; set; } } +/// The agents this runtime ships, named so a consumer can tell them apart from authored ones. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetBuiltinsResult +{ + /// The subset of `names` a user is allowed to turn off. A shipped agent outside this list is always active and a client should not offer a toggle for it. + [JsonPropertyName("disableableNames")] + public IList DisableableNames { get => field ??= []; set; } + + /// Every agent name this runtime ships. + [JsonPropertyName("names")] + public IList Names { get => field ??= []; set; } + + /// The subset of `names` defined by a shipped YAML definition. The remainder are special-cased in code and have no definition to load. + [JsonPropertyName("yamlBasedNames")] + public IList YamlBasedNames { get => field ??= []; set; } +} + +/// A shipped agent, named and described. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class BuiltinAgentSummary +{ + /// One-line description of what the agent does. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// The agent name, as it appears in `getBuiltins`. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} + +/// The shipped agents available under the requested flags. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetAvailableBuiltinsResult +{ + /// Available shipped agents, in the runtime's own order. + [JsonPropertyName("agents")] + public IList Agents { get => field ??= []; set; } +} + +/// The feature flags to evaluate shipped agents against. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetAvailableBuiltinsRequest +{ + /// The surface asking, which gates agents that only apply to one client. Omit or pass null to apply no client filter. + [JsonPropertyName("context")] + public string? Context { get; set; } + + /// Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or pass null for no flags. + [JsonPropertyName("featureFlags")] + public IDictionary? FeatureFlags { get; set; } + + /// Flag overrides keyed by name. A null entry uses the corresponding base flag; false explicitly disables it. Omit or pass null for no overrides. + [JsonPropertyName("overrides")] + public IDictionary? Overrides { get; set; } +} + +/// One shipped agent's definition. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetBuiltinDefinitionResult +{ + /// The agent's definition, serialized as JSON. It carries the authored keys plus the runtime's projected `__nativeCustomAgent` view of the same agent. It is a string rather than an object because the runtime parses it with the agent schema's tolerant shape, which accepts keys this contract does not name. + [JsonPropertyName("definitionJson")] + public string DefinitionJson { get; set; } = string.Empty; +} + +/// The shipped agent whose definition to load. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetBuiltinDefinitionRequest +{ + /// The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside that list is special-cased in code and has no definition, and is reported as an error rather than as an empty definition. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} + +/// One shipped agent, projected for a listing. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetBuiltinListingDefinitionResult +{ + /// The agent projected as a custom agent, serialized as JSON. It is a string rather than an object for the same reason as `getBuiltinDefinition`: the runtime parses the underlying definition with the agent schema's tolerant shape, which accepts keys this contract does not name. + [JsonPropertyName("definitionJson")] + public string DefinitionJson { get; set; } = string.Empty; +} + +/// The shipped agent whose listing entry to load. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetBuiltinListingDefinitionRequest +{ + /// The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here too, from its in-code definition. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} + +/// The model to switch to, and the warning to show when the agent's preference could not be met. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsCustomAgentInitialModelDecisionResult +{ + /// The reasoning effort attached to the selected model preference. Absent when that preference does not specify an effort. + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } + + /// The first available model that matches the agent's preferences. Absent when none of the requested models is available. + [JsonPropertyName("targetModel")] + public string? TargetModel { get; set; } + + /// What to tell the user about an unmet preference. Absent when the preference was met. A warning with no `targetModel` means the agent's models are all unavailable. + [JsonPropertyName("warning")] + public string? Warning { get; set; } +} + +/// The models a custom agent asks for, and the models actually available. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsCustomAgentInitialModelDecisionParams +{ + /// The agent's declared `model:` entry, serialized. A single name or an ordered list of acceptable names. + [JsonPropertyName("agentModelsJson")] + public string AgentModelsJson { get; set; } = string.Empty; + + /// The models available to this session, serialized in the shape the model list carries. + [JsonPropertyName("availableModelsJson")] + public string AvailableModelsJson { get; set; } = string.Empty; +} + /// Loaded instruction source for a session, including path, content, category, location, applicability, and optional description. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class InstructionSource @@ -6868,6 +6996,179 @@ internal sealed class InstructionsGetDiscoveryPathsRequest public IList? ProjectPaths { get; set; } } +/// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class InstalledPlugin +{ + /// Path where the plugin is cached locally. + [JsonPropertyName("cache_path")] + public string? CachePath { get; set; } + + /// Whether the plugin is currently enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// Installation timestamp. + [JsonPropertyName("installed_at")] + public string InstalledAt { get; set; } = string.Empty; + + /// Absolute path of the marketplace directory a live plugin was resolved from. Present only on live, never-persisted records — those synthesized at session start for a directory/local marketplace, whose cache_path points at the real plugin directory on disk rather than a copy under the installed-plugins cache. Its presence is what marks a record as live, and no record carrying it is ever written to the persisted installedPlugins key. + [JsonPropertyName("installed_from")] + public string? InstalledFrom { get; set; } + + /// Marketplace the plugin came from (empty string for direct repo installs). + [JsonPropertyName("marketplace")] + public string Marketplace { get; set; } = string.Empty; + + /// Plugin name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Source for direct repo installs (when marketplace is empty). + [JsonPropertyName("source")] + public JsonElement? Source { get; set; } + + /// Per-plugin source fingerprint (a SHA-256 hash of the plugin's catalog source spec plus its resolved source subtree — NOT a Git commit SHA) captured at marketplace install/update time. Auto-update compares it against the freshly recomputed fingerprint to detect a content change that does not bump the version. Absent for pre-existing installs and for direct (non-marketplace) installs. + [JsonPropertyName("source_sha")] + public string? SourceSha { get; set; } + + /// Version installed (if available). + [JsonPropertyName("version")] + public string? Version { get; set; } +} + +/// An account the host has signed in to, identified by the server it lives on and the login it uses there. The same person can appear more than once when they use both github.com and an Enterprise server. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class LoggedInUser +{ + /// Source account this account was derived from, when one was recorded. + [JsonPropertyName("derivedFrom")] + public string? DerivedFrom { get; set; } + + /// Host the account belongs to, such as `github.com` or an Enterprise server. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; + + /// Account kind, when the host recorded one. Consumers must tolerate new strings. + [JsonPropertyName("kind")] + public string? Kind { get; set; } + + /// Account login on that host. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; +} + +/// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GlobalStateLoadResult +{ + /// Whether the user has answered the prompt suggesting they install the desktop app. + [JsonPropertyName("appInstallNudgeResponded")] + public bool? AppInstallNudgeResponded { get; set; } + + /// Whether the app tip has been shown. + [JsonPropertyName("appTipShown")] + public bool? AppTipShown { get; set; } + + /// Terminals the user has already been asked to set up, so the host does not ask twice. + [JsonPropertyName("askedSetupTerminals")] + public IList? AskedSetupTerminals { get; set; } + + /// When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the once-per-day cap for non-staff users across restarts. + [JsonPropertyName("autoFeedbackLastPromptedAt")] + public string? AutoFeedbackLastPromptedAt { get; set; } + + /// When the host first ran on this machine. + [JsonPropertyName("firstLaunchAt")] + public string? FirstLaunchAt { get; set; } + + /// Plugins installed on this machine. + [JsonPropertyName("installedPlugins")] + public IList? InstalledPlugins { get; set; } + + /// Account used for the most recent sign-in. + [JsonPropertyName("lastLoggedInUser")] + public LoggedInUser? LastLoggedInUser { get; set; } + + /// Every account the host has signed in to on this machine. + [JsonPropertyName("loggedInUsers")] + public IList? LoggedInUsers { get; set; } + + /// Whether the one-off cleanup of stored reasoning summaries has run. + [JsonPropertyName("reasoningSummariesCleanupDone")] + public bool? ReasoningSummariesCleanupDone { get; set; } + + /// Models the user selected recently, most recent first. + [JsonPropertyName("recentModelIds")] + public IList? RecentModelIds { get; set; } + + /// Whether the user declined to trust the sandbox credential proxy CA. + [JsonPropertyName("sandboxCredentialProxyCaDeclined")] + public bool? SandboxCredentialProxyCaDeclined { get; set; } + + /// Whether the sandbox onboarding has been shown. + [JsonPropertyName("sandboxOnboardingShown")] + public bool? SandboxOnboardingShown { get; set; } + + /// Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only behavior. + [JsonPropertyName("staff")] + public bool? Staff { get; set; } + + /// Whether the user was recognized as GitHub staff. + [JsonPropertyName("staffGithub")] + public bool? StaffGitHub { get; set; } + + /// When the staff-only log level migration last ran. + [JsonPropertyName("staffLogLevelMigrationAt")] + public string? StaffLogLevelMigrationAt { get; set; } + + /// Whether the user was recognized as Microsoft staff. + [JsonPropertyName("staffMicrosoft")] + public bool? StaffMicrosoft { get; set; } + + /// When the staff-only model reset last ran. + [JsonPropertyName("staffModelResetAt")] + public string? StaffModelResetAt { get; set; } + + /// When the staff-only update channel migration last ran. + [JsonPropertyName("staffUpdateChannelMigrationAt")] + public string? StaffUpdateChannelMigrationAt { get; set; } + + /// Folders where the user declined the init prompt, so it stays hidden there. + [JsonPropertyName("suppressInitFolders")] + public IList? SuppressInitFolders { get; set; } + + /// Folders the user has marked as trusted. + [JsonPropertyName("trustedFolders")] + public IList? TrustedFolders { get; set; } +} + +/// Selects the configuration directory whose machine-wide state to read. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GlobalStateLoadForConfigDirRequest +{ + /// Copilot configuration directory to read the state document from, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to read the directory the server resolved for itself. + [JsonPropertyName("configDir")] + public string? ConfigDir { get; set; } +} + +/// A single top-level key to record in the host's machine-wide state. The write replaces only that key and leaves the rest of the document untouched, so two writers recording different one-off flags do not overwrite each other. The stored credential keys cannot be written through this method. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GlobalStateWriteKeyRequest +{ + /// Copilot configuration directory to write the state document in, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to write the directory the server resolved for itself. Mirrors `globalState.loadForConfigDir`, so a caller can read and write the same directory. + [JsonPropertyName("configDir")] + public string? ConfigDir { get; set; } + + /// Top-level key to write, named as it appears in the result of `globalState.load`. It must be one of the writable keys that `globalState.writeKey` lists. + [JsonPropertyName("key")] + public string Key { get; set; } = string.Empty; + + /// Value to store for the key. Omit it, or pass null, to remove the key instead. + [JsonPropertyName("value")] + public JsonElement? Value { get; set; } +} + /// A literal choice the command input accepts, with a human-facing description. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SlashCommandInputChoice @@ -6969,7 +7270,7 @@ public sealed class UserSettingMetadata public JsonElement Value { get; set; } } -/// Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. +/// Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class UserSettingsGetResult { @@ -6978,15 +7279,6 @@ public sealed class UserSettingsGetResult public IDictionary Settings { get => field ??= new Dictionary(); set; } } -/// Outcome of writing user settings. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UserSettingsSetResult -{ - /// Top-level keys whose write landed in settings.json but is shadowed by a value still present in the legacy config.json (config.json wins on read). The write does not take effect until the legacy value is removed. - [JsonPropertyName("shadowedKeys")] - public IList ShadowedKeys { get => field ??= []; set; } -} - /// Partial user settings to write to settings.json. Each top-level key is written individually, replacing the existing value; a key whose value is null is removed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] internal sealed class UserSettingsSetRequest @@ -6996,6 +7288,217 @@ internal sealed class UserSettingsSetRequest public JsonElement Settings { get; set; } } +/// Owner, name, and host of a GitHub repository, as resolved from a git remote URL. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class GitHubRepositoryIdentity +{ + /// Host the remote points at, for example `github.com` or a GitHub Enterprise hostname. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; + + /// Repository name, without the owner prefix or the `.git` suffix. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Repository owner login (user or organization). + [JsonPropertyName("owner")] + public string Owner { get; set; } = string.Empty; +} + +/// The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubRepositoryAtPathResult +{ + /// Resolved repository identity, or null when the selected remote resolves to no GitHub host. + [JsonPropertyName("repository")] + public GitHubRepositoryIdentity? Repository { get; set; } +} + +/// Working-tree path whose owning GitHub repository should be resolved. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubRepositoryAtPathRequest +{ + /// Absolute path to a directory inside the git working tree to resolve. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; +} + +/// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubOwnersRequestIdResult +{ + /// Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`. + [JsonPropertyName("requestId")] + public long RequestId { get; set; } +} + +/// A GitHub login the authenticated user may act as: their own account, or an organization they belong to. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubOwnerOption +{ + /// The owner's GitHub login. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; + + /// Which kind of owner this is. The authenticated user's own account is always reported as `user`. + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; +} + +/// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubOwnersListResult +{ + /// Why no owners could be listed, phrased for a user. Present when the listing failed in a way the caller should render rather than raise. + [JsonPropertyName("message")] + public string? Message { get; set; } + + /// The owners, on success: the authenticated user first, then the organizations they belong to. + [JsonPropertyName("owners")] + public IList? Owners { get; set; } + + /// A malformed request or an unreadable credential, which the caller raises instead of rendering. Kept a field rather than a dispatch error so it stays distinct from `message`, which the caller renders. + [JsonPropertyName("throwError")] + public string? ThrowError { get; set; } + + /// A line the caller should log. Present only alongside `message`, and only for failures worth recording. + [JsonPropertyName("warning")] + public string? Warning { get; set; } +} + +/// Credential to list owners under, and the request id that makes the listing cancellable. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubOwnersListRequest +{ + /// The credential the listing runs under, carried opaquely because its shape is the host's own and the runtime only resolves a token and a GitHub host from it. No credential travels: this selects one the runtime already holds. + [JsonPropertyName("authInfo")] + public JsonElement AuthInfo { get; set; } + + /// Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled before use, released after being abandoned, or already used is refused rather than silently running uncancellable. + [JsonPropertyName("requestId")] + public long RequestId { get; set; } +} + +/// Whether the id named a running owner listing. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubOwnersCancelResult +{ + /// True when a listing with the id was running and the cancel stopped it. False when the id was never registered, was registered but unused, was released after being abandoned, or its listing had ended. An unused id is released and cannot start a later listing. + [JsonPropertyName("canceled")] + public bool Canceled { get; set; } +} + +/// The owner listing to abandon. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubOwnersCancelRequest +{ + /// Request id the listing was started with. + [JsonPropertyName("requestId")] + public long RequestId { get; set; } +} + +/// The remote the checked-out branch tracks. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitCurrentBranchRemoteResult +{ + /// Name of the tracked remote. Reports `origin` whenever the working tree has no tracking configuration to read, including on a detached HEAD, so this is never null and never empty. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("remote")] + public string Remote { get; set; } = string.Empty; +} + +/// Working-tree path a git query applies to. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitCwdRequest +{ + /// Absolute path to a directory inside the git working tree to query. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("cwd")] + public string Cwd { get; set; } = string.Empty; +} + +/// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionWorkingDirectoryContext +{ + /// Merge-base commit SHA (fork point from the remote default branch). + [JsonPropertyName("baseCommit")] + public string? BaseCommit { get; set; } + + /// Current git branch name. + [JsonPropertyName("branch")] + public string? Branch { get; set; } + + /// Current working directory path. + [JsonPropertyName("cwd")] + public string Cwd { get; set; } = string.Empty; + + /// Root directory of the git repository, resolved via git rev-parse. + [JsonPropertyName("gitRoot")] + public string? GitRoot { get; set; } + + /// Head commit of the current git branch. + [JsonPropertyName("headCommit")] + public string? HeadCommit { get; set; } + + /// Hosting platform type of the repository. + [JsonPropertyName("hostType")] + public SessionWorkingDirectoryContextHostType? HostType { get; set; } + + /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps). + [JsonPropertyName("repository")] + public string? Repository { get; set; } + + /// Raw host string from the git remote URL (e.g. "github.com", "dev.azure.com"). + [JsonPropertyName("repositoryHost")] + public string? RepositoryHost { get; set; } +} + +/// A GitHub repository one of a working tree's remotes points at. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitRemoteRepository +{ + /// GitHub host serving the repository, which is not `github.com` for a GitHub Enterprise remote. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; + + /// Repository name, without the owner. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Account or organization owning the repository. + [JsonPropertyName("owner")] + public string Owner { get; set; } = string.Empty; + + /// Name of the first remote that produced this distinct repository entry, such as `origin` or `upstream`. + [JsonPropertyName("remoteName")] + public string RemoteName { get; set; } = string.Empty; +} + +/// The GitHub repositories a working tree's remotes point at. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitReposFromRemotesResult +{ + /// One entry per distinct GitHub repository, in the order git reports the first remote for each repository. Empty when no remote points at a GitHub host, which a caller should read as `not connected to GitHub`. Failing to read the remotes is an error, not an empty list. + [JsonPropertyName("repositories")] + public IList Repositories { get => field ??= []; set; } +} + +/// Git working tree whose GitHub remotes should be listed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitReposFromRemotesRequest +{ + /// Absolute path to the root of the git working tree. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("gitRoot")] + public string GitRoot { get; set; } = string.Empty; +} + /// Validated device-managed settings discovered before a session exists. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class ManagedSettingsReadResult @@ -7047,6 +7550,10 @@ public sealed class ManagedSettingMeta [JsonPropertyName("overridable")] public bool Overridable { get; set; } + /// Original managed value when the runtime adjusted it to a supported effective value. Omitted when no adjustment was needed. + [JsonPropertyName("requested")] + public string? Requested { get; set; } + /// Channel that supplied this scalar value, matching a `layers[].source`: `device`, `server`, or `policyHelper`. These scalar defaults select one winning channel, not a mixed source. Treat unknown values as additional channels; more may be added. [JsonPropertyName("source")] public string Source { get; set; } = string.Empty; @@ -7060,6 +7567,14 @@ public sealed class ManagedSettingsMeta [JsonPropertyName("autoTier")] public ManagedSettingMeta? AutoTier { get; set; } + /// Lock state and provenance of `values.contextTier`. + [JsonPropertyName("contextTier")] + public ManagedSettingMeta? ContextTier { get; set; } + + /// Lock state and provenance of `values.effortLevel`. + [JsonPropertyName("effortLevel")] + public ManagedSettingMeta? EffortLevel { get; set; } + /// Lock state and provenance of `values.model`. [JsonPropertyName("model")] public ManagedSettingMeta? Model { get; set; } @@ -7122,7 +7637,15 @@ public sealed class ManagedSettingsValues [JsonPropertyName("autoTier")] public AutoTier? AutoTier { get; set; } - /// Managed default model identifier, as configured. New sessions start with it; it can name a model the account cannot use, so hosts match it against the listed models. + /// Managed context-tier default for the managed concrete model. + [JsonPropertyName("contextTier")] + public ContextTier? ContextTier { get; set; } + + /// Managed reasoning-effort default for the managed concrete model. The runtime clamps it to an entitled effort when model availability is known. + [JsonPropertyName("effortLevel")] + public string? EffortLevel { get; set; } + + /// Managed default model identifier. When model availability was resolved, aliases and family names are projected to a concrete available model ID; otherwise the configured value is returned. [JsonPropertyName("model")] public string? Model { get; set; } } @@ -7171,6 +7694,10 @@ internal sealed class ManagedSettingsResolveRequest /// Opaque account identifier returned by `account.getAllUsers`. When omitted, the current account is used, or device policy only when no account is signed in. [JsonPropertyName("selectionId")] public string? SelectionId { get; set; } + + /// Working directory used to run an organization policy helper. When omitted, sessionless resolution does not run the helper. + [JsonPropertyName("workingDirectory")] + public string? WorkingDirectory { get; set; } } /// The authoring JSON schema for managed settings recognized by this runtime. @@ -8308,6 +8835,126 @@ internal sealed class SessionsEnrichMetadataRequest public IList Sessions { get => field ??= []; set; } } +/// The workspace record that was written. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsCreateWorkspaceResult +{ + /// The created workspace record, as JSON. + [JsonPropertyName("workspaceJson")] + public string WorkspaceJson { get; set; } = string.Empty; +} + +/// A working-directory context together with the client that produced it. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionWorkingDirectoryContextWithClient +{ + /// Merge-base commit SHA. + [JsonPropertyName("baseCommit")] + public string? BaseCommit { get; set; } + + /// Current git branch name. + [JsonPropertyName("branch")] + public string? Branch { get; set; } + + /// Name of the client that created the session. + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } + + /// Current working directory path. + [JsonPropertyName("cwd")] + public string Cwd { get; set; } = string.Empty; + + /// Root directory of the git repository. + [JsonPropertyName("gitRoot")] + public string? GitRoot { get; set; } + + /// Head commit of the current git branch. + [JsonPropertyName("headCommit")] + public string? HeadCommit { get; set; } + + /// Hosting platform type of the repository. + [JsonPropertyName("hostType")] + public string? HostType { get; set; } + + /// Repository identifier derived from the git remote URL. + [JsonPropertyName("repository")] + public string? Repository { get; set; } + + /// Raw host string from the git remote URL. + [JsonPropertyName("repositoryHost")] + public string? RepositoryHost { get; set; } +} + +/// Identity, state location and starting context for a workspace record. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsCreateWorkspaceRequest +{ + /// Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. + [JsonPropertyName("context")] + public SessionWorkingDirectoryContextWithClient? Context { get; set; } + + /// `windows` (any letter case) selects Windows path rules. Any other value selects POSIX path rules. + [JsonPropertyName("convention")] + public string Convention { get; set; } = string.Empty; + + /// User-supplied display name for the workspace. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Session ID the workspace record belongs to. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Directory the session's state is written under when no session filesystem provider is configured. Ignored when a provider is configured; the provider's session state path is used instead. + [JsonPropertyName("sessionStatePath")] + public string SessionStatePath { get; set; } = string.Empty; +} + +/// The workspace record on disk, omitted when the session has none. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsLoadWorkspaceResult +{ + /// The workspace record, as JSON. Omitted when the record does not exist. + [JsonPropertyName("workspaceJson")] + public string? WorkspaceJson { get; set; } +} + +/// Where the session's state lives, as a root directory and the session ID under it. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsLoadWorkspaceRequest +{ + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Root directory every session's state directory sits under. + [JsonPropertyName("sessionsHome")] + public string SessionsHome { get; set; } = string.Empty; +} + +/// The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsUpdateWorkspaceFieldsResult +{ +} + +/// Where the session's state lives, plus workspace-schema fields to merge into its workspace record. Stored keys outside the schema are not preserved, and a stored `fork_count` is never replaced. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsUpdateWorkspaceFieldsRequest +{ + /// Workspace-schema fields to merge into the record, as a JSON object. Fields the object omits keep their stored values, except stored keys outside the schema are not preserved and a stored `fork_count` is never replaced. + [JsonPropertyName("fieldsJson")] + public string FieldsJson { get; set; } = string.Empty; + + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Root directory every session's state directory sits under. + [JsonPropertyName("sessionsHome")] + public string SessionsHome { get; set; } = string.Empty; +} + /// Reload all hooks (user, plugin, optionally repo) and apply them to the active session. Call after installing or removing plugins so their hooks take effect immediately. No-op when no active session matches the given sessionId. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SessionsReloadPluginHooksResult @@ -8355,47 +9002,6 @@ public sealed class SessionsSetAdditionalPluginsResult { } -/// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class InstalledPlugin -{ - /// Path where the plugin is cached locally. - [JsonPropertyName("cache_path")] - public string? CachePath { get; set; } - - /// Whether the plugin is currently enabled. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } - - /// Installation timestamp. - [JsonPropertyName("installed_at")] - public string InstalledAt { get; set; } = string.Empty; - - /// Absolute path of the marketplace directory a live plugin was resolved from. Present only on live, never-persisted records — those synthesized at session start for a directory/local marketplace, whose cache_path points at the real plugin directory on disk rather than a copy under the installed-plugins cache. Its presence is what marks a record as live, and no record carrying it is ever written to the persisted installedPlugins key. - [JsonPropertyName("installed_from")] - public string? InstalledFrom { get; set; } - - /// Marketplace the plugin came from (empty string for direct repo installs). - [JsonPropertyName("marketplace")] - public string Marketplace { get; set; } = string.Empty; - - /// Plugin name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// Source for direct repo installs (when marketplace is empty). - [JsonPropertyName("source")] - public JsonElement? Source { get; set; } - - /// Per-plugin source fingerprint (a SHA-256 hash of the plugin's catalog source spec plus its resolved source subtree — NOT a Git commit SHA) captured at marketplace install/update time. Auto-update compares it against the freshly recomputed fingerprint to detect a content change that does not bump the version. Absent for pre-existing installs and for direct (non-marketplace) installs. - [JsonPropertyName("source_sha")] - public string? SourceSha { get; set; } - - /// Version installed (if available). - [JsonPropertyName("version")] - public string? Version { get; set; } -} - /// Manager-wide additional plugins to register; replaces any previously-configured set. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] internal sealed class SessionsSetAdditionalPluginsRequest @@ -8883,6 +9489,137 @@ internal sealed class AgentRegistrySpawnRequest public AgentRegistrySpawnPermissionMode? PermissionMode { get; set; } } +/// Feature availability. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ConnectorDiscoveryCapabilities +{ + /// API version. + [JsonPropertyName("apiVersion")] + public long ApiVersion { get; set; } + + /// Availability. + [JsonPropertyName("availability")] + public ConnectorDiscoveryAvailability Availability { get; set; } + + /// Whether results are cached. + [JsonPropertyName("conditionalCache")] + public bool ConditionalCache { get; set; } + + /// Whether accounts are selected by opaque ID. + [JsonPropertyName("opaqueAccountSelection")] + public bool OpaqueAccountSelection { get; set; } +} + +/// Account metadata. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ConnectorDiscoveryAuthInfo +{ + /// Host. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; + + /// Login. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; + + /// Authentication type. + [JsonPropertyName("type")] + public AuthInfoType Type { get; set; } +} + +/// Eligible account. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ConnectorDiscoveryAccount +{ + /// Opaque account ID. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; + + /// Account metadata. + [JsonPropertyName("authInfo")] + public ConnectorDiscoveryAuthInfo AuthInfo { get => field ??= new(); set; } +} + +/// Eligible accounts. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ConnectorDiscoveryAccountList +{ + /// Eligible accounts. + [JsonPropertyName("accounts")] + public IList Accounts { get => field ??= []; set; } + + /// Availability. + [JsonPropertyName("availability")] + public ConnectorDiscoveryAvailability Availability { get; set; } +} + +/// Entry. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ConnectorDiscoveryCatalogEntry +{ + /// Description. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Display name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; + + /// Logo. + [JsonPropertyName("logo")] + public string? Logo { get; set; } + + /// Name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Release tag. + [JsonPropertyName("releaseTag")] + public string? ReleaseTag { get; set; } + + /// Status. + [JsonPropertyName("status")] + public ConnectorCatalogStatus Status { get; set; } + + /// Tier. + [JsonPropertyName("tier")] + public string? Tier { get; set; } +} + +/// Entries for the selected account. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ConnectorDiscoveryCatalogResult +{ + /// Opaque account ID. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; + + /// Entries. + [JsonPropertyName("connectors")] + public IList Connectors { get => field ??= []; set; } + + /// Refresh time in Unix epoch milliseconds. + [JsonPropertyName("refreshedAtMs")] + public long RefreshedAtMs { get; set; } + + /// Revision. + [JsonPropertyName("revision")] + public long Revision { get; set; } +} + +/// Selected account. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ConnectorDiscoveryAccountRequest +{ + /// Opaque account ID. + [RegularExpression("^\\S+$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [MaxLength(2048)] + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; +} + /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] internal sealed class SessionSuspendRequest @@ -10298,6 +11035,7 @@ public sealed class SessionSetCredentialsResult [JsonDerivedType(typeof(SettableAuthInfoToken), "token")] [JsonDerivedType(typeof(SettableAuthInfoCopilotApiToken), "copilot-api-token")] [JsonDerivedType(typeof(SettableAuthInfoUser), "user")] +[JsonDerivedType(typeof(SettableAuthInfoAccount), "account")] [JsonDerivedType(typeof(SettableAuthInfoGhCli), "gh-cli")] [JsonDerivedType(typeof(SettableAuthInfoApiKey), "api-key")] public partial class SettableAuthInfo @@ -10428,6 +11166,24 @@ public partial class SettableAuthInfoUser : SettableAuthInfo public required string Login { get; set; } } +/// An interactive account whose model provider owns its credentials. It carries no GitHub credential. +/// The account variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SettableAuthInfoAccount : SettableAuthInfo +{ + /// + [JsonIgnore] + public override string Type => "account"; + + /// Host coordinate owned by the account's model provider. + [JsonPropertyName("host")] + public required string Host { get; set; } + + /// Login identifying the provider-owned account. + [JsonPropertyName("login")] + public required string Login { get; set; } +} + /// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. /// The gh-cli variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] @@ -10804,6 +11560,11 @@ public partial class AuthReadValueActiveAccount : AuthReadValue [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("account")] public AccountStatus? Account { get; set; } + + /// Credential-free identity metadata for the active account, including resolved Copilot user information when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("authInfo")] + public AuthIdentity? AuthInfo { get; set; } } /// Neutral authentication status summary. @@ -11073,10 +11834,35 @@ public partial class AuthLoginStepNeedsInteraction : AuthLoginStep public override string Kind => "needs-interaction"; } -/// Terminal result of an interactive login flow. +/// A credential-free account choice after sign-in. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class AuthLoginAccount +{ + /// Host coordinate owned by the selected account's provider. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; + + /// Provider kind that owns this account choice. + [JsonPropertyName("kind")] + public AccountKind Kind { get; set; } + + /// Human-readable login for the account choice. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; + + /// Opaque identifier supplied to the next login step to select this account. + [JsonPropertyName("selectionId")] + public string SelectionId { get; set; } = string.Empty; +} + +/// Result of an interactive login flow. Pending consent or account selection is not terminal. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class AuthLoginResultDto { + /// Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 first. + [JsonPropertyName("accounts")] + public IList? Accounts { get; set; } + /// Host that was signed in, when completed. [Url] [StringSyntax(StringSyntaxAttribute.Uri)] @@ -11087,7 +11873,7 @@ public sealed class AuthLoginResultDto [JsonPropertyName("login")] public string? Login { get; set; } - /// Terminal disposition of the login. + /// Current disposition of the login, including pending user decisions. [JsonPropertyName("status")] public AuthLoginResultStatus Status { get; set; } } @@ -11100,7 +11886,7 @@ public partial class AuthLoginStepCompleted : AuthLoginStep [JsonIgnore] public override string Kind => "completed"; - /// The terminal login result. + /// Login result. When status is needs-plaintext-consent or needs-account-selection, advance with the user's decision to continue. [JsonPropertyName("result")] public required AuthLoginResultDto Result { get; set; } } @@ -12716,6 +13502,14 @@ public sealed class CurrentModel [JsonPropertyName("planBaseModelId")] public string? PlanBaseModelId { get; set; } + /// Provider of planBaseModelId, when that saved selection is provider-qualified. + [JsonPropertyName("planBaseProviderId")] + public string? PlanBaseProviderId { get; set; } + + /// Provider selected for this model. Together with modelId, identifies a catalog entry even when providers serve the same model. Omitted for unattributed legacy selections. + [JsonPropertyName("providerId")] + public string? ProviderId { get; set; } + /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } @@ -12833,7 +13627,7 @@ public sealed class ModelPickerPersistenceRequest /// Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModelSwitchToRequest +public sealed class ModelSwitchToRequest { /// Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. [JsonPropertyName("autoTier")] @@ -12859,7 +13653,76 @@ internal sealed class ModelSwitchToRequest [JsonPropertyName("modelChangeScope")] public string? ModelChangeScope { get; set; } - /// Model selection id to switch to, as returned by `list`. A bare id (e.g. `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. + /// Model id to switch to, as returned by `list`. Include providerId to select an exact catalog entry when providers share the id. Without providerId, a bare id retains incumbent selection behavior; a registry-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a BYOK model. + [JsonPropertyName("modelId")] + public required string ModelId { get; set; } + + /// Optional settings context and explicit-override flags used to persist a picker selection. + [JsonPropertyName("pickerPersistence")] + public ModelPickerPersistenceRequest? PickerPersistence { get; set; } + + /// Provider id from the selected list entry's provider reference. Selects this exact provider/model pair; an unavailable pair fails rather than using another provider. Omit for deterministic legacy bare-model selection. + [JsonPropertyName("providerId")] + public string? ProviderId { get; set; } + + /// Reasoning effort level to use for the model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. "none" disables reasoning. Pass null to clear any session effort override and fall back to the model's default. When omitted, the session's current effort is kept. + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } + + /// Reasoning summary mode to request for supported model clients. + [JsonPropertyName("reasoningSummary")] + public ReasoningSummary? ReasoningSummary { get; set; } + + /// Optional repository settings scope to persist after the switch commits. + [JsonPropertyName("repoScope")] + public string? RepoScope { get; set; } + + /// Require the target to be currently available and enabled before applying the switch. + [JsonPropertyName("requireAvailable")] + public bool? RequireAvailable { get; set; } + + /// When true, evaluate context-window compaction policy before applying the switch. + [JsonPropertyName("runCompactionPreflight")] + public bool? RunCompactionPreflight { get; set; } + + /// Origin to record on the effective `session.model_change` event for trusted in-process calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. + [JsonPropertyName("source")] + public ModelChangeSource? Source { get; set; } + + /// Output verbosity level to request for supported models. + [JsonPropertyName("verbosity")] + public Verbosity? Verbosity { get; set; } +} + +/// Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ModelSwitchToRequestWithSession +{ + /// Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + + /// Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. + [JsonPropertyName("compactionDecision")] + public string? CompactionDecision { get; set; } + + /// Explicit context tier for the selected model. `"default"` / `"long_context"` apply the requested tier; omit this field to use normal model behavior with no explicit tier. + [JsonPropertyName("contextTier")] + public ContextTier? ContextTier { get; set; } + + /// When true, defer this switch (enqueue it) if another model change is already queued, even when no turn is active — so it drains last (FIFO) and wins over the already-queued change. Intended for genuine user-initiated model selections; internal restore/reapply switches omit it and apply immediately when no turn is active. When no other model change is queued this has no effect (a switch still applies immediately unless a turn is active). + [JsonPropertyName("deferIfModelChangeQueued")] + public bool? DeferIfModelChangeQueued { get; set; } + + /// Override individual model capabilities resolved by the runtime. + [JsonPropertyName("modelCapabilities")] + public ModelCapabilitiesOverride? ModelCapabilities { get; set; } + + /// Settings scope used when persisting the selected model. + [JsonPropertyName("modelChangeScope")] + public string? ModelChangeScope { get; set; } + + /// Model id to switch to, as returned by `list`. Include providerId to select an exact catalog entry when providers share the id. Without providerId, a bare id retains incumbent selection behavior; a registry-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a BYOK model. [JsonPropertyName("modelId")] public string ModelId { get; set; } = string.Empty; @@ -12867,6 +13730,10 @@ internal sealed class ModelSwitchToRequest [JsonPropertyName("pickerPersistence")] public ModelPickerPersistenceRequest? PickerPersistence { get; set; } + /// Provider id from the selected list entry's provider reference. Selects this exact provider/model pair; an unavailable pair fails rather than using another provider. Omit for deterministic legacy bare-model selection. + [JsonPropertyName("providerId")] + public string? ProviderId { get; set; } + /// Reasoning effort level to use for the model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. "none" disables reasoning. Pass null to clear any session effort override and fall back to the model's default. When omitted, the session's current effort is kept. [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } @@ -12987,6 +13854,10 @@ internal sealed class ModelApplyStartupOverlayRequest [JsonPropertyName("repoModel")] public string? RepoModel { get; set; } + /// Provider owning the repository-selected model. Omitted repository provider identity retains legacy bare-model behavior. + [JsonPropertyName("repoModelProviderId")] + public string? RepoModelProviderId { get; set; } + /// Reasoning effort selected by repository settings, when configured. [JsonPropertyName("repoReasoningEffort")] public string? RepoReasoningEffort { get; set; } @@ -13000,6 +13871,32 @@ internal sealed class ModelApplyStartupOverlayRequest public string SessionId { get; set; } = string.Empty; } +/// RPC data type for SessionModelClearStartupSeed operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionModelClearStartupSeedResult +{ + /// Gets or sets the cleared value. + [JsonPropertyName("cleared")] + public bool Cleared { get; set; } +} + +/// RPC data type for SessionModelClearStartupSeed operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionModelClearStartupSeedRequest +{ + /// Gets or sets the expectedModel value. + [JsonPropertyName("expectedModel")] + public string ExpectedModel { get; set; } = string.Empty; + + /// Provider captured with the startup seed; omission matches only an unqualified seed. + [JsonPropertyName("expectedProviderId")] + public string? ExpectedProviderId { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// The applied host allowlist and effective session model policy after intersection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class ModelSetAllowedModelsResult @@ -13056,6 +13953,65 @@ internal sealed class ModelSetReasoningEffortRequest public string SessionId { get; set; } = string.Empty; } +/// Availability of a server-advertised routing preference. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class AutoTierStatus +{ + /// Whether the provider permits selecting this preference. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// Human-readable explanation of availability. + [JsonPropertyName("message")] + public string? Message { get; set; } + + /// Extensible machine-readable unavailability reason. + [JsonPropertyName("reason")] + public string? Reason { get; set; } +} + +/// A server-advertised routing preference. Identifiers and execution types are extensible. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class AutoTierDescriptor +{ + /// Description displayed beside the preference. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// Human-readable label, not a routing identifier. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; + + /// Opaque routing identifier transmitted unchanged to the provider. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Current account-specific availability. + [JsonPropertyName("status")] + public AutoTierStatus Status { get => field ??= new(); set; } + + /// Execution kind; this client supports `auto` preferences on the Auto model. + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; +} + +/// Account-bound discovery metadata for the virtual `auto` model. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class AutoTierMetadata +{ + /// Provider-default preference, used only when no explicit preference exists. + [JsonPropertyName("defaultTier")] + public string DefaultTier { get; set; } = string.Empty; + + /// Provider that supplied this metadata, when the catalog is provider-attributed. + [JsonPropertyName("providerId")] + public string? ProviderId { get; set; } + + /// Routing preferences in the server's presentation order. + [JsonPropertyName("tiers")] + public IList Tiers { get => field ??= []; set; } +} + /// Cost-category metadata for a CAPI model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SessionModelPriceCategory @@ -13094,6 +14050,10 @@ public sealed class ModelProviderDescriptor [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SessionModelList { + /// Ordered Auto routing preferences discovered for this session's account. + [JsonPropertyName("auto")] + public AutoTierMetadata? Auto { get; set; } + /// Available models, ordered with the most preferred default first. Includes both Copilot (CAPI) models and any registry BYOK models; a BYOK model appears under its provider-qualified selection id (`provider/id`). [JsonPropertyName("list")] public IList List { get => field ??= []; set; } @@ -13227,6 +14187,10 @@ internal sealed class ModeSetRequest [JsonPropertyName("planModelConfigured")] public bool? PlanModelConfigured { get; set; } + /// Provider owning planModel. Omit for legacy bare-model selection. + [JsonPropertyName("planModelProviderId")] + public string? PlanModelProviderId { get; set; } + /// Reasoning effort to use with the dedicated plan model. [JsonPropertyName("planReasoningEffort")] public string? PlanReasoningEffort { get; set; } @@ -13922,6 +14886,19 @@ internal sealed class WorkspacesSaveLargePasteRequest public string SessionId { get; set; } = string.Empty; } +/// Complete text used to generate one session diff. These are display contents, with the same text decoding as the patch, not a file-restore contract. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkspaceDiffContents +{ + /// Complete current text read when computing the diff. Omitted for a deleted file; an empty string represents an existing empty file. + [JsonPropertyName("after")] + public string? After { get; set; } + + /// Complete text before the session first changed the file. Omitted when the file did not exist; an empty string represents an existing empty file. + [JsonPropertyName("before")] + public string? Before { get; set; } +} + /// A single changed file and its unified diff. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class WorkspaceDiffFileChange @@ -13930,6 +14907,10 @@ public sealed class WorkspaceDiffFileChange [JsonPropertyName("changeType")] public WorkspaceDiffFileChangeType ChangeType { get; set; } + /// Full text used for this patch, only when includeContents was requested for session mode. Omitted for binary, oversized or unavailable contents, and for fallback results. Read isFallback and isTruncated before treating an absent value as a missing file. + [JsonPropertyName("contents")] + public WorkspaceDiffContents? Contents { get; set; } + /// Unified diff content for the file. Empty when the diff was truncated. [JsonPropertyName("diff")] public string Diff { get; set; } = string.Empty; @@ -13978,12 +14959,33 @@ public sealed class WorkspaceDiffResult /// Parameters for computing a workspace diff. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesDiffRequest +public sealed class WorkspacesDiffRequest { /// When true, ignore whitespace-only changes (git `--ignore-all-space`). Defaults to false. [JsonPropertyName("ignoreWhitespace")] public bool? IgnoreWhitespace { get; set; } + /// Include the full before/after text used to compute each session diff. Defaults to false; true is accepted only for session mode. Existing capture/read limits still apply, and binary or unavailable contents are not returned. This can substantially increase response size. + [JsonPropertyName("includeContents")] + public bool? IncludeContents { get; set; } + + /// Diff mode requested by the client. + [JsonPropertyName("mode")] + public required WorkspaceDiffMode Mode { get; set; } +} + +/// Parameters for computing a workspace diff. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkspacesDiffRequestWithSession +{ + /// When true, ignore whitespace-only changes (git `--ignore-all-space`). Defaults to false. + [JsonPropertyName("ignoreWhitespace")] + public bool? IgnoreWhitespace { get; set; } + + /// Include the full before/after text used to compute each session diff. Defaults to false; true is accepted only for session mode. Existing capture/read limits still apply, and binary or unavailable contents are not returned. This can substantially increase response size. + [JsonPropertyName("includeContents")] + public bool? IncludeContents { get; set; } + /// Diff mode requested by the client. [JsonPropertyName("mode")] public WorkspaceDiffMode Mode { get; set; } @@ -15485,6 +16487,32 @@ internal sealed class SessionSkillsEnsureLoadedRequest public string SessionId { get; set; } = string.Empty; } +/// The IDE a host is connected to, as reported to the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionConnectedIdeInfo +{ + /// Display name of the connected IDE, for example `VS Code`. + [JsonPropertyName("ideName")] + public string IdeName { get; set; } = string.Empty; + + /// Absolute path of the workspace folder the IDE has open. + [JsonPropertyName("workspaceFolder")] + public string WorkspaceFolder { get; set; } = string.Empty; +} + +/// Records which IDE the host is connected to, or clears it. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionMcpSetConnectedIdeInfoParams +{ + /// The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports that it is disconnected. + [JsonPropertyName("ide")] + public SessionConnectedIdeInfo? Ide { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Recorded MCP server connection failure. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class McpServerFailureInfo @@ -15591,6 +16619,10 @@ public sealed class McpServer /// Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured. [JsonPropertyName("status")] public McpServerStatus Status { get; set; } + + /// Configured URL for an HTTP/SSE server, regardless of configuration source. Omitted for local and in-memory servers. + [JsonPropertyName("url")] + public string? Url { get; set; } } /// MCP servers configured for the session, with their connection status and host-level state. @@ -15615,6 +16647,73 @@ internal sealed class SessionMcpListRequest public string SessionId { get; set; } = string.Empty; } +/// Observational state for a matching already materialized MCP server. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpConfiguredServerState +{ + /// Observed connection error, when the materialized server failed. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Observed connection status. This is not a configuration or readiness guarantee. + [JsonPropertyName("status")] + public McpServerStatus Status { get; set; } +} + +/// Effective MCP configuration entry. Configuration enablement is distinct from the optional live observation. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpConfiguredServer +{ + /// Human-readable display name supplied by configuration. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// Whether this configured server is enabled after session configuration and policy filtering. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// Observed state from an already materialized matching server. Omitted when no live graph has this configured server; it never determines configuration enablement. + [JsonPropertyName("live")] + public McpConfiguredServerState? Live { get; set; } + + /// Server name (config key). + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Configuration provenance: user, workspace, plugin, builtin, or managed. + [JsonPropertyName("source")] + public McpServerSource? Source { get; set; } + + /// Plugin name that provided this server, when source is plugin. + [JsonPropertyName("sourcePlugin")] + public string? SourcePlugin { get; set; } + + /// Plugin version that provided this server, when source is plugin. + [JsonPropertyName("sourcePluginVersion")] + public string? SourcePluginVersion { get; set; } +} + +/// Effective MCP configuration with optional live observations from matching already materialized servers. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpConfiguredServerList +{ + /// Effective configured MCP servers. + [JsonPropertyName("servers")] + public IList Servers { get => field ??= []; set; } +} + +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionMcpListConfiguredRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class McpToolUi @@ -19057,6 +20156,15 @@ internal sealed class SessionExtensionsReloadRequest public string SessionId { get; set; } = string.Empty; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionExtensionsReconcileRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Attachment union accepted by push input, covering files, directories, GitHub objects, blobs, snippets, and extension context. /// Polymorphic base type discriminated by type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] @@ -20179,6 +21287,53 @@ internal sealed class HandlePendingToolCallRequest public string SessionId { get; set; } = string.Empty; } +/// External tool call of the session or one of its sub-agents that is still waiting for session.tools.handlePendingToolCall. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PendingExternalToolRequest +{ + /// Sub-agent instance identifier (the envelope agentId of its events) of the agent that issued the call; absent for calls issued by the root agent. + [JsonPropertyName("agentId")] + public string? AgentId { get; set; } + + /// Arguments to pass to the external tool. + [JsonPropertyName("arguments")] + public JsonElement? Arguments { get; set; } + + /// Stable identity of the provider that offered the tool, for hosts that route extension-owned tools by provider. + [JsonPropertyName("providerId")] + public string? ProviderId { get; set; } + + /// Request ID to pass to session.tools.handlePendingToolCall. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// Tool call ID assigned to this external tool invocation. + [JsonPropertyName("toolCallId")] + public string ToolCallId { get; set; } = string.Empty; + + /// Name of the external tool to invoke. + [JsonPropertyName("toolName")] + public string ToolName { get; set; } = string.Empty; +} + +/// External tool calls still waiting for a result. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PendingExternalToolRequestList +{ + /// External tool calls of the session and its sub-agents that session.tools.handlePendingToolCall would still accept, in the order they were requested. Includes calls that were pending when the session was last suspended and resumed with pending work continued, so a host that did not observe the original external_tool.requested event can answer them. + [JsonPropertyName("items")] + public IList Items { get => field ??= []; set; } +} + +/// No parameters; returns the external tool calls of the session and its sub-agents that are still waiting for a result. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ToolsListPendingRequestsRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Resolve, build, and validate the runtime tool list for this session. Subagent sessions and consumer flows that need an initialized tool set before `send` invoke this. Default base-class implementation is a no-op for sessions that don't support tool validation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class ToolsInitializeAndValidateResult @@ -22949,7 +24104,7 @@ internal sealed class SessionMetadataActivityRequest /// Token-usage breakdown for the session's current context window. public sealed class MetadataContextInfoResultContextInfo { - /// Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%). + /// Output reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%). [JsonPropertyName("bufferTokens")] public long BufferTokens { get; set; } @@ -22961,7 +24116,11 @@ public sealed class MetadataContextInfoResultContextInfo [JsonPropertyName("conversationTokens")] public long ConversationTokens { get; set; } - /// Prompt token limit plus the model's full output token limit. + /// Provider-owned model display label, or the Auto routing label. Presentation only; modelName remains the tokenization identity. + [JsonPropertyName("displayModelName")] + public string? DisplayModelName { get; set; } + + /// Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays. [JsonPropertyName("limit")] public long Limit { get; set; } @@ -22973,10 +24132,14 @@ public sealed class MetadataContextInfoResultContextInfo [JsonPropertyName("modelName")] public string ModelName { get; set; } = string.Empty; - /// Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified). + /// Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified. [JsonPropertyName("promptTokenLimit")] public long PromptTokenLimit { get; set; } + /// Provider whose catalog supplies this context window. Omitted when provider identity is unavailable. + [JsonPropertyName("provider")] + public ModelProviderRef? Provider { get; set; } + /// Tokens consumed by the system prompt. [JsonPropertyName("systemTokens")] public long SystemTokens { get; set; } @@ -23003,14 +24166,18 @@ public sealed class MetadataContextInfoResult [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] internal sealed class MetadataContextInfoRequest { - /// Maximum output tokens allowed by the target model. Pass 0 if unknown. + /// Requested output allowance to reserve against the combined context ceiling. Pass 0 to resolve the session's request cap, falling back to the model's advertised output limit. [JsonPropertyName("outputTokenLimit")] public long OutputTokenLimit { get; set; } - /// Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. + /// Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from the session. [JsonPropertyName("promptTokenLimit")] public long PromptTokenLimit { get; set; } + /// Provider owning selectedModel. Omit to use the current provider for the current model, or deterministic legacy lookup for a different model. + [JsonPropertyName("providerId")] + public string? ProviderId { get; set; } + /// Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. [JsonPropertyName("selectedModel")] public string? SelectedModel { get; set; } @@ -23023,7 +24190,7 @@ internal sealed class MetadataContextInfoRequest /// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. public sealed class MetadataContextAttributionResultContextAttributionCategories { - /// Output reserve plus post-blocking-threshold buffer. + /// Overlapping output reservation plus post-blocking-threshold buffer. [JsonPropertyName("buffer")] public long Buffer { get; set; } @@ -23091,7 +24258,7 @@ public sealed class MetadataContextAttributionResultContextAttributionEntry /// Per-source token attribution snapshot for the current context window. The heaviest individual messages are available separately via `metadata.getContextHeaviestMessages`. public sealed class MetadataContextAttributionResultContextAttribution { - /// Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. + /// Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. [JsonPropertyName("bufferTokens")] public long BufferTokens { get; set; } @@ -23111,7 +24278,7 @@ public sealed class MetadataContextAttributionResultContextAttribution [JsonPropertyName("entries")] public IList Entries { get => field ??= []; set; } - /// Prompt limit plus the model's output reserve: the full context window `categories.freeSpace` and `categories.buffer` are measured against. Mirrors `SessionContextInfo.limit`. + /// Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`. [JsonPropertyName("limit")] public long Limit { get; set; } @@ -23123,7 +24290,7 @@ public sealed class MetadataContextAttributionResultContextAttribution [JsonPropertyName("modelSource")] public string ModelSource { get; set; } = string.Empty; - /// Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. + /// Effective input budget after reserving requested output against the combined context ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. [JsonPropertyName("promptTokenLimit")] public long PromptTokenLimit { get; set; } @@ -23203,43 +24370,6 @@ public sealed class MetadataRecordContextChangeResult { } -/// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionWorkingDirectoryContext -{ - /// Merge-base commit SHA (fork point from the remote default branch). - [JsonPropertyName("baseCommit")] - public string? BaseCommit { get; set; } - - /// Current git branch name. - [JsonPropertyName("branch")] - public string? Branch { get; set; } - - /// Current working directory path. - [JsonPropertyName("cwd")] - public string Cwd { get; set; } = string.Empty; - - /// Root directory of the git repository, resolved via git rev-parse. - [JsonPropertyName("gitRoot")] - public string? GitRoot { get; set; } - - /// Head commit of the current git branch. - [JsonPropertyName("headCommit")] - public string? HeadCommit { get; set; } - - /// Hosting platform type of the repository. - [JsonPropertyName("hostType")] - public SessionWorkingDirectoryContextHostType? HostType { get; set; } - - /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps). - [JsonPropertyName("repository")] - public string? Repository { get; set; } - - /// Raw host string from the git remote URL (e.g. "github.com", "dev.azure.com"). - [JsonPropertyName("repositoryHost")] - public string? RepositoryHost { get; set; } -} - /// Updated working-directory/git context to record on the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] internal sealed class MetadataRecordContextChangeRequest @@ -24702,191 +25832,254 @@ internal sealed class ReleaseEventInterestParams public string SessionId { get; set; } = string.Empty; } -/// Request count and cost metrics for this model. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsModelMetricRequests +internal sealed class SessionUsageGetMetricsRequest { - /// User-initiated premium request cost (with multiplier applied). - [JsonPropertyName("cost")] - public double Cost { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Number of API requests made with this model. - [JsonPropertyName("count")] - public long Count { get; set; } +/// Internal absolute code-change totals reported by the owning host. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionUsageSetCodeChangesRequest +{ + /// Absolute changed-file count; omission preserves the previous count. + [JsonPropertyName("filesCount")] + public double? FilesCount { get; set; } + + /// Absolute added-line total, replacing the previous reading. + [JsonPropertyName("linesAdded")] + public double LinesAdded { get; set; } + + /// Absolute removed-line total, replacing the previous reading. + [JsonPropertyName("linesRemoved")] + public double LinesRemoved { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Per-model token-detail entry containing the accumulated token count for one token type. +/// Existing delegation guidance for an account without delegation access. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsModelMetricTokenDetail +public sealed class SessionQuotaDelegateWarning { - /// Accumulated token count for this token type. - [JsonPropertyName("tokenCount")] - public long TokenCount { get; set; } + /// User-facing guidance. + [JsonPropertyName("text")] + public string Text { get; set; } = string.Empty; + + /// Link to upgrade the account. + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; } -/// Token usage metrics for this model. +/// Session quota snapshot, preserving the incumbent numeric units and reset metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsModelMetricUsage +public sealed class SessionQuotaSnapshot { - /// Total tokens read from prompt cache. - [JsonPropertyName("cacheReadTokens")] - public long CacheReadTokens { get; set; } + /// Entitled quantity in this quota's units. + [JsonPropertyName("entitlementRequests")] + public double EntitlementRequests { get; set; } - /// Total tokens written to prompt cache. - [JsonPropertyName("cacheWriteTokens")] - public long CacheWriteTokens { get; set; } + /// Whether the provider reports available quota. + [JsonPropertyName("hasQuota")] + public bool? HasQuota { get; set; } - /// Total input tokens consumed. - [JsonPropertyName("inputTokens")] - public long InputTokens { get; set; } + /// Whether the entitlement is unlimited. + [JsonPropertyName("isUnlimitedEntitlement")] + public bool IsUnlimitedEntitlement { get; set; } - /// Total output tokens produced. - [JsonPropertyName("outputTokens")] - public long OutputTokens { get; set; } + /// Additional usage in this quota's units. + [JsonPropertyName("overage")] + public double Overage { get; set; } - /// Total output tokens used for reasoning. - [JsonPropertyName("reasoningTokens")] - public long? ReasoningTokens { get; set; } -} + /// Whether additional usage is allowed after quota exhaustion. + [JsonPropertyName("overageAllowedWithExhaustedQuota")] + public bool OverageAllowedWithExhaustedQuota { get; set; } -/// Per-model usage metrics, including request counts/costs, token usage, nano-AI units, and per-token-type details. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsModelMetric -{ - /// Latest known prompt-cache expiration for this model. A timestamp in the past indicates that the observed cache has expired. - [JsonPropertyName("cacheExpiresAt")] - public DateTimeOffset? CacheExpiresAt { get; set; } + /// Additional-usage budget cap, when provided. + [JsonPropertyName("overageEntitlement")] + public double? OverageEntitlement { get; set; } + + /// Percentage of the entitlement remaining. + [JsonPropertyName("remainingPercentage")] + public double RemainingPercentage { get; set; } - /// Request count and cost metrics for this model. - [JsonPropertyName("requests")] - public UsageMetricsModelMetricRequests Requests { get => field ??= new(); set; } + /// Quota reset time in milliseconds since the Unix epoch, when known. + [JsonPropertyName("resetDateEpochMs")] + public double? ResetDateEpochMs { get; set; } - /// Token count details per type. - [JsonPropertyName("tokenDetails")] - public IDictionary? TokenDetails { get; set; } + /// Whether the reset time is estimated. + [JsonPropertyName("resetDateEstimated")] + public bool? ResetDateEstimated { get; set; } - /// Accumulated nano-AI units cost for this model. - [JsonPropertyName("totalNanoAiu")] - public double? TotalNanoAiu { get; set; } + /// Whether this quota uses token-based billing. + [JsonPropertyName("tokenBasedBilling")] + public bool? TokenBasedBilling { get; set; } + + /// Whether usage is allowed after quota exhaustion. + [JsonPropertyName("usageAllowedWithExhaustedQuota")] + public bool UsageAllowedWithExhaustedQuota { get; set; } - /// Token usage metrics for this model. - [JsonPropertyName("usage")] - public UsageMetricsModelMetricUsage Usage { get => field ??= new(); set; } + /// Consumed quantity in this quota's units. + [JsonPropertyName("usedRequests")] + public double UsedRequests { get; set; } } -/// Usage attributed to one agent instance, including its identity, API duration, AI units, and per-model breakdown. +/// The incumbent session-owned quota and account projection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsAgentMetric +public sealed class SessionQuotaGetResult { - /// Human-readable label for this subagent invocation, copied from the originating `subagent.started` event. For task-tool subagents this is the invocation's task description rather than the agent's configured display name, so group by `agentName` for stable per-agent labels. - [JsonPropertyName("agentDisplayName")] - public string? AgentDisplayName { get; set; } + /// Whether the account can sign up for Copilot Free. + [JsonPropertyName("canSignupForCopilotFree")] + public bool CanSignupForCopilotFree { get; set; } - /// Configured agent name, when this is a subagent. - [JsonPropertyName("agentName")] - public string? AgentName { get; set; } + /// Whether the account can delegate tasks to GitHub. + [JsonPropertyName("delegateAvailable")] + public bool DelegateAvailable { get; set; } - /// Per-model usage for this agent, keyed by model identifier. - [JsonPropertyName("modelMetrics")] - public IDictionary ModelMetrics { get => field ??= new Dictionary(); set; } + /// Existing delegation warning for a free account. + [JsonPropertyName("delegateWarning")] + public SessionQuotaDelegateWarning? DelegateWarning { get; set; } - /// Time spent in model API calls by this agent, in milliseconds. - [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonPropertyName("totalApiDurationMs")] - public TimeSpan TotalApiDuration { get; set; } + /// Whether dynamic workflows are enabled for the active account. + [JsonPropertyName("dynamicWorkflowsEnabled")] + public bool DynamicWorkflowsEnabled { get; set; } - /// Accumulated nano-AI units cost for this agent. - [JsonPropertyName("totalNanoAiu")] - public double TotalNanoAiu { get; set; } -} + /// Whether dynamic workflows are visible under the session's feature flags. + [JsonPropertyName("dynamicWorkflowsUiVisible")] + public bool DynamicWorkflowsUiVisible { get; set; } -/// Aggregated code change metrics. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsCodeChanges -{ - /// Distinct file paths modified during the session. - [JsonPropertyName("filesModified")] - public IList FilesModified { get => field ??= []; set; } + /// Whether the account uses the free limited Copilot plan. + [JsonPropertyName("isFreeUser")] + public bool IsFreeUser { get; set; } - /// Number of distinct files modified. - [JsonPropertyName("filesModifiedCount")] - public long FilesModifiedCount { get; set; } + /// Whether the account uses token-based billing. + [JsonPropertyName("isTbbUser")] + public bool IsTbbUser { get; set; } - /// Total lines of code added. - [JsonPropertyName("linesAdded")] - public long LinesAdded { get; set; } + /// Whether model cost columns should be shown. + [JsonPropertyName("modelCostColumnVisible")] + public bool ModelCostColumnVisible { get; set; } - /// Total lines of code removed. - [JsonPropertyName("linesRemoved")] - public long LinesRemoved { get; set; } + /// Resolved Copilot plan tier. + [JsonPropertyName("planTier")] + public SessionQuotaPlanTier PlanTier { get; set; } + + /// Whether premium-request costs are billable. + [JsonPropertyName("premiumRequestsBillable")] + public bool PremiumRequestsBillable { get; set; } + + /// Provider-owned account quota state, including snapshots without numeric balances. + [JsonPropertyName("providerQuotas")] + public IList? ProviderQuotas { get; set; } + + /// Known quota snapshots, keyed by the provider's quota type. + [JsonPropertyName("snapshots")] + public IDictionary Snapshots { get => field ??= new Dictionary(); set; } + + /// Upgrade link for a free account. + [JsonPropertyName("upgradeUrl")] + public string? UpgradeUrl { get; set; } } -/// Session-wide token-detail entry containing the accumulated token count for one token type. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsTokenDetail +internal sealed class SessionQuotaGetRequest { - /// Accumulated token count for this token type. - [JsonPropertyName("tokenCount")] - public long TokenCount { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Accumulated session usage metrics, including premium request cost, token counts, model breakdown, and code-change totals. +/// The incumbent session-owned quota and account projection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageGetMetricsResult +public sealed class SessionQuotaRefreshResult { - /// Per-agent usage metrics, keyed by agent instance identifier. The main conversation uses the stable key `main`. - [JsonPropertyName("agentMetrics")] - public IDictionary? AgentMetrics { get; set; } + /// Whether the account can sign up for Copilot Free. + [JsonPropertyName("canSignupForCopilotFree")] + public bool CanSignupForCopilotFree { get; set; } - /// Aggregated code change metrics. - [JsonPropertyName("codeChanges")] - public UsageMetricsCodeChanges CodeChanges { get => field ??= new(); set; } + /// Whether the account can delegate tasks to GitHub. + [JsonPropertyName("delegateAvailable")] + public bool DelegateAvailable { get; set; } - /// Currently active model identifier. - [JsonPropertyName("currentModel")] - public string? CurrentModel { get; set; } + /// Existing delegation warning for a free account. + [JsonPropertyName("delegateWarning")] + public SessionQuotaDelegateWarning? DelegateWarning { get; set; } - /// Input tokens from the most recent main-agent API call. - [JsonPropertyName("lastCallInputTokens")] - public long LastCallInputTokens { get; set; } + /// Whether dynamic workflows are enabled for the active account. + [JsonPropertyName("dynamicWorkflowsEnabled")] + public bool DynamicWorkflowsEnabled { get; set; } - /// Output tokens from the most recent main-agent API call. - [JsonPropertyName("lastCallOutputTokens")] - public long LastCallOutputTokens { get; set; } + /// Whether dynamic workflows are visible under the session's feature flags. + [JsonPropertyName("dynamicWorkflowsUiVisible")] + public bool DynamicWorkflowsUiVisible { get; set; } - /// Per-model token and request metrics, keyed by model identifier. - [JsonPropertyName("modelMetrics")] - public IDictionary ModelMetrics { get => field ??= new Dictionary(); set; } + /// Whether the account uses the free limited Copilot plan. + [JsonPropertyName("isFreeUser")] + public bool IsFreeUser { get; set; } - /// ISO 8601 timestamp when the session started. - [JsonPropertyName("sessionStartTime")] - public DateTimeOffset SessionStartTime { get; set; } + /// Whether the account uses token-based billing. + [JsonPropertyName("isTbbUser")] + public bool IsTbbUser { get; set; } - /// Session-wide per-token-type accumulated token counts. - [JsonPropertyName("tokenDetails")] - public IDictionary? TokenDetails { get; set; } + /// Whether model cost columns should be shown. + [JsonPropertyName("modelCostColumnVisible")] + public bool ModelCostColumnVisible { get; set; } - /// Total time spent in model API calls (milliseconds). - [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonPropertyName("totalApiDurationMs")] - public TimeSpan TotalApiDuration { get; set; } + /// Resolved Copilot plan tier. + [JsonPropertyName("planTier")] + public SessionQuotaPlanTier PlanTier { get; set; } - /// Session-wide accumulated nano-AI units cost. - [JsonPropertyName("totalNanoAiu")] - public double? TotalNanoAiu { get; set; } + /// Whether premium-request costs are billable. + [JsonPropertyName("premiumRequestsBillable")] + public bool PremiumRequestsBillable { get; set; } - /// Total user-initiated premium request cost across all models (may be fractional due to multipliers). - [JsonPropertyName("totalPremiumRequestCost")] - public double TotalPremiumRequestCost { get; set; } + /// Provider-owned account quota state, including snapshots without numeric balances. + [JsonPropertyName("providerQuotas")] + public IList? ProviderQuotas { get; set; } - /// Raw count of user-initiated API requests. - [JsonPropertyName("totalUserRequests")] - public long TotalUserRequests { get; set; } + /// Known quota snapshots, keyed by the provider's quota type. + [JsonPropertyName("snapshots")] + public IDictionary Snapshots { get => field ??= new Dictionary(); set; } + + /// Upgrade link for a free account. + [JsonPropertyName("upgradeUrl")] + public string? UpgradeUrl { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionUsageGetMetricsRequest +internal sealed class SessionQuotaRefreshRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// A pending session quota warning, consumed once by takeWarnings. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class QuotaWarningProjection +{ + /// User-facing warning text. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + /// Related account or upgrade link. + [JsonPropertyName("url")] + public string? Url { get; set; } + + /// Existing warning category. + [JsonPropertyName("warningType")] + public string WarningType { get; set; } = string.Empty; +} + +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionQuotaTakeWarningsRequest { /// Target session identifier. [JsonPropertyName("sessionId")] @@ -27242,69 +28435,6 @@ public override void Write(Utf8JsonWriter writer, ModelPolicyState value, JsonSe } -/// The neutral kind of a model provider — the model analog of `AccountKind`. A model provider is the live, entitled source a model came from; central code never branches on this beyond a single dispatch. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct ModelProviderKind : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public ModelProviderKind(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// GitHub Copilot / CAPI models, spawned by a github-resolving account that holds a Copilot seat. - public static ModelProviderKind Copilot { get; } = new("copilot"); - - /// Microsoft 365 Copilot (Loki) inference models, spawned by a resolvable Entra-derived Loki account. - public static ModelProviderKind Loki { get; } = new("loki"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ModelProviderKind left, ModelProviderKind right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ModelProviderKind left, ModelProviderKind right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is ModelProviderKind other && Equals(other); - - /// - public bool Equals(ModelProviderKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - - /// - public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - - /// - public override string ToString() => Value; - - /// Provides a for serializing instances. - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter - { - /// - public override ModelProviderKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, ModelProviderKind value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderKind)); - } - } -} - - /// State of the persistent certificate authority of the sandbox credential proxy. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -31370,6 +32500,69 @@ public override void Write(Utf8JsonWriter writer, SlashCommandKind value, JsonSe } +/// Hosting platform type of the repository. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SessionWorkingDirectoryContextHostType : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SessionWorkingDirectoryContextHostType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The working directory repository is hosted on GitHub. + public static SessionWorkingDirectoryContextHostType GitHub { get; } = new("github"); + + /// The working directory repository is hosted on Azure DevOps. + public static SessionWorkingDirectoryContextHostType Ado { get; } = new("ado"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is SessionWorkingDirectoryContextHostType other && Equals(other); + + /// + public bool Equals(SessionWorkingDirectoryContextHostType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override SessionWorkingDirectoryContextHostType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, SessionWorkingDirectoryContextHostType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionWorkingDirectoryContextHostType)); + } + } +} + + /// Severity of a managed-settings validation finding. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -32765,6 +33958,228 @@ public override void Write(Utf8JsonWriter writer, AgentRegistrySpawnPermissionMo } +/// Availability. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ConnectorDiscoveryAvailability : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ConnectorDiscoveryAvailability(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Enabled. + public static ConnectorDiscoveryAvailability Enabled { get; } = new("enabled"); + + /// Disabled. + public static ConnectorDiscoveryAvailability Disabled { get; } = new("disabled"); + + /// Unavailable. + public static ConnectorDiscoveryAvailability Unavailable { get; } = new("unavailable"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorDiscoveryAvailability left, ConnectorDiscoveryAvailability right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorDiscoveryAvailability left, ConnectorDiscoveryAvailability right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ConnectorDiscoveryAvailability other && Equals(other); + + /// + public bool Equals(ConnectorDiscoveryAvailability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ConnectorDiscoveryAvailability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ConnectorDiscoveryAvailability value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorDiscoveryAvailability)); + } + } +} + + +/// Authentication type. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AuthInfoType : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AuthInfoType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Authentication provided by a GitHub App HMAC credential. + public static AuthInfoType Hmac { get; } = new("hmac"); + + /// Authentication resolved from environment-provided credentials. + public static AuthInfoType Env { get; } = new("env"); + + /// Authentication from an interactive user sign-in. + public static AuthInfoType User { get; } = new("user"); + + /// Authentication from a selected provider-owned account, without a GitHub credential. + public static AuthInfoType Account { get; } = new("account"); + + /// Authentication delegated to the GitHub CLI. + public static AuthInfoType GhCli { get; } = new("gh-cli"); + + /// Authentication from an API key credential. + public static AuthInfoType ApiKey { get; } = new("api-key"); + + /// Authentication from a GitHub token. + public static AuthInfoType Token { get; } = new("token"); + + /// Authentication from an SDK GitHub token callback. + public static AuthInfoType TokenProvider { get; } = new("token-provider"); + + /// Authentication from a Copilot API token. + public static AuthInfoType CopilotApiToken { get; } = new("copilot-api-token"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AuthInfoType left, AuthInfoType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AuthInfoType left, AuthInfoType right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AuthInfoType other && Equals(other); + + /// + public bool Equals(AuthInfoType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override AuthInfoType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AuthInfoType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AuthInfoType)); + } + } +} + + +/// Authoritative service connection state for one Connector. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ConnectorCatalogStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ConnectorCatalogStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The Connector is available but not connected. + public static ConnectorCatalogStatus NotConnected { get; } = new("not_connected"); + + /// The Connector service is still completing connection or consent. + public static ConnectorCatalogStatus Pending { get; } = new("pending"); + + /// The Connector is connected and may contribute MCP servers. + public static ConnectorCatalogStatus Connected { get; } = new("connected"); + + /// The Connector service reports an unusable connection. + public static ConnectorCatalogStatus Error { get; } = new("error"); + + /// The service returned a future or unrecognized state. + public static ConnectorCatalogStatus Unknown { get; } = new("unknown"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ConnectorCatalogStatus other && Equals(other); + + /// + public bool Equals(ConnectorCatalogStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ConnectorCatalogStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ConnectorCatalogStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorCatalogStatus)); + } + } +} + + /// The UI mode the agent was in when this message was sent. Defaults to the session's current mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -34217,87 +35632,6 @@ public override void Write(Utf8JsonWriter writer, PermissionDecisionSurface valu } -/// Authentication type. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct AuthInfoType : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public AuthInfoType(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// Authentication provided by a GitHub App HMAC credential. - public static AuthInfoType Hmac { get; } = new("hmac"); - - /// Authentication resolved from environment-provided credentials. - public static AuthInfoType Env { get; } = new("env"); - - /// Authentication from an interactive user sign-in. - public static AuthInfoType User { get; } = new("user"); - - /// Authentication delegated to the GitHub CLI. - public static AuthInfoType GhCli { get; } = new("gh-cli"); - - /// Authentication from an API key credential. - public static AuthInfoType ApiKey { get; } = new("api-key"); - - /// Authentication from a GitHub token. - public static AuthInfoType Token { get; } = new("token"); - - /// Authentication from an SDK GitHub token callback. - public static AuthInfoType TokenProvider { get; } = new("token-provider"); - - /// Authentication from a Copilot API token. - public static AuthInfoType CopilotApiToken { get; } = new("copilot-api-token"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AuthInfoType left, AuthInfoType right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AuthInfoType left, AuthInfoType right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is AuthInfoType other && Equals(other); - - /// - public bool Equals(AuthInfoType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - - /// - public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - - /// - public override string ToString() => Value; - - /// Provides a for serializing instances. - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter - { - /// - public override AuthInfoType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, AuthInfoType value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AuthInfoType)); - } - } -} - - /// The provider kind stamped on a signed-in account. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -34436,7 +35770,7 @@ public override void Write(Utf8JsonWriter writer, LoginProviderKind value, JsonS } -/// Terminal disposition of a login persistence attempt. +/// Disposition of a login attempt, including pending user decisions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -34456,12 +35790,15 @@ public AuthLoginResultStatus(string value) /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The credential was persisted and the account is signed in. + /// The credential was persisted and the selected account is signed in. public static AuthLoginResultStatus Completed { get; } = new("completed"); /// Persistence needs explicit consent to store the token in plaintext. public static AuthLoginResultStatus NeedsPlaintextConsent { get; } = new("needs-plaintext-consent"); + /// Credentials are saved; select an account using a returned selectionId as advance input to complete sign-in. + public static AuthLoginResultStatus NeedsAccountSelection { get; } = new("needs-account-selection"); + /// The user declined plaintext persistence. public static AuthLoginResultStatus Declined { get; } = new("declined"); @@ -37811,78 +39148,6 @@ public override void Write(Utf8JsonWriter writer, ConnectorAuthorizationScope va } -/// Authoritative service connection state for one Connector. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorCatalogStatus : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public ConnectorCatalogStatus(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// The Connector is available but not connected. - public static ConnectorCatalogStatus NotConnected { get; } = new("not_connected"); - - /// The Connector service is still completing connection or consent. - public static ConnectorCatalogStatus Pending { get; } = new("pending"); - - /// The Connector is connected and may contribute MCP servers. - public static ConnectorCatalogStatus Connected { get; } = new("connected"); - - /// The Connector service reports an unusable connection. - public static ConnectorCatalogStatus Error { get; } = new("error"); - - /// The service returned a future or unrecognized state. - public static ConnectorCatalogStatus Unknown { get; } = new("unknown"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is ConnectorCatalogStatus other && Equals(other); - - /// - public bool Equals(ConnectorCatalogStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - - /// - public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - - /// - public override string ToString() => Value; - - /// Provides a for serializing instances. - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter - { - /// - public override ConnectorCatalogStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, ConnectorCatalogStatus value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorCatalogStatus)); - } - } -} - - /// Live MCP status of one Connector-owned runtime server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -40268,69 +41533,6 @@ public override void Write(Utf8JsonWriter writer, WorkspaceSummaryHostType value } -/// Hosting platform type of the repository. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct SessionWorkingDirectoryContextHostType : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public SessionWorkingDirectoryContextHostType(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// The working directory repository is hosted on GitHub. - public static SessionWorkingDirectoryContextHostType GitHub { get; } = new("github"); - - /// The working directory repository is hosted on Azure DevOps. - public static SessionWorkingDirectoryContextHostType Ado { get; } = new("ado"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is SessionWorkingDirectoryContextHostType other && Equals(other); - - /// - public bool Equals(SessionWorkingDirectoryContextHostType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - - /// - public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - - /// - public override string ToString() => Value; - - /// Provides a for serializing instances. - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter - { - /// - public override SessionWorkingDirectoryContextHostType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, SessionWorkingDirectoryContextHostType value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionWorkingDirectoryContextHostType)); - } - } -} - - /// Rust-owned settings predicates exposed across the SDK boundary. Raw feature-flag names are intentionally not part of the contract. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -40975,6 +42177,87 @@ public override void Write(Utf8JsonWriter writer, EventsAgentScope value, JsonSe } +/// Copilot plan tier used by the session quota projection. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SessionQuotaPlanTier : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SessionQuotaPlanTier(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Copilot Free. + public static SessionQuotaPlanTier Free { get; } = new("free"); + + /// Copilot education entitlement. + public static SessionQuotaPlanTier Edu { get; } = new("edu"); + + /// Copilot Pro. + public static SessionQuotaPlanTier Pro { get; } = new("pro"); + + /// Copilot Pro+. + public static SessionQuotaPlanTier ProPlus { get; } = new("pro_plus"); + + /// Copilot Business. + public static SessionQuotaPlanTier Business { get; } = new("business"); + + /// Copilot Enterprise. + public static SessionQuotaPlanTier Enterprise { get; } = new("enterprise"); + + /// Copilot Max. + public static SessionQuotaPlanTier Max { get; } = new("max"); + + /// The account's plan tier is not known. + public static SessionQuotaPlanTier Unknown { get; } = new("unknown"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SessionQuotaPlanTier left, SessionQuotaPlanTier right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SessionQuotaPlanTier left, SessionQuotaPlanTier right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is SessionQuotaPlanTier other && Equals(other); + + /// + public bool Equals(SessionQuotaPlanTier other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override SessionQuotaPlanTier Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, SessionQuotaPlanTier value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionQuotaPlanTier)); + } + } +} + + /// Client population used for the prediction baseline. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -42080,6 +43363,12 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; + /// GlobalState APIs. + public ServerGlobalStateApi GlobalState => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + /// Commands APIs. public ServerCommandsApi Commands => field ?? @@ -42092,6 +43381,24 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; + /// GitHubRepository APIs. + public ServerGitHubRepositoryApi GitHubRepository => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + + /// GitHubOwners APIs. + public ServerGitHubOwnersApi GitHubOwners => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + + /// Git APIs. + public ServerGitApi Git => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + /// ManagedSettings APIs. public ServerManagedSettingsApi ManagedSettings => field ?? @@ -42127,6 +43434,12 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell field ?? Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; + + /// Connectors APIs. + public ServerConnectorsApi Connectors => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; } /// Provides server-scoped Environments APIs. @@ -43389,6 +44702,64 @@ public async Task GetDiscoveryPathsAsync(IList? var request = new AgentsGetDiscoveryPathsRequest { ProjectPaths = projectPaths, ExcludeHostAgents = excludeHostAgents }; return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getDiscoveryPaths", [request], cancellationToken); } + + /// Lists the agents this runtime ships, by name. A consumer separating shipped agents from ones the user or a plugin authored should compare against these names rather than against `AgentInfo.source`: an authored agent may carry the `builtin` source while not being one of these, and the runtime treats the two as separate questions. `disableableNames` is the subset a user may turn off, which a client needs to decide whether to offer a toggle. `yamlBasedNames` is the subset backed by a shipped YAML definition, which a client needs before asking the runtime to load one. + /// The to monitor for cancellation requests. The default is . + /// The agents this runtime ships, named so a consumer can tell them apart from authored ones. + internal async Task GetBuiltinsAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getBuiltins", [], cancellationToken); + } + + /// Lists the shipped agents a client should offer right now, filtered by the feature flags it passes. `getBuiltins` names every agent the runtime knows about; some of those are gated, so a client rendering a picker wants this narrower list together with the description to show beside each name. + /// Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or pass null for no flags. + /// Flag overrides keyed by name. A null entry uses the corresponding base flag; false explicitly disables it. Omit or pass null for no overrides. + /// The surface asking, which gates agents that only apply to one client. Omit or pass null to apply no client filter. + /// The to monitor for cancellation requests. The default is . + /// The shipped agents available under the requested flags. + internal async Task GetAvailableBuiltinsAsync(IDictionary? featureFlags = null, IDictionary? overrides = null, string? context = null, CancellationToken cancellationToken = default) + { + var request = new AgentsGetAvailableBuiltinsRequest { FeatureFlags = featureFlags, Overrides = overrides, Context = context }; + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getAvailableBuiltins", [request], cancellationToken); + } + + /// Loads one shipped agent's YAML definition, for a client that needs what the agent declares rather than only its name. `getBuiltins` reports which names have a definition to load: a name outside its `yamlBasedNames` is special-cased in code and has none. The definition crosses as its own JSON rather than as contract-typed fields, because the runtime parses it with the agent schema's tolerant shape and re-typing it here would drop the keys that shape accepts and this one does not. The projected `__nativeCustomAgent` view the runtime derives is included, so a caller reading the declared model and a caller rendering the agent see the same definition. + /// The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside that list is special-cased in code and has no definition, and is reported as an error rather than as an empty definition. + /// The to monitor for cancellation requests. The default is . + /// One shipped agent's definition. + internal async Task GetBuiltinDefinitionAsync(string name, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(name); + + var request = new AgentsGetBuiltinDefinitionRequest { Name = name }; + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getBuiltinDefinition", [request], cancellationToken); + } + + /// Projects one shipped agent the way a picker lists it, reading only the metadata at the head of the definition file and stopping before the prompt body. `getBuiltinDefinition` answers the whole definition instead, so a client listing every shipped agent should prefer this one: the cost of a listing grows with the number of agents, and the prompt body is the part a listing never shows. The two also differ in shape. This returns the projected custom agent on its own, whereas `getBuiltinDefinition` returns the authored definition with that projection nested under `__nativeCustomAgent`. + /// The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here too, from its in-code definition. + /// The to monitor for cancellation requests. The default is . + /// One shipped agent, projected for a listing. + internal async Task GetBuiltinListingDefinitionAsync(string name, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(name); + + var request = new AgentsGetBuiltinListingDefinitionRequest { Name = name }; + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getBuiltinListingDefinition", [request], cancellationToken); + } + + /// Resolves the model a custom agent asks for against the models actually available, and answers both the model to switch to and the warning a user should see when the agent's preference cannot be met. A custom agent may name several acceptable models in preference order, so the decision is a match rather than a lookup, and an agent whose preference is unavailable is a normal outcome that produces a warning rather than an error. A host must call this rather than pick the first available name itself, because the preference order and the wording of the warning are what keep one installation's agent selection the same as another's. + /// The agent's declared `model:` entry, serialized. A single name or an ordered list of acceptable names. + /// The models available to this session, serialized in the shape the model list carries. + /// The to monitor for cancellation requests. The default is . + /// The model to switch to, and the warning to show when the agent's preference could not be met. + internal async Task CustomAgentInitialModelDecisionAsync(string agentModelsJson, string availableModelsJson, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(agentModelsJson); + ArgumentNullException.ThrowIfNull(availableModelsJson); + + var request = new AgentsCustomAgentInitialModelDecisionParams { AgentModelsJson = agentModelsJson, AvailableModelsJson = availableModelsJson }; + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.customAgentInitialModelDecision", [request], cancellationToken); + } } /// Provides server-scoped Instructions APIs. @@ -43425,6 +44796,49 @@ public async Task GetDiscoveryPathsAsync(IListProvides server-scoped GlobalState APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerGlobalStateApi +{ + private readonly JsonRpc _rpc; + + internal ServerGlobalStateApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Reads the host's machine-wide state: which plugins are installed and the one-off flags and timestamps that record what the user has already been shown or migrated. This is the state that outlives a single session and a single workspace, so a host reads it to decide whether to run a first-launch step, offer an onboarding prompt, or skip one it has already completed. The stored credentials are deliberately not part of this result; a caller that needs an authenticated identity asks the account methods for it instead. Reading is non-destructive and every field is optional, because a fresh install has recorded nothing yet. + /// The to monitor for cancellation requests. The default is . + /// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. + internal async Task LoadAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "globalState.load", [], cancellationToken); + } + + /// Reads the host's machine-wide state exactly as `globalState.load` does, but from a caller-supplied configuration directory instead of the one the server resolved for itself. Use this when a consumer scopes a session to its own Copilot home — the SDK's per-session `configDir` override — so the state read matches the directory that session actually uses. An absent or empty `configDir` resolves the server's own home, making this identical to `globalState.load`. The stored credentials are omitted here for the same reason they are omitted from `globalState.load`: a caller that needs an authenticated identity asks the account methods instead, so pointing this at another directory cannot be used to read the credentials kept in it. + /// Copilot configuration directory to read the state document from, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to read the directory the server resolved for itself. + /// The to monitor for cancellation requests. The default is . + /// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. + internal async Task LoadForConfigDirAsync(string? configDir = null, CancellationToken cancellationToken = default) + { + var request = new GlobalStateLoadForConfigDirRequest { ConfigDir = configDir }; + return await CopilotClient.InvokeRpcAsync(_rpc, "globalState.loadForConfigDir", [request], cancellationToken); + } + + /// Records one top-level key in the host's machine-wide state, the counterpart to `globalState.load`. A host calls this to remember that it has shown an onboarding step, asked a one-off question, or completed a migration, so the next run can skip it. Only the named key is replaced and the rest of the document is preserved, which lets two writers record different flags without overwriting each other; passing no value removes the key instead. Only the keys a host records itself are writable: `appInstallNudgeResponded`, `appTipShown`, `askedSetupTerminals`, `autoFeedbackLastPromptedAt`, `firstLaunchAt`, `recentModelIds`, `sandboxCredentialProxyCaDeclined` and `sandboxOnboardingShown`. Every other key is refused, including `installedPlugins`, the stored credentials, `trustedFolders`, the staff flags and the signed-in accounts. Plugin enablement must use the plugin APIs, which apply repository and managed-policy checks. + /// Top-level key to write, named as it appears in the result of `globalState.load`. It must be one of the writable keys that `globalState.writeKey` lists. + /// Copilot configuration directory to write the state document in, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to write the directory the server resolved for itself. Mirrors `globalState.loadForConfigDir`, so a caller can read and write the same directory. + /// Value to store for the key. Omit it, or pass null, to remove the key instead. + /// The to monitor for cancellation requests. The default is . + internal async Task WriteKeyAsync(string key, string? configDir = null, object? value = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(key); + + var request = new GlobalStateWriteKeyRequest { Key = key, ConfigDir = configDir, Value = CopilotClient.ToJsonElementForWire(value) }; + await CopilotClient.InvokeRpcAsync(_rpc, "globalState.writeKey", [request], cancellationToken); + } +} + /// Provides server-scoped Commands APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class ServerCommandsApi @@ -43474,31 +44888,138 @@ internal ServerUserSettingsApi(JsonRpc rpc) _rpc = rpc; } - /// Drops this runtime process's in-memory user settings cache so the next settings read observes disk. + /// Lists every known user setting from settings.json, each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. /// The to monitor for cancellation requests. The default is . - public async Task ReloadAsync(CancellationToken cancellationToken = default) - { - await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.reload", [], cancellationToken); - } - - /// Lists every known user setting (settings.json overlaid with the legacy config.json, config.json wins), each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. - /// The to monitor for cancellation requests. The default is . - /// Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. + /// Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. public async Task GetAsync(CancellationToken cancellationToken = default) { return await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.get", [], cancellationToken); } - /// Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. Returns the keys whose new value is shadowed by a legacy config.json entry (config.json wins on read), which the runtime leaves in place — such writes do not take effect until the legacy value is removed. + /// Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. /// Partial user settings to write, as a free-form object keyed by setting name. /// The to monitor for cancellation requests. The default is . - /// Outcome of writing user settings. - public async Task SetAsync(object settings, CancellationToken cancellationToken = default) + public async Task SetAsync(object settings, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(settings); var request = new UserSettingsSetRequest { Settings = CopilotClient.ToJsonElementForWire(settings)!.Value }; - return await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.set", [request], cancellationToken); + await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.set", [request], cancellationToken); + } +} + +/// Provides server-scoped GitHubRepository APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerGitHubRepositoryApi +{ + private readonly JsonRpc _rpc; + + internal ServerGitHubRepositoryApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Resolves the GitHub repository that owns a working-tree path by reading the selected git remote configured for it, preferring `origin`. Returns a null `repository` when the path is inside a git working tree but that selected remote does not resolve to a GitHub host. Fails when the path is not inside a git working tree at all, so a caller can tell 'not a repository' apart from 'a repository with no GitHub remote'. + /// Absolute path to a directory inside the git working tree to resolve. + /// The to monitor for cancellation requests. The default is . + /// The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. + internal async Task AtPathAsync(string path, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(path); + + var request = new GitHubRepositoryAtPathRequest { Path = path }; + return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubRepository.atPath", [request], cancellationToken); + } +} + +/// Provides server-scoped GitHubOwners APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerGitHubOwnersApi +{ + private readonly JsonRpc _rpc; + + internal ServerGitHubOwnersApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Registers a cancellable owner listing and returns its request id. Separate from `gitHubOwners.list` so the id exists before the listing starts: a caller that abandons the listing the moment it begins would otherwise have nothing to name in `gitHubOwners.cancel`. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. + /// The to monitor for cancellation requests. The default is . + /// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. + internal async Task NextRequestIdAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubOwners.nextRequestId", [], cancellationToken); + } + + /// Lists the logins the authenticated user may act as — their own account first, then the organizations they belong to — by asking the GitHub API under the supplied credential. No credential travels in the request: `authInfo` selects one the runtime already holds, and the runtime resolves the token and the GitHub host from it. A failure the caller should render arrives as `message`; one it should raise arrives as `throwError`. + /// Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled before use, released after being abandoned, or already used is refused rather than silently running uncancellable. + /// The credential the listing runs under, carried opaquely because its shape is the host's own and the runtime only resolves a token and a GitHub host from it. No credential travels: this selects one the runtime already holds. + /// The to monitor for cancellation requests. The default is . + /// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. + internal async Task ListAsync(long requestId, object authInfo, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(authInfo); + + var request = new GitHubOwnersListRequest { RequestId = requestId, AuthInfo = CopilotClient.ToJsonElementForWire(authInfo)!.Value }; + return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubOwners.list", [request], cancellationToken); + } + + /// Abandons an owner listing started with the given request id. Answers `canceled: true` while a listing with that id is running. Answers `canceled: false` when the id was never registered, was registered but not used, was released after being abandoned, or its listing has ended. Canceling an unused id releases it, and a later `list` with that id is refused. The cancel acts only on owner listings and never reaches another request of the host. + /// Request id the listing was started with. + /// The to monitor for cancellation requests. The default is . + /// Whether the id named a running owner listing. + internal async Task CancelAsync(long requestId, CancellationToken cancellationToken = default) + { + var request = new GitHubOwnersCancelRequest { RequestId = requestId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubOwners.cancel", [request], cancellationToken); + } +} + +/// Provides server-scoped Git APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerGitApi +{ + private readonly JsonRpc _rpc; + + internal ServerGitApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Reads the remote that the branch checked out in a working tree tracks, as `branch.<name>.remote` configures it. Reports `origin` rather than failing whenever there is no tracking configuration to read — on a detached HEAD, on a branch with no upstream, or when git itself fails — because a caller asking which remote to talk to needs an answer it can act on, not an error. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. + /// Absolute path to a directory inside the git working tree to query. + /// The to monitor for cancellation requests. The default is . + /// The remote the checked-out branch tracks. + internal async Task CurrentBranchRemoteAsync(string cwd, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(cwd); + + var request = new GitCwdRequest { Cwd = cwd }; + return await CopilotClient.InvokeRpcAsync(_rpc, "git.currentBranchRemote", [request], cancellationToken); + } + + /// Collects the repository context of a working directory in one call: working tree root, repository identifier and host, current branch, and the HEAD and base commits. Every repository field is omitted when the path is not inside a git working tree, and the requested path is echoed back as `cwd`. The answer is the same `SessionWorkingDirectoryContext` that `session.metadata.recordContextChange` accepts, so a caller polling for a context change can forward the result unchanged. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. It can become public once an SDK consumer needs to derive session context from a directory itself. + /// Absolute path to a directory inside the git working tree to query. + /// The to monitor for cancellation requests. The default is . + /// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. + internal async Task WorkingDirectoryContextAsync(string cwd, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(cwd); + + var request = new GitCwdRequest { Cwd = cwd }; + return await CopilotClient.InvokeRpcAsync(_rpc, "git.workingDirectoryContext", [request], cancellationToken); + } + + /// Lists the GitHub repositories a working tree's remotes point at, one entry per distinct repository, so a caller can resolve a base and head repository without parsing remote URLs itself. When several remotes name the same repository, only the first is listed, and the entry keeps that remote name. Remotes pointing at no GitHub host are left out, so an empty list means the tree reaches GitHub through no remote. Failing to read the remotes is reported as an error rather than as an empty list, because the two mean different things to a caller. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. + /// Absolute path to the root of the git working tree. + /// The to monitor for cancellation requests. The default is . + /// The GitHub repositories a working tree's remotes point at. + internal async Task ReposFromRemotesAsync(string gitRoot, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(gitRoot); + + var request = new GitReposFromRemotesRequest { GitRoot = gitRoot }; + return await CopilotClient.InvokeRpcAsync(_rpc, "git.reposFromRemotes", [request], cancellationToken); } } @@ -43532,14 +45053,19 @@ public async Task ClearCacheAsync(CancellationToken cancellationToken = default) /// Opaque account identifier returned by `account.getAllUsers`. When omitted, the current account is used, or device policy only when no account is signed in. /// GitHub token to resolve instead of the current account. The call fails when the token cannot be resolved. /// Embedding client identity for server policy requests, as in session creation. Omit for the CLI identity. + /// Working directory used to run an organization policy helper. When omitted, sessionless resolution does not run the helper. /// The to monitor for cancellation requests. The default is . /// Effective enterprise managed settings for an account, resolved without a session. - public async Task ResolveAsync(string? selectionId = null, string? gitHubToken = null, string? clientName = null, CancellationToken cancellationToken = default) + public async Task ResolveAsync(string? selectionId = null, string? gitHubToken = null, string? clientName = null, string? workingDirectory = null, CancellationToken cancellationToken = default) { - var request = new ManagedSettingsResolveRequest { SelectionId = selectionId, GitHubToken = gitHubToken, ClientName = clientName }; + var request = new ManagedSettingsResolveRequest { SelectionId = selectionId, GitHubToken = gitHubToken, ClientName = clientName, WorkingDirectory = workingDirectory }; return await CopilotClient.InvokeRpcAsync(_rpc, "managedSettings.resolve", [request], cancellationToken); } + /// Compatibility overload preserving the positional CancellationToken parameter from before workingDirectory was added. + public Task ResolveAsync(string? selectionId, string? gitHubToken, string? clientName, CancellationToken cancellationToken) + => ResolveAsync(selectionId, gitHubToken, clientName, workingDirectory: null, cancellationToken); + /// Returns the managed-settings authoring JSON schema with descriptive `x-composition` annotations aligned with the shared settings-engine vocabulary. These annotations are not a complete runtime composition contract: model, effortLevel, and contextTier remain coupled. Use `managedSettings.compose` for the runtime's effective result. Performs no I/O. /// The to monitor for cancellation requests. The default is . /// The authoring JSON schema for managed settings recognized by this runtime. @@ -43946,6 +45472,54 @@ public async Task EnrichMetadataAsync(IList(_rpc, "sessions.enrichMetadata", [request], cancellationToken); } + /// Creates the workspace record for a session that has not been opened yet. A host that hands a session off to another application — writing the record and then launching that application against the session ID — needs the record on disk before any session exists to carry it, which the session-scoped workspace methods cannot do. Replaces any existing record and resets the checkpoint index. When writing to the local filesystem, a stored `fork_count` survives on disk. Returns the record it built, so a surviving stored `fork_count` can differ from the answer. + /// Session ID the workspace record belongs to. + /// Directory the session's state is written under when no session filesystem provider is configured. Ignored when a provider is configured; the provider's session state path is used instead. + /// `windows` (any letter case) selects Windows path rules. Any other value selects POSIX path rules. + /// Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. + /// User-supplied display name for the workspace. + /// The to monitor for cancellation requests. The default is . + /// The workspace record that was written. + internal async Task CreateWorkspaceAsync(string sessionId, string sessionStatePath, string convention, SessionWorkingDirectoryContextWithClient? context = null, string? name = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(sessionId); + ArgumentNullException.ThrowIfNull(sessionStatePath); + ArgumentNullException.ThrowIfNull(convention); + + var request = new SessionsCreateWorkspaceRequest { SessionId = sessionId, SessionStatePath = sessionStatePath, Convention = convention, Context = context, Name = name }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.createWorkspace", [request], cancellationToken); + } + + /// Reads a session's workspace record straight from disk, without opening the session. Resuming by session ID has to know where the session lives before it can connect, so the lookup cannot come from the session-scoped workspace methods, which resolve their location from a live session's context. Returns no record when the file is absent. + /// Root directory every session's state directory sits under. + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + /// The to monitor for cancellation requests. The default is . + /// The workspace record on disk, omitted when the session has none. + internal async Task LoadWorkspaceAsync(string sessionsHome, string sessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(sessionsHome); + ArgumentNullException.ThrowIfNull(sessionId); + + var request = new SessionsLoadWorkspaceRequest { SessionsHome = sessionsHome, SessionId = sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.loadWorkspace", [request], cancellationToken); + } + + /// Merges fields into a session's workspace record on disk, creating the record when it is absent. The counterpart to `sessions.loadWorkspace`, for the same before-the-session-exists case. It preserves stored workspace-schema fields the request does not supply, does not preserve stored keys outside the workspace schema, and never replaces a stored `fork_count`. + /// Root directory every session's state directory sits under. + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + /// Workspace-schema fields to merge into the record, as a JSON object. Fields the object omits keep their stored values, except stored keys outside the schema are not preserved and a stored `fork_count` is never replaced. + /// The to monitor for cancellation requests. The default is . + /// The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays. + internal async Task UpdateWorkspaceFieldsAsync(string sessionsHome, string sessionId, string fieldsJson, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(sessionsHome); + ArgumentNullException.ThrowIfNull(sessionId); + ArgumentNullException.ThrowIfNull(fieldsJson); + + var request = new SessionsUpdateWorkspaceFieldsRequest { SessionsHome = sessionsHome, SessionId = sessionId, FieldsJson = fieldsJson }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.updateWorkspaceFields", [request], cancellationToken); + } + /// Reloads user, plugin, and (optionally) repo hooks on the active session. /// Active session ID to reload hooks for. /// When true, skip repo-level hooks. Use before folder trust is confirmed; loadDeferredRepoHooks loads them post-trust. @@ -44092,6 +45666,58 @@ public async Task SpawnAsync(string cwd, string? agent } } +/// Provides server-scoped Connectors APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerConnectorsApi +{ + private readonly JsonRpc _rpc; + + internal ServerConnectorsApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Returns feature availability. + /// The to monitor for cancellation requests. The default is . + /// Feature availability. + public async Task GetCapabilitiesAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.getCapabilities", [], cancellationToken); + } + + /// Returns eligible accounts. + /// The to monitor for cancellation requests. The default is . + /// Eligible accounts. + public async Task GetAccountsAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.getAccounts", [], cancellationToken); + } + + /// Lists entries for the selected account. + /// Opaque account ID. + /// The to monitor for cancellation requests. The default is . + /// Entries for the selected account. + public async Task ListAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + var request = new ConnectorDiscoveryAccountRequest { AccountId = accountId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.list", [request], cancellationToken); + } + + /// Refreshes entries for the selected account. + /// Opaque account ID. + /// The to monitor for cancellation requests. The default is . + /// Entries for the selected account. + public async Task RefreshAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + var request = new ConnectorDiscoveryAccountRequest { AccountId = accountId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.refresh", [request], cancellationToken); + } +} + /// Provides typed session-scoped RPC methods. public sealed class SessionRpc { @@ -44356,6 +45982,12 @@ internal SessionRpc(CopilotSession session) Interlocked.CompareExchange(ref field, new(_session), null) ?? field; + /// Quota APIs. + public QuotaApi Quota => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; + /// LimitPrediction APIs. public LimitPredictionApi LimitPrediction => field ?? @@ -45373,7 +47005,7 @@ public async Task GetCurrentAsync(CancellationToken cancellationTo } /// Switches the session to a model and optional reasoning configuration. - /// Model selection id to switch to, as returned by `list`. A bare id (e.g. `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. + /// Model id to switch to, as returned by `list`. Include providerId to select an exact catalog entry when providers share the id. Without providerId, a bare id retains incumbent selection behavior; a registry-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a BYOK model. /// Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. /// Reasoning effort level to use for the model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. "none" disables reasoning. Pass null to clear any session effort override and fall back to the model's default. When omitted, the session's current effort is kept. /// Reasoning summary mode to request for supported model clients. @@ -45395,10 +47027,23 @@ public async Task SwitchToAsync(string modelId, AutoTier? a ArgumentNullException.ThrowIfNull(modelId); _session.ThrowIfDisposed(); - var request = new ModelSwitchToRequest { SessionId = _session.SessionId, ModelId = modelId, AutoTier = autoTier, ReasoningEffort = reasoningEffort, ReasoningSummary = reasoningSummary, Verbosity = verbosity, ModelCapabilities = modelCapabilities, ContextTier = contextTier, Source = source, DeferIfModelChangeQueued = deferIfModelChangeQueued, CompactionDecision = compactionDecision, RunCompactionPreflight = runCompactionPreflight, RepoScope = repoScope, ModelChangeScope = modelChangeScope, RequireAvailable = requireAvailable, PickerPersistence = pickerPersistence }; + var request = new ModelSwitchToRequestWithSession { SessionId = _session.SessionId, ModelId = modelId, AutoTier = autoTier, ReasoningEffort = reasoningEffort, ReasoningSummary = reasoningSummary, Verbosity = verbosity, ModelCapabilities = modelCapabilities, ContextTier = contextTier, Source = source, DeferIfModelChangeQueued = deferIfModelChangeQueued, CompactionDecision = compactionDecision, RunCompactionPreflight = runCompactionPreflight, RepoScope = repoScope, ModelChangeScope = modelChangeScope, RequireAvailable = requireAvailable, PickerPersistence = pickerPersistence }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.model.switchTo", [request], cancellationToken); } + /// Switches the session to a model and optional reasoning configuration. + /// Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. + /// The to monitor for cancellation requests. The default is . + /// The model identifier active on the session after the switch. + public async Task SwitchToAsync(ModelSwitchToRequest request, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(request.ModelId); + _session.ThrowIfDisposed(); + var wireRequest = new ModelSwitchToRequestWithSession { SessionId = _session.SessionId, ModelId = request.ModelId, ProviderId = request.ProviderId, AutoTier = request.AutoTier, ReasoningEffort = request.ReasoningEffort, ReasoningSummary = request.ReasoningSummary, Verbosity = request.Verbosity, ModelCapabilities = request.ModelCapabilities, ContextTier = request.ContextTier, Source = request.Source, DeferIfModelChangeQueued = request.DeferIfModelChangeQueued, CompactionDecision = request.CompactionDecision, RunCompactionPreflight = request.RunCompactionPreflight, RepoScope = request.RepoScope, ModelChangeScope = request.ModelChangeScope, RequireAvailable = request.RequireAvailable, PickerPersistence = request.PickerPersistence }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.model.switchTo", [wireRequest], cancellationToken); + } + /// Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`. /// Auto preference to activate when a future user turn using the `auto` model safely mints a replacement model and token pair. Pass null to return to provider-default Auto routing. /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. @@ -45418,6 +47063,7 @@ public async Task SwitchAutoTierAsync(AutoTier? autoT /// Startup default model from the enterprise policy helper, when configured. Weakest of the managed sources: it applies only when neither device nor server policy names a model, and an explicit user selection still wins. /// Effective default Auto routing preference from user and managed settings. Applies only to fresh sessions and never replaces a per-session selection. /// Model selected by repository settings, when configured. + /// Provider owning the repository-selected model. Omitted repository provider identity retains legacy bare-model behavior. /// Reasoning effort selected by repository settings, when configured. /// Context tier selected by repository settings, when configured. /// Auto routing preference selected by repository settings, when configured. Applied only when the overlay selects the Auto model; beside a concrete model it stays dormant. @@ -45427,14 +47073,27 @@ public async Task SwitchAutoTierAsync(AutoTier? autoT /// Context tier paired with the effective organization-managed model. Applies only when that concrete managed model is selected; it is ignored for Auto and for CLI, resume, or user overrides. /// The to monitor for cancellation requests. The default is . /// The model identifier active on the session after the switch. - internal async Task ApplyStartupOverlayAsync(string? deviceManagedModel = null, string? serverManagedModel = null, string? policyHelperModel = null, AutoTier? autoTier = null, string? repoModel = null, string? repoReasoningEffort = null, string? repoContextTier = null, string? repoAutoTier = null, string? cliModel = null, bool? deferredResume = null, string? managedReasoningEffort = null, string? managedContextTier = null, CancellationToken cancellationToken = default) + internal async Task ApplyStartupOverlayAsync(string? deviceManagedModel = null, string? serverManagedModel = null, string? policyHelperModel = null, AutoTier? autoTier = null, string? repoModel = null, string? repoModelProviderId = null, string? repoReasoningEffort = null, string? repoContextTier = null, string? repoAutoTier = null, string? cliModel = null, bool? deferredResume = null, string? managedReasoningEffort = null, string? managedContextTier = null, CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); - var request = new ModelApplyStartupOverlayRequest { SessionId = _session.SessionId, DeviceManagedModel = deviceManagedModel, ServerManagedModel = serverManagedModel, PolicyHelperModel = policyHelperModel, AutoTier = autoTier, RepoModel = repoModel, RepoReasoningEffort = repoReasoningEffort, RepoContextTier = repoContextTier, RepoAutoTier = repoAutoTier, CliModel = cliModel, DeferredResume = deferredResume, ManagedReasoningEffort = managedReasoningEffort, ManagedContextTier = managedContextTier }; + var request = new ModelApplyStartupOverlayRequest { SessionId = _session.SessionId, DeviceManagedModel = deviceManagedModel, ServerManagedModel = serverManagedModel, PolicyHelperModel = policyHelperModel, AutoTier = autoTier, RepoModel = repoModel, RepoModelProviderId = repoModelProviderId, RepoReasoningEffort = repoReasoningEffort, RepoContextTier = repoContextTier, RepoAutoTier = repoAutoTier, CliModel = cliModel, DeferredResume = deferredResume, ManagedReasoningEffort = managedReasoningEffort, ManagedContextTier = managedContextTier }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.model.applyStartupOverlay", [request], cancellationToken); } + /// Clears an unchanged startup model/provider seed before default-model resolution. Never clears a resumed or user-selected model. + /// The expectedModel parameter. + /// Provider captured with the startup seed; omission matches only an unqualified seed. + /// The to monitor for cancellation requests. The default is . + internal async Task ClearStartupSeedAsync(string expectedModel, string? expectedProviderId = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(expectedModel); + _session.ThrowIfDisposed(); + + var request = new SessionModelClearStartupSeedRequest { SessionId = _session.SessionId, ExpectedModel = expectedModel, ExpectedProviderId = expectedProviderId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.model.clearStartupSeed", [request], cancellationToken); + } + /// Replaces or clears the host-supplied model allowlist for a running session. /// Exact model IDs to permit, or null to clear the host restriction. /// The to monitor for cancellation requests. The default is . @@ -45501,6 +47160,7 @@ public async Task GetAsync(CancellationToken cancellationToken = de /// Session whose plan-mode base state should be inherited. /// Whether a dedicated plan model is configured. /// Dedicated model to use in plan mode, when configured. + /// Provider owning planModel. Omit for legacy bare-model selection. /// Reasoning effort to use with the dedicated plan model. /// Context tier to use with the dedicated plan model. /// Explicit response to a model-switch compaction preflight. @@ -45510,11 +47170,11 @@ public async Task GetAsync(CancellationToken cancellationToken = de /// Action to perform when leaving plan mode. /// The to monitor for cancellation requests. The default is . /// Outcome of a session mode change, including any model switch it triggered and follow-up the host must perform. - public async Task SetAsync(SessionMode mode, SessionMode? expectedMode = null, string? inheritPlanBaseFromSessionId = null, bool? planModelConfigured = null, string? planModel = null, string? planReasoningEffort = null, string? planContextTier = null, string? compactionDecision = null, bool? restorePlanModel = null, bool? persistPlanSelection = null, ModelPickerSettingsContext? pickerSettingsContext = null, string? planExitAction = null, CancellationToken cancellationToken = default) + public async Task SetAsync(SessionMode mode, SessionMode? expectedMode = null, string? inheritPlanBaseFromSessionId = null, bool? planModelConfigured = null, string? planModel = null, string? planModelProviderId = null, string? planReasoningEffort = null, string? planContextTier = null, string? compactionDecision = null, bool? restorePlanModel = null, bool? persistPlanSelection = null, ModelPickerSettingsContext? pickerSettingsContext = null, string? planExitAction = null, CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); - var request = new ModeSetRequest { SessionId = _session.SessionId, Mode = mode, ExpectedMode = expectedMode, InheritPlanBaseFromSessionId = inheritPlanBaseFromSessionId, PlanModelConfigured = planModelConfigured, PlanModel = planModel, PlanReasoningEffort = planReasoningEffort, PlanContextTier = planContextTier, CompactionDecision = compactionDecision, RestorePlanModel = restorePlanModel, PersistPlanSelection = persistPlanSelection, PickerSettingsContext = pickerSettingsContext, PlanExitAction = planExitAction }; + var request = new ModeSetRequest { SessionId = _session.SessionId, Mode = mode, ExpectedMode = expectedMode, InheritPlanBaseFromSessionId = inheritPlanBaseFromSessionId, PlanModelConfigured = planModelConfigured, PlanModel = planModel, PlanModelProviderId = planModelProviderId, PlanReasoningEffort = planReasoningEffort, PlanContextTier = planContextTier, CompactionDecision = compactionDecision, RestorePlanModel = restorePlanModel, PersistPlanSelection = persistPlanSelection, PickerSettingsContext = pickerSettingsContext, PlanExitAction = planExitAction }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mode.set", [request], cancellationToken); } } @@ -45891,9 +47551,21 @@ public async Task DiffAsync(WorkspaceDiffMode mode, bool? i { _session.ThrowIfDisposed(); - var request = new WorkspacesDiffRequest { SessionId = _session.SessionId, Mode = mode, IgnoreWhitespace = ignoreWhitespace }; + var request = new WorkspacesDiffRequestWithSession { SessionId = _session.SessionId, Mode = mode, IgnoreWhitespace = ignoreWhitespace }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.workspaces.diff", [request], cancellationToken); } + + /// Computes a diff for the session workspace. Never rejects for a busy session: a `session`-mode diff that cannot read the session's file-change captures falls back to an unstaged git diff with `isFallback: true` and reports why in `unavailableReason`. + /// Parameters for computing a workspace diff. + /// The to monitor for cancellation requests. The default is . + /// Workspace diff result for the requested mode. + public async Task DiffAsync(WorkspacesDiffRequest request, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + _session.ThrowIfDisposed(); + var wireRequest = new WorkspacesDiffRequestWithSession { SessionId = _session.SessionId, Mode = request.Mode, IgnoreWhitespace = request.IgnoreWhitespace, IncludeContents = request.IncludeContents }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.workspaces.diff", [wireRequest], cancellationToken); + } } /// Provides session-scoped AutopilotObjective APIs. @@ -46402,7 +48074,18 @@ internal McpApi(CopilotSession session) _session = session; } - /// Lists MCP servers configured for the session, their connection status, and host-level state. The host-level state (disabled/filtered servers, failed/needs-auth/pending connections, mcp3p policy, full config) is empty/zero when no MCP host has been initialized for the session. + /// Records the IDE the host is connected to, so the agent's system prompt can name it and its workspace folder. Null or an omitted `ide` clears the recorded value, which is how a host reports that it is disconnected; there is no separate clear method. Both `ideName` and `workspaceFolder` are required together, because half a state cannot be attributed to a project. + /// The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports that it is disconnected. + /// The to monitor for cancellation requests. The default is . + internal async Task SetConnectedIdeInfoAsync(SessionConnectedIdeInfo? ide = null, CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionMcpSetConnectedIdeInfoParams { SessionId = _session.SessionId, Ide = ide }; + await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.setConnectedIdeInfo", [request], cancellationToken); + } + + /// Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers. /// The to monitor for cancellation requests. The default is . /// MCP servers configured for the session, with their connection status and host-level state. public async Task ListAsync(CancellationToken cancellationToken = default) @@ -46413,6 +48096,17 @@ public async Task ListAsync(CancellationToken cancellationToken = return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.list", [request], cancellationToken); } + /// Lists effective MCP configuration without starting, restarting, authenticating, or waiting for servers. An optional live observation is from an already materialized matching server; this is not a readiness guarantee. + /// The to monitor for cancellation requests. The default is . + /// Effective MCP configuration with optional live observations from matching already materialized servers. + public async Task ListConfiguredAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionMcpListConfiguredRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.listConfigured", [request], cancellationToken); + } + /// Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. /// Name of the connected MCP server whose tools to list. /// The to monitor for cancellation requests. The default is . @@ -47711,7 +49405,7 @@ public async Task ListAsync(CancellationToken cancellationToken = return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.extensions.list", [request], cancellationToken); } - /// Enables an extension for the session. + /// Enables an extension for the session and persists the preference when the session has a settings store. Hosts synchronizing effective membership should use extensions.reconcile instead. /// Source-qualified extension ID to enable. /// The to monitor for cancellation requests. The default is . public async Task EnableAsync(string id, CancellationToken cancellationToken = default) @@ -47723,7 +49417,7 @@ public async Task EnableAsync(string id, CancellationToken cancellationToken = d await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.extensions.enable", [request], cancellationToken); } - /// Disables an extension for the session. + /// Disables an extension for the session and persists the preference when the session has a settings store. Hosts synchronizing effective membership should use extensions.reconcile instead. /// Source-qualified extension ID to disable. /// The to monitor for cancellation requests. The default is . public async Task DisableAsync(string id, CancellationToken cancellationToken = default) @@ -47745,6 +49439,17 @@ public async Task ReloadAsync(CancellationToken cancellationToken = default) await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.extensions.reload", [request], cancellationToken); } + /// Host-only reconciliation of authoritative session-effective extension membership and enablement. Refreshes runtime-owned discovery and preferences without persisting settings, installing plugins, or restarting unchanged activations. Returns ExtensionList only after required starts and process/contribution cleanup settle. Takes no caller inventory or overrides. Missing controllers, unready/incomplete discovery, unavailable workspaces, superseded inputs, and lifecycle failures are errors, not empty membership. Independently proven revocations may be applied before an error; retry converges without restarting healthy activations. Error data contains lifecycleChangesApplied and code: extension_reconciliation_host_required, extension_reconciliation_unavailable, extension_reconciliation_not_ready, extension_reconciliation_discovery_failed, extension_reconciliation_workspace_unavailable, extension_reconciliation_superseded, or extension_reconciliation_lifecycle_failed. Mark host reconciliation state applied only on success. On older runtimes, method-not-found must not fall back to global discovery and persistent extension disables. + /// The to monitor for cancellation requests. The default is . + /// Extensions discovered for the session, with their current status. + public async Task ReconcileAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionExtensionsReconcileRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.extensions.reconcile", [request], cancellationToken); + } + /// Push attachments into the next user-message turn from an extension. The host should surface them as composer pills and forward them via the next session.send call. Callable only by extension-owned connections. /// Attachments to push into the next user-message turn. extension_context entries take the slim shape; standard variants take their full AttachmentSchema shape. /// Optional canvas instance binding the push for provenance. When supplied, the runtime resolves the canvas, verifies it is owned by the calling extension, and stamps canvasId/instanceId onto each extension_context entry. When omitted, no resolution runs and those fields stay unset on the attachment. @@ -47834,6 +49539,17 @@ public async Task HandlePendingToolCallAsync(string return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.tools.handlePendingToolCall", [request], cancellationToken); } + /// Lists the external tool calls of the session and its sub-agents that are still waiting for session.tools.handlePendingToolCall. + /// The to monitor for cancellation requests. The default is . + /// External tool calls still waiting for a result. + public async Task ListPendingRequestsAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new ToolsListPendingRequestsRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.tools.listPendingRequests", [request], cancellationToken); + } + /// Resolves, builds, and validates the runtime tool list for the session. /// The to monitor for cancellation requests. The default is . /// Resolve, build, and validate the runtime tool list for this session. Subagent sessions and consumer flows that need an initialized tool set before `send` invoke this. Default base-class implementation is a no-op for sessions that don't support tool validation. @@ -48658,16 +50374,17 @@ public async Task ActivityAsync(CancellationToken cancellationT } /// Returns the token breakdown for the session's current context window for a given model. - /// Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. - /// Maximum output tokens allowed by the target model. Pass 0 if unknown. + /// Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from the session. + /// Requested output allowance to reserve against the combined context ceiling. Pass 0 to resolve the session's request cap, falling back to the model's advertised output limit. /// Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. + /// Provider owning selectedModel. Omit to use the current provider for the current model, or deterministic legacy lookup for a different model. /// The to monitor for cancellation requests. The default is . /// Token breakdown for the session's current context window, or null if uninitialized. - public async Task ContextInfoAsync(long promptTokenLimit, long outputTokenLimit, string? selectedModel = null, CancellationToken cancellationToken = default) + public async Task ContextInfoAsync(long promptTokenLimit, long outputTokenLimit, string? selectedModel = null, string? providerId = null, CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); - var request = new MetadataContextInfoRequest { SessionId = _session.SessionId, PromptTokenLimit = promptTokenLimit, OutputTokenLimit = outputTokenLimit, SelectedModel = selectedModel }; + var request = new MetadataContextInfoRequest { SessionId = _session.SessionId, PromptTokenLimit = promptTokenLimit, OutputTokenLimit = outputTokenLimit, SelectedModel = selectedModel, ProviderId = providerId }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.metadata.contextInfo", [request], cancellationToken); } @@ -49341,6 +51058,64 @@ public async Task GetMetricsAsync(CancellationToken cance var request = new SessionUsageGetMetricsRequest { SessionId = _session.SessionId }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.usage.getMetrics", [request], cancellationToken); } + + /// Internal host-only replacement of absolute code-change totals reported by a relay host. + /// Absolute added-line total, replacing the previous reading. + /// Absolute removed-line total, replacing the previous reading. + /// Absolute changed-file count; omission preserves the previous count. + /// The to monitor for cancellation requests. The default is . + internal async Task SetCodeChangesAsync(double linesAdded, double linesRemoved, double? filesCount = null, CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionUsageSetCodeChangesRequest { SessionId = _session.SessionId, LinesAdded = linesAdded, LinesRemoved = linesRemoved, FilesCount = filesCount }; + await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.usage.setCodeChanges", [request], cancellationToken); + } +} + +/// Provides session-scoped Quota APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class QuotaApi +{ + private readonly CopilotSession _session; + + internal QuotaApi(CopilotSession session) + { + _session = session; + } + + /// Gets the session's current quota and account projection without making a network request. + /// The to monitor for cancellation requests. The default is . + /// The incumbent session-owned quota and account projection. + public async Task GetAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionQuotaGetRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.quota.get", [request], cancellationToken); + } + + /// Refreshes the session's provider model catalog bypassing its cache, folds quota snapshots into session state, and returns the updated projection. Failures leave the last known quota intact. + /// The to monitor for cancellation requests. The default is . + /// The incumbent session-owned quota and account projection. + public async Task RefreshAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionQuotaRefreshRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.quota.refresh", [request], cancellationToken); + } + + /// Returns and clears the session's pending quota warnings. Reading or refreshing quota does not drain warnings. + /// The to monitor for cancellation requests. The default is . + /// Pending warnings returned and cleared by session.quota.takeWarnings. + public async Task> TakeWarningsAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionQuotaTakeWarningsRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync>(_session.Rpc, "session.quota.takeWarnings", [request], cancellationToken); + } } /// Provides session-scoped LimitPrediction APIs. @@ -50042,6 +51817,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.AgentInterruptedData), TypeInfoPropertyName = "SessionEventsAgentInterruptedData")] [JsonSerializable(typeof(GitHub.Copilot.AgentInterruptedEvent), TypeInfoPropertyName = "SessionEventsAgentInterruptedEvent")] [JsonSerializable(typeof(GitHub.Copilot.AgentModelPolicy), TypeInfoPropertyName = "SessionEventsAgentModelPolicy")] +[JsonSerializable(typeof(GitHub.Copilot.AiCreditsStatus), TypeInfoPropertyName = "SessionEventsAiCreditsStatus")] [JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseActivityEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseActivityEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseCompletedEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseCompletedEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseFailedEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseFailedEvent")] @@ -50257,8 +52033,11 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.ModelCallFinishedOutcome), TypeInfoPropertyName = "SessionEventsModelCallFinishedOutcome")] [JsonSerializable(typeof(GitHub.Copilot.ModelCallStartData), TypeInfoPropertyName = "SessionEventsModelCallStartData")] [JsonSerializable(typeof(GitHub.Copilot.ModelCallStartEvent), TypeInfoPropertyName = "SessionEventsModelCallStartEvent")] +[JsonSerializable(typeof(GitHub.Copilot.ModelCallWebSocketFallbackReason), TypeInfoPropertyName = "SessionEventsModelCallWebSocketFallbackReason")] [JsonSerializable(typeof(GitHub.Copilot.ModelChangeSource), TypeInfoPropertyName = "SessionEventsModelChangeSource")] [JsonSerializable(typeof(GitHub.Copilot.ModelDeselectedReason), TypeInfoPropertyName = "SessionEventsModelDeselectedReason")] +[JsonSerializable(typeof(GitHub.Copilot.ModelProviderKind), TypeInfoPropertyName = "SessionEventsModelProviderKind")] +[JsonSerializable(typeof(GitHub.Copilot.ModelProviderRef), TypeInfoPropertyName = "SessionEventsModelProviderRef")] [JsonSerializable(typeof(GitHub.Copilot.OmittedBinaryOmittedReason), TypeInfoPropertyName = "SessionEventsOmittedBinaryOmittedReason")] [JsonSerializable(typeof(GitHub.Copilot.OmittedBinaryResult), TypeInfoPropertyName = "SessionEventsOmittedBinaryResult")] [JsonSerializable(typeof(GitHub.Copilot.OmittedBinaryType), TypeInfoPropertyName = "SessionEventsOmittedBinaryType")] @@ -50332,12 +52111,22 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.PermissionRule), TypeInfoPropertyName = "SessionEventsPermissionRule")] [JsonSerializable(typeof(GitHub.Copilot.PermissionSandboxPathGrant), TypeInfoPropertyName = "SessionEventsPermissionSandboxPathGrant")] [JsonSerializable(typeof(GitHub.Copilot.PermissionSandboxPathGrantAccess), TypeInfoPropertyName = "SessionEventsPermissionSandboxPathGrantAccess")] +[JsonSerializable(typeof(GitHub.Copilot.PermissionWriteFileContent), TypeInfoPropertyName = "SessionEventsPermissionWriteFileContent")] +[JsonSerializable(typeof(GitHub.Copilot.PermissionWriteFileEdit), TypeInfoPropertyName = "SessionEventsPermissionWriteFileEdit")] [JsonSerializable(typeof(GitHub.Copilot.PersistedBinaryImage), TypeInfoPropertyName = "SessionEventsPersistedBinaryImage")] [JsonSerializable(typeof(GitHub.Copilot.PersistedBinaryImageType), TypeInfoPropertyName = "SessionEventsPersistedBinaryImageType")] [JsonSerializable(typeof(GitHub.Copilot.PersistedBinaryResult), TypeInfoPropertyName = "SessionEventsPersistedBinaryResult")] [JsonSerializable(typeof(GitHub.Copilot.PlanChangedOperation), TypeInfoPropertyName = "SessionEventsPlanChangedOperation")] [JsonSerializable(typeof(GitHub.Copilot.PromptCacheBreakData), TypeInfoPropertyName = "SessionEventsPromptCacheBreakData")] [JsonSerializable(typeof(GitHub.Copilot.PromptCacheBreakEvent), TypeInfoPropertyName = "SessionEventsPromptCacheBreakEvent")] +[JsonSerializable(typeof(GitHub.Copilot.ProviderQuotaAccessState), TypeInfoPropertyName = "SessionEventsProviderQuotaAccessState")] +[JsonSerializable(typeof(GitHub.Copilot.ProviderQuotaAcquisitionStatus), TypeInfoPropertyName = "SessionEventsProviderQuotaAcquisitionStatus")] +[JsonSerializable(typeof(GitHub.Copilot.ProviderQuotaBudgetMetadata), TypeInfoPropertyName = "SessionEventsProviderQuotaBudgetMetadata")] +[JsonSerializable(typeof(GitHub.Copilot.ProviderQuotaCapacityState), TypeInfoPropertyName = "SessionEventsProviderQuotaCapacityState")] +[JsonSerializable(typeof(GitHub.Copilot.ProviderQuotaObservationKind), TypeInfoPropertyName = "SessionEventsProviderQuotaObservationKind")] +[JsonSerializable(typeof(GitHub.Copilot.ProviderQuotaQuantityKind), TypeInfoPropertyName = "SessionEventsProviderQuotaQuantityKind")] +[JsonSerializable(typeof(GitHub.Copilot.ProviderQuotaState), TypeInfoPropertyName = "SessionEventsProviderQuotaState")] +[JsonSerializable(typeof(GitHub.Copilot.ProviderQuotaUnit), TypeInfoPropertyName = "SessionEventsProviderQuotaUnit")] [JsonSerializable(typeof(GitHub.Copilot.ReasoningSummary), TypeInfoPropertyName = "SessionEventsReasoningSummary")] [JsonSerializable(typeof(GitHub.Copilot.RecommendedAutoTier), TypeInfoPropertyName = "SessionEventsRecommendedAutoTier")] [JsonSerializable(typeof(GitHub.Copilot.RemediationAction), TypeInfoPropertyName = "SessionEventsRemediationAction")] @@ -50476,6 +52265,16 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.ToolUserRequestedData), TypeInfoPropertyName = "SessionEventsToolUserRequestedData")] [JsonSerializable(typeof(GitHub.Copilot.ToolUserRequestedEvent), TypeInfoPropertyName = "SessionEventsToolUserRequestedEvent")] [JsonSerializable(typeof(GitHub.Copilot.UIEphemeralQueryPhase), TypeInfoPropertyName = "SessionEventsUIEphemeralQueryPhase")] +[JsonSerializable(typeof(GitHub.Copilot.UsageAccountingIdentity), TypeInfoPropertyName = "SessionEventsUsageAccountingIdentity")] +[JsonSerializable(typeof(GitHub.Copilot.UsageGetMetricsResult), TypeInfoPropertyName = "SessionEventsUsageGetMetricsResult")] +[JsonSerializable(typeof(GitHub.Copilot.UsageMetricsAgentMetric), TypeInfoPropertyName = "SessionEventsUsageMetricsAgentMetric")] +[JsonSerializable(typeof(GitHub.Copilot.UsageMetricsCodeChanges), TypeInfoPropertyName = "SessionEventsUsageMetricsCodeChanges")] +[JsonSerializable(typeof(GitHub.Copilot.UsageMetricsModelMetric), TypeInfoPropertyName = "SessionEventsUsageMetricsModelMetric")] +[JsonSerializable(typeof(GitHub.Copilot.UsageMetricsModelMetricRequests), TypeInfoPropertyName = "SessionEventsUsageMetricsModelMetricRequests")] +[JsonSerializable(typeof(GitHub.Copilot.UsageMetricsModelMetricTokenDetail), TypeInfoPropertyName = "SessionEventsUsageMetricsModelMetricTokenDetail")] +[JsonSerializable(typeof(GitHub.Copilot.UsageMetricsModelMetricUsage), TypeInfoPropertyName = "SessionEventsUsageMetricsModelMetricUsage")] +[JsonSerializable(typeof(GitHub.Copilot.UsageMetricsProviderModelMetric), TypeInfoPropertyName = "SessionEventsUsageMetricsProviderModelMetric")] +[JsonSerializable(typeof(GitHub.Copilot.UsageMetricsTokenDetail), TypeInfoPropertyName = "SessionEventsUsageMetricsTokenDetail")] [JsonSerializable(typeof(GitHub.Copilot.UserInputCompletedData), TypeInfoPropertyName = "SessionEventsUserInputCompletedData")] [JsonSerializable(typeof(GitHub.Copilot.UserInputCompletedEvent), TypeInfoPropertyName = "SessionEventsUserInputCompletedEvent")] [JsonSerializable(typeof(GitHub.Copilot.UserInputRequestedData), TypeInfoPropertyName = "SessionEventsUserInputRequestedData")] @@ -50536,13 +52335,23 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(AgentSelectRequest))] [JsonSerializable(typeof(AgentSelectResult))] [JsonSerializable(typeof(AgentSetPromptRequest))] +[JsonSerializable(typeof(AgentsCustomAgentInitialModelDecisionParams))] +[JsonSerializable(typeof(AgentsCustomAgentInitialModelDecisionResult))] [JsonSerializable(typeof(AgentsDiscoverRequest))] +[JsonSerializable(typeof(AgentsGetAvailableBuiltinsRequest))] +[JsonSerializable(typeof(AgentsGetAvailableBuiltinsResult))] +[JsonSerializable(typeof(AgentsGetBuiltinDefinitionRequest))] +[JsonSerializable(typeof(AgentsGetBuiltinDefinitionResult))] +[JsonSerializable(typeof(AgentsGetBuiltinListingDefinitionRequest))] +[JsonSerializable(typeof(AgentsGetBuiltinListingDefinitionResult))] +[JsonSerializable(typeof(AgentsGetBuiltinsResult))] [JsonSerializable(typeof(AgentsGetDiscoveryPathsRequest))] [JsonSerializable(typeof(AuthEnumerateQuery))] [JsonSerializable(typeof(AuthEnumerateValue))] [JsonSerializable(typeof(AuthIdentity))] [JsonSerializable(typeof(AuthIdentityMetadata))] [JsonSerializable(typeof(AuthInfo))] +[JsonSerializable(typeof(AuthLoginAccount))] [JsonSerializable(typeof(AuthLoginAdvanceRequest))] [JsonSerializable(typeof(AuthLoginBeginRequest))] [JsonSerializable(typeof(AuthLoginBegun))] @@ -50555,11 +52364,15 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(AuthValidationError))] [JsonSerializable(typeof(AuthWrite))] [JsonSerializable(typeof(AuthWriteResult))] +[JsonSerializable(typeof(AutoTierDescriptor))] +[JsonSerializable(typeof(AutoTierMetadata))] +[JsonSerializable(typeof(AutoTierStatus))] [JsonSerializable(typeof(AutopilotObjectiveCreditLimit))] [JsonSerializable(typeof(AutopilotObjectiveGetStateResult))] [JsonSerializable(typeof(AutopilotObjectiveState))] [JsonSerializable(typeof(BuiltInModelCatalog))] [JsonSerializable(typeof(BuiltInModelCatalogEntry))] +[JsonSerializable(typeof(BuiltinAgentSummary))] [JsonSerializable(typeof(BuiltinToolDescriptor))] [JsonSerializable(typeof(BuiltinToolFormat))] [JsonSerializable(typeof(BuiltinToolInputSchema))] @@ -50628,6 +52441,13 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ConnectorConnectResult))] [JsonSerializable(typeof(ConnectorContinueRequest))] [JsonSerializable(typeof(ConnectorDisconnectResult))] +[JsonSerializable(typeof(ConnectorDiscoveryAccount))] +[JsonSerializable(typeof(ConnectorDiscoveryAccountList))] +[JsonSerializable(typeof(ConnectorDiscoveryAccountRequest))] +[JsonSerializable(typeof(ConnectorDiscoveryAuthInfo))] +[JsonSerializable(typeof(ConnectorDiscoveryCapabilities))] +[JsonSerializable(typeof(ConnectorDiscoveryCatalogEntry))] +[JsonSerializable(typeof(ConnectorDiscoveryCatalogResult))] [JsonSerializable(typeof(ConnectorReconcileRequest))] [JsonSerializable(typeof(ConnectorReconcileRequestWithSession))] [JsonSerializable(typeof(ConnectorRuntimeStatus))] @@ -50702,12 +52522,29 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(FolderTrustAddParams))] [JsonSerializable(typeof(FolderTrustCheckParams))] [JsonSerializable(typeof(FolderTrustCheckResult))] +[JsonSerializable(typeof(GitCurrentBranchRemoteResult))] +[JsonSerializable(typeof(GitCwdRequest))] [JsonSerializable(typeof(GitHubEnvironment))] +[JsonSerializable(typeof(GitHubOwnerOption))] +[JsonSerializable(typeof(GitHubOwnersCancelRequest))] +[JsonSerializable(typeof(GitHubOwnersCancelResult))] +[JsonSerializable(typeof(GitHubOwnersListRequest))] +[JsonSerializable(typeof(GitHubOwnersListResult))] +[JsonSerializable(typeof(GitHubOwnersRequestIdResult))] +[JsonSerializable(typeof(GitHubRepositoryAtPathRequest))] +[JsonSerializable(typeof(GitHubRepositoryAtPathResult))] +[JsonSerializable(typeof(GitHubRepositoryIdentity))] [JsonSerializable(typeof(GitHubTelemetryClientInfo))] [JsonSerializable(typeof(GitHubTelemetryEvent))] [JsonSerializable(typeof(GitHubTelemetryNotification))] [JsonSerializable(typeof(GitHubTokenAcquireRequest))] [JsonSerializable(typeof(GitHubTokenAcquireResult))] +[JsonSerializable(typeof(GitRemoteRepository))] +[JsonSerializable(typeof(GitReposFromRemotesRequest))] +[JsonSerializable(typeof(GitReposFromRemotesResult))] +[JsonSerializable(typeof(GlobalStateLoadForConfigDirRequest))] +[JsonSerializable(typeof(GlobalStateLoadResult))] +[JsonSerializable(typeof(GlobalStateWriteKeyRequest))] [JsonSerializable(typeof(HandlePendingToolCallRequest))] [JsonSerializable(typeof(HandlePendingToolCallResult))] [JsonSerializable(typeof(HistoryAbortManualCompactionResult))] @@ -50751,6 +52588,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(IDictionary))] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(IList))] +[JsonSerializable(typeof(IList))] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(InstallationCatalogueIdentity))] @@ -50781,6 +52619,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(LocalSessionMetadataValue))] [JsonSerializable(typeof(LogRequest))] [JsonSerializable(typeof(LogResult))] +[JsonSerializable(typeof(LoggedInUser))] [JsonSerializable(typeof(LspInitializeRequest))] [JsonSerializable(typeof(ManagedSettingMeta))] [JsonSerializable(typeof(ManagedSettingsComposeLayer))] @@ -50832,6 +52671,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(McpConfigUpdateRequest))] [JsonSerializable(typeof(McpConfigureGitHubRequest))] [JsonSerializable(typeof(McpConfigureGitHubResult))] +[JsonSerializable(typeof(McpConfiguredServer))] +[JsonSerializable(typeof(McpConfiguredServerList))] +[JsonSerializable(typeof(McpConfiguredServerState))] [JsonSerializable(typeof(McpDiagnosticDetails))] [JsonSerializable(typeof(McpDiagnosticSourceConfiguration))] [JsonSerializable(typeof(McpDisableRequest))] @@ -50998,7 +52840,6 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ModelProviderOperationOutcome))] [JsonSerializable(typeof(ModelProviderPrepareConfigurationRequest))] [JsonSerializable(typeof(ModelProviderProvenance))] -[JsonSerializable(typeof(ModelProviderRef))] [JsonSerializable(typeof(ModelProviderStatus))] [JsonSerializable(typeof(ModelProviderWarning))] [JsonSerializable(typeof(ModelSetAllowedModelsRequest))] @@ -51009,6 +52850,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ModelSwitchAutoTierResult))] [JsonSerializable(typeof(ModelSwitchConfirmation))] [JsonSerializable(typeof(ModelSwitchToRequest))] +[JsonSerializable(typeof(ModelSwitchToRequestWithSession))] [JsonSerializable(typeof(ModelSwitchToResult))] [JsonSerializable(typeof(ModelWarningText))] [JsonSerializable(typeof(ModelsListRequest))] @@ -51022,6 +52864,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(OptionsUpdateAdditionalContentExclusionPolicy))] [JsonSerializable(typeof(OptionsUpdateAdditionalContentExclusionPolicyRule))] [JsonSerializable(typeof(OptionsUpdateAdditionalContentExclusionPolicyRuleSource))] +[JsonSerializable(typeof(PendingExternalToolRequest))] +[JsonSerializable(typeof(PendingExternalToolRequestList))] [JsonSerializable(typeof(PendingPermissionRequest))] [JsonSerializable(typeof(PendingPermissionRequestList))] [JsonSerializable(typeof(PermissionDecision))] @@ -51163,6 +53007,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(QueueWithdrawMessageRequest))] [JsonSerializable(typeof(QueueWithdrawMessageResult))] [JsonSerializable(typeof(QueuedCommandResult))] +[JsonSerializable(typeof(QuotaWarningProjection))] [JsonSerializable(typeof(RegisterEventInterestParams))] [JsonSerializable(typeof(RegisterEventInterestResult))] [JsonSerializable(typeof(ReleaseEventInterestParams))] @@ -51244,6 +53089,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionCommandsListRequestWithSession))] [JsonSerializable(typeof(SessionCompletionItem))] [JsonSerializable(typeof(SessionCompletionsGetTriggerCharactersRequest))] +[JsonSerializable(typeof(SessionConnectedIdeInfo))] [JsonSerializable(typeof(SessionConnectorsGetAccountRequest))] [JsonSerializable(typeof(SessionConnectorsGetCapabilitiesRequest))] [JsonSerializable(typeof(SessionConnectorsGetStatusRequest))] @@ -51253,6 +53099,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionEnrichMetadataResult))] [JsonSerializable(typeof(SessionEventLogTailRequest))] [JsonSerializable(typeof(SessionExtensionsListRequest))] +[JsonSerializable(typeof(SessionExtensionsReconcileRequest))] [JsonSerializable(typeof(SessionExtensionsReloadRequest))] [JsonSerializable(typeof(SessionFsAppendFileRequest))] [JsonSerializable(typeof(SessionFsError))] @@ -51312,6 +53159,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionLoadDeferredRepoHooksResult))] [JsonSerializable(typeof(SessionManagedSettingsGetRequest))] [JsonSerializable(typeof(SessionMcpAppsGetHostContextRequest))] +[JsonSerializable(typeof(SessionMcpListConfiguredRequest))] [JsonSerializable(typeof(SessionMcpListRequest))] [JsonSerializable(typeof(SessionMcpMoveLoadingToBackgroundRequest))] [JsonSerializable(typeof(SessionMcpOauthCancelLoginRequest))] @@ -51320,6 +53168,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionMcpOauthPrepareLoginResult))] [JsonSerializable(typeof(SessionMcpReloadRequest))] [JsonSerializable(typeof(SessionMcpRemoveGitHubRequest))] +[JsonSerializable(typeof(SessionMcpSetConnectedIdeInfoParams))] [JsonSerializable(typeof(SessionMetadataActivityRequest))] [JsonSerializable(typeof(SessionMetadataGetClientMetadataRequest))] [JsonSerializable(typeof(SessionMetadataGetContextAttributionRequest))] @@ -51328,6 +53177,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionMetadataSnapshotRequest))] [JsonSerializable(typeof(SessionMetadataSnapshotWorkspace))] [JsonSerializable(typeof(SessionModeGetRequest))] +[JsonSerializable(typeof(SessionModelClearStartupSeedRequest))] +[JsonSerializable(typeof(SessionModelClearStartupSeedResult))] [JsonSerializable(typeof(SessionModelGetCurrentRequest))] [JsonSerializable(typeof(SessionModelList))] [JsonSerializable(typeof(SessionModelListRequest))] @@ -51359,6 +53210,13 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionQueueProcessRequest))] [JsonSerializable(typeof(SessionQueueRemoveMostRecentRequest))] [JsonSerializable(typeof(SessionQueueSnapshotRequest))] +[JsonSerializable(typeof(SessionQuotaDelegateWarning))] +[JsonSerializable(typeof(SessionQuotaGetRequest))] +[JsonSerializable(typeof(SessionQuotaGetResult))] +[JsonSerializable(typeof(SessionQuotaRefreshRequest))] +[JsonSerializable(typeof(SessionQuotaRefreshResult))] +[JsonSerializable(typeof(SessionQuotaSnapshot))] +[JsonSerializable(typeof(SessionQuotaTakeWarningsRequest))] [JsonSerializable(typeof(SessionRemoteDisableRequest))] [JsonSerializable(typeof(SessionSandboxGetEnforcementStatusRequest))] [JsonSerializable(typeof(SessionScheduleHasSelfPacedRequest))] @@ -51395,9 +53253,11 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionUpdateOptionsParams))] [JsonSerializable(typeof(SessionUpdateOptionsResult))] [JsonSerializable(typeof(SessionUsageGetMetricsRequest))] +[JsonSerializable(typeof(SessionUsageSetCodeChangesRequest))] [JsonSerializable(typeof(SessionVisibilityGetRequest))] [JsonSerializable(typeof(SessionWorkflowPauseAtCheckpointResult))] [JsonSerializable(typeof(SessionWorkingDirectoryContext))] +[JsonSerializable(typeof(SessionWorkingDirectoryContextWithClient))] [JsonSerializable(typeof(SessionWorkspacesAutopilotObjectiveExistsRequest))] [JsonSerializable(typeof(SessionWorkspacesDeleteAutopilotObjectiveRequest))] [JsonSerializable(typeof(SessionWorkspacesGetWorkspaceRequest))] @@ -51410,6 +53270,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionsClientMetadataEntry))] [JsonSerializable(typeof(SessionsCloseRequest))] [JsonSerializable(typeof(SessionsCloseResult))] +[JsonSerializable(typeof(SessionsCreateWorkspaceRequest))] +[JsonSerializable(typeof(SessionsCreateWorkspaceResult))] [JsonSerializable(typeof(SessionsDeleteRequest))] [JsonSerializable(typeof(SessionsEnrichMetadataRequest))] [JsonSerializable(typeof(SessionsFindByPrefixRequest))] @@ -51433,6 +53295,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionsListNonEmptySessionIdsResult))] [JsonSerializable(typeof(SessionsListRequest))] [JsonSerializable(typeof(SessionsLoadDeferredRepoHooksRequest))] +[JsonSerializable(typeof(SessionsLoadWorkspaceRequest))] +[JsonSerializable(typeof(SessionsLoadWorkspaceResult))] [JsonSerializable(typeof(SessionsOpenProgress))] [JsonSerializable(typeof(SessionsPruneOldRequest))] [JsonSerializable(typeof(SessionsReadPersistedEventsRequest))] @@ -51448,6 +53312,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionsStartRemoteControlRequest))] [JsonSerializable(typeof(SessionsStopRemoteControlRequest))] [JsonSerializable(typeof(SessionsTransferRemoteControlRequest))] +[JsonSerializable(typeof(SessionsUpdateWorkspaceFieldsRequest))] +[JsonSerializable(typeof(SessionsUpdateWorkspaceFieldsResult))] [JsonSerializable(typeof(SettableAuthInfo))] [JsonSerializable(typeof(ShellCancelUserRequestedRequest))] [JsonSerializable(typeof(ShellCredentials))] @@ -51540,6 +53406,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ToolsGetBuiltinDescriptorsResult))] [JsonSerializable(typeof(ToolsGetCurrentMetadataResult))] [JsonSerializable(typeof(ToolsInitializeAndValidateResult))] +[JsonSerializable(typeof(ToolsListPendingRequestsRequest))] [JsonSerializable(typeof(ToolsListRequest))] [JsonSerializable(typeof(ToolsSetRequest))] [JsonSerializable(typeof(ToolsSetResult))] @@ -51568,19 +53435,10 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(UIUserInputResponse))] [JsonSerializable(typeof(UpdateSubagentSettingsRequest))] [JsonSerializable(typeof(UpdateSubagentSettingsRequestSubagents))] -[JsonSerializable(typeof(UsageGetMetricsResult))] -[JsonSerializable(typeof(UsageMetricsAgentMetric))] -[JsonSerializable(typeof(UsageMetricsCodeChanges))] -[JsonSerializable(typeof(UsageMetricsModelMetric))] -[JsonSerializable(typeof(UsageMetricsModelMetricRequests))] -[JsonSerializable(typeof(UsageMetricsModelMetricTokenDetail))] -[JsonSerializable(typeof(UsageMetricsModelMetricUsage))] -[JsonSerializable(typeof(UsageMetricsTokenDetail))] [JsonSerializable(typeof(UserRequestedShellCommandResult))] [JsonSerializable(typeof(UserSettingMetadata))] [JsonSerializable(typeof(UserSettingsGetResult))] [JsonSerializable(typeof(UserSettingsSetRequest))] -[JsonSerializable(typeof(UserSettingsSetResult))] [JsonSerializable(typeof(VisibilityGetResult))] [JsonSerializable(typeof(VisibilitySetRequest))] [JsonSerializable(typeof(VisibilitySetResult))] @@ -51624,6 +53482,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(WorkflowToolResumeRequest))] [JsonSerializable(typeof(WorkflowToolRunOptions))] [JsonSerializable(typeof(WorkflowToolRunRequest))] +[JsonSerializable(typeof(WorkspaceDiffContents))] [JsonSerializable(typeof(WorkspaceDiffFileChange))] [JsonSerializable(typeof(WorkspaceDiffResult))] [JsonSerializable(typeof(WorkspacesAddSummaryRequest))] @@ -51636,6 +53495,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(WorkspacesCreateFileRequest))] [JsonSerializable(typeof(WorkspacesDeleteAutopilotObjectiveResult))] [JsonSerializable(typeof(WorkspacesDiffRequest))] +[JsonSerializable(typeof(WorkspacesDiffRequestWithSession))] [JsonSerializable(typeof(WorkspacesEnsureRequest))] [JsonSerializable(typeof(WorkspacesGetWorkspaceResult))] [JsonSerializable(typeof(WorkspacesGetWorkspaceResultWorkspace))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index c482a8a69f..6519c9bbae 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -132,6 +132,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(SessionPermissionRecoveryEvent), "session.permission_recovery")] [JsonDerivedType(typeof(SessionPermissionsChangedEvent), "session.permissions_changed")] [JsonDerivedType(typeof(SessionPlanChangedEvent), "session.plan_changed")] +[JsonDerivedType(typeof(SessionQuotaObservationEvent), "session.quota_observation")] [JsonDerivedType(typeof(SessionRemoteSteerableChangedEvent), "session.remote_steerable_changed")] [JsonDerivedType(typeof(SessionResumeEvent), "session.resume")] [JsonDerivedType(typeof(SessionScheduleCancelledEvent), "session.schedule_cancelled")] @@ -483,6 +484,8 @@ internal sealed partial class SessionEventJsonConverter : JsonConverterA provider-owned quota observation, distinct from per-call usage and charge accounting. +/// Represents the session.quota_observation event. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed partial class SessionQuotaObservationEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.quota_observation"; + + /// The session.quota_observation event payload. + [JsonPropertyName("data")] + public required SessionQuotaObservationData Data { get; set; } +} + /// Working directory and git context at session start. /// Represents the session.context_changed event. public sealed partial class SessionContextChangedEvent : SessionEvent @@ -2361,7 +2378,7 @@ public sealed partial class SessionManagedSettingsResolvedEvent : SessionEvent public required SessionManagedSettingsResolvedData Data { get; set; } } -/// Runtime enforcement of enterprise managed settings: fires when the session blocks or caps a runtime action because enterprise policy governs it, so SDK clients can explain *why* an action was governed. Unlike `session.managed_settings_resolved` (which reports *what* is managed), this reports a concrete governed action — e.g. a user or host tried to turn on a bypass-permissions escalation while policy disables it. Emitted live (not persisted to the session event log) on user/host-initiated attempts only, never for silent policy application. Marked experimental while the managed-settings surface stabilizes. +/// Runtime enforcement of enterprise managed settings: fires when the session blocks or caps a runtime action because enterprise policy governs it, so SDK clients can explain *why* an action was governed. Unlike `session.managed_settings_resolved` (which reports *what* is managed), this reports a concrete governed action — e.g. a user or host tried to turn on bypass permissions or Assisted Permissions while the corresponding policy disables it. Emitted live (not persisted to the session event log) on user/host-initiated attempts only, never for silent policy application. Marked experimental while the managed-settings surface stabilizes. /// Represents the session.managed_settings_enforced event. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class SessionManagedSettingsEnforcedEvent : SessionEvent @@ -2761,6 +2778,11 @@ public sealed partial class SessionStartData [JsonPropertyName("autoTier")] public AutoTier? AutoTier { get; set; } + /// True when autoTier is a managed-policy default. Omitted for user-authored and legacy values. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("autoTierManaged")] + public bool? AutoTierManaged { get; set; } + /// Working directory and git context at session start. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("context")] @@ -2771,6 +2793,11 @@ public sealed partial class SessionStartData [JsonPropertyName("contextTier")] public ContextTier? ContextTier { get; set; } + /// True when contextTier is a managed-policy default. Omitted for user-authored and legacy values. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("contextTierManaged")] + public bool? ContextTierManaged { get; set; } + /// Version string of the Copilot application. [JsonPropertyName("copilotVersion")] public required string CopilotVersion { get; set; } @@ -2789,11 +2816,21 @@ public sealed partial class SessionStartData [JsonPropertyName("producer")] public required string Producer { get; set; } + /// Provider of selectedModel at creation time, when explicitly selected. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("providerId")] + public string? ProviderId { get; set; } + /// Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max"). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } + /// True when the reasoning effort is a managed-policy default bound to reasoningEffortModel. Omitted for agent-authored, user-authored, independent, and legacy effort. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffortManaged")] + public bool? ReasoningEffortManaged { get; set; } + /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningEffortModel")] @@ -2850,6 +2887,11 @@ public sealed partial class SessionResumeData [JsonPropertyName("autoTier")] public AutoTier? AutoTier { get; set; } + /// True when autoTier is a managed-policy default. Omitted for user-authored and legacy values. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("autoTierManaged")] + public bool? AutoTierManaged { get; set; } + /// Updated working directory and git context at resume time. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("context")] @@ -2860,6 +2902,11 @@ public sealed partial class SessionResumeData [JsonPropertyName("contextTier")] public ContextTier? ContextTier { get; set; } + /// True when contextTier is a managed-policy default. Omitted for user-authored and legacy values. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("contextTierManaged")] + public bool? ContextTierManaged { get; set; } + /// 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 or omitted, pending work is normally marked as interrupted unless the resume passively joined live work owned by another client; sessionWasActive distinguishes that case. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("continuePendingWork")] @@ -2874,11 +2921,21 @@ public sealed partial class SessionResumeData [JsonPropertyName("eventsFileSizeBytes")] public long? EventsFileSizeBytes { get; set; } + /// Provider of selectedModel at resume time, when explicitly selected. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("providerId")] + public string? ProviderId { get; set; } + /// Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max"). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } + /// True when the reasoning effort is a managed-policy default bound to reasoningEffortModel. Omitted for agent-authored, user-authored, independent, and legacy effort. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffortManaged")] + public bool? ReasoningEffortManaged { get; set; } + /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningEffortModel")] @@ -3161,6 +3218,11 @@ public sealed partial class SessionModelChangeData [JsonPropertyName("autoTier")] public AutoTier? AutoTier { get; set; } + /// True when autoTier is a managed-policy default. Omitted for user-authored and legacy values. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("autoTierManaged")] + public bool? AutoTierManaged { get; set; } + /// Reason the change happened, when not user-initiated. `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path, or `"refusal_fallback"` when the active model declined a request (content refusal) and the runtime switched to the configured refusal-fallback model. UI clients can use this to render contextual copy. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("cause")] @@ -3171,6 +3233,11 @@ public sealed partial class SessionModelChangeData [JsonPropertyName("contextTier")] public ContextTier? ContextTier { get; set; } + /// True when contextTier is a managed-policy default. Omitted for user-authored and legacy values. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("contextTierManaged")] + public bool? ContextTierManaged { get; set; } + /// Newly selected model identifier. [JsonPropertyName("newModel")] public required string NewModel { get; set; } @@ -3185,6 +3252,11 @@ public sealed partial class SessionModelChangeData [JsonPropertyName("previousModel")] public string? PreviousModel { get; set; } + /// Provider of previousModel, when known. A provider-only change is a model selection change even when the model identifiers are equal. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("previousProviderId")] + public string? PreviousProviderId { get; set; } + /// Reasoning effort level before the model change, if applicable. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("previousReasoningEffort")] @@ -3200,11 +3272,21 @@ public sealed partial class SessionModelChangeData [JsonPropertyName("previousVerbosity")] public Verbosity? PreviousVerbosity { get; set; } + /// Provider selected for newModel. Omitted for legacy or unattributed selections; never inferred from a later selection. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("providerId")] + public string? ProviderId { get; set; } + /// Reasoning effort level after the model change, if applicable. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } + /// True when the reasoning effort is a managed-policy default bound to reasoningEffortModel. Omitted for agent-authored, user-authored, independent, and legacy effort. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffortManaged")] + public bool? ReasoningEffortManaged { get; set; } + /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningEffortModel")] @@ -3438,6 +3520,12 @@ public sealed partial class SessionSnapshotRewindData /// Session termination metrics including usage statistics, code changes, and shutdown reason. public sealed partial class SessionShutdownData { + /// Complete accounting state captured atomically with receipt watermarks. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] + [JsonPropertyName("accountingSnapshot")] + internal UsageGetMetricsResult? AccountingSnapshot { get; set; } + /// Per-agent usage breakdown, keyed by agent instance identifier. The main conversation uses the stable key `main`. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("agentMetrics")] @@ -3476,6 +3564,11 @@ public sealed partial class SessionShutdownData [JsonPropertyName("modelMetrics")] public required IDictionary ModelMetrics { get; set; } + /// Provider/model accounting preserved for replay without consulting current provider selection. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("providerModelMetrics")] + public UsageMetricsProviderModelMetric[]? ProviderModelMetrics { get; set; } + /// Unix timestamp (milliseconds) when the session started. [JsonPropertyName("sessionStartTime")] public required long SessionStartTime { get; set; } @@ -3515,11 +3608,22 @@ public sealed partial class SessionShutdownData [JsonInclude] [JsonPropertyName("totalPremiumRequests")] internal double? TotalPremiumRequests { get; set; } + + /// Per-source accounting sequence watermarks subsuming preceding usage receipts. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("usageAccountingWatermarks")] + public IDictionary? UsageAccountingWatermarks { get; set; } } /// Durable session usage checkpoint for reconstructing aggregate accounting on resume. public sealed partial class SessionUsageCheckpointData { + /// Complete accounting state captured atomically with the receipt watermarks. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] + [JsonPropertyName("accountingSnapshot")] + internal UsageGetMetricsResult? AccountingSnapshot { get; set; } + /// Internal per-model prompt-cache state used to restore expiration tracking on resume. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonInclude] @@ -3532,6 +3636,11 @@ public sealed partial class SessionUsageCheckpointData [JsonPropertyName("promptCacheBreakState")] internal JsonElement[]? PromptCacheBreakState { get; set; } + /// Provider/model accounting snapshot that subsumes preceding usage receipts. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("providerModelMetrics")] + public UsageMetricsProviderModelMetric[]? ProviderModelMetrics { get; set; } + /// Session-wide accumulated nano-AI units cost at checkpoint time. [JsonPropertyName("totalNanoAiu")] public required double TotalNanoAiu { get; set; } @@ -3541,6 +3650,20 @@ public sealed partial class SessionUsageCheckpointData [JsonInclude] [JsonPropertyName("totalPremiumRequests")] internal double? TotalPremiumRequests { get; set; } + + /// Highest accounted sequence per source session; no per-call deduplication history is retained. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("usageAccountingWatermarks")] + public IDictionary? UsageAccountingWatermarks { get; set; } +} + +/// A provider-owned quota observation, distinct from per-call usage and charge accounting. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed partial class SessionQuotaObservationData +{ + /// The admitted provider's state observation. Admission observations never contain quantities or reset/percentage semantics. + [JsonPropertyName("observation")] + public required ProviderQuotaState Observation { get; set; } } /// Working directory and git context at session start. @@ -4735,6 +4858,11 @@ public sealed partial class AssistantMessageData [JsonPropertyName("phase")] public string? Phase { get; set; } + /// Opaque origin provider identity captured at dispatch. Only an exact match with the target provider permits replay of reasoningOpaque, encryptedContent, reasoningBlocks, serverTools, and provider response/item identifiers. Matching model ids or wire protocols is insufficient. Omitted for legacy or unattributed messages; the runtime excludes unproven opaque state when projecting to a known provider. Text content and client tool requests/results remain portable semantic history. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("providerId")] + public string? ProviderId { get; set; } + /// Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping. `reasoningText` and `reasoningOpaque` are a lossy derived view of these blocks, retained for display. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningBlocks")] @@ -4857,6 +4985,16 @@ public sealed partial class AssistantUsageData [JsonPropertyName("acceptedPredictionTokens")] public long? AcceptedPredictionTokens { get; set; } + /// Runtime-owned identity shared with the durable accounting receipt. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("accounting")] + public UsageAccountingIdentity? Accounting { get; set; } + + /// Availability of this call's reported AI-credit amount. Missing billing is unavailable, not a zero charge. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("aiCreditsStatus")] + public AiCreditsStatus? AiCreditsStatus { get; set; } + /// Completion ID from the model provider (e.g., chatcmpl-abc123). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("apiCallId")] @@ -4989,6 +5127,11 @@ public sealed partial class AssistantUsageData [JsonPropertyName("model")] public required string Model { get; set; } + /// Model display name captured for this call, not derived from the current selection. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("modelDisplayName")] + public string? ModelDisplayName { get; set; } + /// Fixed-set provider family serving the bring-your-own-key model (for example "openai", "anthropic", "azure_openai", "ollama", "llama_cpp", or "other"). Never the caller-supplied provider name. Absent for Copilot-served models. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("modelProvider")] @@ -5022,6 +5165,11 @@ public sealed partial class AssistantUsageData [JsonPropertyName("parentToolCallId")] public string? ParentToolCallId { get; set; } + /// Provider identity and product label captured by the actual call's dispatch path. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("provider")] + public ModelProviderRef? Provider { get; set; } + /// GitHub request tracing ID (x-github-request-id header) for server-side log correlation. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("providerCallId")] @@ -5053,6 +5201,11 @@ public sealed partial class AssistantUsageData [JsonPropertyName("rejectedPredictionTokens")] public long? RejectedPredictionTokens { get; set; } + /// Serialized (uncompressed) byte length of the request body. A content-free size signal. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("requestBodyBytes")] + public long? RequestBodyBytes { get; set; } + /// Per-request treatment/eligibility signal returned by the Copilot API in the `X-GitHub-Copilot-Request-TE` response header for the associated model call; `false` when the header was absent or unparseable. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("rte")] @@ -5097,6 +5250,17 @@ public sealed partial class AssistantUsageData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("transport")] public AssistantUsageTransport? Transport { get; set; } + + /// Milliseconds spent on the WebSocket attempt before falling back to HTTP. + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("websocketFallbackAfterMs")] + public TimeSpan? WebsocketFallbackAfter { get; set; } + + /// Why the call was carried by the HTTP fallback of a WebSocket-capable dispatcher; absent when no fallback occurred. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("websocketFallbackReason")] + public ModelCallWebSocketFallbackReason? WebsocketFallbackReason { get; set; } } /// A detected loss of a previously cached prompt prefix. @@ -5351,11 +5515,21 @@ public sealed partial class ModelCallFailureData [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } + /// Serialized (uncompressed) byte length of the failed request body. A content-free size signal. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("requestBodyBytes")] + public long? RequestBodyBytes { get; set; } + /// Content-free structural summary of the failing request. Contains only counts and shape flags (no prompt content), so it is safe for unrestricted telemetry. Populated only for client-error (4xx) failures. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestFingerprint")] public ModelCallFailureRequestFingerprint? RequestFingerprint { get; set; } + /// Zero-based orchestrator retry index of the failed attempt. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("retryAttempt")] + public long? RetryAttempt { get; set; } + /// Per-request treatment/eligibility signal returned by the Copilot API in the `X-GitHub-Copilot-Request-TE` response header for the associated model call; `false` when the header was absent or unparseable. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("rte")] @@ -5379,6 +5553,17 @@ public sealed partial class ModelCallFailureData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("transport")] public ModelCallFailureTransport? Transport { get; set; } + + /// Milliseconds spent on the WebSocket attempt before falling back to HTTP. + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("websocketFallbackAfterMs")] + public TimeSpan? WebsocketFallbackAfter { get; set; } + + /// Why the failed call was carried by the HTTP fallback of a WebSocket-capable dispatcher; absent when no fallback occurred. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("websocketFallbackReason")] + public ModelCallWebSocketFallbackReason? WebsocketFallbackReason { get; set; } } /// Internal telemetry result for one logical model operation after all orchestrator-owned retries settle. @@ -7130,7 +7315,7 @@ public sealed partial class SessionManagedSettingsResolvedData public required ManagedSettingsResolvedSource Source { get; set; } } -/// Runtime enforcement of enterprise managed settings: fires when the session blocks or caps a runtime action because enterprise policy governs it, so SDK clients can explain *why* an action was governed. Unlike `session.managed_settings_resolved` (which reports *what* is managed), this reports a concrete governed action — e.g. a user or host tried to turn on a bypass-permissions escalation while policy disables it. Emitted live (not persisted to the session event log) on user/host-initiated attempts only, never for silent policy application. Marked experimental while the managed-settings surface stabilizes. +/// Runtime enforcement of enterprise managed settings: fires when the session blocks or caps a runtime action because enterprise policy governs it, so SDK clients can explain *why* an action was governed. Unlike `session.managed_settings_resolved` (which reports *what* is managed), this reports a concrete governed action — e.g. a user or host tried to turn on bypass permissions or Assisted Permissions while the corresponding policy disables it. Emitted live (not persisted to the session event log) on user/host-initiated attempts only, never for silent policy application. Marked experimental while the managed-settings surface stabilizes. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class SessionManagedSettingsEnforcedData { @@ -7138,7 +7323,7 @@ public sealed partial class SessionManagedSettingsEnforcedData [JsonPropertyName("action")] public required ManagedSettingsEnforcedAction Action { get; set; } - /// For a `bypass_permissions_blocked` action, which permission-escalation primitive was refused. Absent for actions without a specific escalation primitive. + /// For a `bypass_permissions_blocked` action, which permission-mode or escalation primitive was refused. Absent for actions without a specific primitive. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("escalation")] public ManagedSettingsEnforcedEscalation? Escalation { get; set; } @@ -7151,7 +7336,7 @@ public sealed partial class SessionManagedSettingsEnforcedData [JsonPropertyName("message")] public required string Message { get; set; } - /// The managed setting key responsible for the enforcement (e.g. `permissions.disableBypassPermissionsMode`). + /// The managed setting key responsible for the enforcement (for example `permissions.disableBypassPermissionsMode` or `permissions.disableAssistedPermissionsMode`). [JsonPropertyName("setting")] public required string Setting { get; set; } } @@ -7823,6 +8008,256 @@ public sealed partial class HandoffRepository public required string Owner { get; set; } } +/// Request count and cost metrics for this model. +/// Nested data type for UsageMetricsModelMetricRequests. +public sealed partial class UsageMetricsModelMetricRequests +{ + /// User-initiated premium request cost (with multiplier applied). + [JsonPropertyName("cost")] + public required double Cost { get; set; } + + /// Number of API requests made with this model. + [JsonPropertyName("count")] + public required long Count { get; set; } +} + +/// Per-model token-detail entry containing the accumulated token count for one token type. +/// Nested data type for UsageMetricsModelMetricTokenDetail. +public sealed partial class UsageMetricsModelMetricTokenDetail +{ + /// Accumulated token count for this token type. + [JsonPropertyName("tokenCount")] + public required long TokenCount { get; set; } +} + +/// Token usage metrics for this model. +/// Nested data type for UsageMetricsModelMetricUsage. +public sealed partial class UsageMetricsModelMetricUsage +{ + /// Total tokens read from prompt cache. + [JsonPropertyName("cacheReadTokens")] + public required long CacheReadTokens { get; set; } + + /// Total tokens written to prompt cache. + [JsonPropertyName("cacheWriteTokens")] + public required long CacheWriteTokens { get; set; } + + /// Total input tokens consumed. + [JsonPropertyName("inputTokens")] + public required long InputTokens { get; set; } + + /// Total output tokens produced. + [JsonPropertyName("outputTokens")] + public required long OutputTokens { get; set; } + + /// Total output tokens used for reasoning. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningTokens")] + public long? ReasoningTokens { get; set; } +} + +/// Per-model usage metrics, including request counts/costs, token usage, nano-AI units, and per-token-type details. +/// Nested data type for UsageMetricsModelMetric. +public sealed partial class UsageMetricsModelMetric +{ + /// Completeness of this model's reported AI-credit subtotal. Explicitly reported zero is complete, not unavailable. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("aiCreditsStatus")] + public AiCreditsStatus? AiCreditsStatus { get; set; } + + /// Latest known prompt-cache expiration for this model. A timestamp in the past indicates that the observed cache has expired. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("cacheExpiresAt")] + public DateTimeOffset? CacheExpiresAt { get; set; } + + /// Request count and cost metrics for this model. + [JsonPropertyName("requests")] + public required UsageMetricsModelMetricRequests Requests { get; set; } + + /// Token count details per type. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("tokenDetails")] + public IDictionary? TokenDetails { get; set; } + + /// Accumulated nano-AI units cost for this model. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("totalNanoAiu")] + public double? TotalNanoAiu { get; set; } + + /// Token usage metrics for this model. + [JsonPropertyName("usage")] + public required UsageMetricsModelMetricUsage Usage { get; set; } +} + +/// Usage attributed to one agent instance, including its identity, API duration, AI units, and per-model breakdown. +/// Nested data type for UsageMetricsAgentMetric. +public sealed partial class UsageMetricsAgentMetric +{ + /// Human-readable label for this subagent invocation, copied from the originating `subagent.started` event. For task-tool subagents this is the invocation's task description rather than the agent's configured display name, so group by `agentName` for stable per-agent labels. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("agentDisplayName")] + public string? AgentDisplayName { get; set; } + + /// Configured agent name, when this is a subagent. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("agentName")] + public string? AgentName { get; set; } + + /// Per-model usage for this agent, keyed by model identifier. + [JsonPropertyName("modelMetrics")] + public required IDictionary ModelMetrics { get; set; } + + /// Time spent in model API calls by this agent, in milliseconds. + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonPropertyName("totalApiDurationMs")] + public required TimeSpan TotalApiDuration { get; set; } + + /// Accumulated nano-AI units cost for this agent. + [JsonPropertyName("totalNanoAiu")] + public required double TotalNanoAiu { get; set; } +} + +/// Aggregated code change metrics. +/// Nested data type for UsageMetricsCodeChanges. +public sealed partial class UsageMetricsCodeChanges +{ + /// Distinct file paths modified during the session. + [JsonPropertyName("filesModified")] + public required string[] FilesModified { get; set; } + + /// Number of distinct files modified. + [JsonPropertyName("filesModifiedCount")] + public required long FilesModifiedCount { get; set; } + + /// Total lines of code added. + [JsonPropertyName("linesAdded")] + public required long LinesAdded { get; set; } + + /// Total lines of code removed. + [JsonPropertyName("linesRemoved")] + public required long LinesRemoved { get; set; } +} + +/// A neutral reference to the model provider that produced a model: an opaque id, a human-readable label, and the provider kind. Carried on each enumerated Model so consumers can group by provider without reaching into a provider-shaped internal type. +/// Nested data type for ModelProviderRef. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed partial class ModelProviderRef +{ + /// Opaque, stable id of the provider that produced this model. Matches the enumerated `ModelProviderDescriptor.id`. + [JsonPropertyName("id")] + public required string Id { get; set; } + + /// The provider kind. + [JsonPropertyName("kind")] + public required ModelProviderKind Kind { get; set; } + + /// Human-readable provider label, owned by the runtime so every consumer renders identical text. + [JsonPropertyName("label")] + public required string Label { get; set; } +} + +/// Usage for one recorded provider and model, without merging identical model IDs across providers. +/// Nested data type for UsageMetricsProviderModelMetric. +public sealed partial class UsageMetricsProviderModelMetric +{ + /// Request, token, and cost totals for this provider/model. + [JsonPropertyName("metrics")] + public required UsageMetricsModelMetric Metrics { get; set; } + + /// Model display name captured at call time, when known. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("modelDisplayName")] + public string? ModelDisplayName { get; set; } + + /// Model identity, or null for legacy aggregate-only usage. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } + + /// Provider identity and product label captured when the call was dispatched; null when unknown. + [JsonPropertyName("provider")] + public ModelProviderRef? Provider { get; set; } +} + +/// Session-wide token-detail entry containing the accumulated token count for one token type. +/// Nested data type for UsageMetricsTokenDetail. +public sealed partial class UsageMetricsTokenDetail +{ + /// Accumulated token count for this token type. + [JsonPropertyName("tokenCount")] + public required long TokenCount { get; set; } +} + +/// Accumulated session usage metrics, including premium request cost, token counts, model breakdown, and code-change totals. +/// Nested data type for UsageGetMetricsResult. +public sealed partial class UsageGetMetricsResult +{ + /// Per-agent usage metrics, keyed by agent instance identifier. The main conversation uses the stable key `main`. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("agentMetrics")] + public IDictionary? AgentMetrics { get; set; } + + /// Completeness of totalNanoAiu. Numeric totals remain the reported subtotal; zero is not a claim of free usage when unavailable. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("aiCreditsStatus")] + public AiCreditsStatus? AiCreditsStatus { get; set; } + + /// Aggregated code change metrics. + [JsonPropertyName("codeChanges")] + public required UsageMetricsCodeChanges CodeChanges { get; set; } + + /// Currently active model identifier. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("currentModel")] + public string? CurrentModel { get; set; } + + /// Input tokens from the most recent main-agent API call. + [JsonPropertyName("lastCallInputTokens")] + public required long LastCallInputTokens { get; set; } + + /// Output tokens from the most recent main-agent API call. + [JsonPropertyName("lastCallOutputTokens")] + public required long LastCallOutputTokens { get; set; } + + /// Per-model token and request metrics, keyed by model identifier. + [JsonPropertyName("modelMetrics")] + public required IDictionary ModelMetrics { get; set; } + + /// + /// Provider-attributed model metrics, ordered by first observed provider and model. + /// Null attribution preserves historical usage whose provider or model was not recorded. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("providerModelMetrics")] + public UsageMetricsProviderModelMetric[]? ProviderModelMetrics { get; set; } + + /// ISO 8601 timestamp when the session started. + [JsonPropertyName("sessionStartTime")] + public required DateTimeOffset SessionStartTime { get; set; } + + /// Session-wide per-token-type accumulated token counts. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("tokenDetails")] + public IDictionary? TokenDetails { get; set; } + + /// Total time spent in model API calls (milliseconds). + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonPropertyName("totalApiDurationMs")] + public required TimeSpan TotalApiDuration { get; set; } + + /// Session-wide accumulated nano-AI units cost. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("totalNanoAiu")] + public double? TotalNanoAiu { get; set; } + + /// Total user-initiated premium request cost across all models (may be fractional due to multipliers). + [JsonPropertyName("totalPremiumRequestCost")] + public required double TotalPremiumRequestCost { get; set; } + + /// Raw count of user-initiated API requests. + [JsonPropertyName("totalUserRequests")] + public required long TotalUserRequests { get; set; } +} + /// Request count and cost metrics. /// Nested data type for ShutdownModelMetricRequests. public sealed partial class ShutdownModelMetricRequests @@ -7971,28 +8406,177 @@ internal sealed partial class UsageCheckpointModelCacheState public required string ModelId { get; set; } } -/// Token usage detail for a single billing category. -/// Nested data type for CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail. -public sealed partial class CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail +/// Authoritative budget measurements and policy metadata, independent of provider. +/// Nested data type for ProviderQuotaBudgetMetadata. +public sealed partial class ProviderQuotaBudgetMetadata { - /// Number of tokens in this billing batch. - [JsonPropertyName("batchSize")] - public required long BatchSize { get; set; } + /// Exact measured consumption in the budget's unit. + [JsonPropertyName("consumed")] + public required double Consumed { get; set; } - /// Cost per batch of tokens. - [JsonPropertyName("costPerBatch")] - public required long CostPerBatch { get; set; } + /// Exact budget entitlement, retaining incumbent fractional-unit compatibility. + [JsonPropertyName("entitlement")] + public required double Entitlement { get; set; } - /// Model responsible for this billing entry. + /// Usage beyond entitlement, in the budget's unit. + [JsonPropertyName("overage")] + public required double Overage { get; set; } + + /// Whether additional usage is allowed when the budget is exhausted. + [JsonPropertyName("overageAllowedWhenExhausted")] + public required bool OverageAllowedWhenExhausted { get; set; } + + /// Optional additional-usage budget cap. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("model")] - public string? Model { get; set; } + [JsonPropertyName("overageLimit")] + public double? OverageLimit { get; set; } - /// Total token count for this entry. - [JsonPropertyName("tokenCount")] - public required long TokenCount { get; set; } + /// Service-reported remaining percentage for authoritative budget presentation. + [JsonPropertyName("remainingPercentage")] + public required double RemainingPercentage { get; set; } - /// Token category (e.g., "input", "output"). + /// Reset instant in epoch milliseconds when this budget actually defines a window. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("resetAtEpochMs")] + public double? ResetAtEpochMs { get; set; } + + /// Whether the reset instant is an estimate. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("resetEstimated")] + public bool? ResetEstimated { get; set; } + + /// Whether this budget uses token-based billing. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("tokenBasedBilling")] + public bool? TokenBasedBilling { get; set; } + + /// Whether the budget has unlimited entitlement. + [JsonPropertyName("unlimited")] + public required bool Unlimited { get; set; } + + /// Whether service policy allows continued usage after exhaustion. + [JsonPropertyName("usageAllowedWhenExhausted")] + public required bool UsageAllowedWhenExhausted { get; set; } +} + +/// An account quota reading. Absence of a quantity is unknown, never zero. +/// Nested data type for ProviderQuotaState. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed partial class ProviderQuotaState +{ + /// Whether the service permits access, independently of balance. + [JsonPropertyName("accessState")] + public required ProviderQuotaAccessState AccessState { get; set; } + + /// Service error code or client acquisition category, separate from the business-state reason. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("acquisitionError")] + public string? AcquisitionError { get; set; } + + /// Acquisition outcome. Unavailable/failed readings have no service verdict; access/capacity unavailable are compatibility placeholders only. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("acquisitionStatus")] + public ProviderQuotaAcquisitionStatus? AcquisitionStatus { get; set; } + + /// Independently reported signed 64-bit available quantity. Zero does not override the service access/capacity verdict; omission and null are preserved. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("availableQuantity")] + public long? AvailableQuantity { get; set; } + + /// Optional metadata for an authoritative budget. Advisory balances and admission-only observations do not populate this. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("budgetMetadata")] + public ProviderQuotaBudgetMetadata? BudgetMetadata { get; set; } + + /// Service capacity state; missing quantities do not imply exhaustion. + [JsonPropertyName("capacityState")] + public required ProviderQuotaCapacityState CapacityState { get; set; } + + /// Key for the backwards-compatible snapshots projection, when the authoritative budget supports that contract. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("compatibilityKey")] + public string? CompatibilityKey { get; set; } + + /// Independently reported signed 64-bit entitlement. -1 is an unlimited sentinel, not a capacity-state rewrite; omission and null are preserved. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("entitledQuantity")] + public long? EntitledQuantity { get; set; } + + /// Explicit service admission flag, when reported. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("hasQuota")] + public bool? HasQuota { get; set; } + + /// HTTP status from acquisition, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("httpStatus")] + public long? HttpStatus { get; set; } + + /// Whether this is a GET account reading or a pre-response admission observation. Observations are never merged across kinds. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("observationKind")] + public ProviderQuotaObservationKind? ObservationKind { get; set; } + + /// When the runtime observed this reading, not a charge timestamp or guarantee that consumption has settled. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("observedAt")] + public string? ObservedAt { get; set; } + + /// Provider/account whose service reported this state. + [JsonPropertyName("provider")] + public required ModelProviderRef Provider { get; set; } + + /// Whether quantities are authoritative budget measurements, advisory balances, or absent. Only authoritative budgets support percentage presentation. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("quantityKind")] + public ProviderQuotaQuantityKind? QuantityKind { get; set; } + + /// Service-owned quota identifier within this provider. + [JsonPropertyName("quotaId")] + public required string QuotaId { get; set; } + + /// Service-reported explanation for the state. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// Service name owning this reading. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("service")] + public string? Service { get; set; } + + /// Service-reported quota source. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("source")] + public string? Source { get; set; } + + /// Unit of entitledQuantity and availableQuantity. + [JsonPropertyName("unit")] + public required ProviderQuotaUnit Unit { get; set; } +} + +/// Token usage detail for a single billing category. +/// Nested data type for CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail. +public sealed partial class CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail +{ + /// Number of tokens in this billing batch. + [JsonPropertyName("batchSize")] + public required long BatchSize { get; set; } + + /// Cost per batch of tokens. + [JsonPropertyName("costPerBatch")] + public required long CostPerBatch { get; set; } + + /// Model responsible for this billing entry. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("model")] + public string? Model { get; set; } + + /// Total token count for this entry. + [JsonPropertyName("tokenCount")] + public required long TokenCount { get; set; } + + /// Token category (e.g., "input", "output"). [JsonPropertyName("tokenType")] public required string TokenType { get; set; } } @@ -8022,6 +8606,11 @@ internal sealed partial class CompactionCompleteCompactionTokensUsedCopilotUsage /// Nested data type for CompactionCompleteCompactionTokensUsed. public sealed partial class CompactionCompleteCompactionTokensUsed { + /// Whether the compaction's credit amount was reported for all, some, or none of its calls. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("aiCreditsStatus")] + public AiCreditsStatus? AiCreditsStatus { get; set; } + /// Cached input tokens reused in the compaction LLM call. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("cacheReadTokens")] @@ -8054,10 +8643,20 @@ public sealed partial class CompactionCompleteCompactionTokensUsed [JsonPropertyName("model")] public string? Model { get; set; } + /// Model display name captured for the compaction call. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("modelDisplayName")] + public string? ModelDisplayName { get; set; } + /// Output tokens produced by the compaction LLM call. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("outputTokens")] public long? OutputTokens { get; set; } + + /// Provider captured for the compaction call; absent when attribution is unknown. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("provider")] + public ModelProviderRef? Provider { get; set; } } /// Original request-level and effective conversation reasoning effort for a provider history boundary; the historical type name is retained for compatibility. @@ -9217,6 +9816,23 @@ public sealed partial class AssistantMessageToolRequest public AssistantMessageToolRequestType? Type { get; set; } } +/// Ordered accounting identity assigned under the source session's emission lock. +/// Nested data type for UsageAccountingIdentity. +public sealed partial class UsageAccountingIdentity +{ + /// Monotonically increasing sequence within the source session. + [JsonPropertyName("sequence")] + public required long Sequence { get; set; } + + /// Session that assigned this accounting sequence. + [JsonPropertyName("sourceSessionId")] + public required string SourceSessionId { get; set; } + + /// Existing API call identifier, or a runtime-generated identity when none was supplied. + [JsonPropertyName("usageId")] + public required string UsageId { get; set; } +} + /// Token usage detail for a single billing category. /// Nested data type for AssistantUsageCopilotUsageTokenDetail. public sealed partial class AssistantUsageCopilotUsageTokenDetail @@ -9331,6 +9947,16 @@ internal sealed partial class AssistantUsageQuotaSnapshot /// Nested data type for ModelCallFailureRequestFingerprint. public sealed partial class ModelCallFailureRequestFingerprint { + /// Summed byte length of opaque or encrypted reasoning payloads. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("encryptedContentBytes")] + public long? EncryptedContentBytes { get; set; } + + /// Summed byte length of inline image payloads (data URLs and base64 sources). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("imageBytes")] + public long? ImageBytes { get; set; } + /// Total number of image content parts. [JsonPropertyName("imagePartCount")] public required long ImagePartCount { get; set; } @@ -9352,6 +9978,11 @@ public sealed partial class ModelCallFailureRequestFingerprint [JsonPropertyName("namelessToolCallCount")] public required long NamelessToolCallCount { get; set; } + /// Number of messages carrying opaque or encrypted reasoning. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningItemCount")] + public long? ReasoningItemCount { get; set; } + /// Total number of tool calls across assistant messages. [JsonPropertyName("toolCallCount")] public required long ToolCallCount { get; set; } @@ -11208,6 +11839,34 @@ public override bool? ManagedApprovalRequired public string? Warning { get; set; } } +/// A named, complete UTF-8 text snapshot for a pending file operation. +/// Nested data type for PermissionWriteFileContent. +public sealed partial class PermissionWriteFileContent +{ + /// Complete file contents, including an empty string for an empty file. + [JsonPropertyName("content")] + public required string Content { get; set; } + + /// Absolute path in the session filesystem namespace. + [JsonPropertyName("path")] + public required string Path { get; set; } +} + +/// Complete text snapshots for one proposed file change. At least one side is present. Different before and after paths describe a move. +/// Nested data type for PermissionWriteFileEdit. +public sealed partial class PermissionWriteFileEdit +{ + /// Complete file content the operation plans to write. Absent only when the file is being deleted. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("after")] + public PermissionWriteFileContent? After { get; set; } + + /// File content read while planning the operation. Absent only when the file is being created. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("before")] + public PermissionWriteFileContent? Before { get; set; } +} + /// File write permission request. /// The write variant of . public sealed partial class PermissionRequestWrite : PermissionRequest @@ -11224,6 +11883,11 @@ public sealed partial class PermissionRequestWrite : PermissionRequest [JsonPropertyName("diff")] public required string Diff { get; set; } + /// Complete before/after text previews captured while planning this request. Omitted when a complete preview is unavailable, including pre-read sandbox requests and non-UTF-8 files. Missing sides mean creation or deletion, not unavailable content. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("fileEdits")] + public PermissionWriteFileEdit[]? FileEdits { get; set; } + /// Path of the file being written to. [JsonPropertyName("fileName")] public required string FileName { get; set; } @@ -11872,6 +12536,11 @@ public sealed partial class PermissionPromptRequestWrite : PermissionPromptReque [JsonPropertyName("diff")] public required string Diff { get; set; } + /// Complete before/after text previews captured while planning this request. Omitted when a complete preview is unavailable, including pre-read sandbox requests and non-UTF-8 files. Missing sides mean creation or deletion, not unavailable content. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("fileEdits")] + public PermissionWriteFileEdit[]? FileEdits { get; set; } + /// Path of the file being written to. [JsonPropertyName("fileName")] public required string FileName { get; set; } @@ -13261,7 +13930,7 @@ public sealed partial class McpAppToolCallCompleteToolMeta public McpAppToolCallCompleteToolMetaUI? Ui { get; set; } } -/// Routing preference used when the session model is `auto`. `fast` is an integrator-only latency preset and is not a first-party GitHub Copilot product preference. +/// Extensible routing preference for the virtual `auto` model. New identifiers must be advertised and enabled by the provider. `fast` is an integrator-only latency preset. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] public readonly struct AutoTier : IEquatable @@ -13302,10 +13971,10 @@ public AutoTier(string value) public override bool Equals(object? obj) => obj is AutoTier other && Equals(other); /// - public bool Equals(AutoTier other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AutoTier other) => string.Equals(Value, other.Value, StringComparison.Ordinal); /// - public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value); /// public override string ToString() => Value; @@ -14406,7 +15075,7 @@ public override void Write(Utf8JsonWriter writer, ModelDeselectedReason value, J } } -/// Auto preferences that Copilot API can recommend. +/// Enabled Auto preferences that Copilot API can recommend. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] public readonly struct RecommendedAutoTier : IEquatable @@ -14444,10 +15113,10 @@ public RecommendedAutoTier(string value) public override bool Equals(object? obj) => obj is RecommendedAutoTier other && Equals(other); /// - public bool Equals(RecommendedAutoTier other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(RecommendedAutoTier other) => string.Equals(Value, other.Value, StringComparison.Ordinal); /// - public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value); /// public override string ToString() => Value; @@ -14788,6 +15457,132 @@ public override void Write(Utf8JsonWriter writer, HandoffSourceType value, JsonS } } +/// Whether the accumulated numeric AI-credit subtotal covers the observed calls. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AiCreditsStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AiCreditsStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Every observed call supplied an AI-credit amount, including an explicit zero. + public static AiCreditsStatus Complete { get; } = new("complete"); + + /// Some calls supplied amounts and some did not; the numeric value is only a reported subtotal. + public static AiCreditsStatus Partial { get; } = new("partial"); + + /// No AI-credit amount was reported for the observed calls. Numeric zero is not a zero-cost claim. + public static AiCreditsStatus Unavailable { get; } = new("unavailable"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AiCreditsStatus left, AiCreditsStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AiCreditsStatus left, AiCreditsStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AiCreditsStatus other && Equals(other); + + /// + public bool Equals(AiCreditsStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override AiCreditsStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AiCreditsStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AiCreditsStatus)); + } + } +} + +/// The neutral kind of a model provider — the model analog of `AccountKind`. A model provider is the live, entitled source a model came from; central code never branches on this beyond a single dispatch. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelProviderKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelProviderKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// GitHub Copilot / CAPI models, spawned by a github-resolving account that holds a Copilot seat. + public static ModelProviderKind Copilot { get; } = new("copilot"); + + /// Microsoft 365 Copilot (Loki) inference models, spawned by a resolvable Entra-derived Loki account. + public static ModelProviderKind Loki { get; } = new("loki"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderKind left, ModelProviderKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelProviderKind left, ModelProviderKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ModelProviderKind other && Equals(other); + + /// + public bool Equals(ModelProviderKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ModelProviderKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ModelProviderKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderKind)); + } + } +} + /// Whether the session ended normally ("routine") or due to a crash/fatal error ("error"). [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -14849,6 +15644,408 @@ public override void Write(Utf8JsonWriter writer, ShutdownType value, JsonSerial } } +/// Access policy reported by the quota service. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ProviderQuotaAccessState : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ProviderQuotaAccessState(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Access is allowed. + public static ProviderQuotaAccessState Allowed { get; } = new("allowed"); + + /// Access is denied. + public static ProviderQuotaAccessState Denied { get; } = new("denied"); + + /// Quota is not required for this access. + public static ProviderQuotaAccessState NotRequired { get; } = new("not_required"); + + /// The service did not supply a recognized access state. + public static ProviderQuotaAccessState Unknown { get; } = new("unknown"); + + /// Client-only compatibility placeholder when acquisitionStatus is unavailable or failed; not an observed service verdict. + public static ProviderQuotaAccessState Unavailable { get; } = new("unavailable"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderQuotaAccessState left, ProviderQuotaAccessState right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderQuotaAccessState left, ProviderQuotaAccessState right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ProviderQuotaAccessState other && Equals(other); + + /// + public bool Equals(ProviderQuotaAccessState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ProviderQuotaAccessState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ProviderQuotaAccessState value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderQuotaAccessState)); + } + } +} + +/// Acquisition state, independent of the service's business access/capacity verdict. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ProviderQuotaAcquisitionStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ProviderQuotaAcquisitionStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A service quota observation was received. + public static ProviderQuotaAcquisitionStatus Succeeded { get; } = new("succeeded"); + + /// No service snapshot is available for this caller or surface. + public static ProviderQuotaAcquisitionStatus Unavailable { get; } = new("unavailable"); + + /// Acquisition failed; inspect httpStatus/acquisitionError without inferring a business verdict. + public static ProviderQuotaAcquisitionStatus Failed { get; } = new("failed"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderQuotaAcquisitionStatus left, ProviderQuotaAcquisitionStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderQuotaAcquisitionStatus left, ProviderQuotaAcquisitionStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ProviderQuotaAcquisitionStatus other && Equals(other); + + /// + public bool Equals(ProviderQuotaAcquisitionStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ProviderQuotaAcquisitionStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ProviderQuotaAcquisitionStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderQuotaAcquisitionStatus)); + } + } +} + +/// Capacity is independent of whether a numeric balance was supplied. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ProviderQuotaCapacityState : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ProviderQuotaCapacityState(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Capacity is available. + public static ProviderQuotaCapacityState Available { get; } = new("available"); + + /// The service explicitly reports exhaustion. + public static ProviderQuotaCapacityState Exhausted { get; } = new("exhausted"); + + /// The reported entitlement is unlimited. + public static ProviderQuotaCapacityState Unlimited { get; } = new("unlimited"); + + /// No billable quota is required. + public static ProviderQuotaCapacityState NotRequired { get; } = new("not_required"); + + /// The service reports that capacity is not applicable. + public static ProviderQuotaCapacityState NotApplicable { get; } = new("not_applicable"); + + /// No recognized capacity state was supplied. + public static ProviderQuotaCapacityState Unknown { get; } = new("unknown"); + + /// Client-only compatibility placeholder when acquisitionStatus is unavailable or failed; not an observed service verdict. + public static ProviderQuotaCapacityState Unavailable { get; } = new("unavailable"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderQuotaCapacityState left, ProviderQuotaCapacityState right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderQuotaCapacityState left, ProviderQuotaCapacityState right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ProviderQuotaCapacityState other && Equals(other); + + /// + public bool Equals(ProviderQuotaCapacityState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ProviderQuotaCapacityState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ProviderQuotaCapacityState value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderQuotaCapacityState)); + } + } +} + +/// Timing and purpose of a provider quota observation. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ProviderQuotaObservationKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ProviderQuotaObservationKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// An explicit account read; consumption may still be subject to service aggregation delay. + public static ProviderQuotaObservationKind AccountSnapshot { get; } = new("account_snapshot"); + + /// A verdict attached before an inference response. May be cached; never a post-charge balance. + public static ProviderQuotaObservationKind AdmissionState { get; } = new("admission_state"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderQuotaObservationKind left, ProviderQuotaObservationKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderQuotaObservationKind left, ProviderQuotaObservationKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ProviderQuotaObservationKind other && Equals(other); + + /// + public bool Equals(ProviderQuotaObservationKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ProviderQuotaObservationKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ProviderQuotaObservationKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderQuotaObservationKind)); + } + } +} + +/// Interpretation permitted for independently reported quantities. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ProviderQuotaQuantityKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ProviderQuotaQuantityKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Quantities form an authoritative available-of-entitled budget suitable for percentage presentation. + public static ProviderQuotaQuantityKind AuthoritativeBudget { get; } = new("authoritative_budget"); + + /// Raw advisory quantities; do not infer permission, usage percentage, reset cadence, or post-charge balance. + public static ProviderQuotaQuantityKind AdvisoryBalance { get; } = new("advisory_balance"); + + /// No quantity measurement, as with admission-only response headers. + public static ProviderQuotaQuantityKind None { get; } = new("none"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderQuotaQuantityKind left, ProviderQuotaQuantityKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderQuotaQuantityKind left, ProviderQuotaQuantityKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ProviderQuotaQuantityKind other && Equals(other); + + /// + public bool Equals(ProviderQuotaQuantityKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ProviderQuotaQuantityKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ProviderQuotaQuantityKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderQuotaQuantityKind)); + } + } +} + +/// Units explicitly reported by a quota provider. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ProviderQuotaUnit : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ProviderQuotaUnit(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// AI credits, not tokens or currency. + public static ProviderQuotaUnit AiCredits { get; } = new("ai_credits"); + + /// Requests charged against an entitlement. + public static ProviderQuotaUnit Requests { get; } = new("requests"); + + /// Model tokens. + public static ProviderQuotaUnit Tokens { get; } = new("tokens"); + + /// The observation does not establish a quantity unit. + public static ProviderQuotaUnit Unknown { get; } = new("unknown"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderQuotaUnit left, ProviderQuotaUnit right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderQuotaUnit left, ProviderQuotaUnit right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ProviderQuotaUnit other && Equals(other); + + /// + public bool Equals(ProviderQuotaUnit other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ProviderQuotaUnit Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ProviderQuotaUnit value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderQuotaUnit)); + } + } +} + /// What initiated a conversation compaction. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -16872,6 +18069,76 @@ public override void Write(Utf8JsonWriter writer, AssistantUsageTransport value, } } +/// Why a WebSocket-capable model call was carried by the HTTP fallback. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelCallWebSocketFallbackReason : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelCallWebSocketFallbackReason(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The WebSocket connection could not be established. + public static ModelCallWebSocketFallbackReason ConnectFailed { get; } = new("connect_failed"); + + /// No usable WebSocket connection was available for the request. + public static ModelCallWebSocketFallbackReason ConnectionUnavailable { get; } = new("connection_unavailable"); + + /// Sending the request over the WebSocket failed. + public static ModelCallWebSocketFallbackReason SendFailed { get; } = new("send_failed"); + + /// The WebSocket returned a retryable API error. + public static ModelCallWebSocketFallbackReason ApiError { get; } = new("api_error"); + + /// The WebSocket transport failed before any output reached the consumer. + public static ModelCallWebSocketFallbackReason TransportFailed { get; } = new("transport_failed"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelCallWebSocketFallbackReason left, ModelCallWebSocketFallbackReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelCallWebSocketFallbackReason left, ModelCallWebSocketFallbackReason right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ModelCallWebSocketFallbackReason other && Equals(other); + + /// + public bool Equals(ModelCallWebSocketFallbackReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ModelCallWebSocketFallbackReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ModelCallWebSocketFallbackReason value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelCallWebSocketFallbackReason)); + } + } +} + /// For HTTP 400 failures only: whether the response carried a structured CAPI error envelope (structured_error, a deterministic validation failure) or no error body (bodyless, the transient gateway/proxy signature). Absent for non-400 failures. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -17271,7 +18538,7 @@ public override void Write(Utf8JsonWriter writer, AbortReason value, JsonSeriali } } -/// Configuration source: user, workspace, plugin, builtin, or managed. +/// Configuration source: user, workspace, plugin, builtin, managed, or account. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] public readonly struct McpServerSource : IEquatable @@ -17305,6 +18572,9 @@ public McpServerSource(string value) /// Server supplied by a trusted host-managed catalog. public static McpServerSource Managed { get; } = new("managed"); + /// Server contributed by a signed-in account; enablement and organization policy still apply. + public static McpServerSource Account { get; } = new("account"); + /// Returns a value indicating whether two instances are equivalent. public static bool operator ==(McpServerSource left, McpServerSource right) => left.Equals(right); @@ -19634,6 +20904,9 @@ public PermissionApprovalEvaluationReasonCode(string value) /// The script path was not authorized for inspection. public static PermissionApprovalEvaluationReasonCode PathNotAuthorized { get; } = new("path-not-authorized"); + /// A code source was excluded from review by content exclusion policy. + public static PermissionApprovalEvaluationReasonCode ContentExcluded { get; } = new("content-excluded"); + /// The script working directory was invalid. public static PermissionApprovalEvaluationReasonCode InvalidWorkingDirectory { get; } = new("invalid-working-directory"); @@ -19670,6 +20943,24 @@ public PermissionApprovalEvaluationReasonCode(string value) /// The script argument binding could not be reviewed. public static PermissionApprovalEvaluationReasonCode ArgumentBindingUnreviewable { get; } = new("argument-binding-unreviewable"); + /// The shell command could not be analyzed for execution evidence. + public static PermissionApprovalEvaluationReasonCode UnsupportedCommandShape { get; } = new("unsupported-command-shape"); + + /// The shell command used a code source that cannot be bound for review. + public static PermissionApprovalEvaluationReasonCode UnsupportedSource { get; } = new("unsupported-source"); + + /// The shell command used a code source computed at run time. + public static PermissionApprovalEvaluationReasonCode DynamicSource { get; } = new("dynamic-source"); + + /// The shell command referenced more code sources than can be reviewed. + public static PermissionApprovalEvaluationReasonCode TooManySources { get; } = new("too-many-sources"); + + /// A code-bearing executable could not be inspected. + public static PermissionApprovalEvaluationReasonCode ExecutableUnavailable { get; } = new("executable-unavailable"); + + /// A code-bearing executable exceeded the binding size limit. + public static PermissionApprovalEvaluationReasonCode ExecutableTooLarge { get; } = new("executable-too-large"); + /// The script review metadata was malformed. public static PermissionApprovalEvaluationReasonCode MalformedScriptActionReview { get; } = new("malformed-script-action-review"); @@ -20984,7 +22275,7 @@ public ManagedSettingsEnforcedAction(string value) /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// An attempt to turn on a bypass-permissions ("yolo") escalation was refused or capped because policy disables bypass-permissions mode. + /// An attempt to enter a permission mode governed by managed policy was refused or capped. The `setting` and `escalation` fields identify whether this was bypass permissions or Assisted Permissions. public static ManagedSettingsEnforcedAction BypassPermissionsBlocked { get; } = new("bypass_permissions_blocked"); /// Returns a value indicating whether two instances are equivalent. @@ -21023,7 +22314,7 @@ public override void Write(Utf8JsonWriter writer, ManagedSettingsEnforcedAction } } -/// For a `bypass_permissions_blocked` action, which permission-escalation primitive was refused. +/// For a `bypass_permissions_blocked` action, which permission-mode or escalation primitive was refused. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] public readonly struct ManagedSettingsEnforcedEscalation : IEquatable @@ -21048,7 +22339,7 @@ public ManagedSettingsEnforcedEscalation(string value) /// Automatic approval of all tool permission requests. public static ManagedSettingsEnforcedEscalation ApproveAll { get; } = new("approve_all"); - /// Assisted mode — keeps normal prompt paths and adds an LLM recommendation, distinct from allow-all. + /// Assisted Permissions — uses an LLM review to reduce prompts, distinct from allow-all and not a hard security boundary. public static ManagedSettingsEnforcedEscalation AssistedApproval { get; } = new("assisted_approval"); /// Unrestricted filesystem access outside the session's allowed directories. @@ -21471,7 +22762,7 @@ public McpServerStatus(string value) /// The server is configured but disabled. public static McpServerStatus Disabled { get; } = new("disabled"); - /// The server was intentionally stopped and can be restarted on demand when policy permits; a server quarantined by restrictive managed policy stays stopped and cannot be restarted until the policy allows it. + /// The server is not running: it may not have started yet, may have been explicitly stopped, or may be quarantined by restrictive managed policy. It can be restarted on demand when policy permits. public static McpServerStatus Stopped { get; } = new("stopped"); /// The server is not configured for this session. @@ -21828,6 +23119,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(ModelCallFinishedEvent))] [JsonSerializable(typeof(ModelCallStartData))] [JsonSerializable(typeof(ModelCallStartEvent))] +[JsonSerializable(typeof(ModelProviderRef))] [JsonSerializable(typeof(OmittedBinaryResult))] [JsonSerializable(typeof(PendingMessagesModifiedData))] [JsonSerializable(typeof(PendingMessagesModifiedEvent))] @@ -21894,10 +23186,14 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(PermissionResultDeniedNoApprovalRuleAndCouldNotRequestFromUser))] [JsonSerializable(typeof(PermissionRule))] [JsonSerializable(typeof(PermissionSandboxPathGrant))] +[JsonSerializable(typeof(PermissionWriteFileContent))] +[JsonSerializable(typeof(PermissionWriteFileEdit))] [JsonSerializable(typeof(PersistedBinaryImage))] [JsonSerializable(typeof(PersistedBinaryResult))] [JsonSerializable(typeof(PromptCacheBreakData))] [JsonSerializable(typeof(PromptCacheBreakEvent))] +[JsonSerializable(typeof(ProviderQuotaBudgetMetadata))] +[JsonSerializable(typeof(ProviderQuotaState))] [JsonSerializable(typeof(ResponsesReasoning))] [JsonSerializable(typeof(SamplingCompletedData))] [JsonSerializable(typeof(SamplingCompletedEvent))] @@ -22012,6 +23308,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SessionPermissionsChangedEvent))] [JsonSerializable(typeof(SessionPlanChangedData))] [JsonSerializable(typeof(SessionPlanChangedEvent))] +[JsonSerializable(typeof(SessionQuotaObservationData))] +[JsonSerializable(typeof(SessionQuotaObservationEvent))] [JsonSerializable(typeof(SessionRemoteSteerableChangedData))] [JsonSerializable(typeof(SessionRemoteSteerableChangedEvent))] [JsonSerializable(typeof(SessionResumeData))] @@ -22143,7 +23441,17 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(ToolUserRequestedEvent))] [JsonSerializable(typeof(UiEphemeralQueryData))] [JsonSerializable(typeof(UiEphemeralQueryEvent))] +[JsonSerializable(typeof(UsageAccountingIdentity))] [JsonSerializable(typeof(UsageCheckpointModelCacheState))] +[JsonSerializable(typeof(UsageGetMetricsResult))] +[JsonSerializable(typeof(UsageMetricsAgentMetric))] +[JsonSerializable(typeof(UsageMetricsCodeChanges))] +[JsonSerializable(typeof(UsageMetricsModelMetric))] +[JsonSerializable(typeof(UsageMetricsModelMetricRequests))] +[JsonSerializable(typeof(UsageMetricsModelMetricTokenDetail))] +[JsonSerializable(typeof(UsageMetricsModelMetricUsage))] +[JsonSerializable(typeof(UsageMetricsProviderModelMetric))] +[JsonSerializable(typeof(UsageMetricsTokenDetail))] [JsonSerializable(typeof(UserInputCompletedData))] [JsonSerializable(typeof(UserInputCompletedEvent))] [JsonSerializable(typeof(UserInputRequestedData))] diff --git a/dotnet/src/JsonRpc.cs b/dotnet/src/JsonRpc.cs index 26d4fe297a..8d45eb6f4a 100644 --- a/dotnet/src/JsonRpc.cs +++ b/dotnet/src/JsonRpc.cs @@ -1039,11 +1039,12 @@ private sealed class IncomingRequestCancellation : IDisposable public IncomingRequestCancellation(long id, CancellationToken connectionClosedToken) { Id = id; - _combinedSource = CancellationTokenSource.CreateLinkedTokenSource(_requestSource.Token, connectionClosedToken); _registration = _requestSource.Token.Register(static state => { ((TaskCompletionSource)state!).TrySetResult(); }, _requestCancelled); + // Cancellation callbacks run in reverse registration order: propagate to handlers before replying. + _combinedSource = CancellationTokenSource.CreateLinkedTokenSource(_requestSource.Token, connectionClosedToken); } public long Id { get; } diff --git a/dotnet/src/Session.StructuredOutput.cs b/dotnet/src/Session.StructuredOutput.cs index 6df54d121e..c190c13ee3 100644 --- a/dotnet/src/Session.StructuredOutput.cs +++ b/dotnet/src/Session.StructuredOutput.cs @@ -192,10 +192,10 @@ void ProcessEvent(SessionEvent evt) } pendingEvents.Clear(); } - await Task.WhenAny(completion.Task, JsonRpc.Completion, _eventChannel.Reader.Completion); + await Task.WhenAny(completion.Task, _eventDispatchStopped.Task, JsonRpc.Completion, _eventChannel.Reader.Completion); if (!completion.Task.IsCompleted) { - throw new IOException("The session closed before a final structured response was received."); + completion.TrySetException(new IOException("The session closed before a final structured response was received.")); } return await completion.Task; } diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index f0cdde8999..d6788ae0ac 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -59,8 +59,10 @@ namespace GitHub.Copilot; public sealed partial class CopilotSession : IAsyncDisposable { private IReadOnlyDictionary _toolHandlers = new Dictionary(StringComparer.Ordinal); - private readonly Dictionary> _commandHandlers = []; - private readonly Dictionary>> _bearerTokenProviders = new(StringComparer.Ordinal); + private Dictionary> _commandHandlers = new(StringComparer.Ordinal); + private IReadOnlyDictionary>> _bearerTokenProviders = new Dictionary>>(StringComparer.Ordinal); + private readonly object _callbackLock = new(); + private bool _callbacksRetired; private readonly ConcurrentDictionary _pendingExternalTools = new(StringComparer.Ordinal); private readonly CancellationTokenSource _externalToolLifetime = new(); private readonly ILogger _logger; @@ -73,16 +75,29 @@ public sealed partial class CopilotSession : IAsyncDisposable private volatile Func>? _elicitationHandler; private volatile Func>? _exitPlanModeHandler; private volatile Func>? _autoModeSwitchHandler; + private volatile ISkillProvider? _skillProvider; private ImmutableArray _eventHandlers = ImmutableArray.Empty; - private sealed record EventSubscription(Type EventType, Action Handler, bool RootAgentOnly); + private sealed class EventSubscription(Type eventType, Action handler, bool rootAgentOnly) + { + private Action? _handler = handler; + + public Type EventType { get; } = eventType; + public bool RootAgentOnly { get; } = rootAgentOnly; + + // Keep the user handler out of the retained unsubscribe callback's closure. + public static EventSubscription Create(Action handler, bool rootAgentOnly) where T : SessionEvent => + new(typeof(T), evt => handler((T)evt), rootAgentOnly); + + public void Invoke(SessionEvent sessionEvent) => Volatile.Read(ref _handler)?.Invoke(sessionEvent); + + public void ClearHandler() => Interlocked.Exchange(ref _handler, null); + } private SessionHooks? _hooks; - private readonly SemaphoreSlim _hooksLock = new(1, 1); private readonly SemaphoreSlim _setToolsLock = new(1, 1); private Dictionary>>? _transformCallbacks; - private readonly SemaphoreSlim _transformCallbacksLock = new(1, 1); private IReadOnlyList _openCanvases = Array.Empty(); @@ -93,9 +108,10 @@ private sealed record EventSubscription(Type EventType, Action Han /// Channel that serializes event dispatch. enqueues; /// a single background consumer () dequeues and /// invokes handlers one at a time, preserving arrival order. + /// Shutdown can drain concurrently without waiting for an in-flight handler. /// - private readonly Channel _eventChannel = Channel.CreateUnbounded( - new() { SingleReader = true }); + private readonly Channel _eventChannel = Channel.CreateUnbounded(); + private readonly TaskCompletionSource _eventDispatchStopped = new(TaskCreationOptions.RunContinuationsAsynchronously); /// /// Fixed name of the runtime's built-in tool-search tool. A client can @@ -213,19 +229,56 @@ internal void RemoveFromClient() } /// - /// Stops the session's event consumer () without - /// making an RPC. and - /// use this on error paths where a - /// locally registered session fails before it can be returned to the caller: - /// starts the consumer eagerly, and no caller - /// ever receives the failed session to dispose it. Safe to call more than once — - /// is idempotent. + /// Retires the event consumer without making an RPC or joining an admitted handler. + /// Releases queued events and subscriptions and fails pending response waiters. + /// Also used for registered sessions that fail before being returned to the caller. + /// Safe to call more than once. /// internal void CloseEventChannel() { + _eventDispatchStopped.TrySetResult(true); _eventChannel.Writer.TryComplete(); + while (_eventChannel.Reader.TryRead(out _)) + { + // Discard queued events; retirement must not dispatch their handlers. + } + var subscriptions = ImmutableInterlocked.InterlockedExchange(ref _eventHandlers, ImmutableArray.Empty); + foreach (var subscription in subscriptions) + { + subscription.ClearHandler(); + } + } + + /// Releases local callback ownership without an RPC or joining admitted handlers. + internal void Retire() + { + lock (_callbackLock) + { + _callbacksRetired = true; + Volatile.Write(ref _toolHandlers, new Dictionary(StringComparer.Ordinal)); + _commandHandlers = new Dictionary>(StringComparer.Ordinal); + _bearerTokenProviders = new Dictionary>>(StringComparer.Ordinal); + _permissionHandler = null; + _mcpAuthHandler = null; + _userInputHandler = null; + _elicitationHandler = null; + _exitPlanModeHandler = null; + _autoModeSwitchHandler = null; + _skillProvider = null; + _hooks = null; + _transformCallbacks = null; + ClientSessionApis.ProviderToken = null; + ClientSessionApis.Canvas = null; + ClientSessionApis.SessionFs = null; + } + CancelPendingExternalTools(); + CloseEventChannel(); + ReleaseGitHubTokenProviderRegistration(); } + private void ThrowIfCallbacksRetired() => + ObjectDisposedException.ThrowIf(_callbacksRetired, this); + /// /// Removes the session from its parent client and stops its event consumer. /// Used on session-creation/resume error paths where the session was registered @@ -233,8 +286,7 @@ internal void CloseEventChannel() /// internal void Unregister() { - CancelPendingExternalTools(); - CloseEventChannel(); + Retire(); RemoveFromClient(); } @@ -369,6 +421,7 @@ public async Task SendAsync(MessageOptions options, CancellationToken ca /// Thrown if the timeout is reached before the session becomes idle. /// Thrown if the is cancelled. /// Thrown if the session has been disposed. + /// Thrown if session event delivery stops before a final response arrives. /// /// /// This is a convenience method that combines with waiting for @@ -454,6 +507,11 @@ void Handler(SessionEvent evt) }); try { + await Task.WhenAny(tcs.Task, _eventDispatchStopped.Task); + if (!tcs.Task.IsCompleted) + { + tcs.TrySetException(new IOException("The session closed before a final response was received.")); + } var result = await tcs.Task; LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null, "CopilotSession.SendAndWaitAsync complete. Elapsed={Elapsed}, SessionId={SessionId}, CompletedBy={CompletedBy}, AssistantMessageReceived={AssistantMessageReceived}", @@ -488,6 +546,8 @@ void Handler(SessionEvent evt) /// /// A callback to be invoked when a session event occurs. /// An that, when disposed, unsubscribes the handler. + /// Thrown if the session has been disposed. + /// Thrown if session event delivery has stopped. /// /// /// Events include assistant messages, tool executions, errors, and session state changes. @@ -498,6 +558,10 @@ void Handler(SessionEvent evt) /// A handler will never be called concurrently with itself or with other handlers /// on the same session. /// + /// + /// Unsubscribing or closing the session releases handlers that have not been admitted + /// for dispatch. A handler already admitted may finish; disposal does not wait for it. + /// /// /// /// @@ -529,9 +593,20 @@ private ActionDisposable OnCore(Action handler, bool rootAgentOnly) where ArgumentNullException.ThrowIfNull(handler); ThrowIfDisposed(); - var subscription = new EventSubscription(typeof(T), evt => handler((T)evt), rootAgentOnly); + var subscription = EventSubscription.Create(handler, rootAgentOnly); ImmutableInterlocked.Update(ref _eventHandlers, array => array.Add(subscription)); - return new ActionDisposable(() => ImmutableInterlocked.Update(ref _eventHandlers, array => array.Remove(subscription))); + if (_eventDispatchStopped.Task.IsCompleted) + { + subscription.ClearHandler(); + ImmutableInterlocked.Update(ref _eventHandlers, array => array.Remove(subscription)); + ThrowIfDisposed(); + throw new IOException("The session event dispatch has stopped."); + } + return new ActionDisposable(() => + { + subscription.ClearHandler(); + ImmutableInterlocked.Update(ref _eventHandlers, array => array.Remove(subscription)); + }); } /// @@ -567,12 +642,20 @@ private async Task ProcessEventsAsync() { await foreach (var sessionEvent in _eventChannel.Reader.ReadAllAsync()) { + if (_eventDispatchStopped.Task.IsCompleted) + { + return; + } var dispatchTimestamp = Stopwatch.GetTimestamp(); var eventType = sessionEvent.GetType(); // Preserve wire attribution without moving waiters ahead of earlier user handlers. var isRootAgentEvent = string.IsNullOrEmpty(sessionEvent.AgentId); foreach (var subscription in _eventHandlers) { + if (_eventDispatchStopped.Task.IsCompleted) + { + return; + } if (!subscription.EventType.IsAssignableFrom(eventType) || (subscription.RootAgentOnly && !isRootAgentEvent)) { @@ -580,7 +663,7 @@ private async Task ProcessEventsAsync() } try { - subscription.Handler(sessionEvent); + subscription.Invoke(sessionEvent); } catch (Exception ex) { @@ -606,7 +689,16 @@ private async Task ProcessEventsAsync() /// internal void RegisterTools(ICollection tools) { - Volatile.Write(ref _toolHandlers, BuildToolHandlerMap(tools)); + SetToolHandlers(BuildToolHandlerMap(tools)); + } + + private void SetToolHandlers(Dictionary handlers) + { + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + Volatile.Write(ref _toolHandlers, handlers); + } } private static Dictionary BuildToolHandlerMap(ICollection tools) @@ -693,9 +785,10 @@ private static Dictionary ToJsonElementDictionary(IDictiona /// are the same as the tools supplied when creating or resuming a session. /// /// - /// Tool handlers switch after the runtime accepts the replacement. Tool calls already running finish with the handlers - /// that started them. If the runtime rejects the replacement, the previous handlers remain installed and the exception is - /// propagated. Concurrent calls are applied in order. + /// Tool handlers switch when the runtime's acceptance response arrives, before subsequent tool requests are dispatched. + /// Tool calls already running finish with the handlers that started them. If the runtime rejects the replacement, the + /// previous handlers remain installed and the exception is propagated. Concurrent calls are applied in order. + /// Acceptance after local retirement fails the replacement without restoring its handlers. /// /// /// The agent sees the new tools from its next model request, which can fall within a turn in progress. A model request @@ -714,9 +807,8 @@ public async Task SetToolsAsync(ICollection tools, Cancel var wireTools = tools.Select(ToProtocolExternalToolDefinition).ToList(); var handlers = BuildToolHandlerMap(tools); - // Cancelling while an earlier call holds the lock sends nothing. Once this call holds it, the - // request runs to completion even if the caller stops waiting, so an accepted replacement still - // installs its handlers. + // Cancelling before sending the request leaves handlers unchanged. Once sent, the request runs + // to completion even if the caller stops waiting, so an accepted replacement still installs its handlers. var replacement = ReplaceToolsAsync(wireTools, handlers, cancellationToken); try { @@ -741,8 +833,13 @@ private async Task ReplaceToolsAsync( await _setToolsLock.WaitAsync(lockCancellationToken); try { - await Rpc.Tools.SetAsync(wireTools, CancellationToken.None); - Volatile.Write(ref _toolHandlers, handlers); + // SemaphoreSlim can grant a released slot while its cancellation continuation is pending. + lockCancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var request = new ToolsSetRequest { SessionId = SessionId, Tools = wireTools }; + await CopilotClient.InvokeRpcAsync( + JsonRpc, "session.tools.set", [request], null, CancellationToken.None, + onResponseInline: _ => SetToolHandlers(handlers)); } finally { @@ -757,8 +854,12 @@ private async Task ReplaceToolsAsync( /// The tool if found; otherwise, null. internal AIFunction? GetTool(string name) { - var handlers = Volatile.Read(ref _toolHandlers); - return handlers.TryGetValue(name, out var tool) ? tool : null; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + var handlers = Volatile.Read(ref _toolHandlers); + return handlers.TryGetValue(name, out var tool) ? tool : null; + } } /// @@ -774,13 +875,21 @@ internal void RegisterPermissionHandler( Func>? handler, bool managedSettingsEnabled) { - _permissionHandler = handler; - _managedSettingsEnabled = managedSettingsEnabled; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + _permissionHandler = handler; + _managedSettingsEnabled = managedSettingsEnabled; + } } internal void RegisterMcpAuthHandler(Func>? handler) { - _mcpAuthHandler = handler; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + _mcpAuthHandler = handler; + } } /// @@ -790,7 +899,12 @@ internal void RegisterMcpAuthHandler(Func>? /// A task that resolves with the permission decision. internal async Task HandlePermissionRequestAsync(JsonElement permissionRequestData) { - var handler = _permissionHandler; + Func>? handler; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + handler = _permissionHandler; + } if (handler == null) { @@ -855,7 +969,12 @@ private async Task HandleBroadcastEventAsync(SessionEvent sessionEvent) if (data.ResolvedByHook == true) return; // Already resolved by a permissionRequest hook; no client action needed. - var handler = _permissionHandler; + Func>? handler; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + handler = _permissionHandler; + } if (handler is null) return; // This client doesn't handle permissions; another client will. @@ -869,7 +988,12 @@ private async Task HandleBroadcastEventAsync(SessionEvent sessionEvent) if (string.IsNullOrEmpty(data.RequestId)) return; - var handler = _mcpAuthHandler; + Func>? handler; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + handler = _mcpAuthHandler; + } if (handler is null) { if (_logger.IsEnabled(LogLevel.Warning)) @@ -1358,7 +1482,11 @@ private async Task ExecutePermissionAndRespondAsync(string requestId, Permission /// The handler to invoke when user input is requested. internal void RegisterUserInputHandler(Func> handler) { - _userInputHandler = handler; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + _userInputHandler = handler; + } } /// @@ -1367,11 +1495,18 @@ internal void RegisterUserInputHandler(FuncThe command definitions to register. internal void RegisterCommands(IEnumerable? commands) { - _commandHandlers.Clear(); - if (commands is null) return; - foreach (var cmd in commands) + var handlers = new Dictionary>(StringComparer.Ordinal); + if (commands is not null) + { + foreach (var cmd in commands) + { + handlers[cmd.Name] = cmd.Handler; + } + } + lock (_callbackLock) { - _commandHandlers[cmd.Name] = cmd.Handler; + ThrowIfCallbacksRetired(); + _commandHandlers = handlers; } } @@ -1381,7 +1516,11 @@ internal void RegisterCommands(IEnumerable? commands) /// The handler to invoke when an elicitation request is received. internal void RegisterElicitationHandler(Func>? handler) { - _elicitationHandler = handler; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + _elicitationHandler = handler; + } } /// @@ -1390,7 +1529,11 @@ internal void RegisterElicitationHandler(FuncThe handler to invoke when an exit-plan-mode request is received. internal void RegisterExitPlanModeHandler(Func>? handler) { - _exitPlanModeHandler = handler; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + _exitPlanModeHandler = handler; + } } /// @@ -1399,7 +1542,73 @@ internal void RegisterExitPlanModeHandler(FuncThe handler to invoke when an auto-mode-switch request is received. internal void RegisterAutoModeSwitchHandler(Func>? handler) { - _autoModeSwitchHandler = handler; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + _autoModeSwitchHandler = handler; + } + } + + /// + /// Registers the session-scoped skill provider callback. + /// + internal void RegisterSkillProvider(ISkillProvider? provider) + { + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + _skillProvider = provider; + } + } + + internal void ClearSkillProvider() + { + lock (_callbackLock) + { + _skillProvider = null; + } + } + + internal async ValueTask HandleSkillProviderListAsync(CancellationToken cancellationToken) + { + ISkillProvider provider; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + provider = _skillProvider ?? throw new InvalidOperationException($"No skill provider for session: {SessionId}"); + } + + try + { + var skills = await provider.ListSkillsAsync(cancellationToken).ConfigureAwait(false); + return new CopilotClient.SkillProviderListResult(skills?.ToList() ?? []); + } + catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) + { + LogSkillProviderFailed(ex, "listSkills", SessionId); + throw new InvalidOperationException("Skill provider listSkills failed", ex); + } + } + + internal async ValueTask HandleSkillProviderReadAsync(string name, CancellationToken cancellationToken) + { + ISkillProvider provider; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + provider = _skillProvider ?? throw new InvalidOperationException($"No skill provider for session: {SessionId}"); + } + + try + { + var markdown = await provider.ReadSkillAsync(name, cancellationToken).ConfigureAwait(false); + return new CopilotClient.SkillProviderReadResult(markdown); + } + catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) + { + LogSkillProviderFailed(ex, "readSkill", SessionId); + throw new InvalidOperationException("Skill provider readSkill failed", ex); + } } /// @@ -1416,17 +1625,20 @@ internal void RegisterAutoModeSwitchHandler(FuncMap of provider name to callback, or null/empty to clear. internal void RegisterBearerTokenProviders(IReadOnlyDictionary>>? providers) { - _bearerTokenProviders.Clear(); - if (providers is null || providers.Count == 0) + var callbacks = new Dictionary>>(StringComparer.Ordinal); + if (providers is not null) { - ClientSessionApis.ProviderToken = null; - return; + foreach (var (name, callback) in providers) + { + callbacks[name] = callback; + } } - foreach (var (name, callback) in providers) + lock (_callbackLock) { - _bearerTokenProviders[name] = callback; + ThrowIfCallbacksRetired(); + _bearerTokenProviders = callbacks; + ClientSessionApis.ProviderToken = callbacks.Count == 0 ? null : new BearerTokenProviderHandler(this); } - ClientSessionApis.ProviderToken = new BearerTokenProviderHandler(this); } /// @@ -1437,10 +1649,15 @@ private sealed class BearerTokenProviderHandler(CopilotSession session) : IProvi { public async Task GetTokenAsync(ProviderTokenAcquireRequest request, CancellationToken cancellationToken = default) { - if (!session._bearerTokenProviders.TryGetValue(request.ProviderName, out var callback)) + Func>? callback; + lock (session._callbackLock) { - throw new InvalidOperationException( - $"No bearer-token provider registered for provider \"{request.ProviderName}\""); + session.ThrowIfCallbacksRetired(); + if (!session._bearerTokenProviders.TryGetValue(request.ProviderName, out callback)) + { + throw new InvalidOperationException( + $"No bearer-token provider registered for provider \"{request.ProviderName}\""); + } } var token = await callback(new ProviderTokenArgs { ProviderName = request.ProviderName, SessionId = request.SessionId }).ConfigureAwait(false); return new ProviderTokenAcquireResult { Token = token }; @@ -1523,7 +1740,20 @@ private void RemoveOpenCanvas(string instanceId) internal void SetCanvasHandler(ICanvasHandler? handler) { - ClientSessionApis.Canvas = handler is null ? null : new CanvasHandlerAdapter(handler); + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + ClientSessionApis.Canvas = handler is null ? null : new CanvasHandlerAdapter(handler); + } + } + + internal void SetSessionFsHandler(Rpc.ISessionFsHandler handler) + { + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + ClientSessionApis.SessionFs = handler; + } } private static readonly JsonElement NullJsonElement = JsonElement.Parse("null"); @@ -1592,7 +1822,13 @@ public async Task InvokeAsync(CanvasProviderInvokeActionRequest request, /// private async Task ExecuteCommandAndRespondAsync(string requestId, string commandName, string command, string args) { - if (!_commandHandlers.TryGetValue(commandName, out var handler)) + Func? handler; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + _commandHandlers.TryGetValue(commandName, out handler); + } + if (handler is null) { try { @@ -1652,7 +1888,12 @@ await handler(new CommandContext /// private async Task HandleElicitationRequestAsync(ElicitationContext context, string requestId) { - var handler = _elicitationHandler; + Func>? handler; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + handler = _elicitationHandler; + } if (handler is null) return; try @@ -1852,7 +2093,12 @@ public async Task ConfirmAsync(string message, CancellationToken cancellat /// A task that resolves with the user's response. internal async Task HandleUserInputRequestAsync(UserInputRequest request) { - var handler = _userInputHandler ?? throw new InvalidOperationException("No user input handler registered"); + Func> handler; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + handler = _userInputHandler ?? throw new InvalidOperationException("No user input handler registered"); + } var invocation = new UserInputInvocation { SessionId = SessionId @@ -1874,7 +2120,12 @@ internal async Task HandleUserInputRequestAsync(UserInputRequ /// A task that resolves with the user's decision. internal async Task HandleExitPlanModeRequestAsync(ExitPlanModeRequest request) { - var handler = _exitPlanModeHandler; + Func>? handler; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + handler = _exitPlanModeHandler; + } if (handler is null) { return new ExitPlanModeResult { Approved = true }; @@ -1897,7 +2148,12 @@ internal async Task HandleExitPlanModeRequestAsync(ExitPlanM /// A task that resolves with the user's decision. internal async Task HandleAutoModeSwitchRequestAsync(AutoModeSwitchRequest request) { - var handler = _autoModeSwitchHandler; + Func>? handler; + lock (_callbackLock) + { + ThrowIfCallbacksRetired(); + handler = _autoModeSwitchHandler; + } if (handler is null) { return AutoModeSwitchResponse.No; @@ -1919,15 +2175,11 @@ internal async Task HandleAutoModeSwitchRequestAsync(Aut /// The hooks configuration. internal void RegisterHooks(SessionHooks hooks) { - _hooksLock.Wait(); - try + lock (_callbackLock) { + ThrowIfCallbacksRetired(); _hooks = hooks; } - finally - { - _hooksLock.Release(); - } } /// @@ -1938,16 +2190,12 @@ internal void RegisterHooks(SessionHooks hooks) /// A task that resolves with the hook output. internal async Task HandleHooksInvokeAsync(string hookType, JsonElement input) { - await _hooksLock.WaitAsync(); SessionHooks? hooks; - try + lock (_callbackLock) { + ThrowIfCallbacksRetired(); hooks = _hooks; } - finally - { - _hooksLock.Release(); - } if (hooks == null) { @@ -2051,15 +2299,11 @@ internal void RegisterHooks(SessionHooks hooks) /// The transform callbacks keyed by section identifier. internal void RegisterTransformCallbacks(Dictionary>>? callbacks) { - _transformCallbacksLock.Wait(); - try + lock (_callbackLock) { + ThrowIfCallbacksRetired(); _transformCallbacks = callbacks; } - finally - { - _transformCallbacksLock.Release(); - } } /// @@ -2070,15 +2314,11 @@ internal void RegisterTransformCallbacks(Dictionary HandleSystemMessageTransformAsync(JsonElement sections) { Dictionary>>? callbacks; - await _transformCallbacksLock.WaitAsync(); - try + lock (_callbackLock) { + ThrowIfCallbacksRetired(); callbacks = _transformCallbacks; } - finally - { - _transformCallbacksLock.Release(); - } var parsed = JsonSerializer.Deserialize( sections.GetRawText(), @@ -2224,7 +2464,7 @@ public async Task SetModelAsync(string model, SetModelOptions options, Cancellat if (options.ResetAutoTier) { - var request = new ModelSwitchToRequest + var request = new ModelSwitchToRequestWithSession { SessionId = SessionId, ModelId = model, @@ -2236,7 +2476,7 @@ public async Task SetModelAsync(string model, SetModelOptions options, Cancellat await CopilotClient.InvokeRpcAsync( Rpc, "session.model.switchTo", - [WithExplicitNullAutoTier(request, RpcJsonContext.Default.ModelSwitchToRequest)], + [WithExplicitNullAutoTier(request, RpcJsonContext.Default.ModelSwitchToRequestWithSession)], cancellationToken); return; } @@ -2373,6 +2613,11 @@ public async Task LogAsync(string message, SessionLogLevel? level = null, bool? /// /// After calling this method, the session object can no longer be used. /// + /// + /// Stops subscribed event delivery and releases queued events and subscriptions. + /// Handlers already admitted for dispatch may finish; disposal does not wait for them. + /// Pending response waiters fail if the session closes before their final response. + /// /// /// /// @@ -2392,6 +2637,7 @@ public async ValueTask DisposeAsync() return; } + ClearSkillProvider(); CancelPendingExternalTools(); CloseEventChannel(); @@ -2414,20 +2660,10 @@ public async ValueTask DisposeAsync() } finally { - ReleaseGitHubTokenProviderRegistration(); + Retire(); RemoveFromClient(); GC.SuppressFinalize(this); } - - _eventHandlers = ImmutableInterlocked.InterlockedExchange(ref _eventHandlers, ImmutableArray.Empty); - Volatile.Write(ref _toolHandlers, new Dictionary(StringComparer.Ordinal)); - _commandHandlers.Clear(); - - _permissionHandler = null; - _userInputHandler = null; - _elicitationHandler = null; - _exitPlanModeHandler = null; - _autoModeSwitchHandler = null; } [LoggerMessage(Level = LogLevel.Error, Message = "Unhandled exception in broadcast event handler")] @@ -2445,6 +2681,9 @@ public async ValueTask DisposeAsync() [LoggerMessage(Level = LogLevel.Error, Message = "Permission handler or response delivery failed. SessionId={SessionId}, RequestId={RequestId}")] private partial void LogPermissionHandlerOrDeliveryFailed(Exception exception, string sessionId, string requestId); + [LoggerMessage(Level = LogLevel.Warning, Message = "Skill provider {Operation} failed. SessionId={SessionId}")] + private partial void LogSkillProviderFailed(Exception exception, string operation, string sessionId); + internal record SendMessageRequest { public string SessionId { get; init; } = string.Empty; diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 7ed04c279b..768cc29da3 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -52,6 +52,74 @@ internal static class Diagnostics internal const string Experimental = "GHCP001"; } +/// +/// Describes a skill supplied by a session-scoped . +/// +/// +/// Experimental. The runtime validates descriptor limits, name syntax, and +/// case-insensitive uniqueness when it reads the provider catalog. +/// +[Experimental(Diagnostics.Experimental)] +public sealed class SkillProviderDescriptor +{ + /// Stable skill name. + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// Human-readable description of what the skill teaches the model. + [JsonPropertyName("description")] + public required string Description { get; set; } + + /// + /// Whether users can invoke the skill explicitly. When unset, the runtime default is . + /// + [JsonPropertyName("userInvocable")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? UserInvocable { get; set; } + + /// + /// Whether model-initiated invocation is disabled. When unset, the runtime default is . + /// + [JsonPropertyName("disableModelInvocation")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? DisableModelInvocation { get; set; } + + /// Optional argument hint shown for explicit invocations. + [JsonPropertyName("argumentHint")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ArgumentHint { get; set; } +} + +/// +/// Provides session-scoped skills to the Copilot runtime. +/// +/// +/// Experimental. Implementations can be called concurrently and must be safe for +/// concurrent use. The provider is not persisted; supply it again when resuming a +/// session. Returning from +/// reports that the requested skill was not found. +/// +[Experimental(Diagnostics.Experimental)] +public interface ISkillProvider +{ + /// Lists the skills currently available from this provider. + /// + /// Cancels when the runtime abandons the request (for example, on timeout, session disposal, + /// or provider replacement on resume) or the connection closes. + /// + /// The available skill descriptors. + Task> ListSkillsAsync(CancellationToken cancellationToken); + + /// Reads the SKILL.md markdown for a named skill. + /// The skill name requested by the runtime. + /// + /// Cancels when the runtime abandons the request (for example, on timeout, session disposal, + /// or provider replacement on resume) or the connection closes. + /// + /// The skill markdown, or when the skill is not found. + Task ReadSkillAsync(string name, CancellationToken cancellationToken); +} + /// /// Log level for the Copilot runtime. Use the well-known values exposed as /// static members (, , , @@ -3423,6 +3491,14 @@ public sealed class ManagedSettingsPermissions [JsonPropertyName("disableBypassPermissionsMode")] public string? DisableBypassPermissionsMode { get; set; } + /// + /// When , prevents Assisted Permissions from being + /// activated. Omit or set to to impose no restriction. + /// Serialized as disableAssistedPermissionsMode. + /// + [JsonPropertyName("disableAssistedPermissionsMode")] + public bool? DisableAssistedPermissionsMode { get; set; } + /// Tool-permission patterns that are always denied. [JsonPropertyName("deny")] public IList? Deny { get; set; } @@ -3434,6 +3510,15 @@ public sealed class ManagedSettingsPermissions /// Tool-permission patterns that are allowed without prompting. [JsonPropertyName("allow")] public IList? Allow { get; set; } + + /// + /// Closed-world host boundary expressed as Domain(hostname), + /// Domain(IP), or Domain(*.example.com) rules. Schemes, ports, + /// paths, queries, and fragments are rejected. Multiple managed layers + /// intersect their lists. A present empty list denies all hosts. + /// + [JsonPropertyName("limitTo")] + public IList? LimitTo { get; set; } } /// @@ -3571,7 +3656,9 @@ protected SessionConfigBase(SessionConfigBase? other) : null; ExpAssignments = other.ExpAssignments; EnableManagedSettings = other.EnableManagedSettings; + EnforceManagedModelDefaults = other.EnforceManagedModelDefaults; ManagedSettings = other.ManagedSettings; + SkillProvider = other.SkillProvider; #pragma warning disable GHCP001 Canvases = other.Canvases is not null ? [.. other.Canvases] : null; RequestCanvasRenderer = other.RequestCanvasRenderer; @@ -3714,6 +3801,20 @@ protected SessionConfigBase(SessionConfigBase? other) /// public bool? EnableSkills { get; set; } + /// + /// Session-scoped skill provider. When set, the runtime can list and read + /// skills by calling back into this SDK host. + /// + /// + /// Experimental. The provider is ephemeral and is never persisted; supply it + /// again when resuming a session. In , + /// set to to make provider + /// skills active. Providers can be called concurrently. + /// + [JsonIgnore] + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + public ISkillProvider? SkillProvider { get; set; } + /// /// Built-in skill names to include in the session. In /// , omitting this option excludes all @@ -4099,6 +4200,13 @@ protected SessionConfigBase(SessionConfigBase? other) /// public bool? EnableManagedSettings { get; set; } + /// + /// When true, enforces locked managed model controls for this session. + /// Conflicting model changes are rejected; overridable managed defaults + /// remain mutable. + /// + public bool? EnforceManagedModelDefaults { get; set; } + /// /// Optional managed-settings layer injected at session bootstrap. Currently /// carries a permissions object that composes restrictively with any @@ -4877,6 +4985,7 @@ public sealed class SystemMessageTransformRpcResponse [JsonSerializable(typeof(SectionOverride))] [JsonSerializable(typeof(SessionMetadata))] [JsonSerializable(typeof(SetForegroundSessionResponse))] +[JsonSerializable(typeof(SkillProviderDescriptor))] [JsonSerializable(typeof(SystemMessageConfig))] [JsonSerializable(typeof(ToolBinaryResult))] [JsonSerializable(typeof(ToolBinaryResultType))] diff --git a/dotnet/test/E2E/ScenarioTestingProvidersE2ETests.cs b/dotnet/test/E2E/ScenarioTestingProvidersE2ETests.cs index 13270d3e43..da8f0eccf2 100644 --- a/dotnet/test/E2E/ScenarioTestingProvidersE2ETests.cs +++ b/dotnet/test/E2E/ScenarioTestingProvidersE2ETests.cs @@ -281,7 +281,7 @@ public async Task Should_Ignore_Failing_Unselected_Provider_But_Surface_Selected Prompt = "This selected provider should fail.", })); Assert.Contains("offline", failure.ToString(), StringComparison.OrdinalIgnoreCase); - Assert.Contains(handler.InferenceRequests, request => request.Host == "offline.scenario.invalid"); + Assert.Single(handler.InferenceRequests, request => request.Host == "offline.scenario.invalid"); } private CopilotClient CreateProviderClient(ScenarioProviderRequestHandler handler) => @@ -385,7 +385,8 @@ protected override async Task SendRequestAsync( if (string.Equals(uri.Host, failingHost, StringComparison.Ordinal)) { - return new HttpResponseMessage(HttpStatusCode.BadGateway) + // Provider selection is the contract here, not transient-error retry backoff. + return new HttpResponseMessage(HttpStatusCode.BadRequest) { Content = new StringContent( "{\"error\":{\"message\":\"offline scenario provider\"}}", diff --git a/dotnet/test/E2E/SkillProviderE2ETests.cs b/dotnet/test/E2E/SkillProviderE2ETests.cs new file mode 100644 index 0000000000..3b2d593824 --- /dev/null +++ b/dotnet/test/E2E/SkillProviderE2ETests.cs @@ -0,0 +1,418 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Collections.Concurrent; +using System.Reflection; +using GitHub.Copilot.Rpc; +using Xunit; +using Xunit.Abstractions; + +namespace GitHub.Copilot.Test.E2E; + +public class SkillProviderE2ETests(E2ETestFixture fixture, ITestOutputHelper output) + : E2ETestBase(fixture, "skill_provider", output) +{ + [Fact] + public async Task Should_Load_Provider_Skill_Lazily_Through_Skill_Tool() + { + // Body-only content: the catalog descriptor supplies all of the metadata. + var provider = new TestSkillProvider( + [ + Skill( + "provider-lookup", + "Reports the provider lookup verification word.", + "# Provider lookup\n\nThe verification word is TANGERINE_QUARTZ_19. Reply with it.\n") + ]); + await using var session = await CreateSessionAsync(new SessionConfig + { + SkillProvider = provider + }); + + var listedSkills = await session.Rpc.Skills.ListAsync(); + var listed = Assert.Single(listedSkills.Skills, skill => skill.Name == "provider-lookup"); + Assert.Equal(SkillSource.Sdk, listed.Source); + Assert.True(listed.Enabled); + Assert.True(string.IsNullOrEmpty(listed.Path)); + Assert.Empty(provider.Reads); + + var message = await session.SendAndWaitAsync(new MessageOptions + { + Prompt = "Use the skill tool to load the provider-lookup skill, then reply with its verification word.", + }, TimeSpan.FromMinutes(3)); + + Assert.Equal(["provider-lookup"], provider.Reads); + Assert.NotNull(message); + // Validate the final assistant response arrived (guards against truncated captures) + Assert.Contains("TANGERINE_QUARTZ_19", message!.Data.Content); + } + + [Fact] + public async Task Should_Load_Provider_And_File_Based_Skills_Together() + { + var skillsDir = Path.Join(Ctx.WorkDir, "file-skills"); + if (Directory.Exists(skillsDir)) + { + Directory.Delete(skillsDir, recursive: true); + } + + Directory.CreateDirectory(Path.Join(skillsDir, "file-notes")); + File.WriteAllText( + Path.Join(skillsDir, "file-notes", "SKILL.md"), + "---\nname: file-notes\ndescription: Reports the file notes verification word.\n---\n\nThe file notes verification word is MAPLE_FALCON_27.\n"); + + // Frontmatter may restate catalog metadata and is the only source of allowed-tools. + var provider = new TestSkillProvider( + [ + Skill( + "provider-audit", + "Reports the provider audit verification word.", + "---\nname: provider-audit\nallowed-tools: view\n---\n\nThe provider audit verification word is COBALT_HERON_58.\n") + ]); + await using var session = await CreateSessionAsync(new SessionConfig + { + SkillDirectories = [skillsDir], + SkillProvider = provider + }); + + var listedSkills = await session.Rpc.Skills.ListAsync(); + var fileSkill = Assert.Single(listedSkills.Skills, skill => skill.Name == "file-notes"); + var providerSkill = Assert.Single(listedSkills.Skills, skill => skill.Name == "provider-audit"); + Assert.NotEqual(SkillSource.Sdk, fileSkill.Source); + Assert.False(string.IsNullOrWhiteSpace(fileSkill.Path)); + Assert.Equal(SkillSource.Sdk, providerSkill.Source); + + var message = await session.SendAndWaitAsync(new MessageOptions + { + Prompt = "Use the skill tool to load the file-notes skill and the provider-audit skill, then reply with both verification words.", + }, TimeSpan.FromMinutes(3)); + + Assert.Equal(["provider-audit"], provider.Reads); + Assert.NotNull(message); + Assert.Contains("MAPLE_FALCON_27", message!.Data.Content); + // Validate the final assistant response arrived (guards against truncated captures) + Assert.Contains("COBALT_HERON_58", message.Data.Content); + } + + [Fact] + public async Task Should_Rebind_Skill_Provider_On_Resume() + { + var original = new TestSkillProvider( + [ + Skill( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is AMBER_ALPHA_11.\n") + ]); + var replacement = new TestSkillProvider( + [ + Skill( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is BRONZE_BETA_22.\n") + ]); + var first = await CreateSessionAsync(new SessionConfig + { + SkillProvider = original + }); + var sessionId = first.SessionId; + var ready = await first.SendAndWaitAsync(new MessageOptions + { + Prompt = "Without using any tools or skills, reply with exactly REBIND_READY.", + }, TimeSpan.FromMinutes(3)); + Assert.NotNull(ready); + Assert.Contains("REBIND_READY", ready!.Data.Content); + + await first.DisposeAsync(); + Assert.Empty(original.Reads); + var originalCallsBeforeResume = original.Calls.Count; + + await using var session = await ResumeSessionAsync(sessionId, new ResumeSessionConfig + { + SkillProvider = replacement + }); + + var message = await session.SendAndWaitAsync(new MessageOptions + { + Prompt = "Use the skill tool to load the rebind-check skill, then reply with its verification word.", + }, TimeSpan.FromMinutes(3)); + + Assert.Equal(["rebind-check"], replacement.Reads); + Assert.Equal(originalCallsBeforeResume, original.Calls.Count); + Assert.NotNull(message); + // Validate the final assistant response arrived (guards against truncated captures) + Assert.Contains("BRONZE_BETA_22", message!.Data.Content); + Assert.DoesNotContain("AMBER_ALPHA_11", message.Data.Content); + } + + [Fact] + public async Task Should_Report_Provider_Read_Failure_Without_Leaking_Details() + { + const string secret = "PROVIDER_SECRET_7F3A9C"; + var provider = new TestSkillProvider( + [ + new ProvidedSkill( + new SkillProviderDescriptor + { + Name = "broken-lookup", + Description = "Reports the broken lookup verification word.", + }, + () => throw new InvalidOperationException($"database unavailable: {secret}")) + ]); + var events = new ConcurrentQueue(); + await using var session = await CreateSessionAsync(new SessionConfig + { + SkillProvider = provider, + OnEvent = events.Enqueue + }); + + var message = await session.SendAndWaitAsync(new MessageOptions + { + Prompt = "Use the skill tool to load the broken-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + }, TimeSpan.FromMinutes(3)); + + Assert.Contains("broken-lookup", provider.Reads); + var failures = events + .OfType() + .Where(evt => !evt.Data.Success) + .ToArray(); + var failure = Assert.Single(failures); + Assert.DoesNotContain(secret, string.Join("\n", events.Select(evt => evt.ToJson())), StringComparison.Ordinal); + Assert.NotNull(failure.Data.Error); + Assert.NotNull(message); + // Validate the final assistant response arrived (guards against truncated captures) + Assert.Contains("LOAD_FAILED", message!.Data.Content); + } + + [Fact] + public async Task Should_Report_Missing_Provider_Skill_As_Not_Found() + { + var provider = new TestSkillProvider( + [ + new ProvidedSkill( + new SkillProviderDescriptor + { + Name = "vanished-lookup", + Description = "Reports the vanished lookup verification word.", + }, + () => null) + ]); + var events = new ConcurrentQueue(); + await using var session = await CreateSessionAsync(new SessionConfig + { + SkillProvider = provider, + OnEvent = events.Enqueue + }); + + var message = await session.SendAndWaitAsync(new MessageOptions + { + Prompt = "Use the skill tool to load the vanished-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + }, TimeSpan.FromMinutes(3)); + + Assert.Contains("vanished-lookup", provider.Reads); + var failures = events + .OfType() + .Where(evt => !evt.Data.Success) + .ToArray(); + var failure = Assert.Single(failures); + Assert.Contains("not found", failure.ToJson(), StringComparison.OrdinalIgnoreCase); + Assert.NotNull(message); + // Validate the final assistant response arrived (guards against truncated captures) + Assert.Contains("LOAD_FAILED", message!.Data.Content); + } + + [Fact] + public async Task Should_Keep_Provider_Dormant_When_Skills_Disabled() + { + var provider = new TestSkillProvider( + [ + Skill("dormant-lookup", "Never listed.", "Never read.\n") + ]); + await using var session = await CreateSessionAsync(new SessionConfig + { + EnableSkills = false, + SkillProvider = provider + }); + + await session.Rpc.Skills.EnsureLoadedAsync(); + var listedSkills = await session.Rpc.Skills.ListAsync(); + + Assert.DoesNotContain(listedSkills.Skills, skill => skill.Source == SkillSource.Sdk); + Assert.Empty(provider.Calls); + } + + [Fact] + public async Task Should_Unbind_Provider_When_Resumed_Without_One() + { + var provider = new TestSkillProvider( + [ + Skill("unbound-lookup", "Reports the unbound lookup word.", "Unbound.\n") + ]); + var first = await CreateSessionAsync(new SessionConfig + { + SkillProvider = provider + }); + var before = await first.Rpc.Skills.ListAsync(); + Assert.Contains(before.Skills, skill => skill.Name == "unbound-lookup"); + var callsBeforeResume = provider.Calls.Count; + + UntrackSessionWithoutDetach(first); + + CopilotSession? session = null; + try + { + session = await Ctx.ResumeSessionAsync(Client, first.SessionId, new ResumeSessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + + await session.Rpc.Skills.ReloadAsync(); + var listedSkills = await session.Rpc.Skills.ListAsync(); + + Assert.DoesNotContain(listedSkills.Skills, skill => skill.Source == SkillSource.Sdk); + Assert.Equal(callsBeforeResume, provider.Calls.Count); + } + finally + { + if (session is not null) + { + await session.DisposeAsync(); + } + + await first.DisposeAsync(); + } + } + + [Fact] + public async Task Should_Cancel_A_Blocked_Provider_Call_When_The_Session_Is_Disposed() + { + var provider = new BlockingSkillProvider(); + var session = await CreateSessionAsync(new SessionConfig + { + SkillProvider = provider + }); + + // The list RPC may fail or omit provider skills once the binding is removed; + // only the provider's cancellation matters here. + var list = session.Rpc.Skills.ListAsync(); + await provider.Entered.WaitAsync(TimeSpan.FromSeconds(30)); + + await session.DisposeAsync(); + + await provider.Cancelled.WaitAsync(TimeSpan.FromSeconds(10)); + await Record.ExceptionAsync(() => list); + } + + [Fact] + public async Task Should_Reject_Skill_Provider_For_Cloud_Sessions() + { + var provider = new TestSkillProvider( + [ + Skill("cloud-lookup", "Never listed.", "Never read.\n") + ]); + + var exception = await Assert.ThrowsAsync(() => + Client.CreateSessionAsync(new SessionConfig + { + Cloud = new CloudSessionOptions(), + SkillProvider = provider, + })); + + Assert.Equal("Skill providers are not supported for cloud sessions.", exception.Message); + Assert.Empty(provider.Calls); + } + + private static ProvidedSkill Skill(string name, string description, string markdown) => + new( + new SkillProviderDescriptor + { + Name = name, + Description = description, + }, + () => markdown); + + private static void UntrackSessionWithoutDetach(CopilotSession session) + { + // Match a warm runtime resume: remove the SDK wrapper so resume is allowed, but do not detach the runtime session. + var removeFromClient = typeof(CopilotSession).GetMethod( + "RemoveFromClient", + BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("CopilotSession.RemoveFromClient was not found."); + removeFromClient.Invoke(session, null); + } + + private sealed record ProvidedSkill(SkillProviderDescriptor Descriptor, Func Read); + + private sealed class BlockingSkillProvider : ISkillProvider + { + private readonly TaskCompletionSource _entered = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly TaskCompletionSource _cancelled = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public Task Entered => _entered.Task; + + public Task Cancelled => _cancelled.Task; + + public async Task> ListSkillsAsync(CancellationToken cancellationToken) + { + _entered.TrySetResult(); + try + { + await Task.Delay(Timeout.Infinite, cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Not a token callback: if cancellation resumes this method inline, disposing + // a registration whose callback has not run yet drops it. + _cancelled.TrySetResult(); + throw; + } + + return []; + } + + public Task ReadSkillAsync(string name, CancellationToken cancellationToken) => + Task.FromResult(null); + } + + private sealed class TestSkillProvider(IReadOnlyList skills) : ISkillProvider + { + private readonly object _lock = new(); + private readonly List _calls = []; + + public IReadOnlyList Calls + { + get + { + lock (_lock) + { + return [.. _calls]; + } + } + } + + public IReadOnlyList Reads => Calls + .Where(call => call.StartsWith("read:", StringComparison.Ordinal)) + .Select(call => call["read:".Length..]) + .ToArray(); + + public Task> ListSkillsAsync(CancellationToken cancellationToken) + { + AddCall("list"); + return Task.FromResult>(skills.Select(skill => skill.Descriptor).ToArray()); + } + + public Task ReadSkillAsync(string name, CancellationToken cancellationToken) + { + AddCall($"read:{name}"); + return Task.FromResult(skills.FirstOrDefault(skill => skill.Descriptor.Name == name)?.Read()); + } + + private void AddCall(string call) + { + lock (_lock) + { + _calls.Add(call); + } + } + } +} diff --git a/dotnet/test/E2E/StructuredOutputE2ETests.cs b/dotnet/test/E2E/StructuredOutputE2ETests.cs index b18e93b492..e82e31d008 100644 --- a/dotnet/test/E2E/StructuredOutputE2ETests.cs +++ b/dotnet/test/E2E/StructuredOutputE2ETests.cs @@ -392,6 +392,15 @@ public async Task Rejects_Unsupported_Or_Oversized_Schemas_Before_Admission() { var config = StructuredSessionConfig(); config.Model = model; + config.EnableExperimentalMode = model == "hydrafusion"; + if (model == "hydrafusion") + { + config.FeatureFlags = new Dictionary + { + ["HYDRAFUSION"] = true, + ["HYDRAFUSION_ROLLOUT"] = true, + }; + } config.OnPermissionRequest = PermissionHandler.ApproveAll; await using var session = await Ctx.CreateSessionAsync(client, config); await Assert.ThrowsAsync(() => session.SendAndWaitAsync( diff --git a/dotnet/test/Unit/AutoTierIdentityTests.cs b/dotnet/test/Unit/AutoTierIdentityTests.cs new file mode 100644 index 0000000000..9ca10e346d --- /dev/null +++ b/dotnet/test/Unit/AutoTierIdentityTests.cs @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using GitHub.Copilot.Rpc; +using Xunit; + +#pragma warning disable GHCP001 // Exercise the public model RPC projection. + +namespace GitHub.Copilot.Test.Unit; + +public class AutoTierIdentityTests +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver(), + }; + + [Fact] + public void CaseDistinctTierIdsRemainDistinctAcrossCollectionsAndJson() + { + var custom = new AutoTier("BALANCE"); + Assert.NotEqual(AutoTier.Balance, custom); + Assert.Equal(2, new HashSet { AutoTier.Balance, custom }.Count); + Assert.Equal("\"BALANCE\"", JsonSerializer.Serialize(custom, JsonOptions)); + Assert.Equal(custom, JsonSerializer.Deserialize("\"BALANCE\"", JsonOptions)); + Assert.Equal("balance", AutoTier.Balance.Value); + + var rpc = JsonSerializer.Deserialize("""{"modelId":"auto","autoTier":"BALANCE"}""", JsonOptions); + Assert.NotNull(rpc); + Assert.Equal(custom, rpc.AutoTier); + Assert.NotEqual(AutoTier.Balance, rpc.AutoTier); + } + + [Fact] + public void CaseDistinctRecommendationIdsRemainDistinct() + { + var custom = new RecommendedAutoTier("BALANCE"); + Assert.NotEqual(RecommendedAutoTier.Balance, custom); + Assert.Equal(2, new HashSet { RecommendedAutoTier.Balance, custom }.Count); + Assert.Equal("\"BALANCE\"", JsonSerializer.Serialize(custom, JsonOptions)); + Assert.Equal(custom, JsonSerializer.Deserialize("\"BALANCE\"", JsonOptions)); + } +} diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index d78ee57932..17e5efc4de 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; using System.Text; using System.Text.Json; using GitHub.Copilot.Rpc; @@ -1791,6 +1792,613 @@ async Task BlockedTool(CancellationToken cancellationToken) } } + [Fact] + public Task ForceStopAsync_Does_Not_Invoke_Remaining_Event_Handlers() => + AssertRemainingEventHandlersStoppedAsync(static (client, _, _) => client.ForceStopAsync()); + + [Fact] + public Task DisposeAsync_Does_Not_Invoke_Remaining_Event_Handlers() => + AssertRemainingEventHandlersStoppedAsync(static (_, session, _) => session.DisposeAsync().AsTask()); + + [Fact] + public Task Unsubscribe_Releases_Unadmitted_Event_Handler_Captures() => + AssertRemainingEventHandlersStoppedAsync(static (_, _, unsubscribe) => + { + unsubscribe(); + return Task.CompletedTask; + }); + + [Fact] + public Task ForceStopAsync_Fails_Pending_SendAndWait_After_Send_Response() => + AssertPendingSendAndWaitStoppedAsync(static (client, _) => client.ForceStopAsync()); + + [Fact] + public Task DisposeAsync_Fails_Pending_SendAndWait_After_Send_Response() => + AssertPendingSendAndWaitStoppedAsync(static (_, session) => session.DisposeAsync().AsTask()); + + [Fact] + public async Task ForceStopAsync_Rejects_Event_Handler_Registration() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + + await client.ForceStopAsync(); + + Assert.Throws(() => session.On(_ => { })); + } + + [Theory] + [InlineData(false, true)] + [InlineData(true, true)] + [InlineData(false, false)] + [InlineData(true, false)] + public async Task Stop_Rejects_And_Retires_Session_Initialized_Before_Registration(bool resume, bool force) + { + await using var server = await FakeCopilotServer.StartAsync(); + server.ResponseFactory = request => request.Method == "sessionFs.setProvider" + ? new Dictionary { ["success"] = true } + : throw new InvalidOperationException($"Unexpected RPC method '{request.Method}'."); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + SessionFs = new SessionFsConfig + { + InitialWorkingDirectory = "/", + SessionStatePath = "/session-state", + Conventions = SessionFsSetProviderConventions.Posix + } + }); + await client.StartAsync(); + using var releaseSetup = new ManualResetEventSlim(); + var setupStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + SessionFsProvider CreateProvider(CopilotSession session) + { + setupStarted.SetResult(session); + releaseSetup.Wait(); + return new TextOnlySessionFsProvider(); + } + var creation = Task.Run(() => resume + ? client.ResumeSessionAsync("late-registration", new ResumeSessionConfig { CreateSessionFsProvider = CreateProvider }) + : client.CreateSessionAsync(new SessionConfig { CreateSessionFsProvider = CreateProvider })); + try + { + var session = await setupStarted.Task.WaitAsync(TimeSpan.FromSeconds(5)); + if (force) + { + await client.ForceStopAsync().WaitAsync(TimeSpan.FromSeconds(5)); + } + else + { + await client.StopAsync().WaitAsync(TimeSpan.FromSeconds(5)); + } + releaseSetup.Set(); + var error = await Assert.ThrowsAsync(() => creation.WaitAsync(TimeSpan.FromSeconds(5))); + Assert.Contains("stopped connection", error.Message); + Assert.Throws(() => session.On(_ => { })); + Assert.DoesNotContain(server.Requests, request => request.Method is "session.create" or "session.resume"); + } + finally + { + releaseSetup.Set(); + await Record.ExceptionAsync(() => creation.WaitAsync(TimeSpan.FromSeconds(5))); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Stop_Closes_Session_Admission_Before_Transport_Cleanup(bool resume) + { + await using var server = await FakeCopilotServer.StartAsync(); + server.ResponseFactory = request => request.Method == "sessionFs.setProvider" + ? new Dictionary { ["success"] = true } + : throw new InvalidOperationException($"Unexpected RPC method '{request.Method}'."); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + SessionFs = new SessionFsConfig + { + InitialWorkingDirectory = "/", + SessionStatePath = "/session-state", + Conventions = SessionFsSetProviderConventions.Posix + } + }); + _ = await client.CreateSessionAsync(new SessionConfig { CreateSessionFsProvider = _ => new TextOnlySessionFsProvider() }); + server.DelayDestroy(); + var stop = client.StopAsync(); + await server.DestroyStarted.WaitAsync(TimeSpan.FromSeconds(5)); + try + { + CopilotSession? initialized = null; + SessionFsProvider CreateProvider(CopilotSession session) + { + initialized = session; + return new TextOnlySessionFsProvider(); + } + var creation = resume + ? client.ResumeSessionAsync("late-registration", new ResumeSessionConfig { CreateSessionFsProvider = CreateProvider }) + : client.CreateSessionAsync(new SessionConfig { CreateSessionFsProvider = CreateProvider }); + var error = await Assert.ThrowsAsync(() => creation.WaitAsync(TimeSpan.FromSeconds(5))); + Assert.Contains("stopped connection", error.Message); + Assert.NotNull(initialized); + Assert.Throws(() => initialized.On(_ => { })); + Assert.False(stop.IsCompleted); + } + finally + { + await client.ForceStopAsync().WaitAsync(TimeSpan.FromSeconds(5)); + await stop.WaitAsync(TimeSpan.FromSeconds(5)); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Cloud_Create_Cleanup_Does_Not_Reconnect_During_Stop(bool dispose) + { + await using var server = await FakeCopilotServer.StartAsync(); + server.ResponseFactory = request => request.Method == "sessionFs.setProvider" + ? new Dictionary { ["success"] = true } + : throw new InvalidOperationException($"Unexpected RPC method '{request.Method}'."); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + SessionFs = new SessionFsConfig + { + InitialWorkingDirectory = "/", + SessionStatePath = "/session-state", + Conventions = SessionFsSetProviderConventions.Posix + } + }); + await client.StartAsync(); + using var process = StartExitedProcess(); + await ReplaceConnectionResourcesAsync(client, process); + using var releaseSetup = new ManualResetEventSlim(); + using var reconnectCancellation = new CancellationTokenSource(); + var reconnect = server.ObserveReconnectAsync(reconnectCancellation.Token); + var setupStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var shutdownStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releaseShutdown = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + server.BeforeResponseAsync = async (request, token) => + { + if (request.Method == "runtime.shutdown") + { + shutdownStarted.TrySetResult(); + await releaseShutdown.Task.WaitAsync(token); + } + }; + var creation = client.CreateSessionAsync(new SessionConfig + { + Cloud = new CloudSessionOptions + { + Repository = new CloudSessionRepository { Owner = "github", Name = "copilot-sdk", Branch = "main" } + }, + CreateSessionFsProvider = session => + { + setupStarted.TrySetResult(session); + releaseSetup.Wait(); + return new TextOnlySessionFsProvider(); + } + }); + Task? stop = null; + Exception? failure = null; + var timeout = TimeSpan.FromSeconds(5); + try + { + var session = await setupStarted.Task.WaitAsync(timeout); + stop = dispose ? client.DisposeAsync().AsTask() : client.StopAsync(); + await shutdownStarted.Task.WaitAsync(timeout); + releaseSetup.Set(); + releaseShutdown.TrySetResult(); + + var error = await Assert.ThrowsAsync(() => creation.WaitAsync(timeout)); + Assert.Contains("stopped connection", error.Message); + await stop.WaitAsync(timeout); + Assert.Throws(() => session.On(_ => { })); + Assert.False(server.ReconnectObserved, "Cloud-create cleanup reconnected after session admission closed."); + } + catch (Exception error) + { + failure = error; + } + finally + { + releaseSetup.Set(); + releaseShutdown.TrySetResult(); + foreach (var operation in new Task?[] { creation, stop }) + { + if (operation is null) continue; + try + { + await operation.WaitAsync(timeout); + } + catch (IOException) when (ReferenceEquals(operation, creation)) + { + // The rejected creation is the expected result, not a cleanup failure. + } + catch (Exception cleanupError) + { + failure = failure is null ? cleanupError : new AggregateException(failure, cleanupError); + } + } + reconnectCancellation.Cancel(); + try + { + await reconnect.WaitAsync(timeout); + } + catch (OperationCanceledException) when (reconnectCancellation.IsCancellationRequested) + { + } + catch (Exception cleanupError) + { + failure = failure is null ? cleanupError : new AggregateException(failure, cleanupError); + } + } + if (failure is not null) + { + ExceptionDispatchInfo.Capture(failure).Throw(); + } + } + + [Theory] + [InlineData("hooks", true)] + [InlineData("mcp", true)] + [InlineData("bearer", true)] + [InlineData("transforms", true)] + [InlineData("tools", true)] + [InlineData("commands", true)] + [InlineData("permission", true)] + [InlineData("input", true)] + [InlineData("hooks", false)] + [InlineData("mcp", false)] + [InlineData("bearer", false)] + [InlineData("transforms", false)] + [InlineData("tools", false)] + [InlineData("commands", false)] + [InlineData("permission", false)] + [InlineData("input", false)] + public async Task Retirement_Releases_Retained_Session_Callback_Captures(string registration, bool forceStop) + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var (session, capture) = await CreateCapturedSessionAsync(client, registration); + await using var retainedSession = session; + ForceCollect(); + Assert.True(CaptureIsAlive(capture)); + + if (forceStop) + { + await client.ForceStopAsync(); + } + else + { + await session.DisposeAsync(); + } + ForceCollect(); + + Assert.False(CaptureIsAlive(capture)); + GC.KeepAlive(session); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool CaptureIsAlive(WeakReference capture) => capture.TryGetTarget(out _); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task<(CopilotSession Session, WeakReference Capture)> CreateCapturedSessionAsync( + CopilotClient client, string registration) + { + var capture = new object(); + var weak = new WeakReference(capture); + var config = new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll }; + switch (registration) + { + case "hooks": + config.Hooks = new SessionHooks + { + OnSessionStart = (_, _) => + { + GC.KeepAlive(capture); + return Task.FromResult(null); + } + }; + break; + case "mcp": + config.OnMcpAuthRequest = _ => + { + GC.KeepAlive(capture); + return Task.FromResult(McpAuthResult.Cancel()); + }; + break; + case "bearer": + config.Model = "test-model"; + config.Provider = new ProviderConfig + { + BaseUrl = "http://127.0.0.1", + BearerTokenProvider = _ => + { + GC.KeepAlive(capture); + return Task.FromResult("test-token"); + } + }; + break; + case "transforms": + config.SystemMessage = new SystemMessageConfig + { + Mode = SystemMessageMode.Customize, + Sections = new Dictionary + { + [SystemMessageSection.Tone] = new() + { + Transform = content => + { + GC.KeepAlive(capture); + return Task.FromResult(content); + } + } + } + }; + break; + case "tools": + config.Tools = [AIFunctionFactory.Create(() => + { + GC.KeepAlive(capture); + return "test-result"; + }, "captured_tool")]; + break; + case "commands": + config.Commands = [new CommandDefinition + { + Name = "captured", + Handler = _ => + { + GC.KeepAlive(capture); + return Task.CompletedTask; + } + }]; + break; + case "permission": + config.OnPermissionRequest = (_, _) => + { + GC.KeepAlive(capture); + return Task.FromResult(PermissionDecision.UserNotAvailable()); + }; + break; + case "input": + config.OnUserInputRequest = (_, _) => + { + GC.KeepAlive(capture); + return Task.FromResult(new UserInputResponse { Answer = "test-answer" }); + }; + break; + default: + throw new ArgumentOutOfRangeException(nameof(registration), registration, "Unknown callback registration."); + } + return (await client.CreateSessionAsync(config), weak); + } + + [Fact] + public async Task DisposeAsync_Keeps_Hooks_Through_Detach_Acknowledgement() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var detachStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releaseDetach = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hookStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hooks = new SessionHooks + { + OnSessionEnd = (_, _) => + { + hookStarted.TrySetResult(); + return Task.FromResult(null); + } + }; + var endHook = hooks.OnSessionEnd; + await using var session = await client.CreateSessionAsync(new SessionConfig { Hooks = hooks }); + server.BeforeResponseAsync = async (request, token) => + { + if (request.Method == "session.detach") + { + detachStarted.TrySetResult(); + await releaseDetach.Task.WaitAsync(token); + } + }; + var timeout = TimeSpan.FromSeconds(5); + var disposal = session.DisposeAsync().AsTask(); + Task? hookRequest = null; + Exception? bodyFailure = null; + try + { + await detachStarted.Task.WaitAsync(timeout); + hookRequest = server.SendRequestAsync("hooks.invoke", new Dictionary + { + ["sessionId"] = session.SessionId, + ["hookType"] = "sessionEnd", + ["input"] = new Dictionary + { + ["sessionId"] = session.SessionId, + ["timestamp"] = 1_730_000_000_000L, + ["cwd"] = "workdir", + ["reason"] = "complete" + } + }); + await hookStarted.Task.WaitAsync(timeout); + } + catch (Exception error) + { + bodyFailure = error; + } + finally + { + releaseDetach.TrySetResult(); + foreach (var operation in new Task?[] { disposal, hookRequest }) + { + if (operation is null) continue; + try + { + await operation.WaitAsync(timeout); + } + catch (Exception cleanupError) + { + bodyFailure = bodyFailure is null ? cleanupError : new AggregateException(bodyFailure, cleanupError); + } + } + } + if (bodyFailure is not null) + { + ExceptionDispatchInfo.Capture(bodyFailure).Throw(); + } + Assert.Same(endHook, hooks.OnSessionEnd); + } + + [Fact] + public async Task DisposeAsync_Releases_Event_Handler_Captures() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + var capture = RegisterCapturedEventHandler(session, out _); + + await session.DisposeAsync(); + ForceCollect(); + + Assert.False(capture.TryGetTarget(out _)); + GC.KeepAlive(session); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static WeakReference RegisterCapturedEventHandler(CopilotSession session, out IDisposable subscription) + { + var capture = new object(); + subscription = session.On(_ => GC.KeepAlive(capture)); + return new WeakReference(capture); + } + + private static async Task AssertPendingSendAndWaitStoppedAsync(Func stop) + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + using var cancellation = new CancellationTokenSource(); + var timeout = TimeSpan.FromSeconds(5); + var responseDrained = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + server.AfterResponseAsync = async (request, token) => + { + if (request.Method == "session.send") + { + await server.SendAndDrainSessionEventAsync(session, "assistant.message", new() + { + ["messageId"] = "message-1", + ["content"] = "WAITING_FOR_IDLE" + }, timeout, token); + responseDrained.TrySetResult(); + } + }; + var pending = session.SendAndWaitAsync(new MessageOptions { Prompt = "wait until idle" }, cancellationToken: cancellation.Token); + Exception? bodyFailure = null; + try + { + await responseDrained.Task.WaitAsync(timeout); + await stop(client, session).WaitAsync(timeout); + await Assert.ThrowsAsync(() => pending.WaitAsync(timeout)); + } + catch (Exception error) + { + bodyFailure = error; + } + finally + { + try + { + cancellation.Cancel(); + await pending.WaitAsync(timeout); + } + catch (Exception error) when (error is IOException or OperationCanceledException) + { + // The stopped operation may fault, or the owned cleanup cancellation may end it. + } + catch (Exception cleanupError) + { + bodyFailure = bodyFailure is null ? cleanupError : new AggregateException(bodyFailure, cleanupError); + } + } + if (bodyFailure is not null) + { + ExceptionDispatchInfo.Capture(bodyFailure).Throw(); + } + } + + private static async Task AssertRemainingEventHandlersStoppedAsync(Func stop) + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + // Channel closure may queue a final continuation after disposal; an idle pair owns no thread. + var scheduler = new ConcurrentExclusiveSchedulerPair(TaskScheduler.Default, maxConcurrencyLevel: 1); + var factory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, scheduler.ExclusiveScheduler); + using var releaseHandler = new ManualResetEventSlim(); + var handlerStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var laterHandlerCalls = 0; + CopilotSession? session = null; + Exception? bodyFailure = null; + try + { + // Resume starts the event consumer on the caller's scheduler. A queued task fences + // that consumer's current dispatch without relying on a delay or private SDK state. + session = await factory.StartNew(() => client.ResumeSessionAsync("force-stop-event-handler", new ResumeSessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + })).Unwrap(); + using var first = session.On(_ => + { + handlerStarted.TrySetResult(TaskScheduler.Current); + releaseHandler.Wait(); + }); + using var second = session.On(_ => Interlocked.Increment(ref laterHandlerCalls)); + var capture = RegisterCapturedEventHandler(session, out var capturedSubscription); + using var captureSubscription = capturedSubscription; + await server.SendSessionEventAsync(session.SessionId, "assistant.message", new() + { + ["messageId"] = "force-stop-dispatch", + ["content"] = "STOP_EVENT_DISPATCH" + }); + Assert.Same(scheduler.ExclusiveScheduler, await handlerStarted.Task.WaitAsync(TimeSpan.FromSeconds(5))); + await stop(client, session, () => + { + second.Dispose(); + captureSubscription.Dispose(); + }).WaitAsync(TimeSpan.FromSeconds(5)); + ForceCollect(); + Assert.False(capture.TryGetTarget(out _)); + releaseHandler.Set(); + await factory.StartNew(() => { }).WaitAsync(TimeSpan.FromSeconds(5)); + Assert.Equal(0, Volatile.Read(ref laterHandlerCalls)); + } + catch (Exception error) + { + bodyFailure = error; + } + finally + { + releaseHandler.Set(); + try + { + await factory.StartNew(() => { }).WaitAsync(TimeSpan.FromSeconds(5)); + if (session is not null) + { + await session.DisposeAsync(); + } + } + catch (Exception cleanupError) + { + bodyFailure = bodyFailure is null ? cleanupError : new AggregateException(bodyFailure, cleanupError); + } + } + if (bodyFailure is not null) + { + ExceptionDispatchInfo.Capture(bodyFailure).Throw(); + } + } + [Fact] public async Task ForceStopAsync_Does_Not_Start_Late_External_Tool() { @@ -2503,7 +3111,8 @@ public async Task SendAndWaitAsync_Waits_For_Earlier_Idle_Handler() using var subscription = session.On(_ => { entered.TrySetResult(); - handlerFinished = release.Wait(TimeSpan.FromSeconds(5)); + release.Wait(); + handlerFinished = true; }); var pending = session.SendAndWaitAsync(new MessageOptions { Prompt = "hello" }); await WaitForRequestAsync(server, "session.send"); @@ -2606,14 +3215,17 @@ public async Task CreateSessionAsync_Serializes_ManagedSettings_Permissions() await using var session = await client.CreateSessionAsync(new SessionConfig { + EnforceManagedModelDefaults = true, ManagedSettings = new ManagedSettings { Permissions = new ManagedSettingsPermissions { DisableBypassPermissionsMode = DisableBypassPermissionsModes.Disable, + DisableAssistedPermissionsMode = true, Deny = ["shell(rm*)"], Ask = ["write"], - Allow = [] + Allow = [], + LimitTo = ["Domain(github.com)"] } }, OnPermissionRequest = (_, invocation) => @@ -2624,12 +3236,17 @@ public async Task CreateSessionAsync_Serializes_ManagedSettings_Permissions() }); var request = Assert.Single(server.Requests, request => request.Method == "session.create"); + Assert.True(request.Params.GetProperty("enforceManagedModelDefaults").GetBoolean()); Assert.False(request.Params.TryGetProperty("enableManagedSettings", out _)); var permissions = request.Params.GetProperty("managedSettings").GetProperty("permissions"); Assert.Equal("disable", permissions.GetProperty("disableBypassPermissionsMode").GetString()); + Assert.True(permissions.GetProperty("disableAssistedPermissionsMode").GetBoolean()); Assert.Equal("shell(rm*)", Assert.Single(permissions.GetProperty("deny").EnumerateArray()).GetString()); Assert.Equal("write", Assert.Single(permissions.GetProperty("ask").EnumerateArray()).GetString()); Assert.Empty(permissions.GetProperty("allow").EnumerateArray()); + Assert.Equal( + "Domain(github.com)", + Assert.Single(permissions.GetProperty("limitTo").EnumerateArray()).GetString()); DispatchEvent(session, new PermissionRequestedEvent { @@ -2895,6 +3512,7 @@ public async Task ResumeSessionAsync_Serializes_ManagedSettings_Permissions() await using var session = await client.ResumeSessionAsync("session-managed", new ResumeSessionConfig { + EnforceManagedModelDefaults = true, ManagedSettings = new ManagedSettings { Permissions = new ManagedSettingsPermissions @@ -2907,6 +3525,7 @@ public async Task ResumeSessionAsync_Serializes_ManagedSettings_Permissions() }); var request = Assert.Single(server.Requests, request => request.Method == "session.resume"); + Assert.True(request.Params.GetProperty("enforceManagedModelDefaults").GetBoolean()); var permissions = request.Params.GetProperty("managedSettings").GetProperty("permissions"); Assert.Equal("shell(rm*)", Assert.Single(permissions.GetProperty("deny").EnumerateArray()).GetString()); } @@ -2999,7 +3618,7 @@ private static Process StartExitedProcess() return process; } - private sealed class FakeCopilotServer : IAsyncDisposable + private sealed partial class FakeCopilotServer : IAsyncDisposable { private readonly TcpListener _listener; private readonly CancellationTokenSource _cts = new(); @@ -3018,6 +3637,7 @@ private sealed class FakeCopilotServer : IAsyncDisposable private bool _failRuntimeShutdown; private bool _failSessionCreate; private bool _failSessionSend; + private bool _reconnectObserved; private int _nextMessageId; public bool IncludeTranscriptRecovery { get; set; } @@ -3025,6 +3645,7 @@ private sealed class FakeCopilotServer : IAsyncDisposable public bool UniqueMessageIds { get; set; } public bool FailSessionDelete { get; set; } public bool FailMcpAuthInterestRegistration { get; set; } + public bool ReconnectObserved => Volatile.Read(ref _reconnectObserved); public Func? BeforeSendResponse { get; set; } @@ -3079,6 +3700,12 @@ public void ClearRequests() } } + public async Task ObserveReconnectAsync(CancellationToken cancellationToken) + { + using var connection = await _listener.AcceptTcpClientAsync(cancellationToken); + Volatile.Write(ref _reconnectObserved, true); + } + public void DelayDestroy() { _delayDestroy = true; @@ -3233,8 +3860,9 @@ private async Task RunAsync() { if (root.TryGetProperty("error", out var error)) { - completion.TrySetException(new InvalidOperationException( - error.GetProperty("message").GetString())); + var exception = new InvalidOperationException(error.GetProperty("message").GetString()); + exception.Data["error"] = error.Clone(); + completion.TrySetException(exception); } else { diff --git a/dotnet/test/Unit/ClientSessionSkillProviderTests.cs b/dotnet/test/Unit/ClientSessionSkillProviderTests.cs new file mode 100644 index 0000000000..c99d1f090a --- /dev/null +++ b/dotnet/test/Unit/ClientSessionSkillProviderTests.cs @@ -0,0 +1,397 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#if NET8_0_OR_GREATER +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace GitHub.Copilot.Test.Unit; + +public sealed partial class ClientSessionLifetimeTests +{ + [Fact] + public async Task SkillProvider_Create_And_Resume_Set_Flag_Only_When_Provided() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var provider = new TestSkillProvider + { + ReadHandler = (name, _) => Task.FromResult($"# {name}") + }; + + await using var created = await client.CreateSessionAsync(new SessionConfig + { + SessionId = "skill-create", + SkillProvider = provider + }); + await using var resumed = await client.ResumeSessionAsync("skill-resume", new ResumeSessionConfig + { + SkillProvider = provider + }); + + Assert.True(Assert.Single(server.Requests, request => request.Method == "session.create") + .Params.GetProperty("hasSkillProvider").GetBoolean()); + Assert.True(Assert.Single(server.Requests, request => request.Method == "session.resume") + .Params.GetProperty("hasSkillProvider").GetBoolean()); + + var read = await server.SendRequestAsync("skillProvider.read", SkillReadRequest(resumed.SessionId, "resumed-skill")); + Assert.Equal("# resumed-skill", read.GetProperty("markdown").GetString()); + + await created.DisposeAsync(); + await resumed.DisposeAsync(); + server.ClearRequests(); + + await using var createWithoutProvider = await client.CreateSessionAsync(new SessionConfig + { + SessionId = "skill-create-no-provider" + }); + await using var resumeWithoutProvider = await client.ResumeSessionAsync("skill-resume-no-provider", new ResumeSessionConfig()); + + Assert.False(Assert.Single(server.Requests, request => request.Method == "session.create") + .Params.TryGetProperty("hasSkillProvider", out _)); + Assert.False(Assert.Single(server.Requests, request => request.Method == "session.resume") + .Params.TryGetProperty("hasSkillProvider", out _)); + } + + [Fact] + public async Task SkillProvider_Callbacks_Work_Before_Create_And_Resume_Response() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var provider = new TestSkillProvider(); + var callbackResults = new List>(); + server.BeforeResponseAsync = async (request, _) => + { + if (request.Method is "session.create" or "session.resume" + && request.Params.TryGetProperty("sessionId", out var sessionId)) + { + callbackResults.Add(await server.SendRequestWithoutWaitingAsync( + "skillProvider.list", + SkillListRequest(sessionId.GetString()!))); + } + }; + + await using var created = await client.CreateSessionAsync(new SessionConfig + { + SessionId = "pre-response-create", + SkillProvider = provider + }); + await using var resumed = await client.ResumeSessionAsync("pre-response-resume", new ResumeSessionConfig + { + SkillProvider = provider + }); + + Assert.Equal(2, callbackResults.Count); + foreach (var callback in callbackResults) + { + var result = await callback.WaitAsync(TimeSpan.FromSeconds(5)); + Assert.Equal("dynamic", Assert.Single(result.GetProperty("skills").EnumerateArray()).GetProperty("name").GetString()); + } + } + + [Fact] + public async Task SkillProvider_List_Returns_CamelCase_Descriptors_And_Omits_Null_Optionals() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var provider = new TestSkillProvider + { + ListHandler = _ => Task.FromResult>( + [ + new() + { + Name = "basic", + Description = "Basic skill" + }, + new() + { + Name = "advanced", + Description = "Advanced skill", + UserInvocable = false, + DisableModelInvocation = true, + ArgumentHint = "" + } + ]) + }; + await using var session = await client.CreateSessionAsync(new SessionConfig { SkillProvider = provider }); + + var result = await server.SendRequestAsync("skillProvider.list", SkillListRequest(session.SessionId)); + var skills = result.GetProperty("skills").EnumerateArray().ToArray(); + + Assert.Equal("basic", skills[0].GetProperty("name").GetString()); + Assert.Equal("Basic skill", skills[0].GetProperty("description").GetString()); + Assert.False(skills[0].TryGetProperty("userInvocable", out _)); + Assert.False(skills[0].TryGetProperty("disableModelInvocation", out _)); + Assert.False(skills[0].TryGetProperty("argumentHint", out _)); + Assert.False(skills[0].TryGetProperty("UserInvocable", out _)); + Assert.False(skills[0].TryGetProperty("DisableModelInvocation", out _)); + Assert.False(skills[0].TryGetProperty("ArgumentHint", out _)); + Assert.Equal("advanced", skills[1].GetProperty("name").GetString()); + Assert.False(skills[1].GetProperty("userInvocable").GetBoolean()); + Assert.True(skills[1].GetProperty("disableModelInvocation").GetBoolean()); + Assert.Equal("", skills[1].GetProperty("argumentHint").GetString()); + } + + [Fact] + public async Task SkillProvider_Null_List_Result_Is_Empty() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var provider = new TestSkillProvider + { + ListHandler = _ => Task.FromResult>(null!) + }; + await using var session = await client.CreateSessionAsync(new SessionConfig { SkillProvider = provider }); + + var result = await server.SendRequestAsync("skillProvider.list", SkillListRequest(session.SessionId)); + + Assert.Empty(result.GetProperty("skills").EnumerateArray()); + } + + [Fact] + public async Task SkillProvider_Read_Returns_Markdown_And_Null_For_Missing_Skill() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var provider = new TestSkillProvider + { + ReadHandler = (name, _) => Task.FromResult(name == "known" ? "# Known" : null) + }; + await using var session = await client.CreateSessionAsync(new SessionConfig { SkillProvider = provider }); + + var read = await server.SendRequestAsync("skillProvider.read", SkillReadRequest(session.SessionId, "known")); + + Assert.Equal("# Known", read.GetProperty("markdown").GetString()); + var missing = await server.SendRequestAsync("skillProvider.read", SkillReadRequest(session.SessionId, "missing")); + Assert.True(missing.TryGetProperty("markdown", out var markdown), missing.ToString()); + Assert.Equal(JsonValueKind.Null, markdown.ValueKind); + } + + [Fact] + public async Task SkillProvider_Provider_Failures_Do_Not_Leak_Exception_Details() + { + await using var server = await FakeCopilotServer.StartAsync(); + var logger = new SkillProviderLogger(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url), Logger = logger }); + var listFailure = new InvalidOperationException("secret list failure"); + var readFailure = new InvalidOperationException("secret read failure"); + var provider = new TestSkillProvider + { + ListHandler = _ => throw listFailure, + ReadHandler = (_, _) => throw readFailure + }; + await using var session = await client.CreateSessionAsync(new SessionConfig { SkillProvider = provider }); + + var listError = await Assert.ThrowsAsync(() => + server.SendRequestAsync("skillProvider.list", SkillListRequest(session.SessionId))); + AssertRpcError(listError, "Skill provider listSkills failed"); + + var readError = await Assert.ThrowsAsync(() => + server.SendRequestAsync("skillProvider.read", SkillReadRequest(session.SessionId, "throws"))); + AssertRpcError(readError, "Skill provider readSkill failed"); + + Assert.DoesNotContain("secret", listError.Message, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("secret", readError.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains(logger.Entries, entry => + entry.Message == $"Skill provider listSkills failed. SessionId={session.SessionId}" && entry.Exception == listFailure); + Assert.Contains(logger.Entries, entry => + entry.Message == $"Skill provider readSkill failed. SessionId={session.SessionId}" && entry.Exception == readFailure); + } + + [Fact] + public async Task SkillProvider_Provider_Cancellation_Without_Request_Cancellation_Is_Provider_Failure() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var provider = new TestSkillProvider + { + ListHandler = _ => throw new TaskCanceledException("secret list timeout"), + ReadHandler = (_, _) => throw new TaskCanceledException("secret read timeout") + }; + await using var session = await client.CreateSessionAsync(new SessionConfig { SkillProvider = provider }); + + var listError = await Assert.ThrowsAsync(() => + server.SendRequestAsync("skillProvider.list", SkillListRequest(session.SessionId)).WaitAsync(TimeSpan.FromSeconds(10))); + AssertRpcError(listError, "Skill provider listSkills failed"); + + var readError = await Assert.ThrowsAsync(() => + server.SendRequestAsync("skillProvider.read", SkillReadRequest(session.SessionId, "times-out")).WaitAsync(TimeSpan.FromSeconds(10))); + AssertRpcError(readError, "Skill provider readSkill failed"); + } + + [Fact] + public async Task SkillProvider_Rejects_Unknown_NoProvider_And_Disposed_Sessions() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await client.StartAsync(); + + var unknown = await Assert.ThrowsAsync(() => + server.SendRequestAsync("skillProvider.list", SkillListRequest("unknown-session"))); + AssertRpcError(unknown, "No skill provider for session: unknown-session"); + + await using var noProvider = await client.CreateSessionAsync(new SessionConfig { SessionId = "no-provider" }); + var noProviderError = await Assert.ThrowsAsync(() => + server.SendRequestAsync("skillProvider.list", SkillListRequest(noProvider.SessionId))); + AssertRpcError(noProviderError, "No skill provider for session: no-provider"); + + var providerSession = await client.CreateSessionAsync(new SessionConfig + { + SessionId = "disposed-provider", + SkillProvider = new TestSkillProvider() + }); + await providerSession.DisposeAsync(); + var disposed = await Assert.ThrowsAsync(() => + server.SendRequestAsync("skillProvider.list", SkillListRequest("disposed-provider"))); + AssertRpcError(disposed, "No skill provider for session: disposed-provider"); + } + + [Fact] + public async Task SkillProvider_Cloud_Create_Throws_Before_Connecting() + { + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri("http://127.0.0.1:1") + }); + var provider = new TestSkillProvider(); + + var error = await Assert.ThrowsAsync(() => client.CreateSessionAsync(new SessionConfig + { + Cloud = new CloudSessionOptions + { + Repository = new CloudSessionRepository + { + Owner = "github", + Name = "copilot-sdk", + Branch = "main" + } + }, + SkillProvider = provider + })); + + Assert.Equal("Skill providers are not supported for cloud sessions.", error.Message); + Assert.Equal(0, provider.ListCalls); + Assert.Equal(0, provider.ReadCalls); + } + + [Fact] + public async Task SkillProvider_EmptyMode_Defaults_EnableSkills_To_False() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + Mode = CopilotClientMode.Empty, + BaseDirectory = Path.GetTempPath(), + }); + + await using var session = await client.CreateSessionAsync(new SessionConfig + { + AvailableTools = [], + SkillProvider = new TestSkillProvider() + }); + + var request = Assert.Single(server.Requests, request => request.Method == "session.create"); + Assert.True(request.Params.GetProperty("hasSkillProvider").GetBoolean()); + Assert.False(request.Params.GetProperty("enableSkills").GetBoolean()); + } + + private static Dictionary SkillListRequest(string sessionId) => new() + { + ["sessionId"] = sessionId + }; + + private static Dictionary SkillReadRequest(string sessionId, string name) => new() + { + ["sessionId"] = sessionId, + ["name"] = name + }; + + private static void AssertRpcError(InvalidOperationException exception, string expectedMessage) + { + Assert.Equal(expectedMessage, exception.Message); + var error = Assert.IsType(exception.Data["error"]); + Assert.Equal(-32603, error.GetProperty("code").GetInt32()); + Assert.Equal(expectedMessage, error.GetProperty("message").GetString()); + Assert.False(error.TryGetProperty("data", out _), error.ToString()); + } + + private sealed class SkillProviderLogger : ILogger + { + private readonly ConcurrentQueue<(string Message, Exception? Exception)> _entries = new(); + + public IReadOnlyCollection<(string Message, Exception? Exception)> Entries => _entries; + + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) => + _entries.Enqueue((formatter(state, exception), exception)); + } + + private sealed class TestSkillProvider : ISkillProvider + { + public int ListCalls; + public int ReadCalls; + + public Func>> ListHandler { get; init; } = + _ => Task.FromResult>( + [ + new() + { + Name = "dynamic", + Description = "Dynamic skill" + } + ]); + + public Func> ReadHandler { get; init; } = + (name, _) => Task.FromResult($"# {name}"); + + public Task> ListSkillsAsync(CancellationToken cancellationToken) + { + Interlocked.Increment(ref ListCalls); + return ListHandler(cancellationToken); + } + + public Task ReadSkillAsync(string name, CancellationToken cancellationToken) + { + Interlocked.Increment(ref ReadCalls); + return ReadHandler(name, cancellationToken); + } + } + + private sealed partial class FakeCopilotServer + { + public async Task> SendRequestWithoutWaitingAsync(string method, Dictionary parameters) + { + var stream = _stream ?? throw new InvalidOperationException("Client is not connected."); + var id = Interlocked.Increment(ref _nextRequestId); + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + if (!_pendingRequests.TryAdd(id, completion)) + { + throw new InvalidOperationException("Failed to track callback request."); + } + + await WriteMessageAsync(stream, new Dictionary + { + ["jsonrpc"] = "2.0", + ["id"] = id, + ["method"] = method, + ["params"] = parameters + }, _cts.Token); + + return completion.Task.WaitAsync(_cts.Token); + } + } +} +#endif diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index c67bce8b4a..3ae4c0b009 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -88,6 +88,15 @@ private sealed class TestExtensionLaunchProvider : GitHub.Copilot.Rpc.IExtension Task.FromResult(new GitHub.Copilot.Rpc.ExtensionLaunchProviderResolveResult()); } + private sealed class TestSkillProvider : ISkillProvider + { + public Task> ListSkillsAsync(CancellationToken cancellationToken) => + Task.FromResult>([]); + + public Task ReadSkillAsync(string name, CancellationToken cancellationToken) => + Task.FromResult(null); + } + [Fact] public void SessionConfig_Clone_CopiesAllProperties() { @@ -205,6 +214,28 @@ public void SessionConfig_RefreshCustomInstructions_IsCreateOnlyAndDefaultsToNul Assert.Null(typeof(ResumeSessionConfig).GetProperty(nameof(SessionConfig.RefreshCustomInstructions))); } + [Fact] + public void SessionConfig_Clone_CopiesSkillProvider() + { + var provider = new TestSkillProvider(); + var original = new SessionConfig { SkillProvider = provider }; + + var clone = original.Clone(); + + Assert.Same(provider, clone.SkillProvider); + } + + [Fact] + public void ResumeSessionConfig_Clone_CopiesSkillProvider() + { + var provider = new TestSkillProvider(); + var original = new ResumeSessionConfig { SkillProvider = provider }; + + var clone = original.Clone(); + + Assert.Same(provider, clone.SkillProvider); + } + [Fact] public void SessionConfig_Clone_CollectionsAreIndependent() { diff --git a/dotnet/test/Unit/RpcMcpListTests.cs b/dotnet/test/Unit/RpcMcpListTests.cs new file mode 100644 index 0000000000..6143a6fbbb --- /dev/null +++ b/dotnet/test/Unit/RpcMcpListTests.cs @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#if NET8_0_OR_GREATER +using GitHub.Copilot.Rpc; +using Xunit; + +namespace GitHub.Copilot.Test.Unit; + +public sealed partial class ClientSessionLifetimeTests +{ + [Fact] + public async Task Session_Rpc_Mcp_List_And_ListConfigured_Use_Parameterless_Wire_Contracts() + { + await using var server = await FakeCopilotServer.StartAsync(); + server.ResponseFactory = _ => new Dictionary { ["servers"] = Array.Empty() }; + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url) + }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + server.ClearRequests(); + + var mcp = session.Rpc.Mcp; + Func> legacyList = mcp.ListAsync; + Func> configuredList = mcp.ListConfiguredAsync; + Assert.NotNull(mcp.GetType().GetMethod("ListAsync", [typeof(CancellationToken)])); + Assert.NotNull(mcp.GetType().GetMethod("ListConfiguredAsync", [typeof(CancellationToken)])); + await mcp.ListAsync(); + await legacyList(CancellationToken.None); + await mcp.ListConfiguredAsync(); + await configuredList(CancellationToken.None); + + Assert.Collection( + server.Requests, + request => AssertConnectorRequest(request, "session.mcp.list", session.SessionId), + request => AssertConnectorRequest(request, "session.mcp.list", session.SessionId), + request => AssertConnectorRequest(request, "session.mcp.listConfigured", session.SessionId), + request => AssertConnectorRequest(request, "session.mcp.listConfigured", session.SessionId)); + } +} +#endif diff --git a/dotnet/test/Unit/RpcModelCompatibilityTests.cs b/dotnet/test/Unit/RpcModelCompatibilityTests.cs new file mode 100644 index 0000000000..d638c86d35 --- /dev/null +++ b/dotnet/test/Unit/RpcModelCompatibilityTests.cs @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#if NET8_0_OR_GREATER +using GitHub.Copilot.Rpc; +using Xunit; + +namespace GitHub.Copilot.Test.Unit; + +public sealed partial class ClientSessionLifetimeTests +{ + [Fact] + public void Existing_Model_Switch_Signature_And_Defaults_Are_Preserved() + { + Type[] types = + [ + typeof(string), typeof(AutoTier?), typeof(string), typeof(ReasoningSummary?), + typeof(Verbosity?), typeof(ModelCapabilitiesOverride), typeof(ContextTier?), + typeof(ModelChangeSource?), typeof(bool?), typeof(string), typeof(bool?), + typeof(string), typeof(string), typeof(bool?), typeof(ModelPickerPersistenceRequest), + typeof(CancellationToken), + ]; + var method = typeof(ModelApi).GetMethod("SwitchToAsync", types); + Assert.NotNull(method); + var parameters = method.GetParameters(); + Assert.False(parameters[0].IsOptional); + Assert.Equal("cancellationToken", parameters[^1].Name); + Assert.All(parameters.Skip(1), parameter => + { + Assert.True(parameter.IsOptional); + Assert.True(parameter.HasDefaultValue); + Assert.Null(parameter.DefaultValue); + }); + } + + [Fact] + public async Task Existing_Positional_Model_Switch_Calls_Preserve_Wire_Values() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url) + }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + server.ClearRequests(); + + await session.Rpc.Model.SwitchToAsync("auto", AutoTier.Balance); + await session.Rpc.Model.SwitchToAsync( + "auto", AutoTier.Efficiency, "high", null, null, null, null, null, + true, "compact", true, "repo", "global", true, null, CancellationToken.None); + + var requests = server.Requests.ToArray(); + Assert.Equal(2, requests.Length); + foreach (var request in requests) + { + Assert.Equal("session.model.switchTo", request.Method); + Assert.Equal(session.SessionId, request.Params.GetProperty("sessionId").GetString()); + Assert.Equal("auto", request.Params.GetProperty("modelId").GetString()); + Assert.False(request.Params.TryGetProperty("providerId", out _)); + } + Assert.Equal("balance", requests[0].Params.GetProperty("autoTier").GetString()); + Assert.False(requests[0].Params.TryGetProperty("reasoningEffort", out _)); + Assert.Equal("efficiency", requests[1].Params.GetProperty("autoTier").GetString()); + Assert.Equal("high", requests[1].Params.GetProperty("reasoningEffort").GetString()); + Assert.True(requests[1].Params.GetProperty("deferIfModelChangeQueued").GetBoolean()); + Assert.Equal("compact", requests[1].Params.GetProperty("compactionDecision").GetString()); + Assert.True(requests[1].Params.GetProperty("runCompactionPreflight").GetBoolean()); + Assert.Equal("repo", requests[1].Params.GetProperty("repoScope").GetString()); + Assert.Equal("global", requests[1].Params.GetProperty("modelChangeScope").GetString()); + Assert.True(requests[1].Params.GetProperty("requireAvailable").GetBoolean()); + } + + [Fact] + public async Task Provider_Qualified_Model_Switch_Uses_Request_With_Sdk_Owned_Session() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url) + }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + server.ClearRequests(); + + var result = await session.Rpc.Model.SwitchToAsync(new ModelSwitchToRequest + { + ModelId = "shared-model", + ProviderId = "provider-a", + ReasoningEffort = "high", + }, CancellationToken.None); + + Assert.Null(typeof(ModelSwitchToRequest).GetProperty("SessionId")); + var request = Assert.Single(server.Requests); + Assert.Equal("session.model.switchTo", request.Method); + Assert.Equal(session.SessionId, request.Params.GetProperty("sessionId").GetString()); + Assert.Equal("shared-model", request.Params.GetProperty("modelId").GetString()); + Assert.Equal("provider-a", request.Params.GetProperty("providerId").GetString()); + Assert.Equal("high", request.Params.GetProperty("reasoningEffort").GetString()); + Assert.False(request.Params.TryGetProperty("autoTier", out _)); + // The fake reports "auto"; the SDK must return the server's result, not the requested id. + Assert.Equal("auto", result.ModelId); + } +} +#endif diff --git a/dotnet/test/Unit/RpcWorkspacesCompatibilityTests.cs b/dotnet/test/Unit/RpcWorkspacesCompatibilityTests.cs new file mode 100644 index 0000000000..8653a6b09a --- /dev/null +++ b/dotnet/test/Unit/RpcWorkspacesCompatibilityTests.cs @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#if NET8_0_OR_GREATER +using GitHub.Copilot.Rpc; +using Xunit; + +namespace GitHub.Copilot.Test.Unit; + +public sealed partial class ClientSessionLifetimeTests +{ + [Fact] + public void Workspace_Diff_Preserves_Existing_Signature_And_Defaults() + { + var method = typeof(WorkspacesApi).GetMethod("DiffAsync", + [typeof(WorkspaceDiffMode), typeof(bool?), typeof(CancellationToken)]); + Assert.NotNull(method); + Assert.Equal( + new (string? Name, bool IsOptional)[] + { + ("mode", false), + ("ignoreWhitespace", true), + ("cancellationToken", true), + }, + method.GetParameters().Select(parameter => (parameter.Name, parameter.IsOptional))); + Assert.All(method.GetParameters().Skip(1), parameter => Assert.Null(parameter.DefaultValue)); + } + + [Fact] + public async Task Workspace_Diff_Preserves_Positional_Calls_And_Offers_Contents_Through_Request() + { + await using var server = await FakeCopilotServer.StartAsync(); + server.ResponseFactory = request => request.Method == "session.workspaces.diff" + ? new Dictionary + { + ["mode"] = "session", + ["requestedMode"] = "session", + ["isFallback"] = false, + ["changes"] = Array.Empty(), + } + : throw new InvalidOperationException($"Unexpected workspace diff request '{request.Method}'."); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url) + }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + server.ClearRequests(); + var cancellation = CancellationToken.None; + + await session.Rpc.Workspaces.DiffAsync(WorkspaceDiffMode.Session, false, cancellation); + await session.Rpc.Workspaces.DiffAsync(WorkspaceDiffMode.Session); + await session.Rpc.Workspaces.DiffAsync(WorkspaceDiffMode.Session, cancellationToken: cancellation); + await session.Rpc.Workspaces.DiffAsync(new WorkspacesDiffRequest + { + Mode = WorkspaceDiffMode.Session, + IgnoreWhitespace = false, + IncludeContents = true, + }, cancellation); + await session.Rpc.Workspaces.DiffAsync(new WorkspacesDiffRequest + { + Mode = WorkspaceDiffMode.Session, + IncludeContents = false, + }, cancellation); + + Assert.Null(typeof(WorkspacesDiffRequest).GetProperty("SessionId")); + var requests = server.Requests.ToArray(); + Assert.All(requests, request => Assert.Equal( + ("session.workspaces.diff", session.SessionId, "session"), + (request.Method, request.Params.GetProperty("sessionId").GetString(), + request.Params.GetProperty("mode").GetString()))); + Assert.Equal( + new (bool? IgnoreWhitespace, bool? IncludeContents)[] + { + (false, null), + (null, null), + (null, null), + (false, true), + (null, false), + }, + requests.Select(request => ( + request.Params.TryGetProperty("ignoreWhitespace", out var ignoreWhitespace) + ? ignoreWhitespace.GetBoolean() : (bool?)null, + request.Params.TryGetProperty("includeContents", out var includeContents) + ? includeContents.GetBoolean() : (bool?)null))); + } +} +#endif diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 30ac2608d6..ef41cfa4aa 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -1283,7 +1283,7 @@ public void ModelSwitchRequests_DistinguishRequiredNullFromOmittedOptionalValue( Assert.True(switchAutoTierDocument.RootElement.TryGetProperty("autoTier", out var requiredAutoTier)); Assert.Equal(JsonValueKind.Null, requiredAutoTier.ValueKind); - var switchToType = assembly.GetType("GitHub.Copilot.Rpc.ModelSwitchToRequest"); + var switchToType = assembly.GetType("GitHub.Copilot.Rpc.ModelSwitchToRequestWithSession"); Assert.NotNull(switchToType); var switchToRequest = CreateInternalRequest( switchToType!, diff --git a/dotnet/test/Unit/SetToolsTests.cs b/dotnet/test/Unit/SetToolsTests.cs index 4779a3a70e..c88dccb639 100644 --- a/dotnet/test/Unit/SetToolsTests.cs +++ b/dotnet/test/Unit/SetToolsTests.cs @@ -6,9 +6,11 @@ #pragma warning disable GHCP001 // Live tool replacement is intentionally experimental. using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; using System.Net; using System.Net.Sockets; using System.Reflection; +using System.Runtime.ExceptionServices; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; @@ -122,6 +124,53 @@ public async Task SetToolsAsync_Rejection_Leaves_Handlers_Unchanged() Assert.Equal("old", result); } + [Fact] + public async Task SetToolsAsync_Acceptance_After_Disposal_Cannot_Restore_Handlers_Or_Close_Connection() + { + await using var server = await SetToolsFakeServer.StartAsync(); + var releaseSet = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + server.BeforeSetToolsResponseAsync = _ => releaseSet.Task; + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + var replacement = session.SetToolsAsync([Tool("retired_tool", "must-not-install")]); + var timeout = TimeSpan.FromSeconds(5); + Exception? bodyFailure = null; + try + { + await server.WaitForRequestAsync("session.tools.set").WaitAsync(timeout); + await session.DisposeAsync().AsTask().WaitAsync(timeout); + releaseSet.TrySetResult(); + + await Assert.ThrowsAsync(() => replacement.WaitAsync(timeout)); + await using var liveSession = await client.CreateSessionAsync(new SessionConfig()).WaitAsync(timeout); + Assert.NotEqual(session.SessionId, liveSession.SessionId); + } + catch (Exception error) + { + bodyFailure = error; + } + finally + { + releaseSet.TrySetResult(); + try + { + await replacement.WaitAsync(timeout); + } + catch (ObjectDisposedException) + { + // The acknowledged replacement was rejected by local retirement. + } + catch (Exception cleanupError) + { + bodyFailure = bodyFailure is null ? cleanupError : new AggregateException(bodyFailure, cleanupError); + } + } + if (bodyFailure is not null) + { + ExceptionDispatchInfo.Capture(bodyFailure).Throw(); + } + } + [Fact] public async Task SetToolsAsync_Empty_Set_Removes_Handlers() { @@ -240,6 +289,40 @@ await Assert.ThrowsAnyAsync(() => Assert.Equal("new", response.Params.GetProperty("result").GetProperty("textResultForLlm").GetString()); } + [Fact] + public async Task SetToolsAsync_Publishes_Accepted_Replacement_Before_Next_Tool_Request() + { + await using var server = await SetToolsFakeServer.StartAsync(); + var logger = new SetToolsResponseLogger(); + var releaseSet = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + server.BeforeSetToolsResponseAsync = _ => releaseSet.Task; + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url), Logger = logger }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [Tool("cancel_tool", "old")], + }); + + using var cts = new CancellationTokenSource(); + try + { + var replacement = session.SetToolsAsync([Tool("cancel_tool", "new")], cts.Token); + await server.WaitForRequestAsync("session.tools.set"); + await cts.CancelAsync(); + await Assert.ThrowsAnyAsync(() => replacement); + + releaseSet.SetResult(); + await logger.ResponseReceived.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + var result = await InvokeToolAsync(server, session, "cancel_tool", "cancel-request"); + Assert.Equal("new", result); + } + finally + { + releaseSet.TrySetResult(); + logger.ContinueResponse.TrySetResult(); + } + } + [Fact] public async Task SetToolsAsync_Cancellation_While_Queued_Does_Not_Send_Request() { @@ -274,6 +357,52 @@ await Assert.ThrowsAnyAsync(() => Assert.Equal("first", result); } + [Fact] + public async Task SetToolsAsync_Cancellation_Before_Queued_Waiter_Resumes_Does_Not_Send_Request() + { + await using var server = await SetToolsFakeServer.StartAsync(); + var releaseSet = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + server.BeforeSetToolsResponseAsync = _ => releaseSet.Task; + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + + var first = session.SetToolsAsync([Tool("queued_tool", "first")]); + await server.WaitForRequestAsync("session.tools.set"); + using var cts = new CancellationTokenSource(); + var context = new PausedContinuationContext(); + var previousContext = SynchronizationContext.Current; + Task queued; + try + { + SynchronizationContext.SetSynchronizationContext(context); + queued = session.SetToolsAsync([Tool("queued_tool", "queued")], cts.Token); + } + finally + { + SynchronizationContext.SetSynchronizationContext(previousContext); + releaseSet.TrySetResult(); + } + + await first; + var continuation = await context.Continuation.Task.WaitAsync(TimeSpan.FromSeconds(5)); + try + { + await cts.CancelAsync(); + await Assert.ThrowsAnyAsync(() => queued); + } + finally + { + continuation.Callback(continuation.State); + } + + // A later completed replacement proves the cancelled waiter has left the queue. + await session.SetToolsAsync([Tool("queued_tool", "last")]); + Assert.Equal( + ["Returns first", "Returns last"], + server.Requests.Where(request => request.Method == "session.tools.set") + .Select(request => request.Params.GetProperty("tools")[0].GetProperty("description").GetString())); + } + private static AIFunction Tool(string name, string result) { return AIFunctionFactory.Create( @@ -441,6 +570,40 @@ public void Dispose() } } + private sealed class PausedContinuationContext : SynchronizationContext + { + public TaskCompletionSource<(SendOrPostCallback Callback, object? State)> Continuation { get; } = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + public override void Post(SendOrPostCallback callback, object? state) + { + if (!Continuation.TrySetResult((callback, state))) + { + base.Post(callback, state); + } + } + } + + private sealed class SetToolsResponseLogger : ILogger + { + public TaskCompletionSource ResponseReceived { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + public TaskCompletionSource ContinueResponse { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + public IDisposable? BeginScope(TState state) where TState : notnull => null; + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (state is IReadOnlyList> properties && + properties.Any(entry => entry is { Key: "Method", Value: "session.tools.set" }) && + properties.Any(entry => entry is { Key: "Status", Value: "Succeeded" })) + { + // Hold the RPC awaiter after acceptance without blocking the reader's next tool event. + ResponseReceived.TrySetResult(); + ContinueResponse.Task.GetAwaiter().GetResult(); + } + } + } + private sealed class SetToolsFakeServer : IAsyncDisposable { private static readonly JsonSerializerOptions s_jsonOptions = new(JsonSerializerDefaults.Web) @@ -453,6 +616,7 @@ private sealed class SetToolsFakeServer : IAsyncDisposable private readonly List _requests = []; private readonly object _requestsLock = new(); private readonly Task _serverTask; + private readonly List _requestTasks = []; private NetworkStream? _stream; private string? _nextSetToolsRejection; private readonly Dictionary _setToolsRejections = []; @@ -578,8 +742,19 @@ public async ValueTask DisposeAsync() { } - _cts.Dispose(); - _writeLock.Dispose(); + try + { + await Task.WhenAll(_requestTasks).WaitAsync(TimeSpan.FromSeconds(5)); + } + catch (OperationCanceledException) when (_requestTasks.All(task => task.IsCanceled || task.IsCompletedSuccessfully)) + { + // Shutdown cancellation may stop a writer, but every owned request must finish. + } + finally + { + _cts.Dispose(); + _writeLock.Dispose(); + } } private async Task RunAsync() @@ -602,7 +777,7 @@ private async Task RunAsync() continue; } - _ = HandleRequestAsync(stream, root, id.Clone(), _cts.Token); + _requestTasks.Add(HandleRequestAsync(stream, root, id.Clone(), _cts.Token)); } } diff --git a/go/README.md b/go/README.md index 000ae49086..1737f4fb86 100644 --- a/go/README.md +++ b/go/README.md @@ -145,6 +145,56 @@ Avoid logging it indiscriminately: server-provided data may contain sensitive information. Its fields and data bytes are shared with the wrapped error; copy them before mutation. +## Skill providers (experimental) + +Use `SessionConfig.SkillProvider` or `ResumeSessionConfig.SkillProvider` to +serve session-scoped skills from your application instead of from `SKILL.md` +files on disk. The provider is ephemeral: it is not serialized or persisted, so +re-supply it on every resume. Resuming without a provider unbinds any previous +provider for that session. + + + +```go +type memorySkillProvider struct{} + +func (memorySkillProvider) ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) { + return []rpc.SkillProviderDescriptor{{ + Name: "review", + Description: "Review code using the host application's policy", + }}, nil +} + +func (memorySkillProvider) ReadSkill(ctx context.Context, name string) (string, error) { + if name != "review" { + return "", copilot.ErrSkillNotFound + } + return "Review the changes and call out policy violations.", nil +} + +session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + SkillProvider: memorySkillProvider{}, + // Required only when ClientOptions.Mode is ModeEmpty. + // EnableSkills: copilot.Bool(true), +}) +``` + +`ReadSkill` returns an error for which +`errors.Is(err, copilot.ErrSkillNotFound)` is true when a listed skill no +longer exists. Other errors are reported to the runtime as generic provider +failures so provider error text is not exposed to the model. + +Skill providers are not supported for cloud sessions. Creating a cloud session +with a provider fails before the client connects with: +`Skill providers are not supported for cloud sessions.` + +Provider methods can be called concurrently by the runtime, including from +sub-agents that inherit the root session binding. Implementations must be safe +for concurrent calls and should observe the `context.Context`. The context is +cancelled when the runtime abandons the request: the call times out, the +session disconnects or is deleted, or a resume replaces the provider. It is not +cancelled when the connection closes or the client is force-stopped. + ## Installation confirmation (experimental) Set `ClientOptions.InstallationConfirmationHandler` to receive the runtime's @@ -257,7 +307,8 @@ Implemented with pure-Go FFI (via [purego](https://github.com/ebitengine/purego) - `NewClient(options *ClientOptions) *Client` - Create a new client - `Start(ctx context.Context) error` - Start the CLI server - `Stop() error` - Gracefully stop the CLI server. For an owned stdio process, requests runtime shutdown, closes stdin, and waits up to 10 seconds for host cleanup (including telemetry) and natural exit before falling back to a forced termination. -- `ForceStop()` - Forcefully stop without graceful cleanup +- `ForceStop()` - Forcefully stop without graceful RPC cleanup. Retires local event subscriptions and session callbacks, including entries held by dispatch snapshots or unsubscribe handles. Pending response waits fail without waiting for idle or their context deadlines; already-dispatched handlers are not joined. Late event registration is logged and returns a no-op unsubscribe, and a late tool-replacement acknowledgment cannot restore retired handlers. +- A plain `SendAndWait` that has already received its terminal idle or error preserves that result when local session retirement races the wait. An earlier event handler may also call `Disconnect` on the terminal event without losing that result. Without retirement, the wait still lets earlier synchronous handlers finish; a forced stop does not join those handlers or complete waits whose terminal dispatch they are still blocking. - `CreateSession(ctx context.Context, config *SessionConfig) (*Session, error)` - Create a new session - `ResumeSession(ctx context.Context, sessionID string, config *ResumeSessionConfig) (*Session, error)` - Resume an existing session - `ResumeSessionWithOptions(ctx context.Context, sessionID string, config *ResumeSessionConfig) (*Session, error)` - Resume with additional configuration diff --git a/go/client.go b/go/client.go index 3b61dc4e3f..b924a9b6c2 100644 --- a/go/client.go +++ b/go/client.go @@ -42,6 +42,7 @@ import ( "os/exec" "path/filepath" "regexp" + "runtime/debug" "strconv" "strings" "sync" @@ -60,6 +61,12 @@ import ( // whole-session [ProviderConfig]. Named providers are keyed by their own Name. const defaultBearerTokenProviderName = "default" +type cloudSkillProviderError struct{} + +func (cloudSkillProviderError) Error() string { + return "Skill providers are not supported for cloud sessions." +} + // collectBearerTokenProviders gathers the per-provider [BearerTokenProvider] callbacks // from the singular provider and any named providers, keyed by provider name. The // singular provider uses the implicit name "default"; named providers use their @@ -715,6 +722,14 @@ func (c *Client) logDebugTiming(start time.Time, message string) { // - Force closes the connection // - Kills the CLI process (if spawned by this client) // +// Event processing observes the stop signal before dispatching each remaining +// callback. Callbacks already being dispatched are not awaited and may finish +// after this method returns; an event handler can itself call ForceStop. +// Pending response waits fail when the session is retired, without waiting for +// an idle event or their context deadline. +// Registered session callbacks are released locally, including unadmitted event +// handler entries retained by a dispatch snapshot. +// // Example: // // // If normal stop hangs, force stop @@ -747,7 +762,9 @@ func (c *Client) ForceStop() { c.sessions = make(map[string]*Session) c.sessionsMux.Unlock() for _, session := range sessions { + session.clearSkillProvider() session.cancelPendingExternalTools() + session.stopEventProcessing() } c.clearGitHubTokenProviders() c.closeCopilotRequestAdapter() @@ -876,6 +893,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config == nil { config = &SessionConfig{} } + if config.Cloud != nil && config.SkillProvider != nil { + return nil, cloudSkillProviderError{} + } if config.GitHubToken != "" && config.GitHubTokenProvider != nil { return nil, fmt.Errorf("GitHubToken and GitHubTokenProvider cannot be used together") } @@ -913,6 +933,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.EnableHostGitOperations = config.EnableHostGitOperations req.EnableSessionStore = config.EnableSessionStore req.EnableSkills = config.EnableSkills + if config.SkillProvider != nil { + req.HasSkillProvider = Bool(true) + } req.Tools = config.Tools systemMessage := c.systemMessageForMode(config.SystemMessage) wireSystemMessage, transformCallbacks := extractTransformCallbacks(systemMessage) @@ -978,6 +1001,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.FeatureFlags = &config.FeatureFlags } req.EnableManagedSettings = config.EnableManagedSettings + req.EnforceManagedModelDefaults = config.EnforceManagedModelDefaults req.ManagedSettings = config.ManagedSettings if len(config.Commands) > 0 { @@ -1112,6 +1136,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config.CanvasHandler != nil { s.registerCanvasHandler(config.CanvasHandler) } + if config.SkillProvider != nil { + s.registerSkillProvider(config.SkillProvider) + } if bearerTokenProviders := collectBearerTokenProviders(config.Provider, config.Providers); bearerTokenProviders != nil { s.registerBearerTokenProviders(bearerTokenProviders) } @@ -1411,6 +1438,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.EnableHostGitOperations = config.EnableHostGitOperations req.EnableSessionStore = config.EnableSessionStore req.EnableSkills = config.EnableSkills + if config.SkillProvider != nil { + req.HasSkillProvider = Bool(true) + } if config.SuppressResumeEvent { req.DisableResume = Bool(true) } @@ -1451,6 +1481,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.FeatureFlags = &config.FeatureFlags } req.EnableManagedSettings = config.EnableManagedSettings + req.EnforceManagedModelDefaults = config.EnforceManagedModelDefaults req.ManagedSettings = config.ManagedSettings if config.OnPermissionRequest != nil { req.RequestPermission = Bool(true) @@ -1520,6 +1551,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, if config.CanvasHandler != nil { session.registerCanvasHandler(config.CanvasHandler) } + if config.SkillProvider != nil { + session.registerSkillProvider(config.SkillProvider) + } if bearerTokenProviders := collectBearerTokenProviders(config.Provider, config.Providers); bearerTokenProviders != nil { session.registerBearerTokenProviders(bearerTokenProviders) } @@ -1743,6 +1777,7 @@ func (c *Client) DeleteSession(ctx context.Context, sessionID string) error { delete(c.sessions, sessionID) c.sessionsMux.Unlock() if session != nil { + session.clearSkillProvider() session.releaseGitHubTokenProviderRegistration() } @@ -2590,6 +2625,8 @@ func (c *Client) setupNotificationHandler() { c.client.SetRequestHandler("exitPlanMode.request", jsonrpc2.RequestHandlerFor(c.handleExitPlanModeRequest)) c.client.SetRequestHandler("autoModeSwitch.request", jsonrpc2.RequestHandlerFor(c.handleAutoModeSwitchRequest)) c.client.SetRequestHandler("systemMessage.transform", jsonrpc2.RequestHandlerFor(c.handleSystemMessageTransform)) + c.client.SetRequestContextHandler("skillProvider.list", c.handleSkillProviderList) + c.client.SetRequestContextHandler("skillProvider.read", c.handleSkillProviderRead) rpc.RegisterClientSessionAPIHandlers(c.client, func(sessionID string) *rpc.ClientSessionAPIHandlers { c.sessionsMux.Lock() defer c.sessionsMux.Unlock() @@ -2684,6 +2721,7 @@ func (c *Client) handleConnectionClose() { } c.sessionsMux.Unlock() for _, session := range sessions { + session.clearSkillProvider() session.cancelPendingExternalTools() } // Avoid deadlocking with Stop/ForceStop, which hold startStopMux while @@ -2801,6 +2839,102 @@ func (c *Client) handleSessionEvent(req sessionEventRequest) { } } +func (c *Client) handleSkillProviderList(ctx context.Context, params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var req rpc.SkillProviderListRequest + if err := json.Unmarshal(params, &req); err != nil || req.SessionID == "" { + return nil, &jsonrpc2.Error{Code: -32602, Message: "invalid skill provider list payload"} + } + + provider, rpcErr := c.resolveSkillProvider(req.SessionID) + if rpcErr != nil { + return nil, rpcErr + } + + skills, err := callSkillProvider(ctx, req.SessionID, "listSkills", func() ([]rpc.SkillProviderDescriptor, error) { + return provider.ListSkills(ctx) + }) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: "Skill provider listSkills failed"} + } + if skills == nil { + skills = []rpc.SkillProviderDescriptor{} + } + + raw, err := json.Marshal(rpc.SkillProviderListResult{Skills: skills}) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil +} + +func (c *Client) handleSkillProviderRead(ctx context.Context, params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var req rpc.SkillProviderReadRequest + if err := json.Unmarshal(params, &req); err != nil || req.SessionID == "" || req.Name == "" { + return nil, &jsonrpc2.Error{Code: -32602, Message: "invalid skill provider read payload"} + } + + provider, rpcErr := c.resolveSkillProvider(req.SessionID) + if rpcErr != nil { + return nil, rpcErr + } + + // Classify not-found inside the guarded call: errors.Is runs provider + // Is/Unwrap methods, which may panic. + markdown, err := callSkillProvider(ctx, req.SessionID, "readSkill", func() (*string, error) { + markdown, err := provider.ReadSkill(ctx, req.Name) + if errors.Is(err, ErrSkillNotFound) { + return nil, nil + } + if err != nil { + return nil, err + } + return &markdown, nil + }) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: "Skill provider readSkill failed"} + } + + raw, err := json.Marshal(rpc.SkillProviderReadResult{Markdown: markdown}) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil +} + +func (c *Client) resolveSkillProvider(sessionID string) (SkillProvider, *jsonrpc2.Error) { + c.sessionsMux.Lock() + session := c.sessions[sessionID] + c.sessionsMux.Unlock() + if session == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No skill provider for session: %s", sessionID)} + } + provider := session.getSkillProvider() + if provider == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No skill provider for session: %s", sessionID)} + } + return provider, nil +} + +var errSkillProviderPanic = errors.New("skill provider panicked") + +// callSkillProvider logs provider failures locally and converts panics into +// an error, so the runtime only ever receives a generic failure. +func callSkillProvider[T any](ctx context.Context, sessionID, operation string, call func() (T, error)) (result T, err error) { + defer func() { + if failure := recover(); failure != nil { + log.Printf("skill provider %s panicked: session_id=%s panic=%v\n%s", operation, sessionID, failure, debug.Stack()) + var zero T + result, err = zero, errSkillProviderPanic + } + }() + result, err = call() + // A call the runtime cancelled is expected to fail; it isn't a provider failure. + if err != nil && ctx.Err() == nil { + log.Printf("skill provider %s failed: session_id=%s error=%v", operation, sessionID, err) + } + return result, err +} + // handleUserInputRequest handles a user input request from the CLI server. func (c *Client) handleUserInputRequest(req userInputRequest) (*userInputResponse, *jsonrpc2.Error) { if req.SessionID == "" || req.Question == "" { diff --git a/go/client_test.go b/go/client_test.go index d9f3ffc4cd..c9a6225cde 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -545,9 +545,33 @@ func TestClient_ForceStopAndExternalStopDoNotRequestRuntimeShutdown(t *testing.T externalServer.Stop() } +func TestClient_ForceStopStopsRegisteredSessionEventProcessing(t *testing.T) { + rpcClient, server, shutdownCalled := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + sessions := make(map[string]*Session) + for _, id := range []string{"session-1", "session-2"} { + session := newSession(id, rpcClient, "", false) + t.Cleanup(session.stopEventProcessing) + sessions[id] = session + } + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: sessions, + } + + client.ForceStop() + for _, session := range sessions { + assertSessionEventChannelClosed(t, session) + } + client.ForceStop() + assertRuntimeShutdownNotCalled(t, shutdownCalled) +} + func TestClient_ForceStopCancelsPendingExternalTools(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) session := &Session{ + eventDone: make(chan struct{}), pendingExternalTools: map[string]*pendingExternalTool{ "request-1": {ctx: ctx, cancel: cancel}, }, @@ -567,6 +591,89 @@ func TestClient_ForceStopCancelsPendingExternalTools(t *testing.T) { } } +func TestClient_ForceStopDropsRemainingEventHandlers(t *testing.T) { + for _, fromHandler := range []bool{false, true} { + name := "while handler is blocked" + if fromHandler { + name = "from event handler" + } + t.Run(name, func(t *testing.T) { + session := &Session{ + eventCh: make(chan SessionEvent, 1), + eventDone: make(chan struct{}), + } + client := &Client{sessions: map[string]*Session{"session-1": session}} + entered := make(chan struct{}) + release := make(chan struct{}) + releaseHandler := sync.OnceFunc(func() { close(release) }) + forceStopped := make(chan struct{}) + consumerExited := make(chan struct{}) + unexpected := make(chan string, 3) + t.Cleanup(func() { + session.stopEventProcessing() + releaseHandler() + select { + case <-consumerExited: + case <-time.After(2 * time.Second): + t.Error("event consumer did not exit during cleanup") + } + }) + session.On(func(event SessionEvent) { + if _, ok := event.Data.(*SessionIdleData); !ok { + unexpected <- "queued event" + return + } + close(entered) + <-release + if fromHandler { + client.ForceStop() + close(forceStopped) + } + }) + session.On(func(SessionEvent) { unexpected <- "remaining handler" }) + go func() { + defer close(consumerExited) + session.processEvents() + }() + + session.eventCh <- newTestEvent() + select { + case <-entered: + case <-time.After(2 * time.Second): + t.Fatal("event handler did not enter its barrier") + } + session.eventCh <- SessionEvent{Data: &AssistantMessageData{ + Content: "queued", + MessageID: "queued", + }} + if fromHandler { + releaseHandler() + } else { + go func() { + client.ForceStop() + close(forceStopped) + }() + } + select { + case <-forceStopped: + case <-time.After(2 * time.Second): + t.Fatal("ForceStop waited for the in-flight event handler") + } + releaseHandler() + select { + case <-consumerExited: + case <-time.After(2 * time.Second): + t.Fatal("event consumer did not exit after the handler returned") + } + select { + case callback := <-unexpected: + t.Fatalf("ForceStop dispatched %s after cancellation", callback) + default: + } + }) + } +} + func TestClient_ConnectionCloseCancelsPendingExternalTools(t *testing.T) { rpcClient, server, _ := newRuntimeShutdownRpcPair(t) server.SetRequestHandler("session.detach", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { @@ -5187,18 +5294,22 @@ func TestIsTerminal(t *testing.T) { func TestSessionRequests_ManagedSettings(t *testing.T) { settings := &ManagedSettings{ Permissions: &ManagedSettingsPermissions{ - DisableBypassPermissionsMode: DisableBypassPermissionsModeDisable, - Deny: []string{"Shell(git push)"}, - Ask: []string{"Domain(publish.example)"}, - Allow: []string{"Read(**)"}, + DisableBypassPermissionsMode: DisableBypassPermissionsModeDisable, + DisableAssistedPermissionsMode: Bool(true), + Deny: []string{"Shell(git push)"}, + Ask: []string{"Domain(publish.example)"}, + Allow: []string{"Read(**)"}, + LimitTo: []string{"Domain(github.com)"}, }, } expectedPermissions := map[string]any{ - "disableBypassPermissionsMode": "disable", - "deny": []any{"Shell(git push)"}, - "ask": []any{"Domain(publish.example)"}, - "allow": []any{"Read(**)"}, + "disableBypassPermissionsMode": "disable", + "disableAssistedPermissionsMode": true, + "deny": []any{"Shell(git push)"}, + "ask": []any{"Domain(publish.example)"}, + "allow": []any{"Read(**)"}, + "limitTo": []any{"Domain(github.com)"}, } t.Run("direct injection enables managed safeguards", func(t *testing.T) { @@ -5211,7 +5322,11 @@ func TestSessionRequests_ManagedSettings(t *testing.T) { }) t.Run("includes managedSettings on create when set", func(t *testing.T) { - req := createSessionRequest{EnableManagedSettings: Bool(true), ManagedSettings: settings} + req := createSessionRequest{ + EnableManagedSettings: Bool(true), + EnforceManagedModelDefaults: Bool(true), + ManagedSettings: settings, + } data, err := json.Marshal(req) if err != nil { t.Fatalf("Failed to marshal: %v", err) @@ -5223,6 +5338,9 @@ func TestSessionRequests_ManagedSettings(t *testing.T) { if m["enableManagedSettings"] != true { t.Errorf("Expected enableManagedSettings true, got %v", m["enableManagedSettings"]) } + if m["enforceManagedModelDefaults"] != true { + t.Errorf("Expected enforceManagedModelDefaults true, got %v", m["enforceManagedModelDefaults"]) + } ms, ok := m["managedSettings"].(map[string]any) if !ok { t.Fatalf("Expected managedSettings object, got %v", m["managedSettings"]) @@ -5290,6 +5408,7 @@ func TestSessionRequests_ManagedSettings(t *testing.T) { Deny: []string{}, Ask: []string{}, Allow: []string{}, + LimitTo: []string{}, }, }} data, err := json.Marshal(req) @@ -5302,7 +5421,7 @@ func TestSessionRequests_ManagedSettings(t *testing.T) { if perms["disableBypassPermissionsMode"] != "disable" { t.Errorf("Expected disableBypassPermissionsMode preserved, got %v", perms["disableBypassPermissionsMode"]) } - for _, key := range []string{"deny", "ask", "allow"} { + for _, key := range []string{"deny", "ask", "allow", "limitTo"} { if value, ok := perms[key].([]any); !ok || len(value) != 0 { t.Errorf("Expected %s to be an explicit empty array, got %v", key, perms[key]) } diff --git a/go/force_stop_test.go b/go/force_stop_test.go new file mode 100644 index 0000000000..89a5f6c874 --- /dev/null +++ b/go/force_stop_test.go @@ -0,0 +1,412 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package copilot_test + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "runtime" + "strconv" + "strings" + "sync" + "testing" + "time" + + copilot "github.com/github/copilot-sdk/go" +) + +type responseRetirementPeer struct { + listener net.Listener + mu sync.Mutex + conn net.Conn + writeMu sync.Mutex + sent chan struct{} + done chan error +} + +func newResponseRetirementPeer(t *testing.T, expectedSends int) *responseRetirementPeer { + t.Helper() + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + peer := &responseRetirementPeer{ + listener: listener, + sent: make(chan struct{}), + done: make(chan error, 1), + } + t.Cleanup(func() { + if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) { + t.Error("close peer listener:", err) + } + peer.mu.Lock() + conn := peer.conn + peer.mu.Unlock() + if conn != nil { + if err := conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) { + t.Error("close peer connection:", err) + } + } + select { + case err := <-peer.done: + if err != nil { + t.Error("TCP peer:", err) + } + case <-time.After(5 * time.Second): + t.Error("TCP peer did not exit") + } + }) + go func() { peer.done <- peer.serve(expectedSends) }() + return peer +} + +func (p *responseRetirementPeer) serve(expectedSends int) error { + conn, err := p.listener.Accept() + if err != nil { + if errors.Is(err, net.ErrClosed) { + return nil + } + return err + } + p.mu.Lock() + p.conn = conn + p.mu.Unlock() + defer conn.Close() + reader := bufio.NewReader(conn) + sends := 0 + for { + frame, err := readRetirementFrame(reader) + if err != nil { + if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { + return nil + } + return err + } + var request struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + } + if err := json.Unmarshal(frame, &request); err != nil { + return err + } + var result any + switch request.Method { + case "connect": + result = map[string]any{"ok": true, "protocolVersion": copilot.SDKProtocolVersion, "version": "force-stop-test"} + case "session.create": + result = map[string]any{"sessionId": "retirement-session"} + case "session.send": + sends++ + result = map[string]any{"messageId": fmt.Sprintf("message-%d", sends)} + case "ping": + result = map[string]any{"message": "pong", "timestamp": "2026-01-01T00:00:00Z", "protocolVersion": copilot.SDKProtocolVersion} + default: + return fmt.Errorf("unexpected RPC: %s", request.Method) + } + if err := p.write(map[string]any{"jsonrpc": "2.0", "id": request.ID, "result": result}); err != nil { + return err + } + if request.Method == "session.send" && sends == expectedSends { + close(p.sent) + } + } +} + +func readRetirementFrame(reader *bufio.Reader) ([]byte, error) { + length := -1 + for { + line, err := reader.ReadString('\n') + if err != nil { + if errors.Is(err, io.EOF) && line == "" && length == -1 { + return nil, io.EOF + } + return nil, fmt.Errorf("read frame header: %w", err) + } + if line == "\r\n" { + break + } + name, value, ok := strings.Cut(line, ":") + if !ok { + return nil, fmt.Errorf("invalid frame header: %q", line) + } + if strings.EqualFold(name, "Content-Length") { + if length != -1 { + return nil, errors.New("duplicate Content-Length") + } + length, err = strconv.Atoi(strings.TrimSpace(value)) + if err != nil { + return nil, err + } + } + } + if length <= 0 || length > 128*1024 { + return nil, fmt.Errorf("invalid frame length: %d", length) + } + body := make([]byte, length) + if _, err := io.ReadFull(reader, body); err != nil { + return nil, err + } + return body, nil +} + +func (p *responseRetirementPeer) write(message any) error { + body, err := json.Marshal(message) + if err != nil { + return err + } + p.writeMu.Lock() + defer p.writeMu.Unlock() + p.mu.Lock() + conn := p.conn + p.mu.Unlock() + _, err = fmt.Fprintf(conn, "Content-Length: %d\r\n\r\n%s", len(body), body) + return err +} + +type responseRetirementCapture struct { + data [64]byte +} + +func capturedRetirementHandler() (copilot.SessionEventHandler, <-chan struct{}) { + capture := &responseRetirementCapture{data: [64]byte{1}} + collected := make(chan struct{}) + runtime.AddCleanup(capture, func(done chan struct{}) { close(done) }, collected) + return func(copilot.SessionEvent) { runtime.KeepAlive(capture) }, collected +} + +func awaitRetirementCapture(t *testing.T, collected <-chan struct{}) { + t.Helper() + deadline := time.Now().Add(5 * time.Second) + for { + runtime.GC() + select { + case <-collected: + return + default: + } + if time.Now().After(deadline) { + t.Fatal("force-stopped retained session still roots an unadmitted callback capture") + } + runtime.Gosched() + } +} + +func TestForceStopReleasesRetainedEventCallbacks(t *testing.T) { + for _, blockedHandler := range []bool{false, true} { + t.Run(fmt.Sprintf("blocked handler=%t", blockedHandler), func(t *testing.T) { + t.Parallel() + peer := newResponseRetirementPeer(t, 0) + client := copilot.NewClient(&copilot.ClientOptions{ + Connection: copilot.URIConnection{URL: peer.listener.Addr().String()}, + }) + t.Cleanup(client.ForceStop) + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + if err := client.Start(ctx); err != nil { + t.Fatal("start:", err) + } + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + SessionID: "retirement-session", OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatal("create:", err) + } + entered := make(chan struct{}) + release := make(chan struct{}) + exited := make(chan struct{}) + admitted := false + releaseHandler := sync.OnceFunc(func() { close(release) }) + t.Cleanup(func() { + client.ForceStop() + releaseHandler() + if admitted { + select { + case <-exited: + case <-time.After(5 * time.Second): + t.Error("admitted event handler did not finish") + } + } + }) + if blockedHandler { + session.On(func(copilot.SessionEvent) { + close(entered) + <-release + close(exited) + }) + } + handler, collected := capturedRetirementHandler() + unsubscribe := session.On(handler) + if blockedHandler { + if err := peer.write(map[string]any{ + "jsonrpc": "2.0", "method": "session.event", + "params": map[string]any{ + "sessionId": session.SessionID, + "event": map[string]any{ + "id": "00000000-0000-4000-8000-000000000002", "parentId": nil, + "timestamp": "2026-01-01T00:00:00Z", "type": "session.idle", + "ephemeral": true, "data": map[string]any{}, + }, + }, + }); err != nil { + t.Fatal("notify:", err) + } + select { + case <-entered: + admitted = true + case <-ctx.Done(): + t.Fatal("event handler did not enter:", ctx.Err()) + } + } + client.ForceStop() + awaitRetirementCapture(t, collected) + runtime.KeepAlive(session) + runtime.KeepAlive(unsubscribe) + }) + } +} + +func createRetirementHookSession(t *testing.T, ctx context.Context, client *copilot.Client) (*copilot.Session, <-chan struct{}) { + t.Helper() + capture := &responseRetirementCapture{data: [64]byte{2}} + collected := make(chan struct{}) + runtime.AddCleanup(capture, func(done chan struct{}) { close(done) }, collected) + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + SessionID: "retirement-session", OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Hooks: &copilot.SessionHooks{ + OnSessionEnd: func(copilot.SessionEndHookInput, copilot.HookInvocation) (*copilot.SessionEndHookOutput, error) { + runtime.KeepAlive(capture) + return nil, nil + }, + }, + }) + if err != nil { + t.Fatal("create:", err) + } + return session, collected +} + +func TestForceStopReleasesRetainedHookCallbacks(t *testing.T) { + peer := newResponseRetirementPeer(t, 0) + client := copilot.NewClient(&copilot.ClientOptions{ + Connection: copilot.URIConnection{URL: peer.listener.Addr().String()}, + }) + t.Cleanup(client.ForceStop) + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + if err := client.Start(ctx); err != nil { + t.Fatal("start:", err) + } + session, collected := createRetirementHookSession(t, ctx, client) + client.ForceStop() + awaitRetirementCapture(t, collected) + runtime.KeepAlive(session) +} + +func TestForceStopRejectsAcknowledgedPlainResponseWaits(t *testing.T) { + for _, count := range []int{1, 2} { + for _, blockedHandler := range []bool{false, true} { + t.Run(fmt.Sprintf("%d waits/blocked handler=%t", count, blockedHandler), func(t *testing.T) { + t.Parallel() + peer := newResponseRetirementPeer(t, count) + client := copilot.NewClient(&copilot.ClientOptions{ + Connection: copilot.URIConnection{URL: peer.listener.Addr().String()}, + }) + t.Cleanup(client.ForceStop) + operationCtx, cancelOperation := context.WithTimeout(t.Context(), 10*time.Second) + defer cancelOperation() + if err := client.Start(operationCtx); err != nil { + t.Fatal("start:", err) + } + + session, err := client.CreateSession(operationCtx, &copilot.SessionConfig{ + SessionID: "retirement-session", OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatal("create session:", err) + } + + entered := make(chan struct{}) + release := make(chan struct{}) + releaseHandler := sync.OnceFunc(func() { close(release) }) + t.Cleanup(releaseHandler) + if blockedHandler { + session.On(func(copilot.SessionEvent) { + close(entered) + <-release + }) + } + waitCtx, cancelWait := context.WithCancel(t.Context()) + results := make(chan error, count) + var waits sync.WaitGroup + t.Cleanup(func() { + cancelWait() + releaseHandler() + waits.Wait() + }) + for range count { + waits.Add(1) + go func() { + defer waits.Done() + _, err := session.SendAndWait(waitCtx, copilot.MessageOptions{Prompt: "hold the response"}) + results <- err + }() + } + select { + case <-peer.sent: + case <-operationCtx.Done(): + t.Fatal("send acknowledgments:", operationCtx.Err()) + } + // This later reply proves every preceding send acknowledgment reached the transport. + if _, err := client.Ping(operationCtx, "barrier"); err != nil { + t.Fatal("ping:", err) + } + if blockedHandler { + err := peer.write(map[string]any{ + "jsonrpc": "2.0", "method": "session.event", + "params": map[string]any{ + "sessionId": session.SessionID, + "event": map[string]any{ + "id": "00000000-0000-4000-8000-000000000001", "parentId": nil, + "timestamp": "2026-01-01T00:00:00Z", "type": "session.idle", + "ephemeral": true, "data": map[string]any{}, + }, + }, + }) + if err != nil { + t.Fatal("notify:", err) + } + select { + case <-entered: + case <-operationCtx.Done(): + t.Fatal("event handler did not enter:", operationCtx.Err()) + } + } + stopped := make(chan struct{}) + go func() { + client.ForceStop() + close(stopped) + }() + select { + case <-stopped: + case <-operationCtx.Done(): + t.Fatal("force stop joined the admitted handler:", operationCtx.Err()) + } + for range count { + select { + case err := <-results: + if err == nil || !strings.Contains(err.Error(), "session closed before response completed") { + t.Fatalf("expected response retirement error, got %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("acknowledged response wait remained blocked after force stop") + } + } + }) + } + } +} diff --git a/go/internal/e2e/skill_provider_e2e_test.go b/go/internal/e2e/skill_provider_e2e_test.go new file mode 100644 index 0000000000..d6c2dc93fb --- /dev/null +++ b/go/internal/e2e/skill_provider_e2e_test.go @@ -0,0 +1,586 @@ +package e2e + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" +) + +type providedSkill struct { + descriptor rpc.SkillProviderDescriptor + read func() (string, error) +} + +type testSkillProvider struct { + mu sync.Mutex + skills []providedSkill + calls []string +} + +func newTestSkillProvider(skills []providedSkill) *testSkillProvider { + return &testSkillProvider{skills: skills} +} + +func providedSkillWithMarkdown(name, description, markdown string) providedSkill { + return providedSkill{ + descriptor: rpc.SkillProviderDescriptor{Name: name, Description: description}, + read: func() (string, error) { return markdown, nil }, + } +} + +func (p *testSkillProvider) ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.calls = append(p.calls, "list") + skills := make([]rpc.SkillProviderDescriptor, 0, len(p.skills)) + for _, skill := range p.skills { + skills = append(skills, skill.descriptor) + } + return skills, nil +} + +func (p *testSkillProvider) ReadSkill(ctx context.Context, name string) (string, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.calls = append(p.calls, "read:"+name) + for _, skill := range p.skills { + if skill.descriptor.Name == name { + return skill.read() + } + } + return "", copilot.ErrSkillNotFound +} + +func (p *testSkillProvider) Calls() []string { + p.mu.Lock() + defer p.mu.Unlock() + calls := make([]string, len(p.calls)) + copy(calls, p.calls) + return calls +} + +func (p *testSkillProvider) Reads() []string { + calls := p.Calls() + reads := make([]string, 0, len(calls)) + for _, call := range calls { + if strings.HasPrefix(call, "read:") { + reads = append(reads, strings.TrimPrefix(call, "read:")) + } + } + return reads +} + +// blockingSkillProvider blocks ListSkills until its context is cancelled. +type blockingSkillProvider struct { + entered chan struct{} + cancelled chan struct{} + enterOnce sync.Once + cancelOnce sync.Once +} + +func newBlockingSkillProvider() *blockingSkillProvider { + return &blockingSkillProvider{entered: make(chan struct{}), cancelled: make(chan struct{})} +} + +func (p *blockingSkillProvider) ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) { + p.enterOnce.Do(func() { close(p.entered) }) + <-ctx.Done() + p.cancelOnce.Do(func() { close(p.cancelled) }) + return nil, ctx.Err() +} + +func (p *blockingSkillProvider) ReadSkill(context.Context, string) (string, error) { + return "", copilot.ErrSkillNotFound +} + +func TestSkillProviderE2E(t *testing.T) { + ctx := testharness.NewTestContext(t) + client := ctx.NewClient() + t.Cleanup(func() { client.ForceStop() }) + + t.Run("should load provider skill lazily through skill tool", func(t *testing.T) { + ctx.ConfigureForTest(t) + provider := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown( + "provider-lookup", + "Reports the provider lookup verification word.", + "# Provider lookup\n\nThe verification word is TANGERINE_QUARTZ_19. Reply with it.\n", + ), + }) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + list, err := session.RPC.Skills.List(t.Context()) + if err != nil { + t.Fatalf("Skills.List failed: %v", err) + } + listed := findSkill(list, "provider-lookup") + if listed == nil { + t.Fatal("Expected provider-lookup skill to be listed") + } + if listed.Source != rpc.SkillSourceSDK || !listed.Enabled { + t.Fatalf("Expected provider-lookup source=sdk enabled=true, got %+v", listed) + } + if listed.Path != nil && *listed.Path != "" { + t.Fatalf("Expected provider-lookup path to be empty, got %q", *listed.Path) + } + if reads := provider.Reads(); len(reads) != 0 { + t.Fatalf("Provider reads before skill load = %v, want []", reads) + } + + message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the skill tool to load the provider-lookup skill, then reply with its verification word.", + }) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + + assertEqualStrings(t, provider.Reads(), []string{"provider-lookup"}) + // Validate the final assistant response arrived (guards against truncated captures) + assertAssistantContains(t, message, "TANGERINE_QUARTZ_19") + }) + + t.Run("should load provider and file based skills together", func(t *testing.T) { + ctx.ConfigureForTest(t) + skillsDir := filepath.Join(ctx.WorkDir, "file-skills") + fileSkillDir := filepath.Join(skillsDir, "file-notes") + if err := os.MkdirAll(fileSkillDir, 0755); err != nil { + t.Fatalf("MkdirAll failed: %v", err) + } + if err := os.WriteFile( + filepath.Join(fileSkillDir, "SKILL.md"), + []byte("---\nname: file-notes\ndescription: Reports the file notes verification word.\n---\n\nThe file notes verification word is MAPLE_FALCON_27.\n"), + 0644, + ); err != nil { + t.Fatalf("WriteFile failed: %v", err) + } + provider := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown( + "provider-audit", + "Reports the provider audit verification word.", + "---\nname: provider-audit\nallowed-tools: view\n---\n\nThe provider audit verification word is COBALT_HERON_58.\n", + ), + }) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillDirectories: []string{skillsDir}, + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + list, err := session.RPC.Skills.List(t.Context()) + if err != nil { + t.Fatalf("Skills.List failed: %v", err) + } + fileSkill := findSkill(list, "file-notes") + providerSkill := findSkill(list, "provider-audit") + if fileSkill == nil || fileSkill.Source == rpc.SkillSourceSDK || fileSkill.Path == nil || *fileSkill.Path == "" { + t.Fatalf("Unexpected file-notes skill: %+v", fileSkill) + } + if providerSkill == nil || providerSkill.Source != rpc.SkillSourceSDK { + t.Fatalf("Unexpected provider-audit skill: %+v", providerSkill) + } + + message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the skill tool to load the file-notes skill and the provider-audit skill, then reply with both verification words.", + }) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + + assertEqualStrings(t, provider.Reads(), []string{"provider-audit"}) + assertAssistantContains(t, message, "MAPLE_FALCON_27") + // Validate the final assistant response arrived (guards against truncated captures) + assertAssistantContains(t, message, "COBALT_HERON_58") + }) + + t.Run("should rebind skill provider on resume", func(t *testing.T) { + ctx.ConfigureForTest(t) + original := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is AMBER_ALPHA_11.\n", + ), + }) + replacement := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is BRONZE_BETA_22.\n", + ), + }) + first, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: original, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + sessionID := first.SessionID + if _, err := first.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Without using any tools or skills, reply with exactly REBIND_READY.", + }); err != nil { + t.Fatalf("Initial SendAndWait failed: %v", err) + } + if err := first.Disconnect(); err != nil { + t.Fatalf("Disconnect failed: %v", err) + } + if reads := original.Reads(); len(reads) != 0 { + t.Fatalf("Original provider reads before resume = %v, want []", reads) + } + originalCallsBeforeResume := len(original.Calls()) + + session, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: replacement, + }) + if err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the skill tool to load the rebind-check skill, then reply with its verification word.", + }) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + + assertEqualStrings(t, replacement.Reads(), []string{"rebind-check"}) + if got := len(original.Calls()); got != originalCallsBeforeResume { + t.Fatalf("Original provider call count after resume = %d, want %d", got, originalCallsBeforeResume) + } + // Validate the final assistant response arrived (guards against truncated captures) + assertAssistantContains(t, message, "BRONZE_BETA_22") + assertAssistantNotContains(t, message, "AMBER_ALPHA_11") + }) + + t.Run("should report provider read failure without leaking details", func(t *testing.T) { + ctx.ConfigureForTest(t) + secret := "PROVIDER_SECRET_7F3A9C" + provider := newTestSkillProvider([]providedSkill{ + { + descriptor: rpc.SkillProviderDescriptor{ + Name: "broken-lookup", + Description: "Reports the broken lookup verification word.", + }, + read: func() (string, error) { + return "", errors.New("database unavailable: " + secret) + }, + }, + }) + events := newEventRecorder() + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: provider, + OnEvent: events.Record, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the skill tool to load the broken-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + }) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + + assertContainsString(t, provider.Reads(), "broken-lookup") + failures := failedToolCompletions(events.Snapshot()) + if len(failures) != 1 { + t.Fatalf("Expected exactly 1 failed tool completion, got %d", len(failures)) + } + rawEvents, err := json.Marshal(events.Snapshot()) + if err != nil { + t.Fatalf("Marshal events failed: %v", err) + } + if strings.Contains(string(rawEvents), secret) { + t.Fatalf("Provider error leaked secret in events: %s", rawEvents) + } + // Validate the final assistant response arrived (guards against truncated captures) + assertAssistantContains(t, message, "LOAD_FAILED") + }) + + t.Run("should report missing provider skill as not found", func(t *testing.T) { + ctx.ConfigureForTest(t) + provider := newTestSkillProvider([]providedSkill{ + { + descriptor: rpc.SkillProviderDescriptor{ + Name: "vanished-lookup", + Description: "Reports the vanished lookup verification word.", + }, + read: func() (string, error) { + return "", copilot.ErrSkillNotFound + }, + }, + }) + events := newEventRecorder() + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: provider, + OnEvent: events.Record, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the skill tool to load the vanished-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + }) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + + assertContainsString(t, provider.Reads(), "vanished-lookup") + failures := failedToolCompletions(events.Snapshot()) + if len(failures) != 1 { + t.Fatalf("Expected exactly 1 failed tool completion, got %d", len(failures)) + } + rawFailure, err := json.Marshal(failures[0]) + if err != nil { + t.Fatalf("Marshal failure failed: %v", err) + } + if !strings.Contains(strings.ToLower(string(rawFailure)), "not found") { + t.Fatalf("Expected failure to mention not found, got %s", rawFailure) + } + // Validate the final assistant response arrived (guards against truncated captures) + assertAssistantContains(t, message, "LOAD_FAILED") + }) + + t.Run("should keep provider dormant when skills disabled", func(t *testing.T) { + ctx.ConfigureWithoutSnapshot(t) + provider := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown("dormant-lookup", "Never listed.", "Never read.\n"), + }) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + EnableSkills: copilot.Bool(false), + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + if _, err := session.RPC.Skills.EnsureLoaded(t.Context()); err != nil { + t.Fatalf("Skills.EnsureLoaded failed: %v", err) + } + list, err := session.RPC.Skills.List(t.Context()) + if err != nil { + t.Fatalf("Skills.List failed: %v", err) + } + if sdkSkills := sdkSkills(list); len(sdkSkills) != 0 { + t.Fatalf("Expected no sdk skills when disabled, got %+v", sdkSkills) + } + if calls := provider.Calls(); len(calls) != 0 { + t.Fatalf("Provider calls when skills disabled = %v, want []", calls) + } + }) + + t.Run("should unbind provider when resumed without one", func(t *testing.T) { + ctx.ConfigureWithoutSnapshot(t) + provider := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown("unbound-lookup", "Reports the unbound lookup word.", "Unbound.\n"), + }) + first, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + before, err := first.RPC.Skills.List(t.Context()) + if err != nil { + t.Fatalf("Skills.List before resume failed: %v", err) + } + if findSkill(before, "unbound-lookup") == nil { + t.Fatal("Expected unbound-lookup before resume") + } + callsBeforeResume := len(provider.Calls()) + + session, err := client.ResumeSession(t.Context(), first.SessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + if _, err := session.RPC.Skills.Reload(t.Context()); err != nil { + t.Fatalf("Skills.Reload failed: %v", err) + } + list, err := session.RPC.Skills.List(t.Context()) + if err != nil { + t.Fatalf("Skills.List after resume failed: %v", err) + } + if sdkSkills := sdkSkills(list); len(sdkSkills) != 0 { + t.Fatalf("Expected no sdk skills after unbound resume, got %+v", sdkSkills) + } + if got := len(provider.Calls()); got != callsBeforeResume { + t.Fatalf("Provider call count after unbound resume = %d, want %d", got, callsBeforeResume) + } + }) + + t.Run("should cancel a blocked provider call when the session disconnects", func(t *testing.T) { + ctx.ConfigureWithoutSnapshot(t) + provider := newBlockingSkillProvider() + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + // The list RPC fails once the binding is removed; only the provider's + // cancellation matters here. + go func() { _, _ = session.RPC.Skills.List(context.Background()) }() + select { + case <-provider.entered: + case <-time.After(30 * time.Second): + t.Fatal("provider ListSkills was not called") + } + + if err := session.Disconnect(); err != nil { + t.Fatalf("Disconnect failed: %v", err) + } + select { + case <-provider.cancelled: + case <-time.After(10 * time.Second): + t.Fatal("provider context was not cancelled after Disconnect") + } + }) + + t.Run("should reject skill provider for cloud sessions", func(t *testing.T) { + ctx.ConfigureWithoutSnapshot(t) + provider := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown("cloud-lookup", "Never listed.", "Never read.\n"), + }) + + _, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Cloud: &copilot.CloudSessionOptions{}, + SkillProvider: provider, + }) + if err == nil || err.Error() != "Skill providers are not supported for cloud sessions." { + t.Fatalf("CreateSession error = %v, want cloud skill provider rejection", err) + } + if calls := provider.Calls(); len(calls) != 0 { + t.Fatalf("Provider calls after cloud rejection = %v, want []", calls) + } + }) +} + +type eventRecorder struct { + mu sync.Mutex + events []copilot.SessionEvent +} + +func newEventRecorder() *eventRecorder { + return &eventRecorder{} +} + +func (r *eventRecorder) Record(event copilot.SessionEvent) { + r.mu.Lock() + defer r.mu.Unlock() + r.events = append(r.events, event) +} + +func (r *eventRecorder) Snapshot() []copilot.SessionEvent { + r.mu.Lock() + defer r.mu.Unlock() + events := make([]copilot.SessionEvent, len(r.events)) + copy(events, r.events) + return events +} + +func findSkill(list *rpc.SkillList, name string) *rpc.Skill { + if list == nil { + return nil + } + for i := range list.Skills { + if list.Skills[i].Name == name { + return &list.Skills[i] + } + } + return nil +} + +func sdkSkills(list *rpc.SkillList) []rpc.Skill { + if list == nil { + return nil + } + var matches []rpc.Skill + for _, skill := range list.Skills { + if skill.Source == rpc.SkillSourceSDK { + matches = append(matches, skill) + } + } + return matches +} + +func failedToolCompletions(events []copilot.SessionEvent) []*copilot.ToolExecutionCompleteData { + var failures []*copilot.ToolExecutionCompleteData + for _, event := range events { + if data, ok := event.Data.(*copilot.ToolExecutionCompleteData); ok && !data.Success { + failures = append(failures, data) + } + } + return failures +} + +func assertAssistantNotContains(t *testing.T, event *copilot.SessionEvent, text string) { + t.Helper() + data, ok := event.Data.(*copilot.AssistantMessageData) + if !ok { + t.Fatalf("Expected AssistantMessageData, got %T", event.Data) + } + if strings.Contains(data.Content, text) { + t.Fatalf("Expected assistant response not to contain %q, got %q", text, data.Content) + } +} + +func assertEqualStrings(t *testing.T, got, want []string) { + t.Helper() + if len(got) != len(want) { + t.Fatalf("strings = %v, want %v", got, want) + } + for i := range got { + if got[i] != want[i] { + t.Fatalf("strings = %v, want %v", got, want) + } + } +} + +func assertContainsString(t *testing.T, got []string, want string) { + t.Helper() + for _, value := range got { + if value == want { + return + } + } + t.Fatalf("strings = %v, want to contain %q", got, want) +} diff --git a/go/rpc/mcp_list_test.go b/go/rpc/mcp_list_test.go new file mode 100644 index 0000000000..c2e9c47a09 --- /dev/null +++ b/go/rpc/mcp_list_test.go @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package rpc + +import ( + "context" + "encoding/json" + "io" + "testing" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" +) + +var _ interface { + List(context.Context) (*MCPServerList, error) + ListConfigured(context.Context) (*MCPConfiguredServerList, error) +} = (*MCPAPI)(nil) + +func TestMCPListUsesParameterlessWireContract(t *testing.T) { + clientToServerReader, clientToServerWriter := io.Pipe() + serverToClientReader, serverToClientWriter := io.Pipe() + client := jsonrpc2.NewClient(clientToServerWriter, serverToClientReader) + server := jsonrpc2.NewClient(serverToClientWriter, clientToServerReader) + requests := make(chan map[string]any, 2) + server.SetRequestHandler("session.mcp.list", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request map[string]any + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: err.Error()} + } + requests <- request + return json.RawMessage(`{"servers":[]}`), nil + }) + server.SetRequestHandler("session.mcp.listConfigured", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request map[string]any + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: err.Error()} + } + requests <- request + return json.RawMessage(`{"servers":[]}`), nil + }) + client.Start() + server.Start() + t.Cleanup(func() { + client.Stop() + server.Stop() + _ = clientToServerWriter.Close() + _ = clientToServerReader.Close() + _ = serverToClientWriter.Close() + _ = serverToClientReader.Close() + }) + + mcp := NewSessionRPC(client, "session-1").MCP + if _, err := mcp.List(t.Context()); err != nil { + t.Fatal(err) + } + if _, err := mcp.ListConfigured(t.Context()); err != nil { + t.Fatal(err) + } + got := make([]map[string]any, 2) + for i := range got { + got[i] = <-requests + } + for _, request := range got { + if request["sessionId"] != "session-1" || len(request) != 1 { + t.Fatalf("request = %#v, want only the bound session ID", request) + } + } +} diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index c0af9e58ab..add6bc105c 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -468,6 +468,37 @@ type AgentReloadResult struct { Agents []AgentInfo `json:"agents"` } +// The models a custom agent asks for, and the models actually available. +// Experimental: AgentsCustomAgentInitialModelDecisionParams is part of an experimental API +// and may change or be removed. +// Internal: AgentsCustomAgentInitialModelDecisionParams is an internal SDK API and is not +// part of the public surface. +type AgentsCustomAgentInitialModelDecisionParams struct { + // The agent's declared `model:` entry, serialized. A single name or an ordered list of + // acceptable names. + AgentModelsJSON string `json:"agentModelsJson"` + // The models available to this session, serialized in the shape the model list carries. + AvailableModelsJSON string `json:"availableModelsJson"` +} + +// The model to switch to, and the warning to show when the agent's preference could not be +// met. +// Experimental: AgentsCustomAgentInitialModelDecisionResult is part of an experimental API +// and may change or be removed. +// Internal: AgentsCustomAgentInitialModelDecisionResult is an internal SDK API and is not +// part of the public surface. +type AgentsCustomAgentInitialModelDecisionResult struct { + // The reasoning effort attached to the selected model preference. Absent when that + // preference does not specify an effort. + ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // The first available model that matches the agent's preferences. Absent when none of the + // requested models is available. + TargetModel *string `json:"targetModel,omitempty"` + // What to tell the user about an unmet preference. Absent when the preference was met. A + // warning with no `targetModel` means the agent's models are all unavailable. + Warning *string `json:"warning,omitempty"` +} + // Optional project paths to include in agent discovery. // Experimental: AgentsDiscoverRequest is part of an experimental API and may change or be // removed. @@ -506,6 +537,101 @@ type AgentSetPromptRequest struct { Prompt string `json:"prompt"` } +// The feature flags to evaluate shipped agents against. +// Experimental: AgentsGetAvailableBuiltinsRequest is part of an experimental API and may +// change or be removed. +// Internal: AgentsGetAvailableBuiltinsRequest is an internal SDK API and is not part of the +// public surface. +type AgentsGetAvailableBuiltinsRequest struct { + // The surface asking, which gates agents that only apply to one client. Omit or pass null + // to apply no client filter. + Context *string `json:"context,omitempty"` + // Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or + // pass null for no flags. + FeatureFlags map[string]any `json:"featureFlags,omitzero"` + // Flag overrides keyed by name. A null entry uses the corresponding base flag; false + // explicitly disables it. Omit or pass null for no overrides. + Overrides map[string]any `json:"overrides,omitzero"` +} + +// The shipped agents available under the requested flags. +// Experimental: AgentsGetAvailableBuiltinsResult is part of an experimental API and may +// change or be removed. +// Internal: AgentsGetAvailableBuiltinsResult is an internal SDK API and is not part of the +// public surface. +type AgentsGetAvailableBuiltinsResult struct { + // Available shipped agents, in the runtime's own order. + // Internal: Agents is part of the SDK's internal API surface and is not intended for + // external use. + Agents []BuiltinAgentSummary `json:"agents"` +} + +// The shipped agent whose definition to load. +// Experimental: AgentsGetBuiltinDefinitionRequest is part of an experimental API and may +// change or be removed. +// Internal: AgentsGetBuiltinDefinitionRequest is an internal SDK API and is not part of the +// public surface. +type AgentsGetBuiltinDefinitionRequest struct { + // The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside + // that list is special-cased in code and has no definition, and is reported as an error + // rather than as an empty definition. + Name string `json:"name"` +} + +// One shipped agent's definition. +// Experimental: AgentsGetBuiltinDefinitionResult is part of an experimental API and may +// change or be removed. +// Internal: AgentsGetBuiltinDefinitionResult is an internal SDK API and is not part of the +// public surface. +type AgentsGetBuiltinDefinitionResult struct { + // The agent's definition, serialized as JSON. It carries the authored keys plus the + // runtime's projected `__nativeCustomAgent` view of the same agent. It is a string rather + // than an object because the runtime parses it with the agent schema's tolerant shape, + // which accepts keys this contract does not name. + DefinitionJSON string `json:"definitionJson"` +} + +// The shipped agent whose listing entry to load. +// Experimental: AgentsGetBuiltinListingDefinitionRequest is part of an experimental API and +// may change or be removed. +// Internal: AgentsGetBuiltinListingDefinitionRequest is an internal SDK API and is not part +// of the public surface. +type AgentsGetBuiltinListingDefinitionRequest struct { + // The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the + // agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here + // too, from its in-code definition. + Name string `json:"name"` +} + +// One shipped agent, projected for a listing. +// Experimental: AgentsGetBuiltinListingDefinitionResult is part of an experimental API and +// may change or be removed. +// Internal: AgentsGetBuiltinListingDefinitionResult is an internal SDK API and is not part +// of the public surface. +type AgentsGetBuiltinListingDefinitionResult struct { + // The agent projected as a custom agent, serialized as JSON. It is a string rather than an + // object for the same reason as `getBuiltinDefinition`: the runtime parses the underlying + // definition with the agent schema's tolerant shape, which accepts keys this contract does + // not name. + DefinitionJSON string `json:"definitionJson"` +} + +// The agents this runtime ships, named so a consumer can tell them apart from authored ones. +// Experimental: AgentsGetBuiltinsResult is part of an experimental API and may change or be +// removed. +// Internal: AgentsGetBuiltinsResult is an internal SDK API and is not part of the public +// surface. +type AgentsGetBuiltinsResult struct { + // The subset of `names` a user is allowed to turn off. A shipped agent outside this list is + // always active and a client should not offer a toggle for it. + DisableableNames []string `json:"disableableNames"` + // Every agent name this runtime ships. + Names []string `json:"names"` + // The subset of `names` defined by a shipped YAML definition. The remainder are + // special-cased in code and have no definition to load. + YamlBasedNames []string `json:"yamlBasedNames"` +} + // Optional project paths to include when enumerating agent discovery directories. // Experimental: AgentsGetDiscoveryPathsRequest is part of an experimental API and may // change or be removed. @@ -1034,6 +1160,21 @@ func (r RawAuthInfoData) Type() AuthInfoType { return r.Discriminator } +// An interactive account whose model provider owns its credentials. It carries no GitHub +// credential. +// Experimental: AccountAuthInfo is part of an experimental API and may change or be removed. +type AccountAuthInfo struct { + // Host coordinate owned by the account's model provider. + Host string `json:"host"` + // Login identifying the provider-owned account. + Login string `json:"login"` +} + +func (AccountAuthInfo) authInfo() {} +func (AccountAuthInfo) Type() AuthInfoType { + return AuthInfoTypeAccount +} + // Authentication-info input variant for API-key authentication to a non-GitHub LLM // provider, carrying the secret `apiKey` and host. // Experimental: APIKeyAuthInfo is part of an experimental API and may change or be removed. @@ -1193,6 +1334,20 @@ func (UserAuthInfo) Type() AuthInfoType { return AuthInfoTypeUser } +// A credential-free account choice after sign-in. +// Experimental: AuthLoginAccount is part of an experimental API and may change or be +// removed. +type AuthLoginAccount struct { + // Host coordinate owned by the selected account's provider. + Host string `json:"host"` + // Provider kind that owns this account choice. + Kind AccountKind `json:"kind"` + // Human-readable login for the account choice. + Login string `json:"login"` + // Opaque identifier supplied to the next login step to select this account. + SelectionID string `json:"selectionId"` +} + // Advance an in-flight login flow, optionally fulfilling an input-required step. // Experimental: AuthLoginAdvanceRequest is part of an experimental API and may change or be // removed. @@ -1229,15 +1384,18 @@ type AuthLoginCancelRequest struct { FlowID string `json:"flowId"` } -// Terminal result of an interactive login flow. +// Result of an interactive login flow. Pending consent or account selection is not terminal. // Experimental: AuthLoginResultDto is part of an experimental API and may change or be // removed. type AuthLoginResultDto struct { + // Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 + // first. + Accounts []AuthLoginAccount `json:"accounts,omitzero"` // Host that was signed in, when completed. Host *string `json:"host,omitempty"` // Login that was signed in, when completed. Login *string `json:"login,omitempty"` - // Terminal disposition of the login. + // Current disposition of the login, including pending user decisions. Status AuthLoginResultStatus `json:"status"` } @@ -1271,7 +1429,8 @@ func (AuthLoginStepAwaiting) Kind() AuthLoginStepKind { } type AuthLoginStepCompleted struct { - // The terminal login result. + // Login result. When status is needs-plaintext-consent or needs-account-selection, advance + // with the user's decision to continue. Result AuthLoginResultDto `json:"result"` } @@ -1380,6 +1539,9 @@ func (r RawAuthReadValueData) Kind() AuthReadValueKind { type AuthReadValueActiveAccount struct { // The active account, or absent when not logged in. Account *AccountStatus `json:"account,omitempty"` + // Credential-free identity metadata for the active account, including resolved Copilot user + // information when available. + AuthInfo *AuthIdentity `json:"authInfo,omitempty"` } func (AuthReadValueActiveAccount) authReadValue() {} @@ -1540,6 +1702,57 @@ type AutopilotObjectiveState struct { TurnCount int64 `json:"turnCount"` } +// A server-advertised routing preference. Identifiers and execution types are extensible. +// Experimental: AutoTierDescriptor is part of an experimental API and may change or be +// removed. +type AutoTierDescriptor struct { + // Description displayed beside the preference. + Description string `json:"description"` + // Human-readable label, not a routing identifier. + DisplayName string `json:"displayName"` + // Opaque routing identifier transmitted unchanged to the provider. + ID string `json:"id"` + // Current account-specific availability. + Status AutoTierStatus `json:"status"` + // Execution kind; this client supports `auto` preferences on the Auto model. + Type string `json:"type"` +} + +// Account-bound discovery metadata for the virtual `auto` model. +// Experimental: AutoTierMetadata is part of an experimental API and may change or be +// removed. +type AutoTierMetadata struct { + // Provider-default preference, used only when no explicit preference exists. + DefaultTier string `json:"defaultTier"` + // Provider that supplied this metadata, when the catalog is provider-attributed. + ProviderID *string `json:"providerId,omitempty"` + // Routing preferences in the server's presentation order. + Tiers []AutoTierDescriptor `json:"tiers"` +} + +// Availability of a server-advertised routing preference. +// Experimental: AutoTierStatus is part of an experimental API and may change or be removed. +type AutoTierStatus struct { + // Whether the provider permits selecting this preference. + Enabled bool `json:"enabled"` + // Human-readable explanation of availability. + Message *string `json:"message,omitempty"` + // Extensible machine-readable unavailability reason. + Reason *string `json:"reason,omitempty"` +} + +// A shipped agent, named and described. +// Experimental: BuiltinAgentSummary is part of an experimental API and may change or be +// removed. +// Internal: BuiltinAgentSummary is an internal SDK API and is not part of the public +// surface. +type BuiltinAgentSummary struct { + // One-line description of what the agent does. + Description string `json:"description"` + // The agent name, as it appears in `getBuiltins`. + Name string `json:"name"` +} + // The running runtime's complete catalog of well-known built-in model IDs, including // supported models and additional IDs with built-in metadata. // Experimental: BuiltInModelCatalog is part of an experimental API and may change or be @@ -3237,6 +3450,94 @@ type ConnectorDisconnectResult struct { Status ConnectorStatus `json:"status"` } +// Eligible account. +// Experimental: ConnectorDiscoveryAccount is part of an experimental API and may change or +// be removed. +type ConnectorDiscoveryAccount struct { + // Opaque account ID. + AccountID string `json:"accountId"` + // Account metadata. + AuthInfo ConnectorDiscoveryAuthInfo `json:"authInfo"` +} + +// Eligible accounts. +// Experimental: ConnectorDiscoveryAccountList is part of an experimental API and may change +// or be removed. +type ConnectorDiscoveryAccountList struct { + // Eligible accounts. + Accounts []ConnectorDiscoveryAccount `json:"accounts"` + // Availability. + Availability ConnectorDiscoveryAvailability `json:"availability"` +} + +// Selected account. +// Experimental: ConnectorDiscoveryAccountRequest is part of an experimental API and may +// change or be removed. +type ConnectorDiscoveryAccountRequest struct { + // Opaque account ID. + AccountID string `json:"accountId"` +} + +// Account metadata. +// Experimental: ConnectorDiscoveryAuthInfo is part of an experimental API and may change or +// be removed. +type ConnectorDiscoveryAuthInfo struct { + // Host. + Host string `json:"host"` + // Login. + Login string `json:"login"` + // Authentication type. + Type AuthInfoType `json:"type"` +} + +// Feature availability. +// Experimental: ConnectorDiscoveryCapabilities is part of an experimental API and may +// change or be removed. +type ConnectorDiscoveryCapabilities struct { + // API version. + APIVersion int64 `json:"apiVersion"` + // Availability. + Availability ConnectorDiscoveryAvailability `json:"availability"` + // Whether results are cached. + ConditionalCache bool `json:"conditionalCache"` + // Whether accounts are selected by opaque ID. + OpaqueAccountSelection bool `json:"opaqueAccountSelection"` +} + +// Entry. +// Experimental: ConnectorDiscoveryCatalogEntry is part of an experimental API and may +// change or be removed. +type ConnectorDiscoveryCatalogEntry struct { + // Description. + Description *string `json:"description,omitempty"` + // Display name. + DisplayName string `json:"displayName"` + // Logo. + Logo *string `json:"logo,omitempty"` + // Name. + Name string `json:"name"` + // Release tag. + ReleaseTag *string `json:"releaseTag,omitempty"` + // Status. + Status ConnectorCatalogStatus `json:"status"` + // Tier. + Tier *string `json:"tier,omitempty"` +} + +// Entries for the selected account. +// Experimental: ConnectorDiscoveryCatalogResult is part of an experimental API and may +// change or be removed. +type ConnectorDiscoveryCatalogResult struct { + // Opaque account ID. + AccountID string `json:"accountId"` + // Entries. + Connectors []ConnectorDiscoveryCatalogEntry `json:"connectors"` + // Refresh time in Unix epoch milliseconds. + RefreshedAtMs int64 `json:"refreshedAtMs"` + // Revision. + Revision int64 `json:"revision"` +} + // Requests authoritative Connector-to-MCP reconciliation for the pinned account. // Experimental: ConnectorReconcileRequest is part of an experimental API and may change or // be removed. @@ -3634,6 +3935,11 @@ type CurrentModel struct { // no plan override has captured a base model. Persistent agent model requirements apply to // this model rather than the temporary plan model. PlanBaseModelID *string `json:"planBaseModelId,omitempty"` + // Provider of planBaseModelId, when that saved selection is provider-qualified. + PlanBaseProviderID *string `json:"planBaseProviderId,omitempty"` + // Provider selected for this model. Together with modelId, identifies a catalog entry even + // when providers serve the same model. Omitted for unattributed legacy selections. + ProviderID *string `json:"providerId,omitempty"` // Reasoning effort level currently applied to the active model, when one is set. Reads // `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the // two values are reported as a snapshot. @@ -4710,6 +5016,26 @@ type FolderTrustCheckResult struct { Trusted bool `json:"trusted"` } +// The remote the checked-out branch tracks. +// Experimental: GitCurrentBranchRemoteResult is part of an experimental API and may change +// or be removed. +// Internal: GitCurrentBranchRemoteResult is an internal SDK API and is not part of the +// public surface. +type GitCurrentBranchRemoteResult struct { + // Name of the tracked remote. Reports `origin` whenever the working tree has no tracking + // configuration to read, including on a detached HEAD, so this is never null and never + // empty. + Remote string `json:"remote"` +} + +// Working-tree path a git query applies to. +// Experimental: GitCwdRequest is part of an experimental API and may change or be removed. +// Internal: GitCwdRequest is an internal SDK API and is not part of the public surface. +type GitCwdRequest struct { + // Absolute path to a directory inside the git working tree to query. + Cwd string `json:"cwd"` +} + // Safe discovery information. Host-side relay bootstrap credentials are never included. // Experimental: GitHubEnvironment is part of an experimental API and may change or be // removed. @@ -4736,6 +5062,91 @@ type GitHubEnvironment struct { Status string `json:"status"` } +// A GitHub login the authenticated user may act as: their own account, or an organization +// they belong to. +// Experimental: GitHubOwnerOption is part of an experimental API and may change or be +// removed. +// Internal: GitHubOwnerOption is an internal SDK API and is not part of the public surface. +type GitHubOwnerOption struct { + // The owner's GitHub login. + Login string `json:"login"` + // Which kind of owner this is. The authenticated user's own account is always reported as + // `user`. + Type string `json:"type"` +} + +// The owner listing to abandon. +// Experimental: GitHubOwnersCancelRequest is part of an experimental API and may change or +// be removed. +// Internal: GitHubOwnersCancelRequest is an internal SDK API and is not part of the public +// surface. +type GitHubOwnersCancelRequest struct { + // Request id the listing was started with. + RequestID int64 `json:"requestId"` +} + +// Whether the id named a running owner listing. +// Experimental: GitHubOwnersCancelResult is part of an experimental API and may change or +// be removed. +// Internal: GitHubOwnersCancelResult is an internal SDK API and is not part of the public +// surface. +type GitHubOwnersCancelResult struct { + // True when a listing with the id was running and the cancel stopped it. False when the id + // was never registered, was registered but unused, was released after being abandoned, or + // its listing had ended. An unused id is released and cannot start a later listing. + Canceled bool `json:"canceled"` +} + +// Credential to list owners under, and the request id that makes the listing cancellable. +// Experimental: GitHubOwnersListRequest is part of an experimental API and may change or be +// removed. +// Internal: GitHubOwnersListRequest is an internal SDK API and is not part of the public +// surface. +type GitHubOwnersListRequest struct { + // The credential the listing runs under, carried opaquely because its shape is the host's + // own and the runtime only resolves a token and a GitHub host from it. No credential + // travels: this selects one the runtime already holds. + AuthInfo any `json:"authInfo"` + // Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled + // before use, released after being abandoned, or already used is refused rather than + // silently running uncancellable. + RequestID int64 `json:"requestId"` +} + +// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except +// that `throwError` reports a failure the caller is expected to raise rather than render. +// Experimental: GitHubOwnersListResult is part of an experimental API and may change or be +// removed. +// Internal: GitHubOwnersListResult is an internal SDK API and is not part of the public +// surface. +type GitHubOwnersListResult struct { + // Why no owners could be listed, phrased for a user. Present when the listing failed in a + // way the caller should render rather than raise. + Message *string `json:"message,omitempty"` + // The owners, on success: the authenticated user first, then the organizations they belong + // to. + // Internal: Owners is part of the SDK's internal API surface and is not intended for + // external use. + Owners []GitHubOwnerOption `json:"owners,omitzero"` + // A malformed request or an unreadable credential, which the caller raises instead of + // rendering. Kept a field rather than a dispatch error so it stays distinct from `message`, + // which the caller renders. + ThrowError *string `json:"throwError,omitempty"` + // A line the caller should log. Present only alongside `message`, and only for failures + // worth recording. + Warning *string `json:"warning,omitempty"` +} + +// A freshly registered request id. Registering it before the listing starts is what lets a +// cancel that races the request still find the owner listing slot. The id serves one +// listing only. Long-abandoned unused ids can be released by later allocations. +// Experimental: GitHubOwnersRequestIDResult is part of an experimental API and may change +// or be removed. +type GitHubOwnersRequestIDResult struct { + // Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`. + RequestID int64 `json:"requestId"` +} + // Pointer to a GitHub repository. // Experimental: GitHubRepoRef is part of an experimental API and may change or be removed. type GitHubRepoRef struct { @@ -4747,6 +5158,39 @@ type GitHubRepoRef struct { Owner string `json:"owner"` } +// Working-tree path whose owning GitHub repository should be resolved. +// Experimental: GitHubRepositoryAtPathRequest is part of an experimental API and may change +// or be removed. +// Internal: GitHubRepositoryAtPathRequest is an internal SDK API and is not part of the +// public surface. +type GitHubRepositoryAtPathRequest struct { + // Absolute path to a directory inside the git working tree to resolve. + Path string `json:"path"` +} + +// The GitHub repository that owns the requested path, when the selected remote (`origin`, +// else the first) is on a GitHub host. +// Experimental: GitHubRepositoryAtPathResult is part of an experimental API and may change +// or be removed. +// Internal: GitHubRepositoryAtPathResult is an internal SDK API and is not part of the +// public surface. +type GitHubRepositoryAtPathResult struct { + // Resolved repository identity, or null when the selected remote resolves to no GitHub host. + Repository *GitHubRepositoryIdentity `json:"repository,omitempty"` +} + +// Owner, name, and host of a GitHub repository, as resolved from a git remote URL. +// Experimental: GitHubRepositoryIdentity is part of an experimental API and may change or +// be removed. +type GitHubRepositoryIdentity struct { + // Host the remote points at, for example `github.com` or a GitHub Enterprise hostname. + Host string `json:"host"` + // Repository name, without the owner prefix or the `.git` suffix. + Name string `json:"name"` + // Repository owner login (user or organization). + Owner string `json:"owner"` +} + // Client environment metadata describing the process that produced a telemetry event. // Experimental: GitHubTelemetryClientInfo is part of an experimental API and may change or // be removed. @@ -4883,6 +5327,142 @@ func (GitHubTokenAcquireResultToken) Kind() GitHubTokenAcquireResultKind { return GitHubTokenAcquireResultKindToken } +// A GitHub repository one of a working tree's remotes points at. +// Experimental: GitRemoteRepository is part of an experimental API and may change or be +// removed. +// Internal: GitRemoteRepository is an internal SDK API and is not part of the public +// surface. +type GitRemoteRepository struct { + // GitHub host serving the repository, which is not `github.com` for a GitHub Enterprise + // remote. + Host string `json:"host"` + // Repository name, without the owner. + Name string `json:"name"` + // Account or organization owning the repository. + Owner string `json:"owner"` + // Name of the first remote that produced this distinct repository entry, such as `origin` + // or `upstream`. + RemoteName string `json:"remoteName"` +} + +// Git working tree whose GitHub remotes should be listed. +// Experimental: GitReposFromRemotesRequest is part of an experimental API and may change or +// be removed. +// Internal: GitReposFromRemotesRequest is an internal SDK API and is not part of the public +// surface. +type GitReposFromRemotesRequest struct { + // Absolute path to the root of the git working tree. + GitRoot string `json:"gitRoot"` +} + +// The GitHub repositories a working tree's remotes point at. +// Experimental: GitReposFromRemotesResult is part of an experimental API and may change or +// be removed. +// Internal: GitReposFromRemotesResult is an internal SDK API and is not part of the public +// surface. +type GitReposFromRemotesResult struct { + // One entry per distinct GitHub repository, in the order git reports the first remote for + // each repository. Empty when no remote points at a GitHub host, which a caller should read + // as `not connected to GitHub`. Failing to read the remotes is an error, not an empty list. + // Internal: Repositories is part of the SDK's internal API surface and is not intended for + // external use. + Repositories []GitRemoteRepository `json:"repositories"` +} + +// Selects the configuration directory whose machine-wide state to read. +// Experimental: GlobalStateLoadForConfigDirRequest is part of an experimental API and may +// change or be removed. +// Internal: GlobalStateLoadForConfigDirRequest is an internal SDK API and is not part of +// the public surface. +type GlobalStateLoadForConfigDirRequest struct { + // Copilot configuration directory to read the state document from, taking precedence over + // the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to + // read the directory the server resolved for itself. + ConfigDir *string `json:"configDir,omitempty"` +} + +// The host's machine-wide state. Every field is optional because a fresh install has +// recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a +// negative answer. Stored credentials are deliberately absent from this shape. +// Experimental: GlobalStateLoadResult is part of an experimental API and may change or be +// removed. +// Internal: GlobalStateLoadResult is an internal SDK API and is not part of the public +// surface. +type GlobalStateLoadResult struct { + // Whether the user has answered the prompt suggesting they install the desktop app. + AppInstallNudgeResponded *bool `json:"appInstallNudgeResponded,omitempty"` + // Whether the app tip has been shown. + AppTipShown *bool `json:"appTipShown,omitempty"` + // Terminals the user has already been asked to set up, so the host does not ask twice. + AskedSetupTerminals []string `json:"askedSetupTerminals,omitzero"` + // When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the + // once-per-day cap for non-staff users across restarts. + AutoFeedbackLastPromptedAt *string `json:"autoFeedbackLastPromptedAt,omitempty"` + // When the host first ran on this machine. + FirstLaunchAt *string `json:"firstLaunchAt,omitempty"` + // Plugins installed on this machine. + InstalledPlugins []InstalledPlugin `json:"installedPlugins,omitzero"` + // Account used for the most recent sign-in. + // Internal: LastLoggedInUser is part of the SDK's internal API surface and is not intended + // for external use. + LastLoggedInUser *LoggedInUser `json:"lastLoggedInUser,omitempty"` + // Every account the host has signed in to on this machine. + // Internal: LoggedInUsers is part of the SDK's internal API surface and is not intended for + // external use. + LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitzero"` + // Whether the one-off cleanup of stored reasoning summaries has run. + ReasoningSummariesCleanupDone *bool `json:"reasoningSummariesCleanupDone,omitempty"` + // Models the user selected recently, most recent first. + RecentModelIDs []string `json:"recentModelIds,omitzero"` + // Whether the user declined to trust the sandbox credential proxy CA. + SandboxCredentialProxyCaDeclined *bool `json:"sandboxCredentialProxyCaDeclined,omitempty"` + // Whether the sandbox onboarding has been shown. + SandboxOnboardingShown *bool `json:"sandboxOnboardingShown,omitempty"` + // Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only + // behavior. + Staff *bool `json:"staff,omitempty"` + // Whether the user was recognized as GitHub staff. + StaffGitHub *bool `json:"staffGithub,omitempty"` + // When the staff-only log level migration last ran. + StaffLogLevelMigrationAt *string `json:"staffLogLevelMigrationAt,omitempty"` + // Whether the user was recognized as Microsoft staff. + StaffMicrosoft *bool `json:"staffMicrosoft,omitempty"` + // When the staff-only model reset last ran. + StaffModelResetAt *string `json:"staffModelResetAt,omitempty"` + // When the staff-only update channel migration last ran. + StaffUpdateChannelMigrationAt *string `json:"staffUpdateChannelMigrationAt,omitempty"` + // Folders where the user declined the init prompt, so it stays hidden there. + SuppressInitFolders []string `json:"suppressInitFolders,omitzero"` + // Folders the user has marked as trusted. + TrustedFolders []string `json:"trustedFolders,omitzero"` +} + +// A single top-level key to record in the host's machine-wide state. The write replaces +// only that key and leaves the rest of the document untouched, so two writers recording +// different one-off flags do not overwrite each other. The stored credential keys cannot be +// written through this method. +// Experimental: GlobalStateWriteKeyRequest is part of an experimental API and may change or +// be removed. +// Internal: GlobalStateWriteKeyRequest is an internal SDK API and is not part of the public +// surface. +type GlobalStateWriteKeyRequest struct { + // Copilot configuration directory to write the state document in, taking precedence over + // the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to + // write the directory the server resolved for itself. Mirrors + // `globalState.loadForConfigDir`, so a caller can read and write the same directory. + ConfigDir *string `json:"configDir,omitempty"` + // Top-level key to write, named as it appears in the result of `globalState.load`. It must + // be one of the writable keys that `globalState.writeKey` lists. + Key string `json:"key"` + // Value to store for the key. Omit it, or pass null, to remove the key instead. + Value any `json:"value,omitempty"` +} + +// Experimental: GlobalStateWriteKeyResult is part of an experimental API and may change or +// be removed. +type GlobalStateWriteKeyResult struct { +} + // Pending external tool call request ID, with the tool result or an error describing why it // failed. // Experimental: HandlePendingToolCallRequest is part of an experimental API and may change @@ -6083,6 +6663,22 @@ type LocalSessionMetadataValue struct { Summary *string `json:"summary,omitempty"` } +// An account the host has signed in to, identified by the server it lives on and the login +// it uses there. The same person can appear more than once when they use both github.com +// and an Enterprise server. +// Experimental: LoggedInUser is part of an experimental API and may change or be removed. +// Internal: LoggedInUser is an internal SDK API and is not part of the public surface. +type LoggedInUser struct { + // Source account this account was derived from, when one was recorded. + DerivedFrom *string `json:"derivedFrom,omitempty"` + // Host the account belongs to, such as `github.com` or an Enterprise server. + Host string `json:"host"` + // Account kind, when the host recorded one. Consumers must tolerate new strings. + Kind *string `json:"kind,omitempty"` + // Account login on that host. + Login string `json:"login"` +} + // Message text, optional severity level, persistence flag, optional follow-up URL, and // optional tip. // Experimental: LogRequest is part of an experimental API and may change or be removed. @@ -6149,6 +6745,9 @@ type ManagedSettingMeta struct { // Whether users and repositories may choose a different value. `false` means policy locks // the value. Overridable bool `json:"overridable"` + // Original managed value when the runtime adjusted it to a supported effective value. + // Omitted when no adjustment was needed. + Requested *string `json:"requested,omitempty"` // Channel that supplied this scalar value, matching a `layers[].source`: `device`, // `server`, or `policyHelper`. These scalar defaults select one winning channel, not a // mixed source. Treat unknown values as additional channels; more may be added. @@ -6238,6 +6837,10 @@ type ManagedSettingsLayer struct { type ManagedSettingsMeta struct { // Lock state and provenance of `values.autoTier`. AutoTier *ManagedSettingMeta `json:"autoTier,omitempty"` + // Lock state and provenance of `values.contextTier`. + ContextTier *ManagedSettingMeta `json:"contextTier,omitempty"` + // Lock state and provenance of `values.effortLevel`. + EffortLevel *ManagedSettingMeta `json:"effortLevel,omitempty"` // Lock state and provenance of `values.model`. Model *ManagedSettingMeta `json:"model,omitempty"` } @@ -6310,6 +6913,9 @@ type ManagedSettingsResolveRequest struct { // Opaque account identifier returned by `account.getAllUsers`. When omitted, the current // account is used, or device policy only when no account is signed in. SelectionID *string `json:"selectionId,omitempty"` + // Working directory used to run an organization policy helper. When omitted, sessionless + // resolution does not run the helper. + WorkingDirectory *string `json:"workingDirectory,omitempty"` } // Effective enterprise managed settings for an account, resolved without a session. @@ -6387,8 +6993,14 @@ type ManagedSettingsValidateResult struct { type ManagedSettingsValues struct { // Managed Auto routing preference, used when the selected model is `auto`. AutoTier *AutoTier `json:"autoTier,omitempty"` - // Managed default model identifier, as configured. New sessions start with it; it can name - // a model the account cannot use, so hosts match it against the listed models. + // Managed context-tier default for the managed concrete model. + ContextTier *ContextTier `json:"contextTier,omitempty"` + // Managed reasoning-effort default for the managed concrete model. The runtime clamps it to + // an entitled effort when model availability is known. + EffortLevel *string `json:"effortLevel,omitempty"` + // Managed default model identifier. When model availability was resolved, aliases and + // family names are projected to a concrete available model ID; otherwise the configured + // value is returned. Model *string `json:"model,omitempty"` } @@ -6787,6 +7399,48 @@ type MCPConfigUpdateRequest struct { type MCPConfigUpdateResult struct { } +// Effective MCP configuration entry. Configuration enablement is distinct from the optional +// live observation. +// Experimental: MCPConfiguredServer is part of an experimental API and may change or be +// removed. +type MCPConfiguredServer struct { + // Human-readable display name supplied by configuration. + DisplayName *string `json:"displayName,omitempty"` + // Whether this configured server is enabled after session configuration and policy + // filtering. + Enabled bool `json:"enabled"` + // Observed state from an already materialized matching server. Omitted when no live graph + // has this configured server; it never determines configuration enablement. + Live *MCPConfiguredServerState `json:"live,omitempty"` + // Server name (config key) + Name string `json:"name"` + // Configuration provenance: user, workspace, plugin, builtin, or managed. + Source *MCPServerSource `json:"source,omitempty"` + // Plugin name that provided this server, when source is plugin. + SourcePlugin *string `json:"sourcePlugin,omitempty"` + // Plugin version that provided this server, when source is plugin. + SourcePluginVersion *string `json:"sourcePluginVersion,omitempty"` +} + +// Effective MCP configuration with optional live observations from matching already +// materialized servers. +// Experimental: MCPConfiguredServerList is part of an experimental API and may change or be +// removed. +type MCPConfiguredServerList struct { + // Effective configured MCP servers. + Servers []MCPConfiguredServer `json:"servers"` +} + +// Observational state for a matching already materialized MCP server. +// Experimental: MCPConfiguredServerState is part of an experimental API and may change or +// be removed. +type MCPConfiguredServerState struct { + // Observed connection error, when the materialized server failed. + Error *string `json:"error,omitempty"` + // Observed connection status. This is not a configuration or readiness guarantee. + Status MCPServerStatus `json:"status"` +} + // Credential-free authentication identity used to configure GitHub MCP. // Experimental: MCPConfigureGitHubRequest is part of an experimental API and may change or // be removed. @@ -8962,6 +9616,9 @@ type MCPServer struct { // Connection status: connected, failed, needs-auth, pending, disabled, stopped, or // not_configured Status MCPServerStatus `json:"status"` + // Configured URL for an HTTP/SSE server, regardless of configuration source. Omitted for + // local and in-memory servers. + URL *string `json:"url,omitempty"` } // Set to `true` to use defaults, or provide an object with additional auth or OIDC settings. @@ -9346,10 +10003,15 @@ type MetadataContextHeaviestMessagesResult struct { // Experimental: MetadataContextInfoRequest is part of an experimental API and may change or // be removed. type MetadataContextInfoRequest struct { - // Maximum output tokens allowed by the target model. Pass 0 if unknown. + // Requested output allowance to reserve against the combined context ceiling. Pass 0 to + // resolve the session's request cap, falling back to the model's advertised output limit. OutputTokenLimit int64 `json:"outputTokenLimit"` - // Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. + // Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from + // the session. PromptTokenLimit int64 `json:"promptTokenLimit"` + // Provider owning selectedModel. Omit to use the current provider for the current model, or + // deterministic legacy lookup for a different model. + ProviderID *string `json:"providerId,omitempty"` // Model identifier used for tokenization. Omit to use the session default. Used both for // token counting and to compute display values. SelectedModel *string `json:"selectedModel,omitempty"` @@ -9538,6 +10200,11 @@ type Model struct { SupportedContextTiers []string `json:"supportedContextTiers,omitzero"` // Supported reasoning effort levels (only present if model supports reasoning effort) SupportedReasoningEfforts []string `json:"supportedReasoningEfforts,omitzero"` + // Model vendor as the Copilot API reports it, for example "Anthropic" or "Azure OpenAI". + // Open vocabulary, passed through unchanged. It can name the vendor that serves the model + // instead of the one that built it, or a label that is not a vendor, such as + // "Experimental". Absent when the Copilot API reports no vendor. + Vendor *string `json:"vendor,omitempty"` // Warnings the service published for this model, such as a deprecated client version. // Present only when the service published at least one warning. The model remains usable; // hosts should surface these as advisory rather than blocking. @@ -9579,6 +10246,9 @@ type ModelApplyStartupOverlayRequest struct { RepoContextTier *string `json:"repoContextTier,omitempty"` // Model selected by repository settings, when configured. RepoModel *string `json:"repoModel,omitempty"` + // Provider owning the repository-selected model. Omitted repository provider identity + // retains legacy bare-model behavior. + RepoModelProviderID *string `json:"repoModelProviderId,omitempty"` // Reasoning effort selected by repository settings, when configured. RepoReasoningEffort *string `json:"repoReasoningEffort,omitempty"` // Model required by server-managed policy, when configured. @@ -9809,6 +10479,24 @@ type ModelCapabilitiesSupports struct { Vision *bool `json:"vision,omitempty"` } +// Experimental: ModelClearStartupSeedRequest is part of an experimental API and may change +// or be removed. +// Internal: ModelClearStartupSeedRequest is an internal SDK API and is not part of the +// public surface. +type ModelClearStartupSeedRequest struct { + ExpectedModel string `json:"expectedModel"` + // Provider captured with the startup seed; omission matches only an unqualified seed. + ExpectedProviderID *string `json:"expectedProviderId,omitempty"` +} + +// Experimental: ModelClearStartupSeedResult is part of an experimental API and may change +// or be removed. +// Internal: ModelClearStartupSeedResult is an internal SDK API and is not part of the +// public surface. +type ModelClearStartupSeedResult struct { + Cleared bool `json:"cleared"` +} + // List of Copilot models available to the resolved user, including capabilities and billing // metadata. // Experimental: ModelList is part of an experimental API and may change or be removed. @@ -10270,12 +10958,17 @@ type ModelSwitchToRequest struct { ModelCapabilities *ModelCapabilitiesOverride `json:"modelCapabilities,omitempty"` // Settings scope used when persisting the selected model. ModelChangeScope *string `json:"modelChangeScope,omitempty"` - // Model selection id to switch to, as returned by `list`. A bare id (e.g. - // `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id - // (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. + // Model id to switch to, as returned by `list`. Include providerId to select an exact + // catalog entry when providers share the id. Without providerId, a bare id retains + // incumbent selection behavior; a registry-qualified id (`provider/id`, e.g. + // `acme/claude-sonnet`) targets a BYOK model. ModelID string `json:"modelId"` // Optional settings context and explicit-override flags used to persist a picker selection. PickerPersistence *ModelPickerPersistenceRequest `json:"pickerPersistence,omitempty"` + // Provider id from the selected list entry's provider reference. Selects this exact + // provider/model pair; an unavailable pair fails rather than using another provider. Omit + // for deterministic legacy bare-model selection. + ProviderID *string `json:"providerId,omitempty"` // Reasoning effort level to use for the model. CAPI values are model-defined and validated // against the selected model; BYOK providers may define additional values. "none" disables // reasoning. Pass null to clear any session effort override and fall back to the model's @@ -10360,6 +11053,8 @@ type ModeSetRequest struct { PlanModel *string `json:"planModel,omitempty"` // Whether a dedicated plan model is configured. PlanModelConfigured *bool `json:"planModelConfigured,omitempty"` + // Provider owning planModel. Omit for legacy bare-model selection. + PlanModelProviderID *string `json:"planModelProviderId,omitempty"` // Reasoning effort to use with the dedicated plan model. PlanReasoningEffort *string `json:"planReasoningEffort,omitempty"` // Whether leaving plan mode should restore the session's previous model. @@ -10529,6 +11224,39 @@ type OptionsUpdateAdditionalContentExclusionPolicyRuleSource struct { Type string `json:"type"` } +// External tool call of the session or one of its sub-agents that is still waiting for +// session.tools.handlePendingToolCall. +// Experimental: PendingExternalToolRequest is part of an experimental API and may change or +// be removed. +type PendingExternalToolRequest struct { + // Sub-agent instance identifier (the envelope agentId of its events) of the agent that + // issued the call; absent for calls issued by the root agent + AgentID *string `json:"agentId,omitempty"` + // Arguments to pass to the external tool + Arguments any `json:"arguments,omitempty"` + // Stable identity of the provider that offered the tool, for hosts that route + // extension-owned tools by provider + ProviderID *string `json:"providerId,omitempty"` + // Request ID to pass to session.tools.handlePendingToolCall + RequestID string `json:"requestId"` + // Tool call ID assigned to this external tool invocation + ToolCallID string `json:"toolCallId"` + // Name of the external tool to invoke + ToolName string `json:"toolName"` +} + +// External tool calls still waiting for a result. +// Experimental: PendingExternalToolRequestList is part of an experimental API and may +// change or be removed. +type PendingExternalToolRequestList struct { + // External tool calls of the session and its sub-agents that + // session.tools.handlePendingToolCall would still accept, in the order they were requested. + // Includes calls that were pending when the session was last suspended and resumed with + // pending work continued, so a host that did not observe the original + // external_tool.requested event can answer them. + Items []PendingExternalToolRequest `json:"items"` +} + // Pending permission prompt reconstructed from event history, with request ID and // user-facing prompt details. // Experimental: PendingPermissionRequest is part of an experimental API and may change or @@ -12484,6 +13212,87 @@ type ProviderModelConfig struct { WireModel *string `json:"wireModel,omitempty"` } +// Authoritative budget measurements and policy metadata, independent of provider. +// Experimental: ProviderQuotaBudgetMetadata is part of an experimental API and may change +// or be removed. +type ProviderQuotaBudgetMetadata struct { + // Exact measured consumption in the budget's unit. + Consumed float64 `json:"consumed"` + // Exact budget entitlement, retaining incumbent fractional-unit compatibility. + Entitlement float64 `json:"entitlement"` + // Usage beyond entitlement, in the budget's unit. + Overage float64 `json:"overage"` + // Whether additional usage is allowed when the budget is exhausted. + OverageAllowedWhenExhausted bool `json:"overageAllowedWhenExhausted"` + // Optional additional-usage budget cap. + OverageLimit *float64 `json:"overageLimit,omitempty"` + // Service-reported remaining percentage for authoritative budget presentation. + RemainingPercentage float64 `json:"remainingPercentage"` + // Reset instant in epoch milliseconds when this budget actually defines a window. + ResetAtEpochMs *float64 `json:"resetAtEpochMs,omitempty"` + // Whether the reset instant is an estimate. + ResetEstimated *bool `json:"resetEstimated,omitempty"` + // Whether this budget uses token-based billing. + TokenBasedBilling *bool `json:"tokenBasedBilling,omitempty"` + // Whether the budget has unlimited entitlement. + Unlimited bool `json:"unlimited"` + // Whether service policy allows continued usage after exhaustion. + UsageAllowedWhenExhausted bool `json:"usageAllowedWhenExhausted"` +} + +// An account quota reading. Absence of a quantity is unknown, never zero. +// Experimental: ProviderQuotaState is part of an experimental API and may change or be +// removed. +type ProviderQuotaState struct { + // Whether the service permits access, independently of balance. + AccessState ProviderQuotaAccessState `json:"accessState"` + // Service error code or client acquisition category, separate from the business-state + // reason. + AcquisitionError *string `json:"acquisitionError,omitempty"` + // Acquisition outcome. Unavailable/failed readings have no service verdict; access/capacity + // unavailable are compatibility placeholders only. + AcquisitionStatus *ProviderQuotaAcquisitionStatus `json:"acquisitionStatus,omitempty"` + // Independently reported signed 64-bit available quantity. Zero does not override the + // service access/capacity verdict; omission and null are preserved. + AvailableQuantity *int64 `json:"availableQuantity,omitempty"` + // Optional metadata for an authoritative budget. Advisory balances and admission-only + // observations do not populate this. + BudgetMetadata *ProviderQuotaBudgetMetadata `json:"budgetMetadata,omitempty"` + // Service capacity state; missing quantities do not imply exhaustion. + CapacityState ProviderQuotaCapacityState `json:"capacityState"` + // Key for the backwards-compatible snapshots projection, when the authoritative budget + // supports that contract. + CompatibilityKey *string `json:"compatibilityKey,omitempty"` + // Independently reported signed 64-bit entitlement. -1 is an unlimited sentinel, not a + // capacity-state rewrite; omission and null are preserved. + EntitledQuantity *int64 `json:"entitledQuantity,omitempty"` + // Explicit service admission flag, when reported. + HasQuota *bool `json:"hasQuota,omitempty"` + // HTTP status from acquisition, when available. + HTTPStatus *int64 `json:"httpStatus,omitempty"` + // Whether this is a GET account reading or a pre-response admission observation. + // Observations are never merged across kinds. + ObservationKind *ProviderQuotaObservationKind `json:"observationKind,omitempty"` + // When the runtime observed this reading, not a charge timestamp or guarantee that + // consumption has settled. + ObservedAt *string `json:"observedAt,omitempty"` + // Provider/account whose service reported this state. + Provider ModelProviderRef `json:"provider"` + // Whether quantities are authoritative budget measurements, advisory balances, or absent. + // Only authoritative budgets support percentage presentation. + QuantityKind *ProviderQuotaQuantityKind `json:"quantityKind,omitempty"` + // Service-owned quota identifier within this provider. + QuotaID string `json:"quotaId"` + // Service-reported explanation for the state. + Reason *string `json:"reason,omitempty"` + // Service name owning this reading. + Service *string `json:"service,omitempty"` + // Service-reported quota source. + Source *string `json:"source,omitempty"` + // Unit of entitledQuantity and availableQuantity. + Unit ProviderQuotaUnit `json:"unit"` +} + // Short-lived, rotating credential the caller must send on every request, in addition to // `apiKey` if one is present. Omitted when the endpoint does not require one. // Experimental: ProviderSessionToken is part of an experimental API and may change or be @@ -13318,6 +14127,23 @@ type QueueWithdrawMessageResult struct { Removed bool `json:"removed"` } +// Pending warnings returned and cleared by session.quota.takeWarnings. +// Experimental: QuotaTakeWarningsResult is part of an experimental API and may change or be +// removed. +type QuotaTakeWarningsResult []QuotaWarningProjection + +// A pending session quota warning, consumed once by takeWarnings. +// Experimental: QuotaWarningProjection is part of an experimental API and may change or be +// removed. +type QuotaWarningProjection struct { + // User-facing warning text. + Message string `json:"message"` + // Related account or upgrade link. + URL *string `json:"url,omitempty"` + // Existing warning category. + WarningType string `json:"warningType"` +} + // Event type to register consumer interest for, used by runtime gating logic. // Experimental: RegisterEventInterestParams is part of an experimental API and may change // or be removed. @@ -13850,12 +14676,15 @@ type SandboxHostCapability struct { // tooling for Bubblewrap's private network namespace, such as slirp4netns), // `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same // tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback - // support, and a policy that uses it must also set `network.allowLocalNetwork`), - // `denied_paths` (native enforcement of `filesystem.deniedPaths`), `shell` (shell commands - // inside the sandbox), and `filesystem_enumeration` (enumerate-only filesystem grants; on - // Windows this needs Process Security Environment 1.1 filesystem enumeration support, and - // without it sandboxed PowerShell still runs but cannot resolve its current location; other - // platforms always report it). + // support or MXC's PSEC 1.0-only proxy-loopback compatibility capability, and a policy that + // uses it must also set `network.allowLocalNetwork`; compatibility applies only to an + // explicit identity-less runtime proxy, not general host-loopback access, and other policy + // restrictions still apply), `denied_paths` (native enforcement of + // `filesystem.deniedPaths`), `shell` (shell commands inside the sandbox), and + // `filesystem_enumeration` (enumerate-only filesystem grants; on Windows this needs Process + // Security Environment 1.1 filesystem enumeration support, and without it sandboxed + // PowerShell still runs but cannot resolve its current location; other platforms always + // report it). Name string `json:"name"` // Human-readable reason and remedy when the feature is unsupported, such as a package to // install or an OS update. Present only when `supported` is false. @@ -13871,17 +14700,20 @@ type SandboxHostCapability struct { // Bubblewrap's private network namespace, such as slirp4netns. `network_filtering` — host // rules and the sandbox proxy (`network.allowedHosts`, `network.blockedHosts`, // `network.proxy`); on Linux this needs the same tooling as `network`; on Windows it needs -// a version with Process Security Environment 1.1 host-loopback support, and a policy that -// uses it must also set `network.allowLocalNetwork`, because Windows reaches the local -// proxy only together with private-network access. `denied_paths` — native enforcement of -// `filesystem.deniedPaths`; on Windows this needs a version whose sandbox contract reports -// denied-path support. `shell` — shell commands inside the sandbox: bash on macOS and -// Linux, PowerShell on Windows. `filesystem_enumeration` — enumerate-only filesystem -// grants, which PowerShell's drive roots use on Windows; this needs a version with Process -// Security Environment 1.1 filesystem enumeration support. Without it, sandboxed PowerShell -// still runs, but `Get-Location` may report the drive root, `Set-Location` may fail, and -// relative paths may resolve against the drive root; the session also receives a -// `session.warning` with `warningType` `sandbox`. Other platforms always report it. +// Process Security Environment 1.1 host-loopback support or MXC's PSEC 1.0-only +// proxy-loopback compatibility capability, and a policy that uses it must also set +// `network.allowLocalNetwork`, because Windows reaches the local proxy only together with +// private-network access. Compatibility applies only to an explicit identity-less runtime +// proxy, not general host-loopback access, and other policy restrictions still apply. +// `denied_paths` — native enforcement of `filesystem.deniedPaths`; on Windows this needs a +// version whose sandbox contract reports denied-path support. `shell` — shell commands +// inside the sandbox: bash on macOS and Linux, PowerShell on Windows. +// `filesystem_enumeration` — enumerate-only filesystem grants, which PowerShell's drive +// roots use on Windows; this needs a version with Process Security Environment 1.1 +// filesystem enumeration support. Without it, sandboxed PowerShell still runs, but +// `Get-Location` may report the drive root, `Set-Location` may fail, and relative paths may +// resolve against the drive root; the session also receives a `session.warning` with +// `warningType` `sandbox`. Other platforms always report it. // Experimental: SandboxHostCapabilityName is part of an experimental API and may change or // be removed. type SandboxHostCapabilityName string @@ -14475,6 +15307,16 @@ type SessionCompletionItem struct { RangeStart *int64 `json:"rangeStart,omitempty"` } +// The IDE a host is connected to, as reported to the session. +// Experimental: SessionConnectedIdeInfo is part of an experimental API and may change or be +// removed. +type SessionConnectedIdeInfo struct { + // Display name of the connected IDE, for example `VS Code`. + IdeName string `json:"ideName"` + // Absolute path of the workspace folder the IDE has open. + WorkspaceFolder string `json:"workspaceFolder"` +} + // Pre-resolved working-directory context for session startup. // Experimental: SessionContext is part of an experimental API and may change or be removed. type SessionContext struct { @@ -14495,7 +15337,8 @@ type SessionContext struct { // Experimental: SessionContextAttribution is part of an experimental API and may change or // be removed. type SessionContextAttribution struct { - // Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors + // Output reservation overlapping the displayed prompt allowance plus the tokens past the + // effective input budget's buffer-exhaustion blocking threshold. Mirrors // `SessionContextInfo.bufferTokens`. BufferTokens int64 `json:"bufferTokens"` // The six normalized `/context` header buckets, computed from the same tokenization as @@ -14511,9 +15354,9 @@ type SessionContextAttribution struct { // Flat list of per-source attribution entries. Group by `kind` and render unrecognized // kinds generically. Nesting and rollups are expressed via `parentId`. Entries []SessionContextAttributionEntriesItem `json:"entries"` - // Prompt limit plus the model's output reserve: the full context window - // `categories.freeSpace` and `categories.buffer` are measured against. Mirrors - // `SessionContextInfo.limit`. + // Advertised prompt allowance for the selected context tier: the denominator for + // context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. + // Mirrors `SessionContextInfo.limit`. Limit int64 `json:"limit"` // The concrete model id the entire breakdown was tokenized against (feeds the per-model // token multiplier). Under `Auto` (Free/Student) this is the resolved model, not the @@ -14524,8 +15367,8 @@ type SessionContextAttribution struct { // `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected // model), `default` (a fallback before any model is known). ModelSource string `json:"modelSource"` - // Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` - // context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. + // Effective input budget after reserving requested output against the combined context + // ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. PromptTokenLimit int64 `json:"promptTokenLimit"` // Total token count of the current context window the entries are measured against (system // message + conversation messages + tool definitions — the same total reported by @@ -14538,7 +15381,7 @@ type SessionContextAttribution struct { // describe window capacity rather than occupied context, so the values do not sum to // `totalTokens`. type SessionContextAttributionCategories struct { - // Output reserve plus post-blocking-threshold buffer. + // Overlapping output reservation plus post-blocking-threshold buffer. Buffer int64 `json:"buffer"` // Custom-instructions tokens (0 when none are configured). CustomInstructions int64 `json:"customInstructions"` @@ -14588,22 +15431,32 @@ type SessionContextAttributionEntriesItem struct { // Experimental: SessionContextInfo is part of an experimental API and may change or be // removed. type SessionContextInfo struct { - // Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) + // Output reservation overlapping the displayed prompt allowance plus tokens after the + // effective input budget's buffer-exhaustion blocking threshold (default 95%). BufferTokens int64 `json:"bufferTokens"` // Token count at which background compaction starts (configurable percentage of // promptTokenLimit) CompactionThreshold int64 `json:"compactionThreshold"` // Tokens consumed by user/assistant/tool messages ConversationTokens int64 `json:"conversationTokens"` - // Prompt token limit plus the model's full output token limit. + // Provider-owned model display label, or the Auto routing label. Presentation only; + // modelName remains the tokenization identity. + DisplayModelName *string `json:"displayModelName,omitempty"` + // Advertised prompt allowance for the selected context tier, without adding output tokens. + // The denominator for context-usage displays. Limit int64 `json:"limit"` // Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes // deferred tools) MCPToolsTokens int64 `json:"mcpToolsTokens"` // The model used for token counting ModelName string `json:"modelName"` - // Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) + // Effective input budget: the selected tier's prompt allowance bounded by the combined + // context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when + // limits are unspecified. PromptTokenLimit int64 `json:"promptTokenLimit"` + // Provider whose catalog supplies this context window. Omitted when provider identity is + // unavailable. + Provider *ModelProviderRef `json:"provider,omitempty"` // Tokens consumed by the system prompt SystemTokens int64 `json:"systemTokens"` // Tokens consumed by tool definitions sent to the model (excludes deferred tools) @@ -15356,12 +16209,21 @@ type SessionManagedPermissions struct { Ask []string `json:"ask,omitzero"` // Permission rules that block matching operations. Deny has highest precedence. Deny []string `json:"deny,omitzero"` + // When true, prevents Assisted Permissions from being activated. An actively Assisted + // session falls back to Manual Approval while the policy is in force. Omit the key or set + // it to false to impose no restriction. + DisableAssistedPermissionsMode *bool `json:"disableAssistedPermissionsMode,omitempty"` // When set to `disable`, prevents bypass/allow-all permission modes. Advisory auto-approval // remains available because normal prompt paths stay active. Any other value is accepted // rather than failing the session, but is enforced as `disable`: the key is only present to // restrict something, so a mode this runtime cannot interpret fails closed to the most // restrictive one it knows. Omit the key entirely to impose no restriction. DisableBypassPermissionsMode *string `json:"disableBypassPermissionsMode,omitempty"` + // Closed-world host boundary expressed as `Domain(hostname)`, `Domain(IP)`, or + // `Domain(*.example.com)` rules. Schemes, ports, paths, queries, and fragments are rejected + // because every network request must be enforceable at host-level egress. Multiple managed + // sources intersect their lists; an empty list denies all hosts. + LimitTo []string `json:"limitTo,omitzero"` } // Managed settings an SDK host may inject at session startup. Only permissions are accepted @@ -15465,6 +16327,20 @@ type SessionMCPReloadResult struct { type SessionMCPRestartServerResult struct { } +// Records which IDE the host is connected to, or clears it. +// Experimental: SessionMCPSetConnectedIdeInfoParams is part of an experimental API and may +// change or be removed. +type SessionMCPSetConnectedIdeInfoParams struct { + // The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports + // that it is disconnected. + Ide *SessionConnectedIdeInfo `json:"ide,omitempty"` +} + +// Experimental: SessionMCPSetConnectedIdeInfoResult is part of an experimental API and may +// change or be removed. +type SessionMCPSetConnectedIdeInfoResult struct { +} + // Experimental: SessionMCPStartServerResult is part of an experimental API and may change // or be removed. type SessionMCPStartServerResult struct { @@ -15530,10 +16406,30 @@ type SessionMetadataSnapshot struct { WorkspacePath *string `json:"workspacePath"` } +// Experimental: SessionModelClearStartupSeedRequest is part of an experimental API and may +// change or be removed. +// Internal: SessionModelClearStartupSeedRequest is an internal SDK API and is not part of +// the public surface. +type SessionModelClearStartupSeedRequest struct { + ExpectedModel string `json:"expectedModel"` + // Provider captured with the startup seed; omission matches only an unqualified seed. + ExpectedProviderID *string `json:"expectedProviderId,omitempty"` +} + +// Experimental: SessionModelClearStartupSeedResult is part of an experimental API and may +// change or be removed. +// Internal: SessionModelClearStartupSeedResult is an internal SDK API and is not part of +// the public surface. +type SessionModelClearStartupSeedResult struct { + Cleared bool `json:"cleared"` +} + // The list of models available to this session. // Experimental: SessionModelList is part of an experimental API and may change or be // removed. type SessionModelList struct { + // Ordered Auto routing preferences discovered for this session's account. + Auto *AutoTierMetadata `json:"auto,omitempty"` // Available models, ordered with the most preferred default first. Includes both Copilot // (CAPI) models and any registry BYOK models; a BYOK model appears under its // provider-qualified selection id (`provider/id`). @@ -15604,6 +16500,12 @@ type SessionOpenOptions struct { AuthClientIDMetadataURL *string `json:"authClientIdMetadataUrl,omitempty"` // Initial authentication info for the session. AuthInfo AuthInfo `json:"authInfo,omitempty"` + // Whether a CLI host explicitly requested the initial Auto preference. False preserves a + // settings-derived preference without validating availability during creation; execution + // still validates it. Defaults to true and is ignored for non-CLI callers. + // Internal: AutoTierIsExplicit is part of the SDK's internal API surface and is not + // intended for external use. + AutoTierIsExplicit *bool `json:"autoTierIsExplicit,omitempty"` // Allowlist of available tool names. AvailableTools []string `json:"availableTools,omitzero"` // Options scoped to the built-in CAPI (Copilot API) provider. @@ -15671,6 +16573,10 @@ type SessionOpenOptions struct { EnableSkills *bool `json:"enableSkills,omitempty"` // Whether model responses stream as delta events. EnableStreaming *bool `json:"enableStreaming,omitempty"` + // Opt in to enforcing non-overridable managed model controls on session model, Auto-tier, + // reasoning-effort, and context-tier changes. Managed defaults still apply when omitted; + // this option only turns conflicting changes into errors. + EnforceManagedModelDefaults *bool `json:"enforceManagedModelDefaults,omitempty"` // How MCP server environment values are interpreted. EnvValueMode *SessionOpenOptionsEnvValueMode `json:"envValueMode,omitempty"` // Override directory for session event logs. @@ -16162,6 +17068,147 @@ type SessionQueueProcessResult struct { type SessionQueueSetDrainPausedResult struct { } +// Existing delegation guidance for an account without delegation access. +// Experimental: SessionQuotaDelegateWarning is part of an experimental API and may change +// or be removed. +type SessionQuotaDelegateWarning struct { + // User-facing guidance. + Text string `json:"text"` + // Link to upgrade the account. + URL string `json:"url"` +} + +// The incumbent session-owned quota and account projection. +// Experimental: SessionQuotaGetResult is part of an experimental API and may change or be +// removed. +type SessionQuotaGetResult struct { + // Whether the account can sign up for Copilot Free. + CanSignupForCopilotFree bool `json:"canSignupForCopilotFree"` + // Whether the account can delegate tasks to GitHub. + DelegateAvailable bool `json:"delegateAvailable"` + // Existing delegation warning for a free account. + DelegateWarning *SessionQuotaDelegateWarning `json:"delegateWarning,omitempty"` + // Whether dynamic workflows are enabled for the active account. + DynamicWorkflowsEnabled bool `json:"dynamicWorkflowsEnabled"` + // Whether dynamic workflows are visible under the session's feature flags. + DynamicWorkflowsUIVisible bool `json:"dynamicWorkflowsUiVisible"` + // Whether the account uses the free limited Copilot plan. + IsFreeUser bool `json:"isFreeUser"` + // Whether the account uses token-based billing. + IsTbbUser bool `json:"isTbbUser"` + // Whether model cost columns should be shown. + ModelCostColumnVisible bool `json:"modelCostColumnVisible"` + // Resolved Copilot plan tier. + PlanTier SessionQuotaPlanTier `json:"planTier"` + // Whether premium-request costs are billable. + PremiumRequestsBillable bool `json:"premiumRequestsBillable"` + // Provider-owned account quota state, including snapshots without numeric balances. + ProviderQuotas []ProviderQuotaState `json:"providerQuotas,omitzero"` + // Known quota snapshots, keyed by the provider's quota type. + Snapshots map[string]SessionQuotaSnapshot `json:"snapshots"` + // Upgrade link for a free account. + UpgradeURL *string `json:"upgradeUrl,omitempty"` +} + +// The incumbent session-owned quota and account projection. +// Experimental: SessionQuotaProjection is part of an experimental API and may change or be +// removed. +type SessionQuotaProjection struct { + // Whether the account can sign up for Copilot Free. + CanSignupForCopilotFree bool `json:"canSignupForCopilotFree"` + // Whether the account can delegate tasks to GitHub. + DelegateAvailable bool `json:"delegateAvailable"` + // Existing delegation warning for a free account. + DelegateWarning *SessionQuotaDelegateWarning `json:"delegateWarning,omitempty"` + // Whether dynamic workflows are enabled for the active account. + DynamicWorkflowsEnabled bool `json:"dynamicWorkflowsEnabled"` + // Whether dynamic workflows are visible under the session's feature flags. + DynamicWorkflowsUIVisible bool `json:"dynamicWorkflowsUiVisible"` + // Whether the account uses the free limited Copilot plan. + IsFreeUser bool `json:"isFreeUser"` + // Whether the account uses token-based billing. + IsTbbUser bool `json:"isTbbUser"` + // Whether model cost columns should be shown. + ModelCostColumnVisible bool `json:"modelCostColumnVisible"` + // Resolved Copilot plan tier. + PlanTier SessionQuotaPlanTier `json:"planTier"` + // Whether premium-request costs are billable. + PremiumRequestsBillable bool `json:"premiumRequestsBillable"` + // Provider-owned account quota state, including snapshots without numeric balances. + ProviderQuotas []ProviderQuotaState `json:"providerQuotas,omitzero"` + // Known quota snapshots, keyed by the provider's quota type. + Snapshots map[string]SessionQuotaSnapshot `json:"snapshots"` + // Upgrade link for a free account. + UpgradeURL *string `json:"upgradeUrl,omitempty"` +} + +// The incumbent session-owned quota and account projection. +// Experimental: SessionQuotaRefreshResult is part of an experimental API and may change or +// be removed. +type SessionQuotaRefreshResult struct { + // Whether the account can sign up for Copilot Free. + CanSignupForCopilotFree bool `json:"canSignupForCopilotFree"` + // Whether the account can delegate tasks to GitHub. + DelegateAvailable bool `json:"delegateAvailable"` + // Existing delegation warning for a free account. + DelegateWarning *SessionQuotaDelegateWarning `json:"delegateWarning,omitempty"` + // Whether dynamic workflows are enabled for the active account. + DynamicWorkflowsEnabled bool `json:"dynamicWorkflowsEnabled"` + // Whether dynamic workflows are visible under the session's feature flags. + DynamicWorkflowsUIVisible bool `json:"dynamicWorkflowsUiVisible"` + // Whether the account uses the free limited Copilot plan. + IsFreeUser bool `json:"isFreeUser"` + // Whether the account uses token-based billing. + IsTbbUser bool `json:"isTbbUser"` + // Whether model cost columns should be shown. + ModelCostColumnVisible bool `json:"modelCostColumnVisible"` + // Resolved Copilot plan tier. + PlanTier SessionQuotaPlanTier `json:"planTier"` + // Whether premium-request costs are billable. + PremiumRequestsBillable bool `json:"premiumRequestsBillable"` + // Provider-owned account quota state, including snapshots without numeric balances. + ProviderQuotas []ProviderQuotaState `json:"providerQuotas,omitzero"` + // Known quota snapshots, keyed by the provider's quota type. + Snapshots map[string]SessionQuotaSnapshot `json:"snapshots"` + // Upgrade link for a free account. + UpgradeURL *string `json:"upgradeUrl,omitempty"` +} + +// Session quota snapshot, preserving the incumbent numeric units and reset metadata. +// Experimental: SessionQuotaSnapshot is part of an experimental API and may change or be +// removed. +type SessionQuotaSnapshot struct { + // Entitled quantity in this quota's units. + EntitlementRequests float64 `json:"entitlementRequests"` + // Whether the provider reports available quota. + HasQuota *bool `json:"hasQuota,omitempty"` + // Whether the entitlement is unlimited. + IsUnlimitedEntitlement bool `json:"isUnlimitedEntitlement"` + // Additional usage in this quota's units. + Overage float64 `json:"overage"` + // Whether additional usage is allowed after quota exhaustion. + OverageAllowedWithExhaustedQuota bool `json:"overageAllowedWithExhaustedQuota"` + // Additional-usage budget cap, when provided. + OverageEntitlement *float64 `json:"overageEntitlement,omitempty"` + // Percentage of the entitlement remaining. + RemainingPercentage float64 `json:"remainingPercentage"` + // Quota reset time in milliseconds since the Unix epoch, when known. + ResetDateEpochMs *float64 `json:"resetDateEpochMs,omitempty"` + // Whether the reset time is estimated. + ResetDateEstimated *bool `json:"resetDateEstimated,omitempty"` + // Whether this quota uses token-based billing. + TokenBasedBilling *bool `json:"tokenBasedBilling,omitempty"` + // Whether usage is allowed after quota exhaustion. + UsageAllowedWithExhaustedQuota bool `json:"usageAllowedWithExhaustedQuota"` + // Consumed quantity in this quota's units. + UsedRequests float64 `json:"usedRequests"` +} + +// Pending warnings returned and cleared by session.quota.takeWarnings. +// Experimental: SessionQuotaTakeWarningsResult is part of an experimental API and may +// change or be removed. +type SessionQuotaTakeWarningsResult []QuotaWarningProjection + // Experimental: SessionRemoteDisableResult is part of an experimental API and may change or // be removed. type SessionRemoteDisableResult struct { @@ -16293,6 +17340,35 @@ type SessionsCloseResult struct { type SessionsConfigureSessionExtensionsResult struct { } +// Identity, state location and starting context for a workspace record. +// Experimental: SessionsCreateWorkspaceRequest is part of an experimental API and may +// change or be removed. +type SessionsCreateWorkspaceRequest struct { + // Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, + // `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, + // `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. + Context *SessionWorkingDirectoryContextWithClient `json:"context,omitempty"` + // `windows` (any letter case) selects Windows path rules. Any other value selects POSIX + // path rules. + Convention string `json:"convention"` + // User-supplied display name for the workspace + Name *string `json:"name,omitempty"` + // Session ID the workspace record belongs to + SessionID string `json:"sessionId"` + // Directory the session's state is written under when no session filesystem provider is + // configured. Ignored when a provider is configured; the provider's session state path is + // used instead. + SessionStatePath string `json:"sessionStatePath"` +} + +// The workspace record that was written. +// Experimental: SessionsCreateWorkspaceResult is part of an experimental API and may change +// or be removed. +type SessionsCreateWorkspaceResult struct { + // The created workspace record, as JSON + WorkspaceJSON string `json:"workspaceJson"` +} + // Session ID to delete from disk. // Experimental: SessionsDeleteRequest is part of an experimental API and may change or be // removed. @@ -16729,6 +17805,25 @@ type SessionsLoadDeferredRepoHooksRequest struct { SessionID string `json:"sessionId"` } +// Where the session's state lives, as a root directory and the session ID under it. +// Experimental: SessionsLoadWorkspaceRequest is part of an experimental API and may change +// or be removed. +type SessionsLoadWorkspaceRequest struct { + // Session ID naming the state directory under the sessions home. Rejected when it is + // absolute or contains a parent component, so it cannot escape the sessions home. + SessionID string `json:"sessionId"` + // Root directory every session's state directory sits under + SessionsHome string `json:"sessionsHome"` +} + +// The workspace record on disk, omitted when the session has none. +// Experimental: SessionsLoadWorkspaceResult is part of an experimental API and may change +// or be removed. +type SessionsLoadWorkspaceResult struct { + // The workspace record, as JSON. Omitted when the record does not exist. + WorkspaceJSON *string `json:"workspaceJson,omitempty"` +} + // `sessions.open` handoff progress update with step, status, and optional message. // Experimental: SessionsOpenProgress is part of an experimental API and may change or be // removed. @@ -16884,6 +17979,30 @@ type SessionsTransferRemoteControlRequest struct { ToSessionID string `json:"toSessionId"` } +// Where the session's state lives, plus workspace-schema fields to merge into its workspace +// record. Stored keys outside the schema are not preserved, and a stored `fork_count` is +// never replaced. +// Experimental: SessionsUpdateWorkspaceFieldsRequest is part of an experimental API and may +// change or be removed. +type SessionsUpdateWorkspaceFieldsRequest struct { + // Workspace-schema fields to merge into the record, as a JSON object. Fields the object + // omits keep their stored values, except stored keys outside the schema are not preserved + // and a stored `fork_count` is never replaced. + FieldsJSON string `json:"fieldsJson"` + // Session ID naming the state directory under the sessions home. Rejected when it is + // absolute or contains a parent component, so it cannot escape the sessions home. + SessionID string `json:"sessionId"` + // Root directory every session's state directory sits under + SessionsHome string `json:"sessionsHome"` +} + +// The merge completed. The record carries the supplied workspace-schema fields, but a +// stored `fork_count` stays. +// Experimental: SessionsUpdateWorkspaceFieldsResult is part of an experimental API and may +// change or be removed. +type SessionsUpdateWorkspaceFieldsResult struct { +} + // Experimental: SessionSuspendResult is part of an experimental API and may change or be // removed. type SessionSuspendResult struct { @@ -17077,6 +18196,25 @@ type SessionUpdateOptionsResult struct { Success bool `json:"success"` } +// Internal absolute code-change totals reported by the owning host. +// Experimental: SessionUsageSetCodeChangesRequest is part of an experimental API and may +// change or be removed. +// Internal: SessionUsageSetCodeChangesRequest is an internal SDK API and is not part of the +// public surface. +type SessionUsageSetCodeChangesRequest struct { + // Absolute changed-file count; omission preserves the previous count. + FilesCount *float64 `json:"filesCount,omitempty"` + // Absolute added-line total, replacing the previous reading. + LinesAdded float64 `json:"linesAdded"` + // Absolute removed-line total, replacing the previous reading. + LinesRemoved float64 `json:"linesRemoved"` +} + +// Experimental: SessionUsageSetCodeChangesResult is part of an experimental API and may +// change or be removed. +type SessionUsageSetCodeChangesResult struct { +} + // Experimental: SessionWorkflowPauseAtCheckpointResult is part of an experimental API and // may change or be removed. type SessionWorkflowPauseAtCheckpointResult struct { @@ -17108,6 +18246,30 @@ type SessionWorkingDirectoryContext struct { RepositoryHost *string `json:"repositoryHost,omitempty"` } +// A working-directory context together with the client that produced it. +// Experimental: SessionWorkingDirectoryContextWithClient is part of an experimental API and +// may change or be removed. +type SessionWorkingDirectoryContextWithClient struct { + // Merge-base commit SHA + BaseCommit *string `json:"baseCommit,omitempty"` + // Current git branch name + Branch *string `json:"branch,omitempty"` + // Name of the client that created the session + ClientName *string `json:"clientName,omitempty"` + // Current working directory path + Cwd string `json:"cwd"` + // Root directory of the git repository + GitRoot *string `json:"gitRoot,omitempty"` + // Head commit of the current git branch + HeadCommit *string `json:"headCommit,omitempty"` + // Hosting platform type of the repository + HostType *string `json:"hostType,omitempty"` + // Repository identifier derived from the git remote URL + Repository *string `json:"repository,omitempty"` + // Raw host string from the git remote URL + RepositoryHost *string `json:"repositoryHost,omitempty"` +} + // Experimental: SessionWorkspacesCreateDirectoryResult is part of an experimental API and // may change or be removed. type SessionWorkspacesCreateDirectoryResult struct { @@ -17146,6 +18308,10 @@ func (RawSettableAuthInfoData) settableAuthInfo() {} func (r RawSettableAuthInfoData) settableAuthInfoType() SettableAuthInfoType { return r.Discriminator } +func (AccountAuthInfo) settableAuthInfo() {} +func (AccountAuthInfo) settableAuthInfoType() SettableAuthInfoType { + return SettableAuthInfoTypeAccount +} func (APIKeyAuthInfo) settableAuthInfo() {} func (APIKeyAuthInfo) settableAuthInfoType() SettableAuthInfoType { return SettableAuthInfoTypeAPIKey @@ -18032,8 +19198,8 @@ type SkillPlanUninstallRequest struct { PolicySessionID string `json:"policySessionId"` } -// Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched -// separately and lazily. +// Authoritative catalog metadata for one SDK-provided skill. The skill's SKILL.md text is +// fetched separately and lazily. // Experimental: SkillProviderDescriptor is part of an experimental API and may change or be // removed. type SkillProviderDescriptor struct { @@ -18081,15 +19247,18 @@ type SkillProviderReadRequest struct { SessionID string `json:"sessionId"` } -// Complete text-only SKILL.md content returned by an SDK session's skill provider. Related -// files and assets are not supported. +// Text-only SKILL.md content returned by an SDK session's skill provider. YAML frontmatter +// is optional: fields it omits come from the catalog descriptor, fields it declares must +// match the descriptor, and `allowed-tools` is read only from frontmatter. Related files +// and assets are not supported. // Experimental: SkillProviderReadResult is part of an experimental API and may change or be // removed. // Internal: SkillProviderReadResult is an internal SDK API and is not part of the public // surface. type SkillProviderReadResult struct { - // Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. - Markdown string `json:"markdown"` + // SKILL.md text, with or without YAML frontmatter, or null when the provider has no skill + // with the requested name. The runtime enforces a 1 MiB UTF-8 byte limit. + Markdown *string `json:"markdown"` } // Skill names to mark as disabled in global configuration, replacing any previous list. @@ -19309,6 +20478,13 @@ type ToolsGetCurrentMetadataResult struct { type ToolsInitializeAndValidateResult struct { } +// No parameters; returns the external tool calls of the session and its sub-agents that are +// still waiting for a result. +// Experimental: ToolsListPendingRequestsRequest is part of an experimental API and may +// change or be removed. +type ToolsListPendingRequestsRequest struct { +} + // Optional model identifier whose tool overrides should be applied to the listing. // Experimental: ToolsListRequest is part of an experimental API and may change or be // removed. @@ -19859,6 +21035,9 @@ type UsageGetMetricsResult struct { // Per-agent usage metrics, keyed by agent instance identifier. The main conversation uses // the stable key `main`. AgentMetrics map[string]UsageMetricsAgentMetric `json:"agentMetrics,omitzero"` + // Completeness of totalNanoAiu. Numeric totals remain the reported subtotal; zero is not a + // claim of free usage when unavailable. + AiCreditsStatus *AiCreditsStatus `json:"aiCreditsStatus,omitempty"` // Aggregated code change metrics CodeChanges UsageMetricsCodeChanges `json:"codeChanges"` // Currently active model identifier @@ -19869,6 +21048,9 @@ type UsageGetMetricsResult struct { LastCallOutputTokens int64 `json:"lastCallOutputTokens"` // Per-model token and request metrics, keyed by model identifier ModelMetrics map[string]UsageMetricsModelMetric `json:"modelMetrics"` + // Provider-attributed model metrics, ordered by first observed provider and model. + // Null attribution preserves historical usage whose provider or model was not recorded. + ProviderModelMetrics []UsageMetricsProviderModelMetric `json:"providerModelMetrics,omitzero"` // ISO 8601 timestamp when the session started SessionStartTime time.Time `json:"sessionStartTime"` // Session-wide per-token-type accumulated token counts @@ -19923,6 +21105,9 @@ type UsageMetricsCodeChanges struct { // Experimental: UsageMetricsModelMetric is part of an experimental API and may change or be // removed. type UsageMetricsModelMetric struct { + // Completeness of this model's reported AI-credit subtotal. Explicitly reported zero is + // complete, not unavailable. + AiCreditsStatus *AiCreditsStatus `json:"aiCreditsStatus,omitempty"` // Latest known prompt-cache expiration for this model. A timestamp in the past indicates // that the observed cache has expired. CacheExpiresAt *time.Time `json:"cacheExpiresAt,omitempty"` @@ -19970,6 +21155,22 @@ type UsageMetricsModelMetricUsage struct { ReasoningTokens *int64 `json:"reasoningTokens,omitempty"` } +// Usage for one recorded provider and model, without merging identical model IDs across +// providers. +// Experimental: UsageMetricsProviderModelMetric is part of an experimental API and may +// change or be removed. +type UsageMetricsProviderModelMetric struct { + // Request, token, and cost totals for this provider/model. + Metrics UsageMetricsModelMetric `json:"metrics"` + // Model display name captured at call time, when known. + ModelDisplayName *string `json:"modelDisplayName,omitempty"` + // Model identity, or null for legacy aggregate-only usage. + ModelID *string `json:"modelId"` + // Provider identity and product label captured when the call was dispatched; null when + // unknown. + Provider *ModelProviderRef `json:"provider"` +} + // Session-wide token-detail entry containing the accumulated token count for one token type. // Experimental: UsageMetricsTokenDetail is part of an experimental API and may change or be // removed. @@ -19978,6 +21179,20 @@ type UsageMetricsTokenDetail struct { TokenCount int64 `json:"tokenCount"` } +// Internal absolute code-change totals reported by the owning host. +// Experimental: UsageSetCodeChangesRequest is part of an experimental API and may change or +// be removed. +// Internal: UsageSetCodeChangesRequest is an internal SDK API and is not part of the public +// surface. +type UsageSetCodeChangesRequest struct { + // Absolute changed-file count; omission preserves the previous count. + FilesCount *float64 `json:"filesCount,omitempty"` + // Absolute added-line total, replacing the previous reading. + LinesAdded float64 `json:"linesAdded"` + // Absolute removed-line total, replacing the previous reading. + LinesRemoved float64 `json:"linesRemoved"` +} + // Result of a user-requested shell command. // Experimental: UserRequestedShellCommandResult is part of an experimental API and may // change or be removed. @@ -20010,9 +21225,8 @@ type UserSettingMetadata struct { Value any `json:"value"` } -// Per-key metadata for every known user setting (settings.json overlaid with the legacy -// config.json, config.json wins), including settings left at their default. Excludes -// repository- and enterprise-managed overrides. +// Per-key metadata for every known user setting in settings.json, including settings left +// at their default. Excludes repository- and enterprise-managed overrides. // Experimental: UserSettingsGetResult is part of an experimental API and may change or be // removed. type UserSettingsGetResult struct { @@ -20021,11 +21235,6 @@ type UserSettingsGetResult struct { Settings map[string]UserSettingMetadata `json:"settings"` } -// Experimental: UserSettingsReloadResult is part of an experimental API and may change or -// be removed. -type UserSettingsReloadResult struct { -} - // Partial user settings to write to settings.json. Each top-level key is written // individually, replacing the existing value; a key whose value is null is removed. // Experimental: UserSettingsSetRequest is part of an experimental API and may change or be @@ -20035,14 +21244,9 @@ type UserSettingsSetRequest struct { Settings any `json:"settings"` } -// Outcome of writing user settings. // Experimental: UserSettingsSetResult is part of an experimental API and may change or be // removed. type UserSettingsSetResult struct { - // Top-level keys whose write landed in settings.json but is shadowed by a value still - // present in the legacy config.json (config.json wins on read). The write does not take - // effect until the legacy value is removed. - ShadowedKeys []string `json:"shadowedKeys"` } // The approval to add as a session-scoped rule @@ -20975,12 +22179,29 @@ type WorkflowToolRunRequest struct { ToolCallID *string `json:"toolCallId,omitempty"` } +// Complete text used to generate one session diff. These are display contents, with the +// same text decoding as the patch, not a file-restore contract. +// Experimental: WorkspaceDiffContents is part of an experimental API and may change or be +// removed. +type WorkspaceDiffContents struct { + // Complete current text read when computing the diff. Omitted for a deleted file; an empty + // string represents an existing empty file. + After *string `json:"after,omitempty"` + // Complete text before the session first changed the file. Omitted when the file did not + // exist; an empty string represents an existing empty file. + Before *string `json:"before,omitempty"` +} + // A single changed file and its unified diff. // Experimental: WorkspaceDiffFileChange is part of an experimental API and may change or be // removed. type WorkspaceDiffFileChange struct { // Type of change represented by this file diff. ChangeType WorkspaceDiffFileChangeType `json:"changeType"` + // Full text used for this patch, only when includeContents was requested for session mode. + // Omitted for binary, oversized or unavailable contents, and for fallback results. Read + // isFallback and isTruncated before treating an absent value as a missing file. + Contents *WorkspaceDiffContents `json:"contents,omitempty"` // Unified diff content for the file. Empty when the diff was truncated. Diff string `json:"diff"` // Whether the diff content was omitted because it exceeded the per-file size limit. @@ -21093,6 +22314,11 @@ type WorkspacesDeleteAutopilotObjectiveResult struct { type WorkspacesDiffRequest struct { // When true, ignore whitespace-only changes (git `--ignore-all-space`). Defaults to false. IgnoreWhitespace *bool `json:"ignoreWhitespace,omitempty"` + // Include the full before/after text used to compute each session diff. Defaults to false; + // true is accepted only for session mode. Existing capture/read limits still apply, and + // binary or unavailable contents are not returned. This can substantially increase response + // size. + IncludeContents *bool `json:"includeContents,omitempty"` // Diff mode requested by the client. Mode WorkspaceDiffMode `json:"mode"` } @@ -21576,6 +22802,21 @@ const ( AgentRegistrySpawnValidationErrorReasonYoloNotAllowed AgentRegistrySpawnValidationErrorReason = "yolo-not-allowed" ) +// Whether the accumulated numeric AI-credit subtotal covers the observed calls. +// Experimental: AiCreditsStatus is part of an experimental API and may change or be removed. +type AiCreditsStatus string + +const ( + // Every observed call supplied an AI-credit amount, including an explicit zero. + AiCreditsStatusComplete AiCreditsStatus = "complete" + // Some calls supplied amounts and some did not; the numeric value is only a reported + // subtotal. + AiCreditsStatusPartial AiCreditsStatus = "partial" + // No AI-credit amount was reported for the observed calls. Numeric zero is not a zero-cost + // claim. + AiCreditsStatusUnavailable AiCreditsStatus = "unavailable" +) + // Type of GitHub reference // Experimental: AttachmentGitHubReferenceType is part of an experimental API and may change // or be removed. @@ -21636,6 +22877,7 @@ const ( type AuthInfoType string const ( + AuthInfoTypeAccount AuthInfoType = "account" AuthInfoTypeAPIKey AuthInfoType = "api-key" AuthInfoTypeCopilotAPIToken AuthInfoType = "copilot-api-token" AuthInfoTypeEnv AuthInfoType = "env" @@ -21646,16 +22888,19 @@ const ( AuthInfoTypeUser AuthInfoType = "user" ) -// Terminal disposition of a login persistence attempt. +// Disposition of a login attempt, including pending user decisions. // Experimental: AuthLoginResultStatus is part of an experimental API and may change or be // removed. type AuthLoginResultStatus string const ( - // The credential was persisted and the account is signed in. + // The credential was persisted and the selected account is signed in. AuthLoginResultStatusCompleted AuthLoginResultStatus = "completed" // The user declined plaintext persistence. AuthLoginResultStatusDeclined AuthLoginResultStatus = "declined" + // Credentials are saved; select an account using a returned selectionId as advance input to + // complete sign-in. + AuthLoginResultStatusNeedsAccountSelection AuthLoginResultStatus = "needs-account-selection" // Persistence needs explicit consent to store the token in plaintext. AuthLoginResultStatusNeedsPlaintextConsent AuthLoginResultStatus = "needs-plaintext-consent" ) @@ -21720,8 +22965,8 @@ const ( AutopilotObjectiveStatusPaused AutopilotObjectiveStatus = "paused" ) -// Routing preference used when the session model is `auto`. `fast` is an integrator-only -// latency preset and is not a first-party GitHub Copilot product preference. +// Extensible routing preference for the virtual `auto` model. New identifiers must be +// advertised and enabled by the provider. `fast` is an integrator-only latency preset. // Experimental: AutoTier is part of an experimental API and may change or be removed. type AutoTier string @@ -22468,6 +23713,20 @@ const ( ConnectorConnectResultKindPending ConnectorConnectResultKind = "pending" ) +// Availability. +// Experimental: ConnectorDiscoveryAvailability is part of an experimental API and may +// change or be removed. +type ConnectorDiscoveryAvailability string + +const ( + // Disabled. + ConnectorDiscoveryAvailabilityDisabled ConnectorDiscoveryAvailability = "disabled" + // Enabled. + ConnectorDiscoveryAvailabilityEnabled ConnectorDiscoveryAvailability = "enabled" + // Unavailable. + ConnectorDiscoveryAvailabilityUnavailable ConnectorDiscoveryAvailability = "unavailable" +) + // Live MCP status of one Connector-owned runtime server. // Experimental: ConnectorMCPStatus is part of an experimental API and may change or be // removed. @@ -24005,11 +25264,13 @@ const ( MCPServerConfigStdioTypeStdio MCPServerConfigStdioType = "stdio" ) -// Configuration source: user, workspace, plugin, builtin, or managed +// Configuration source: user, workspace, plugin, builtin, managed, or account // Experimental: MCPServerSource is part of an experimental API and may change or be removed. type MCPServerSource string const ( + // Server contributed by a signed-in account; enablement and organization policy still apply. + MCPServerSourceAccount MCPServerSource = "account" // Server bundled with the runtime. MCPServerSourceBuiltin MCPServerSource = "builtin" // Server supplied by a trusted host-managed catalog. @@ -24893,6 +26154,108 @@ const ( ProviderEndpointWireAPIResponses ProviderEndpointWireAPI = "responses" ) +// Access policy reported by the quota service. +// Experimental: ProviderQuotaAccessState is part of an experimental API and may change or +// be removed. +type ProviderQuotaAccessState string + +const ( + // Access is allowed. + ProviderQuotaAccessStateAllowed ProviderQuotaAccessState = "allowed" + // Access is denied. + ProviderQuotaAccessStateDenied ProviderQuotaAccessState = "denied" + // Quota is not required for this access. + ProviderQuotaAccessStateNotRequired ProviderQuotaAccessState = "not_required" + // Client-only compatibility placeholder when acquisitionStatus is unavailable or failed; + // not an observed service verdict. + ProviderQuotaAccessStateUnavailable ProviderQuotaAccessState = "unavailable" + // The service did not supply a recognized access state. + ProviderQuotaAccessStateUnknown ProviderQuotaAccessState = "unknown" +) + +// Acquisition state, independent of the service's business access/capacity verdict. +// Experimental: ProviderQuotaAcquisitionStatus is part of an experimental API and may +// change or be removed. +type ProviderQuotaAcquisitionStatus string + +const ( + // Acquisition failed; inspect httpStatus/acquisitionError without inferring a business + // verdict. + ProviderQuotaAcquisitionStatusFailed ProviderQuotaAcquisitionStatus = "failed" + // A service quota observation was received. + ProviderQuotaAcquisitionStatusSucceeded ProviderQuotaAcquisitionStatus = "succeeded" + // No service snapshot is available for this caller or surface. + ProviderQuotaAcquisitionStatusUnavailable ProviderQuotaAcquisitionStatus = "unavailable" +) + +// Capacity is independent of whether a numeric balance was supplied. +// Experimental: ProviderQuotaCapacityState is part of an experimental API and may change or +// be removed. +type ProviderQuotaCapacityState string + +const ( + // Capacity is available. + ProviderQuotaCapacityStateAvailable ProviderQuotaCapacityState = "available" + // The service explicitly reports exhaustion. + ProviderQuotaCapacityStateExhausted ProviderQuotaCapacityState = "exhausted" + // The service reports that capacity is not applicable. + ProviderQuotaCapacityStateNotApplicable ProviderQuotaCapacityState = "not_applicable" + // No billable quota is required. + ProviderQuotaCapacityStateNotRequired ProviderQuotaCapacityState = "not_required" + // Client-only compatibility placeholder when acquisitionStatus is unavailable or failed; + // not an observed service verdict. + ProviderQuotaCapacityStateUnavailable ProviderQuotaCapacityState = "unavailable" + // No recognized capacity state was supplied. + ProviderQuotaCapacityStateUnknown ProviderQuotaCapacityState = "unknown" + // The reported entitlement is unlimited. + ProviderQuotaCapacityStateUnlimited ProviderQuotaCapacityState = "unlimited" +) + +// Timing and purpose of a provider quota observation. +// Experimental: ProviderQuotaObservationKind is part of an experimental API and may change +// or be removed. +type ProviderQuotaObservationKind string + +const ( + // An explicit account read; consumption may still be subject to service aggregation delay. + ProviderQuotaObservationKindAccountSnapshot ProviderQuotaObservationKind = "account_snapshot" + // A verdict attached before an inference response. May be cached; never a post-charge + // balance. + ProviderQuotaObservationKindAdmissionState ProviderQuotaObservationKind = "admission_state" +) + +// Interpretation permitted for independently reported quantities. +// Experimental: ProviderQuotaQuantityKind is part of an experimental API and may change or +// be removed. +type ProviderQuotaQuantityKind string + +const ( + // Raw advisory quantities; do not infer permission, usage percentage, reset cadence, or + // post-charge balance. + ProviderQuotaQuantityKindAdvisoryBalance ProviderQuotaQuantityKind = "advisory_balance" + // Quantities form an authoritative available-of-entitled budget suitable for percentage + // presentation. + ProviderQuotaQuantityKindAuthoritativeBudget ProviderQuotaQuantityKind = "authoritative_budget" + // No quantity measurement, as with admission-only response headers. + ProviderQuotaQuantityKindNone ProviderQuotaQuantityKind = "none" +) + +// Units explicitly reported by a quota provider. +// Experimental: ProviderQuotaUnit is part of an experimental API and may change or be +// removed. +type ProviderQuotaUnit string + +const ( + // AI credits, not tokens or currency. + ProviderQuotaUnitAiCredits ProviderQuotaUnit = "ai_credits" + // Requests charged against an entitlement. + ProviderQuotaUnitRequests ProviderQuotaUnit = "requests" + // Model tokens. + ProviderQuotaUnitTokens ProviderQuotaUnit = "tokens" + // The observation does not establish a quantity unit. + ProviderQuotaUnitUnknown ProviderQuotaUnit = "unknown" +) + // Type of GitHub reference // Experimental: PushAttachmentGitHubReferenceType is part of an experimental API and may // change or be removed. @@ -25409,6 +26772,30 @@ const ( SessionOpenParamsKindResumeLast SessionOpenParamsKind = "resumeLast" ) +// Copilot plan tier used by the session quota projection. +// Experimental: SessionQuotaPlanTier is part of an experimental API and may change or be +// removed. +type SessionQuotaPlanTier string + +const ( + // Copilot Business. + SessionQuotaPlanTierBusiness SessionQuotaPlanTier = "business" + // Copilot education entitlement. + SessionQuotaPlanTierEdu SessionQuotaPlanTier = "edu" + // Copilot Enterprise. + SessionQuotaPlanTierEnterprise SessionQuotaPlanTier = "enterprise" + // Copilot Free. + SessionQuotaPlanTierFree SessionQuotaPlanTier = "free" + // Copilot Max. + SessionQuotaPlanTierMax SessionQuotaPlanTier = "max" + // Copilot Pro. + SessionQuotaPlanTierPro SessionQuotaPlanTier = "pro" + // Copilot Pro+. + SessionQuotaPlanTierProPlus SessionQuotaPlanTier = "pro_plus" + // The account's plan tier is not known. + SessionQuotaPlanTierUnknown SessionQuotaPlanTier = "unknown" +) + // Status discriminator for SessionsClientMetadataEntry. type SessionsClientMetadataEntryStatus string @@ -25572,6 +26959,7 @@ const ( type SettableAuthInfoType string const ( + SettableAuthInfoTypeAccount SettableAuthInfoType = "account" SettableAuthInfoTypeAPIKey SettableAuthInfoType = "api-key" SettableAuthInfoTypeCopilotAPIToken SettableAuthInfoType = "copilot-api-token" SettableAuthInfoTypeEnv SettableAuthInfoType = "env" @@ -26681,6 +28069,82 @@ func (a *ServerCommandsAPI) List(ctx context.Context) (*CommandList, error) { return &result, nil } +// Experimental: ServerConnectorsAPI contains experimental APIs that may change or be +// removed. +type ServerConnectorsAPI serverAPI + +// GetAccounts returns eligible accounts. +// +// RPC method: connectors.getAccounts. +// +// Returns: Eligible accounts. +func (a *ServerConnectorsAPI) GetAccounts(ctx context.Context) (*ConnectorDiscoveryAccountList, error) { + raw, err := a.client.Request(ctx, "connectors.getAccounts", nil) + if err != nil { + return nil, err + } + var result ConnectorDiscoveryAccountList + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetCapabilities returns feature availability. +// +// RPC method: connectors.getCapabilities. +// +// Returns: Feature availability. +func (a *ServerConnectorsAPI) GetCapabilities(ctx context.Context) (*ConnectorDiscoveryCapabilities, error) { + raw, err := a.client.Request(ctx, "connectors.getCapabilities", nil) + if err != nil { + return nil, err + } + var result ConnectorDiscoveryCapabilities + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Lists entries for the selected account. +// +// RPC method: connectors.list. +// +// Parameters: Selected account. +// +// Returns: Entries for the selected account. +func (a *ServerConnectorsAPI) List(ctx context.Context, params *ConnectorDiscoveryAccountRequest) (*ConnectorDiscoveryCatalogResult, error) { + raw, err := a.client.Request(ctx, "connectors.list", params) + if err != nil { + return nil, err + } + var result ConnectorDiscoveryCatalogResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Refreshes entries for the selected account. +// +// RPC method: connectors.refresh. +// +// Parameters: Selected account. +// +// Returns: Entries for the selected account. +func (a *ServerConnectorsAPI) Refresh(ctx context.Context, params *ConnectorDiscoveryAccountRequest) (*ConnectorDiscoveryCatalogResult, error) { + raw, err := a.client.Request(ctx, "connectors.refresh", params) + if err != nil { + return nil, err + } + var result ConnectorDiscoveryCatalogResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: ServerEnvironmentsAPI contains experimental APIs that may change or be // removed. type ServerEnvironmentsAPI serverAPI @@ -28922,17 +30386,15 @@ type ServerUserAPI serverAPI // removed. type ServerUserSettingsAPI serverAPI -// Get lists every known user setting (settings.json overlaid with the legacy config.json, -// config.json wins), each with its effective value, its default, and whether it is at the -// default — so settings the user has never set still appear with their default value. Does -// not include repository- or enterprise-managed overrides that the runtime layers on top at -// session time. +// Get lists every known user setting from settings.json, each with its effective value, its +// default, and whether it is at the default — so settings the user has never set still +// appear with their default value. Does not include repository- or enterprise-managed +// overrides that the runtime layers on top at session time. // // RPC method: user.settings.get. // -// Returns: Per-key metadata for every known user setting (settings.json overlaid with the -// legacy config.json, config.json wins), including settings left at their default. Excludes -// repository- and enterprise-managed overrides. +// Returns: Per-key metadata for every known user setting in settings.json, including +// settings left at their default. Excludes repository- and enterprise-managed overrides. func (a *ServerUserSettingsAPI) Get(ctx context.Context) (*UserSettingsGetResult, error) { raw, err := a.client.Request(ctx, "user.settings.get", nil) if err != nil { @@ -28945,33 +30407,13 @@ func (a *ServerUserSettingsAPI) Get(ctx context.Context) (*UserSettingsGetResult return &result, nil } -// Reload drops this runtime process's in-memory user settings cache so the next settings -// read observes disk. -// -// RPC method: user.settings.reload. -func (a *ServerUserSettingsAPI) Reload(ctx context.Context) (*UserSettingsReloadResult, error) { - raw, err := a.client.Request(ctx, "user.settings.reload", nil) - if err != nil { - return nil, err - } - var result UserSettingsReloadResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - // Set writes one or more user settings to settings.json, replacing each provided top-level -// key. A key whose value is null is removed. Returns the keys whose new value is shadowed -// by a legacy config.json entry (config.json wins on read), which the runtime leaves in -// place — such writes do not take effect until the legacy value is removed. +// key. A key whose value is null is removed. // // RPC method: user.settings.set. // // Parameters: Partial user settings to write to settings.json. Each top-level key is // written individually, replacing the existing value; a key whose value is null is removed. -// -// Returns: Outcome of writing user settings. func (a *ServerUserSettingsAPI) Set(ctx context.Context, params *UserSettingsSetRequest) (*UserSettingsSetResult, error) { raw, err := a.client.Request(ctx, "user.settings.set", params) if err != nil { @@ -28999,6 +30441,7 @@ type ServerRPC struct { Agents *ServerAgentsAPI Catalog *ServerCatalogAPI Commands *ServerCommandsAPI + Connectors *ServerConnectorsAPI Environments *ServerEnvironmentsAPI Extensions *ServerExtensionsAPI Hooks *ServerHooksAPI @@ -29067,6 +30510,7 @@ func NewServerRPC(client *jsonrpc2.Client) *ServerRPC { r.Agents = (*ServerAgentsAPI)(&r.common) r.Catalog = (*ServerCatalogAPI)(&r.common) r.Commands = (*ServerCommandsAPI)(&r.common) + r.Connectors = (*ServerConnectorsAPI)(&r.common) r.Environments = (*ServerEnvironmentsAPI)(&r.common) r.Extensions = (*ServerExtensionsAPI)(&r.common) r.Hooks = (*ServerHooksAPI)(&r.common) @@ -29092,6 +30536,442 @@ type internalServerAPI struct { client *jsonrpc2.Client } +// Experimental: InternalServerAgentsAPI contains experimental APIs that may change or be +// removed. +type InternalServerAgentsAPI internalServerAPI + +// CustomAgentInitialModelDecision resolves the model a custom agent asks for against the +// models actually available, and answers both the model to switch to and the warning a user +// should see when the agent's preference cannot be met. A custom agent may name several +// acceptable models in preference order, so the decision is a match rather than a lookup, +// and an agent whose preference is unavailable is a normal outcome that produces a warning +// rather than an error. A host must call this rather than pick the first available name +// itself, because the preference order and the wording of the warning are what keep one +// installation's agent selection the same as another's. +// +// RPC method: agents.customAgentInitialModelDecision. +// +// Parameters: The models a custom agent asks for, and the models actually available. +// +// Returns: The model to switch to, and the warning to show when the agent's preference +// could not be met. +// Internal: CustomAgentInitialModelDecision is part of the SDK's internal +// handshake/plumbing; external callers should not use it. +func (a *InternalServerAgentsAPI) CustomAgentInitialModelDecision(ctx context.Context, params *AgentsCustomAgentInitialModelDecisionParams) (*AgentsCustomAgentInitialModelDecisionResult, error) { + raw, err := a.client.Request(ctx, "agents.customAgentInitialModelDecision", params) + if err != nil { + return nil, err + } + var result AgentsCustomAgentInitialModelDecisionResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetAvailableBuiltins lists the shipped agents a client should offer right now, filtered +// by the feature flags it passes. `getBuiltins` names every agent the runtime knows about; +// some of those are gated, so a client rendering a picker wants this narrower list together +// with the description to show beside each name. +// +// RPC method: agents.getAvailableBuiltins. +// +// Parameters: The feature flags to evaluate shipped agents against. +// +// Returns: The shipped agents available under the requested flags. +// Internal: GetAvailableBuiltins is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerAgentsAPI) GetAvailableBuiltins(ctx context.Context, params *AgentsGetAvailableBuiltinsRequest) (*AgentsGetAvailableBuiltinsResult, error) { + raw, err := a.client.Request(ctx, "agents.getAvailableBuiltins", params) + if err != nil { + return nil, err + } + var result AgentsGetAvailableBuiltinsResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetBuiltinDefinition loads one shipped agent's YAML definition, for a client that needs +// what the agent declares rather than only its name. `getBuiltins` reports which names have +// a definition to load: a name outside its `yamlBasedNames` is special-cased in code and +// has none. The definition crosses as its own JSON rather than as contract-typed fields, +// because the runtime parses it with the agent schema's tolerant shape and re-typing it +// here would drop the keys that shape accepts and this one does not. The projected +// `__nativeCustomAgent` view the runtime derives is included, so a caller reading the +// declared model and a caller rendering the agent see the same definition. +// +// RPC method: agents.getBuiltinDefinition. +// +// Parameters: The shipped agent whose definition to load. +// +// Returns: One shipped agent's definition. +// Internal: GetBuiltinDefinition is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerAgentsAPI) GetBuiltinDefinition(ctx context.Context, params *AgentsGetBuiltinDefinitionRequest) (*AgentsGetBuiltinDefinitionResult, error) { + raw, err := a.client.Request(ctx, "agents.getBuiltinDefinition", params) + if err != nil { + return nil, err + } + var result AgentsGetBuiltinDefinitionResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetBuiltinListingDefinition projects one shipped agent the way a picker lists it, reading +// only the metadata at the head of the definition file and stopping before the prompt body. +// `getBuiltinDefinition` answers the whole definition instead, so a client listing every +// shipped agent should prefer this one: the cost of a listing grows with the number of +// agents, and the prompt body is the part a listing never shows. The two also differ in +// shape. This returns the projected custom agent on its own, whereas `getBuiltinDefinition` +// returns the authored definition with that projection nested under `__nativeCustomAgent`. +// +// RPC method: agents.getBuiltinListingDefinition. +// +// Parameters: The shipped agent whose listing entry to load. +// +// Returns: One shipped agent, projected for a listing. +// Internal: GetBuiltinListingDefinition is part of the SDK's internal handshake/plumbing; +// external callers should not use it. +func (a *InternalServerAgentsAPI) GetBuiltinListingDefinition(ctx context.Context, params *AgentsGetBuiltinListingDefinitionRequest) (*AgentsGetBuiltinListingDefinitionResult, error) { + raw, err := a.client.Request(ctx, "agents.getBuiltinListingDefinition", params) + if err != nil { + return nil, err + } + var result AgentsGetBuiltinListingDefinitionResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetBuiltins lists the agents this runtime ships, by name. A consumer separating shipped +// agents from ones the user or a plugin authored should compare against these names rather +// than against `AgentInfo.source`: an authored agent may carry the `builtin` source while +// not being one of these, and the runtime treats the two as separate questions. +// `disableableNames` is the subset a user may turn off, which a client needs to decide +// whether to offer a toggle. `yamlBasedNames` is the subset backed by a shipped YAML +// definition, which a client needs before asking the runtime to load one. +// +// RPC method: agents.getBuiltins. +// +// Returns: The agents this runtime ships, named so a consumer can tell them apart from +// authored ones. +// Internal: GetBuiltins is part of the SDK's internal handshake/plumbing; external callers +// should not use it. +func (a *InternalServerAgentsAPI) GetBuiltins(ctx context.Context) (*AgentsGetBuiltinsResult, error) { + raw, err := a.client.Request(ctx, "agents.getBuiltins", nil) + if err != nil { + return nil, err + } + var result AgentsGetBuiltinsResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: InternalServerGitAPI contains experimental APIs that may change or be +// removed. +type InternalServerGitAPI internalServerAPI + +// CurrentBranchRemote reads the remote that the branch checked out in a working tree +// tracks, as `branch..remote` configures it. Reports `origin` rather than failing +// whenever there is no tracking configuration to read — on a detached HEAD, on a branch +// with no upstream, or when git itself fails — because a caller asking which remote to talk +// to needs an answer it can act on, not an error. Marked internal because it exists to +// carry a CLI call site off the napi boundary onto the SDK contract; it is migration +// plumbing, not a surface consumers are meant to depend on. +// +// RPC method: git.currentBranchRemote. +// +// Parameters: Working-tree path a git query applies to. +// +// Returns: The remote the checked-out branch tracks. +// Internal: CurrentBranchRemote is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerGitAPI) CurrentBranchRemote(ctx context.Context, params *GitCwdRequest) (*GitCurrentBranchRemoteResult, error) { + raw, err := a.client.Request(ctx, "git.currentBranchRemote", params) + if err != nil { + return nil, err + } + var result GitCurrentBranchRemoteResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ReposFromRemotes lists the GitHub repositories a working tree's remotes point at, one +// entry per distinct repository, so a caller can resolve a base and head repository without +// parsing remote URLs itself. When several remotes name the same repository, only the first +// is listed, and the entry keeps that remote name. Remotes pointing at no GitHub host are +// left out, so an empty list means the tree reaches GitHub through no remote. Failing to +// read the remotes is reported as an error rather than as an empty list, because the two +// mean different things to a caller. Marked internal because it exists to carry a CLI call +// site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface +// consumers are meant to depend on. +// +// RPC method: git.reposFromRemotes. +// +// Parameters: Git working tree whose GitHub remotes should be listed. +// +// Returns: The GitHub repositories a working tree's remotes point at. +// Internal: ReposFromRemotes is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerGitAPI) ReposFromRemotes(ctx context.Context, params *GitReposFromRemotesRequest) (*GitReposFromRemotesResult, error) { + raw, err := a.client.Request(ctx, "git.reposFromRemotes", params) + if err != nil { + return nil, err + } + var result GitReposFromRemotesResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// WorkingDirectoryContext collects the repository context of a working directory in one +// call: working tree root, repository identifier and host, current branch, and the HEAD and +// base commits. Every repository field is omitted when the path is not inside a git working +// tree, and the requested path is echoed back as `cwd`. The answer is the same +// `SessionWorkingDirectoryContext` that `session.metadata.recordContextChange` accepts, so +// a caller polling for a context change can forward the result unchanged. Marked internal +// because it exists to carry a CLI call site off the napi boundary onto the SDK contract; +// it is migration plumbing, not a surface consumers are meant to depend on. It can become +// public once an SDK consumer needs to derive session context from a directory itself. +// +// RPC method: git.workingDirectoryContext. +// +// Parameters: Working-tree path a git query applies to. +// +// Returns: Updated working directory and git context. Emitted as the new payload of +// `session.context_changed`. +// Internal: WorkingDirectoryContext is part of the SDK's internal handshake/plumbing; +// external callers should not use it. +func (a *InternalServerGitAPI) WorkingDirectoryContext(ctx context.Context, params *GitCwdRequest) (*SessionWorkingDirectoryContext, error) { + raw, err := a.client.Request(ctx, "git.workingDirectoryContext", params) + if err != nil { + return nil, err + } + var result SessionWorkingDirectoryContext + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: InternalServerGitHubOwnersAPI contains experimental APIs that may change or +// be removed. +type InternalServerGitHubOwnersAPI internalServerAPI + +// Cancel abandons an owner listing started with the given request id. Answers `canceled: +// true` while a listing with that id is running. Answers `canceled: false` when the id was +// never registered, was registered but not used, was released after being abandoned, or its +// listing has ended. Canceling an unused id releases it, and a later `list` with that id is +// refused. The cancel acts only on owner listings and never reaches another request of the +// host. +// +// RPC method: gitHubOwners.cancel. +// +// Parameters: The owner listing to abandon. +// +// Returns: Whether the id named a running owner listing. +// Internal: Cancel is part of the SDK's internal handshake/plumbing; external callers +// should not use it. +func (a *InternalServerGitHubOwnersAPI) Cancel(ctx context.Context, params *GitHubOwnersCancelRequest) (*GitHubOwnersCancelResult, error) { + raw, err := a.client.Request(ctx, "gitHubOwners.cancel", params) + if err != nil { + return nil, err + } + var result GitHubOwnersCancelResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Lists the logins the authenticated user may act as — their own account first, then the +// organizations they belong to — by asking the GitHub API under the supplied credential. No +// credential travels in the request: `authInfo` selects one the runtime already holds, and +// the runtime resolves the token and the GitHub host from it. A failure the caller should +// render arrives as `message`; one it should raise arrives as `throwError`. +// +// RPC method: gitHubOwners.list. +// +// Parameters: Credential to list owners under, and the request id that makes the listing +// cancellable. +// +// Returns: Outcome of an owner listing. Exactly one of `owners` and `message` is present, +// except that `throwError` reports a failure the caller is expected to raise rather than +// render. +// Internal: List is part of the SDK's internal handshake/plumbing; external callers should +// not use it. +func (a *InternalServerGitHubOwnersAPI) List(ctx context.Context, params *GitHubOwnersListRequest) (*GitHubOwnersListResult, error) { + raw, err := a.client.Request(ctx, "gitHubOwners.list", params) + if err != nil { + return nil, err + } + var result GitHubOwnersListResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// NextRequestId registers a cancellable owner listing and returns its request id. Separate +// from `gitHubOwners.list` so the id exists before the listing starts: a caller that +// abandons the listing the moment it begins would otherwise have nothing to name in +// `gitHubOwners.cancel`. The id serves one listing only. Long-abandoned unused ids can be +// released by later allocations. +// +// RPC method: gitHubOwners.nextRequestId. +// +// Returns: A freshly registered request id. Registering it before the listing starts is +// what lets a cancel that races the request still find the owner listing slot. The id +// serves one listing only. Long-abandoned unused ids can be released by later allocations. +// Internal: NextRequestId is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerGitHubOwnersAPI) NextRequestId(ctx context.Context) (*GitHubOwnersRequestIDResult, error) { + raw, err := a.client.Request(ctx, "gitHubOwners.nextRequestId", nil) + if err != nil { + return nil, err + } + var result GitHubOwnersRequestIDResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: InternalServerGitHubRepositoryAPI contains experimental APIs that may +// change or be removed. +type InternalServerGitHubRepositoryAPI internalServerAPI + +// AtPath resolves the GitHub repository that owns a working-tree path by reading the +// selected git remote configured for it, preferring `origin`. Returns a null `repository` +// when the path is inside a git working tree but that selected remote does not resolve to a +// GitHub host. Fails when the path is not inside a git working tree at all, so a caller can +// tell 'not a repository' apart from 'a repository with no GitHub remote'. +// +// RPC method: gitHubRepository.atPath. +// +// Parameters: Working-tree path whose owning GitHub repository should be resolved. +// +// Returns: The GitHub repository that owns the requested path, when the selected remote +// (`origin`, else the first) is on a GitHub host. +// Internal: AtPath is part of the SDK's internal handshake/plumbing; external callers +// should not use it. +func (a *InternalServerGitHubRepositoryAPI) AtPath(ctx context.Context, params *GitHubRepositoryAtPathRequest) (*GitHubRepositoryAtPathResult, error) { + raw, err := a.client.Request(ctx, "gitHubRepository.atPath", params) + if err != nil { + return nil, err + } + var result GitHubRepositoryAtPathResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: InternalServerGlobalStateAPI contains experimental APIs that may change or +// be removed. +type InternalServerGlobalStateAPI internalServerAPI + +// Load reads the host's machine-wide state: which plugins are installed and the one-off +// flags and timestamps that record what the user has already been shown or migrated. This +// is the state that outlives a single session and a single workspace, so a host reads it to +// decide whether to run a first-launch step, offer an onboarding prompt, or skip one it has +// already completed. The stored credentials are deliberately not part of this result; a +// caller that needs an authenticated identity asks the account methods for it instead. +// Reading is non-destructive and every field is optional, because a fresh install has +// recorded nothing yet. +// +// RPC method: globalState.load. +// +// Returns: The host's machine-wide state. Every field is optional because a fresh install +// has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a +// negative answer. Stored credentials are deliberately absent from this shape. +// Internal: Load is part of the SDK's internal handshake/plumbing; external callers should +// not use it. +func (a *InternalServerGlobalStateAPI) Load(ctx context.Context) (*GlobalStateLoadResult, error) { + raw, err := a.client.Request(ctx, "globalState.load", nil) + if err != nil { + return nil, err + } + var result GlobalStateLoadResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// LoadForConfigDir reads the host's machine-wide state exactly as `globalState.load` does, +// but from a caller-supplied configuration directory instead of the one the server resolved +// for itself. Use this when a consumer scopes a session to its own Copilot home — the SDK's +// per-session `configDir` override — so the state read matches the directory that session +// actually uses. An absent or empty `configDir` resolves the server's own home, making this +// identical to `globalState.load`. The stored credentials are omitted here for the same +// reason they are omitted from `globalState.load`: a caller that needs an authenticated +// identity asks the account methods instead, so pointing this at another directory cannot +// be used to read the credentials kept in it. +// +// RPC method: globalState.loadForConfigDir. +// +// Parameters: Selects the configuration directory whose machine-wide state to read. +// +// Returns: The host's machine-wide state. Every field is optional because a fresh install +// has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a +// negative answer. Stored credentials are deliberately absent from this shape. +// Internal: LoadForConfigDir is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerGlobalStateAPI) LoadForConfigDir(ctx context.Context, params *GlobalStateLoadForConfigDirRequest) (*GlobalStateLoadResult, error) { + raw, err := a.client.Request(ctx, "globalState.loadForConfigDir", params) + if err != nil { + return nil, err + } + var result GlobalStateLoadResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// WriteKey records one top-level key in the host's machine-wide state, the counterpart to +// `globalState.load`. A host calls this to remember that it has shown an onboarding step, +// asked a one-off question, or completed a migration, so the next run can skip it. Only the +// named key is replaced and the rest of the document is preserved, which lets two writers +// record different flags without overwriting each other; passing no value removes the key +// instead. Only the keys a host records itself are writable: `appInstallNudgeResponded`, +// `appTipShown`, `askedSetupTerminals`, `autoFeedbackLastPromptedAt`, `firstLaunchAt`, +// `recentModelIds`, `sandboxCredentialProxyCaDeclined` and `sandboxOnboardingShown`. Every +// other key is refused, including `installedPlugins`, the stored credentials, +// `trustedFolders`, the staff flags and the signed-in accounts. Plugin enablement must use +// the plugin APIs, which apply repository and managed-policy checks. +// +// RPC method: globalState.writeKey. +// +// Parameters: A single top-level key to record in the host's machine-wide state. The write +// replaces only that key and leaves the rest of the document untouched, so two writers +// recording different one-off flags do not overwrite each other. The stored credential keys +// cannot be written through this method. +// Internal: WriteKey is part of the SDK's internal handshake/plumbing; external callers +// should not use it. +func (a *InternalServerGlobalStateAPI) WriteKey(ctx context.Context, params *GlobalStateWriteKeyRequest) (*GlobalStateWriteKeyResult, error) { + raw, err := a.client.Request(ctx, "globalState.writeKey", params) + if err != nil { + return nil, err + } + var result GlobalStateWriteKeyResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: InternalServerHostAPI contains experimental APIs that may change or be // removed. type InternalServerHostAPI internalServerAPI @@ -29230,6 +31110,33 @@ func (a *InternalServerSessionsAPI) ConfigureSessionExtensions(ctx context.Conte return &result, nil } +// CreateWorkspace creates the workspace record for a session that has not been opened yet. +// A host that hands a session off to another application — writing the record and then +// launching that application against the session ID — needs the record on disk before any +// session exists to carry it, which the session-scoped workspace methods cannot do. +// Replaces any existing record and resets the checkpoint index. When writing to the local +// filesystem, a stored `fork_count` survives on disk. Returns the record it built, so a +// surviving stored `fork_count` can differ from the answer. +// +// RPC method: sessions.createWorkspace. +// +// Parameters: Identity, state location and starting context for a workspace record. +// +// Returns: The workspace record that was written. +// Internal: CreateWorkspace is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerSessionsAPI) CreateWorkspace(ctx context.Context, params *SessionsCreateWorkspaceRequest) (*SessionsCreateWorkspaceResult, error) { + raw, err := a.client.Request(ctx, "sessions.createWorkspace", params) + if err != nil { + return nil, err + } + var result SessionsCreateWorkspaceResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Deletes one local session from disk after running the same lifecycle hooks as the session // manager. // @@ -29370,14 +31277,73 @@ func (a *InternalServerSessionsAPI) ListNonEmptySessionIds(ctx context.Context, return &result, nil } +// LoadWorkspace reads a session's workspace record straight from disk, without opening the +// session. Resuming by session ID has to know where the session lives before it can +// connect, so the lookup cannot come from the session-scoped workspace methods, which +// resolve their location from a live session's context. Returns no record when the file is +// absent. +// +// RPC method: sessions.loadWorkspace. +// +// Parameters: Where the session's state lives, as a root directory and the session ID under +// it. +// +// Returns: The workspace record on disk, omitted when the session has none. +// Internal: LoadWorkspace is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerSessionsAPI) LoadWorkspace(ctx context.Context, params *SessionsLoadWorkspaceRequest) (*SessionsLoadWorkspaceResult, error) { + raw, err := a.client.Request(ctx, "sessions.loadWorkspace", params) + if err != nil { + return nil, err + } + var result SessionsLoadWorkspaceResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// UpdateWorkspaceFields merges fields into a session's workspace record on disk, creating +// the record when it is absent. The counterpart to `sessions.loadWorkspace`, for the same +// before-the-session-exists case. It preserves stored workspace-schema fields the request +// does not supply, does not preserve stored keys outside the workspace schema, and never +// replaces a stored `fork_count`. +// +// RPC method: sessions.updateWorkspaceFields. +// +// Parameters: Where the session's state lives, plus workspace-schema fields to merge into +// its workspace record. Stored keys outside the schema are not preserved, and a stored +// `fork_count` is never replaced. +// +// Returns: The merge completed. The record carries the supplied workspace-schema fields, +// but a stored `fork_count` stays. +// Internal: UpdateWorkspaceFields is part of the SDK's internal handshake/plumbing; +// external callers should not use it. +func (a *InternalServerSessionsAPI) UpdateWorkspaceFields(ctx context.Context, params *SessionsUpdateWorkspaceFieldsRequest) (*SessionsUpdateWorkspaceFieldsResult, error) { + raw, err := a.client.Request(ctx, "sessions.updateWorkspaceFields", params) + if err != nil { + return nil, err + } + var result SessionsUpdateWorkspaceFieldsResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // InternalServerRPC provides internal SDK server-scoped RPC methods (handshake helpers // etc.). Not part of the public API. type InternalServerRPC struct { // Reuse a single struct instead of allocating one for each service on the heap. common internalServerAPI - Host *InternalServerHostAPI - Sessions *InternalServerSessionsAPI + Agents *InternalServerAgentsAPI + Git *InternalServerGitAPI + GitHubOwners *InternalServerGitHubOwnersAPI + GitHubRepository *InternalServerGitHubRepositoryAPI + GlobalState *InternalServerGlobalStateAPI + Host *InternalServerHostAPI + Sessions *InternalServerSessionsAPI } // Connect performs the SDK server connection handshake and validates the optional @@ -29412,6 +31378,11 @@ func (a *InternalServerRPC) Connect(ctx context.Context, params *ConnectRequest) func NewInternalServerRPC(client *jsonrpc2.Client) *InternalServerRPC { r := &InternalServerRPC{} r.common = internalServerAPI{client: client} + r.Agents = (*InternalServerAgentsAPI)(&r.common) + r.Git = (*InternalServerGitAPI)(&r.common) + r.GitHubOwners = (*InternalServerGitHubOwnersAPI)(&r.common) + r.GitHubRepository = (*InternalServerGitHubRepositoryAPI)(&r.common) + r.GlobalState = (*InternalServerGlobalStateAPI)(&r.common) r.Host = (*InternalServerHostAPI)(&r.common) r.Sessions = (*InternalServerSessionsAPI)(&r.common) return r @@ -30615,7 +32586,9 @@ func (a *EventLogAPI) Tail(ctx context.Context) (*EventLogTailResult, error) { // Experimental: ExtensionsAPI contains experimental APIs that may change or be removed. type ExtensionsAPI sessionAPI -// Disables an extension for the session. +// Disables an extension for the session and persists the preference when the session has a +// settings store. Hosts synchronizing effective membership should use extensions.reconcile +// instead. // // RPC method: session.extensions.disable. // @@ -30636,7 +32609,9 @@ func (a *ExtensionsAPI) Disable(ctx context.Context, params *ExtensionsDisableRe return &result, nil } -// Enables an extension for the session. +// Enables an extension for the session and persists the preference when the session has a +// settings store. Hosts synchronizing effective membership should use extensions.reconcile +// instead. // // RPC method: session.extensions.enable. // @@ -30675,6 +32650,38 @@ func (a *ExtensionsAPI) List(ctx context.Context) (*ExtensionList, error) { return &result, nil } +// Reconcile host-only reconciliation of authoritative session-effective extension +// membership and enablement. Refreshes runtime-owned discovery and preferences without +// persisting settings, installing plugins, or restarting unchanged activations. Returns +// ExtensionList only after required starts and process/contribution cleanup settle. Takes +// no caller inventory or overrides. Missing controllers, unready/incomplete discovery, +// unavailable workspaces, superseded inputs, and lifecycle failures are errors, not empty +// membership. Independently proven revocations may be applied before an error; retry +// converges without restarting healthy activations. Error data contains +// lifecycleChangesApplied and code: extension_reconciliation_host_required, +// extension_reconciliation_unavailable, extension_reconciliation_not_ready, +// extension_reconciliation_discovery_failed, +// extension_reconciliation_workspace_unavailable, extension_reconciliation_superseded, or +// extension_reconciliation_lifecycle_failed. Mark host reconciliation state applied only on +// success. On older runtimes, method-not-found must not fall back to global discovery and +// persistent extension disables. +// +// RPC method: session.extensions.reconcile. +// +// Returns: Extensions discovered for the session, with their current status. +func (a *ExtensionsAPI) Reconcile(ctx context.Context) (*ExtensionList, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.extensions.reconcile", req) + if err != nil { + return nil, err + } + var result ExtensionList + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Reloads extension definitions and processes for the session. // // RPC method: session.extensions.reload. @@ -31295,10 +33302,8 @@ func (a *MCPAPI) IsServerRunning(ctx context.Context, params *MCPIsServerRunning return &result, nil } -// Lists MCP servers configured for the session, their connection status, and host-level -// state. The host-level state (disabled/filtered servers, failed/needs-auth/pending -// connections, mcp3p policy, full config) is empty/zero when no MCP host has been -// initialized for the session. +// Lists materialized MCP servers and their connection status. Cache misses may start and +// wait for MCP servers. // // RPC method: session.mcp.list. // @@ -31317,6 +33322,27 @@ func (a *MCPAPI) List(ctx context.Context) (*MCPServerList, error) { return &result, nil } +// ListConfigured lists effective MCP configuration without starting, restarting, +// authenticating, or waiting for servers. An optional live observation is from an already +// materialized matching server; this is not a readiness guarantee. +// +// RPC method: session.mcp.listConfigured. +// +// Returns: Effective MCP configuration with optional live observations from matching +// already materialized servers. +func (a *MCPAPI) ListConfigured(ctx context.Context) (*MCPConfiguredServerList, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.mcp.listConfigured", req) + if err != nil { + return nil, err + } + var result MCPConfiguredServerList + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // ListTools lists the tools exposed by a connected MCP server on this session's host. This // performs a live `tools/list` request. Tool UI metadata is returned independently of // whether MCP Apps rendering is enabled for the session. @@ -32148,6 +34174,9 @@ func (a *MetadataAPI) ContextInfo(ctx context.Context, params *MetadataContextIn if params != nil { req["outputTokenLimit"] = params.OutputTokenLimit req["promptTokenLimit"] = params.PromptTokenLimit + if params.ProviderID != nil { + req["providerId"] = *params.ProviderID + } if params.SelectedModel != nil { req["selectedModel"] = *params.SelectedModel } @@ -32475,6 +34504,9 @@ func (a *ModeAPI) Set(ctx context.Context, params *ModeSetRequest) (*ModeSetResu if params.PlanModelConfigured != nil { req["planModelConfigured"] = *params.PlanModelConfigured } + if params.PlanModelProviderID != nil { + req["planModelProviderId"] = *params.PlanModelProviderID + } if params.PlanReasoningEffort != nil { req["planReasoningEffort"] = *params.PlanReasoningEffort } @@ -32675,6 +34707,9 @@ func (a *ModelAPI) SwitchTo(ctx context.Context, params *ModelSwitchToRequest) ( if params.PickerPersistence != nil { req["pickerPersistence"] = *params.PickerPersistence } + if params.ProviderID != nil { + req["providerId"] = *params.ProviderID + } if params.ReasoningEffort != nil { req["reasoningEffort"] = *params.ReasoningEffort } @@ -34528,6 +36563,66 @@ func (a *QueueAPI) WithdrawMessage(ctx context.Context, params *QueueWithdrawMes return &result, nil } +// Experimental: QuotaAPI contains experimental APIs that may change or be removed. +type QuotaAPI sessionAPI + +// Gets the session's current quota and account projection without making a network request. +// +// RPC method: session.quota.get. +// +// Returns: The incumbent session-owned quota and account projection. +func (a *QuotaAPI) Get(ctx context.Context) (*SessionQuotaGetResult, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.quota.get", req) + if err != nil { + return nil, err + } + var result SessionQuotaGetResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Refreshes the session's provider model catalog bypassing its cache, folds quota snapshots +// into session state, and returns the updated projection. Failures leave the last known +// quota intact. +// +// RPC method: session.quota.refresh. +// +// Returns: The incumbent session-owned quota and account projection. +func (a *QuotaAPI) Refresh(ctx context.Context) (*SessionQuotaRefreshResult, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.quota.refresh", req) + if err != nil { + return nil, err + } + var result SessionQuotaRefreshResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// TakeWarnings returns and clears the session's pending quota warnings. Reading or +// refreshing quota does not drain warnings. +// +// RPC method: session.quota.takeWarnings. +// +// Returns: Pending warnings returned and cleared by session.quota.takeWarnings. +func (a *QuotaAPI) TakeWarnings(ctx context.Context) (*SessionQuotaTakeWarningsResult, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.quota.takeWarnings", req) + if err != nil { + return nil, err + } + var result SessionQuotaTakeWarningsResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: RemoteAPI contains experimental APIs that may change or be removed. type RemoteAPI sessionAPI @@ -35472,6 +37567,25 @@ func (a *ToolsAPI) InitializeAndValidate(ctx context.Context) (*ToolsInitializeA return &result, nil } +// ListPendingRequests lists the external tool calls of the session and its sub-agents that +// are still waiting for session.tools.handlePendingToolCall. +// +// RPC method: session.tools.listPendingRequests. +// +// Returns: External tool calls still waiting for a result. +func (a *ToolsAPI) ListPendingRequests(ctx context.Context) (*PendingExternalToolRequestList, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.tools.listPendingRequests", req) + if err != nil { + return nil, err + } + var result PendingExternalToolRequestList + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Set atomically replaces the complete externally implemented tool list supplied by the // calling connection. Built-in, MCP/plugin, extension-discovered, subagent, and tools // supplied by other connections remain unchanged. @@ -36353,6 +38467,9 @@ func (a *WorkspacesAPI) Diff(ctx context.Context, params *WorkspacesDiffRequest) if params.IgnoreWhitespace != nil { req["ignoreWhitespace"] = *params.IgnoreWhitespace } + if params.IncludeContents != nil { + req["includeContents"] = *params.IncludeContents + } req["mode"] = params.Mode } raw, err := a.client.Request(ctx, "session.workspaces.diff", req) @@ -36726,6 +38843,7 @@ type SessionRPC struct { Provider *ProviderAPI Providers *ProvidersAPI Queue *QueueAPI + Quota *QuotaAPI Remote *RemoteAPI Sandbox *SandboxAPI Schedule *ScheduleAPI @@ -37064,6 +39182,7 @@ func NewSessionRPC(client *jsonrpc2.Client, sessionID string) *SessionRPC { r.Provider = (*ProviderAPI)(&r.common) r.Providers = (*ProvidersAPI)(&r.common) r.Queue = (*QueueAPI)(&r.common) + r.Quota = (*QuotaAPI)(&r.common) r.Remote = (*RemoteAPI)(&r.common) r.Sandbox = (*SandboxAPI)(&r.common) r.Schedule = (*ScheduleAPI)(&r.common) @@ -37505,6 +39624,35 @@ func (a *InternalMCPAPI) ReloadWithConfig(ctx context.Context, params *MCPReload return &result, nil } +// SetConnectedIdeInfo records the IDE the host is connected to, so the agent's system +// prompt can name it and its workspace folder. Null or an omitted `ide` clears the recorded +// value, which is how a host reports that it is disconnected; there is no separate clear +// method. Both `ideName` and `workspaceFolder` are required together, because half a state +// cannot be attributed to a project. +// +// RPC method: session.mcp.setConnectedIdeInfo. +// +// Parameters: Records which IDE the host is connected to, or clears it. +// Internal: SetConnectedIdeInfo is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalMCPAPI) SetConnectedIdeInfo(ctx context.Context, params *SessionMCPSetConnectedIdeInfoParams) (*SessionMCPSetConnectedIdeInfoResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.Ide != nil { + req["ide"] = *params.Ide + } + } + raw, err := a.client.Request(ctx, "session.mcp.setConnectedIdeInfo", req) + if err != nil { + return nil, err + } + var result SessionMCPSetConnectedIdeInfoResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // UnregisterExternalClient unregisters a previously registered external MCP client by // server name. Marked internal as the paired companion of `registerExternalClient`: only // in-process callers that registered a client this way can meaningfully unregister it. @@ -37580,6 +39728,9 @@ func (a *InternalModelAPI) ApplyStartupOverlay(ctx context.Context, params *Mode if params.RepoModel != nil { req["repoModel"] = *params.RepoModel } + if params.RepoModelProviderID != nil { + req["repoModelProviderId"] = *params.RepoModelProviderID + } if params.RepoReasoningEffort != nil { req["repoReasoningEffort"] = *params.RepoReasoningEffort } @@ -37598,6 +39749,31 @@ func (a *InternalModelAPI) ApplyStartupOverlay(ctx context.Context, params *Mode return &result, nil } +// ClearStartupSeed clears an unchanged startup model/provider seed before default-model +// resolution. Never clears a resumed or user-selected model. +// +// RPC method: session.model.clearStartupSeed. +// Internal: ClearStartupSeed is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalModelAPI) ClearStartupSeed(ctx context.Context, params *SessionModelClearStartupSeedRequest) (*SessionModelClearStartupSeedResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["expectedModel"] = params.ExpectedModel + if params.ExpectedProviderID != nil { + req["expectedProviderId"] = *params.ExpectedProviderID + } + } + raw, err := a.client.Request(ctx, "session.model.clearStartupSeed", req) + if err != nil { + return nil, err + } + var result SessionModelClearStartupSeedResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: InternalQueueAPI contains experimental APIs that may change or be removed. type InternalQueueAPI internalSessionAPI @@ -38125,6 +40301,37 @@ func (a *InternalUIAPI) HandleHumanUserInput(ctx context.Context, params *UIHand return &result, nil } +// Experimental: InternalUsageAPI contains experimental APIs that may change or be removed. +type InternalUsageAPI internalSessionAPI + +// SetCodeChanges internal host-only replacement of absolute code-change totals reported by +// a relay host. +// +// RPC method: session.usage.setCodeChanges. +// +// Parameters: Internal absolute code-change totals reported by the owning host. +// Internal: SetCodeChanges is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalUsageAPI) SetCodeChanges(ctx context.Context, params *SessionUsageSetCodeChangesRequest) (*SessionUsageSetCodeChangesResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.FilesCount != nil { + req["filesCount"] = *params.FilesCount + } + req["linesAdded"] = params.LinesAdded + req["linesRemoved"] = params.LinesRemoved + } + raw, err := a.client.Request(ctx, "session.usage.setCodeChanges", req) + if err != nil { + return nil, err + } + var result SessionUsageSetCodeChangesResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: InternalWorkflowAPI contains experimental APIs that may change or be // removed. type InternalWorkflowAPI internalSessionAPI @@ -38234,6 +40441,7 @@ type InternalSessionRPC struct { Schedule *InternalScheduleAPI Settings *InternalSettingsAPI UI *InternalUIAPI + Usage *InternalUsageAPI Workflow *InternalWorkflowAPI } @@ -38282,6 +40490,7 @@ func NewInternalSessionRPC(client *jsonrpc2.Client, sessionID string) *InternalS r.Schedule = (*InternalScheduleAPI)(&r.common) r.Settings = (*InternalSettingsAPI)(&r.common) r.UI = (*InternalUIAPI)(&r.common) + r.Usage = (*InternalUsageAPI)(&r.common) r.Workflow = (*InternalWorkflowAPI)(&r.common) return r } diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index 739081b803..471e491a87 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -21,6 +21,12 @@ func unmarshalAuthInfo(data []byte) (AuthInfo, error) { } switch raw.Type { + case AuthInfoTypeAccount: + var d AccountAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case AuthInfoTypeAPIKey: var d APIKeyAuthInfo if err := json.Unmarshal(data, &d); err != nil { @@ -85,6 +91,17 @@ func (r RawAuthInfoData) MarshalJSON() ([]byte, error) { }) } +func (r AccountAuthInfo) MarshalJSON() ([]byte, error) { + type alias AccountAuthInfo + return json.Marshal(struct { + Type AuthInfoType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + func (r APIKeyAuthInfo) MarshalJSON() ([]byte, error) { type alias APIKeyAuthInfo return json.Marshal(struct { @@ -2990,6 +3007,58 @@ func (r GitHubTokenAcquireResultToken) MarshalJSON() ([]byte, error) { }) } +func (r InstalledPluginSource) MarshalJSON() ([]byte, error) { + if r.InstalledPluginSourceGitHub != nil { + return json.Marshal(r.InstalledPluginSourceGitHub) + } + if r.InstalledPluginSourceLocal != nil { + return json.Marshal(r.InstalledPluginSourceLocal) + } + if r.InstalledPluginSourceURL != nil { + return json.Marshal(r.InstalledPluginSourceURL) + } + if r.String != nil { + return json.Marshal(r.String) + } + return []byte("null"), nil +} + +func (r *InstalledPluginSource) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + *r = InstalledPluginSource{} + return nil + } + { + var value InstalledPluginSourceGitHub + if err := json.Unmarshal(data, &value); err == nil { + *r = InstalledPluginSource{InstalledPluginSourceGitHub: &value} + return nil + } + } + { + var value InstalledPluginSourceLocal + if err := json.Unmarshal(data, &value); err == nil { + *r = InstalledPluginSource{InstalledPluginSourceLocal: &value} + return nil + } + } + { + var value InstalledPluginSourceURL + if err := json.Unmarshal(data, &value); err == nil { + *r = InstalledPluginSource{InstalledPluginSourceURL: &value} + return nil + } + } + { + var value string + if err := json.Unmarshal(data, &value); err == nil { + *r = InstalledPluginSource{String: &value} + return nil + } + } + return errors.New("data did not match any union variant for InstalledPluginSource") +} + func (r *HandlePendingToolCallRequest) UnmarshalJSON(data []byte) error { type rawHandlePendingToolCallRequest struct { Error *string `json:"error,omitempty"` @@ -3543,58 +3612,6 @@ func (r *InstallationConfirmationRequest) UnmarshalJSON(data []byte) error { return nil } -func (r InstalledPluginSource) MarshalJSON() ([]byte, error) { - if r.InstalledPluginSourceGitHub != nil { - return json.Marshal(r.InstalledPluginSourceGitHub) - } - if r.InstalledPluginSourceLocal != nil { - return json.Marshal(r.InstalledPluginSourceLocal) - } - if r.InstalledPluginSourceURL != nil { - return json.Marshal(r.InstalledPluginSourceURL) - } - if r.String != nil { - return json.Marshal(r.String) - } - return []byte("null"), nil -} - -func (r *InstalledPluginSource) UnmarshalJSON(data []byte) error { - if string(data) == "null" { - *r = InstalledPluginSource{} - return nil - } - { - var value InstalledPluginSourceGitHub - if err := json.Unmarshal(data, &value); err == nil { - *r = InstalledPluginSource{InstalledPluginSourceGitHub: &value} - return nil - } - } - { - var value InstalledPluginSourceLocal - if err := json.Unmarshal(data, &value); err == nil { - *r = InstalledPluginSource{InstalledPluginSourceLocal: &value} - return nil - } - } - { - var value InstalledPluginSourceURL - if err := json.Unmarshal(data, &value); err == nil { - *r = InstalledPluginSource{InstalledPluginSourceURL: &value} - return nil - } - } - { - var value string - if err := json.Unmarshal(data, &value); err == nil { - *r = InstalledPluginSource{String: &value} - return nil - } - } - return errors.New("data did not match any union variant for InstalledPluginSource") -} - func matchesMCPSerializableServerConfigMCPServerConfigHTTP(data []byte) bool { var rawGroup0 struct { Command json.RawMessage `json:"command"` @@ -7708,6 +7725,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { AskUserDisabled *bool `json:"askUserDisabled,omitempty"` AuthClientIDMetadataURL *string `json:"authClientIdMetadataUrl,omitempty"` AuthInfo json.RawMessage `json:"authInfo,omitempty"` + AutoTierIsExplicit *bool `json:"autoTierIsExplicit,omitempty"` AvailableTools []string `json:"availableTools,omitzero"` Capi *CapiSessionOptions `json:"capi,omitempty"` ClientKind *string `json:"clientKind,omitempty"` @@ -7729,6 +7747,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { EnableScriptSafety *bool `json:"enableScriptSafety,omitempty"` EnableSkills *bool `json:"enableSkills,omitempty"` EnableStreaming *bool `json:"enableStreaming,omitempty"` + EnforceManagedModelDefaults *bool `json:"enforceManagedModelDefaults,omitempty"` EnvValueMode *SessionOpenOptionsEnvValueMode `json:"envValueMode,omitempty"` EventsLogDirectory *string `json:"eventsLogDirectory,omitempty"` EventsLogIncludesSubagents *bool `json:"eventsLogIncludesSubagents,omitempty"` @@ -7794,6 +7813,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { } r.AuthInfo = value } + r.AutoTierIsExplicit = raw.AutoTierIsExplicit r.AvailableTools = raw.AvailableTools r.Capi = raw.Capi r.ClientKind = raw.ClientKind @@ -7815,6 +7835,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.EnableScriptSafety = raw.EnableScriptSafety r.EnableSkills = raw.EnableSkills r.EnableStreaming = raw.EnableStreaming + r.EnforceManagedModelDefaults = raw.EnforceManagedModelDefaults r.EnvValueMode = raw.EnvValueMode r.EventsLogDirectory = raw.EventsLogDirectory r.EventsLogIncludesSubagents = raw.EventsLogIncludesSubagents @@ -8140,6 +8161,12 @@ func unmarshalSettableAuthInfo(data []byte) (SettableAuthInfo, error) { } switch raw.Type { + case SettableAuthInfoTypeAccount: + var d AccountAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case SettableAuthInfoTypeAPIKey: var d APIKeyAuthInfo if err := json.Unmarshal(data, &d); err != nil { diff --git a/go/rpc/zsession_encoding.go b/go/rpc/zsession_encoding.go index fa44772dab..b2faf63c1a 100644 --- a/go/rpc/zsession_encoding.go +++ b/go/rpc/zsession_encoding.go @@ -677,6 +677,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSessionQuotaObservation: + var d SessionQuotaObservationData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeSessionRemoteSteerableChanged: var d SessionRemoteSteerableChangedData if err := json.Unmarshal(raw.Data, &d); err != nil { diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index b301571f68..2e1d5c4580 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -212,8 +212,11 @@ const ( SessionEventTypeSessionPermissionRecovery SessionEventType = "session.permission_recovery" // Experimental: SessionEventTypeSessionPermissionsChanged identifies an experimental event // that may change or be removed. - SessionEventTypeSessionPermissionsChanged SessionEventType = "session.permissions_changed" - SessionEventTypeSessionPlanChanged SessionEventType = "session.plan_changed" + SessionEventTypeSessionPermissionsChanged SessionEventType = "session.permissions_changed" + SessionEventTypeSessionPlanChanged SessionEventType = "session.plan_changed" + // Experimental: SessionEventTypeSessionQuotaObservation identifies an experimental event + // that may change or be removed. + SessionEventTypeSessionQuotaObservation SessionEventType = "session.quota_observation" SessionEventTypeSessionRemoteSteerableChanged SessionEventType = "session.remote_steerable_changed" SessionEventTypeSessionResume SessionEventType = "session.resume" SessionEventTypeSessionScheduleCancelled SessionEventType = "session.schedule_cancelled" @@ -349,6 +352,18 @@ type PromptCacheBreakData struct { func (*PromptCacheBreakData) sessionEventData() {} func (*PromptCacheBreakData) Type() SessionEventType { return SessionEventTypePromptCacheBreak } +// A provider-owned quota observation, distinct from per-call usage and charge accounting. +// Experimental: SessionQuotaObservationData is part of an experimental API and may change or be removed. +type SessionQuotaObservationData struct { + // The admitted provider's state observation. Admission observations never contain quantities or reset/percentage semantics. + Observation ProviderQuotaState `json:"observation"` +} + +func (*SessionQuotaObservationData) sessionEventData() {} +func (*SessionQuotaObservationData) Type() SessionEventType { + return SessionEventTypeSessionQuotaObservation +} + // A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume. type SessionAutoTierSwitchFailedData struct { // Auto preference that remains effective after the failed request. @@ -432,6 +447,8 @@ type AssistantMessageData struct { ParentToolCallID *string `json:"parentToolCallId,omitempty"` // Generation phase for phased-output models (e.g., thinking vs. response phases) Phase *string `json:"phase,omitempty"` + // Opaque origin provider identity captured at dispatch. Only an exact match with the target provider permits replay of reasoningOpaque, encryptedContent, reasoningBlocks, serverTools, and provider response/item identifiers. Matching model ids or wire protocols is insufficient. Omitted for legacy or unattributed messages; the runtime excludes unproven opaque state when projecting to a known provider. Text content and client tool requests/results remain portable semantic history. + ProviderID *string `json:"providerId,omitempty"` // Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping. `reasoningText` and `reasoningOpaque` are a lossy derived view of these blocks, retained for display. ReasoningBlocks *AssistantMessageReasoningBlocks `json:"reasoningBlocks,omitempty"` // Opaque/encrypted extended thinking data from Anthropic models. Session-bound and stripped on resume. @@ -830,17 +847,24 @@ func (*HumanResponseRecordedData) Type() SessionEventType { // Durable session usage checkpoint for reconstructing aggregate accounting on resume type SessionUsageCheckpointData struct { + // Complete accounting state captured atomically with the receipt watermarks. + // Internal: AccountingSnapshot is part of the SDK's internal API surface and is not intended for external use. + AccountingSnapshot *UsageGetMetricsResult `json:"accountingSnapshot,omitempty"` // Internal per-model prompt-cache state used to restore expiration tracking on resume // Internal: ModelCacheState is part of the SDK's internal API surface and is not intended for external use. ModelCacheState []UsageCheckpointModelCacheState `json:"modelCacheState,omitzero"` // Internal per-conversation prompt-cache-break detector baselines restored on resume // Internal: PromptCacheBreakState is part of the SDK's internal API surface and is not intended for external use. PromptCacheBreakState []any `json:"promptCacheBreakState,omitzero"` + // Provider/model accounting snapshot that subsumes preceding usage receipts. + ProviderModelMetrics []UsageMetricsProviderModelMetric `json:"providerModelMetrics,omitzero"` // Session-wide accumulated nano-AI units cost at checkpoint time TotalNanoAiu float64 `json:"totalNanoAiu"` // Total number of premium API requests used at checkpoint time // Internal: TotalPremiumRequests is part of the SDK's internal API surface and is not intended for external use. TotalPremiumRequests *float64 `json:"totalPremiumRequests,omitempty"` + // Highest accounted sequence per source session; no per-call deduplication history is retained. + UsageAccountingWatermarks map[string]int64 `json:"usageAccountingWatermarks,omitzero"` } func (*SessionUsageCheckpointData) sessionEventData() {} @@ -1430,8 +1454,12 @@ type ModelCallFailureData struct { QuotaSnapshots map[string]AssistantUsageQuotaSnapshot `json:"quotaSnapshots,omitzero"` // Reasoning effort level used for the failed model call, if applicable ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // Serialized (uncompressed) byte length of the failed request body. A content-free size signal. + RequestBodyBytes *int64 `json:"requestBodyBytes,omitempty"` // Content-free structural summary of the failing request. Contains only counts and shape flags (no prompt content), so it is safe for unrestricted telemetry. Populated only for client-error (4xx) failures. RequestFingerprint *ModelCallFailureRequestFingerprint `json:"requestFingerprint,omitempty"` + // Zero-based orchestrator retry index of the failed attempt + RetryAttempt *int64 `json:"retryAttempt,omitempty"` // Per-request treatment/eligibility signal returned by the Copilot API in the `X-GitHub-Copilot-Request-TE` response header for the associated model call; `false` when the header was absent or unparseable. Rte *bool `json:"rte,omitempty"` // Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation @@ -1442,6 +1470,10 @@ type ModelCallFailureData struct { StatusCode *int32 `json:"statusCode,omitempty"` // Transport used for the failed model call (http or websocket) Transport *ModelCallFailureTransport `json:"transport,omitempty"` + // Milliseconds spent on the WebSocket attempt before falling back to HTTP + WebsocketFallbackAfterMs *int64 `json:"websocketFallbackAfterMs,omitempty"` + // Why the failed call was carried by the HTTP fallback of a WebSocket-capable dispatcher; absent when no fallback occurred + WebsocketFallbackReason *ModelCallWebSocketFallbackReason `json:"websocketFallbackReason,omitempty"` } func (*ModelCallFailureData) sessionEventData() {} @@ -1716,6 +1748,10 @@ func (*ModelCallFinalResultData) Type() SessionEventType { return SessionEventTy type AssistantUsageData struct { // Number of accepted speculative prediction tokens AcceptedPredictionTokens *int64 `json:"acceptedPredictionTokens,omitempty"` + // Runtime-owned identity shared with the durable accounting receipt. + Accounting *UsageAccountingIdentity `json:"accounting,omitempty"` + // Availability of this call's reported AI-credit amount. Missing billing is unavailable, not a zero charge. + AiCreditsStatus *AiCreditsStatus `json:"aiCreditsStatus,omitempty"` // Completion ID from the model provider (e.g., chatcmpl-abc123) APICallID *string `json:"apiCallId,omitempty"` // API endpoint used for this model call, matching CAPI supported_endpoints vocabulary @@ -1772,6 +1808,8 @@ type AssistantUsageData struct { MaxPromptTokens *int64 `json:"maxPromptTokens,omitempty"` // Model identifier used for this API call Model string `json:"model"` + // Model display name captured for this call, not derived from the current selection. + ModelDisplayName *string `json:"modelDisplayName,omitempty"` // Fixed-set provider family serving the bring-your-own-key model (for example "openai", "anthropic", "azure_openai", "ollama", "llama_cpp", or "other"). Never the caller-supplied provider name. Absent for Copilot-served models. ModelProvider *string `json:"modelProvider,omitempty"` // Number of tool calls returned by the model @@ -1784,6 +1822,8 @@ type AssistantUsageData struct { // Parent tool call ID when this usage originates from a sub-agent // Deprecated: ParentToolCallID is deprecated. ParentToolCallID *string `json:"parentToolCallId,omitempty"` + // Provider identity and product label captured by the actual call's dispatch path. + Provider *ModelProviderRef `json:"provider,omitempty"` // GitHub request tracing ID (x-github-request-id header) for server-side log correlation ProviderCallID *string `json:"providerCallId,omitempty"` // Per-quota resource usage snapshots, keyed by quota identifier @@ -1797,6 +1837,8 @@ type AssistantUsageData struct { ReasoningTokens *int64 `json:"reasoningTokens,omitempty"` // Number of rejected speculative prediction tokens RejectedPredictionTokens *int64 `json:"rejectedPredictionTokens,omitempty"` + // Serialized (uncompressed) byte length of the request body. A content-free size signal. + RequestBodyBytes *int64 `json:"requestBodyBytes,omitempty"` // Per-request treatment/eligibility signal returned by the Copilot API in the `X-GitHub-Copilot-Request-TE` response header for the associated model call; `false` when the header was absent or unparseable. Rte *bool `json:"rte,omitempty"` // Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation @@ -1817,6 +1859,10 @@ type AssistantUsageData struct { ToolTokenCount *int64 `json:"toolTokenCount,omitempty"` // Transport used for this model call (http or websocket) Transport *AssistantUsageTransport `json:"transport,omitempty"` + // Milliseconds spent on the WebSocket attempt before falling back to HTTP + WebsocketFallbackAfterMs *int64 `json:"websocketFallbackAfterMs,omitempty"` + // Why the call was carried by the HTTP fallback of a WebSocket-capable dispatcher; absent when no fallback occurred + WebsocketFallbackReason *ModelCallWebSocketFallbackReason `json:"websocketFallbackReason,omitempty"` } func (*AssistantUsageData) sessionEventData() {} @@ -1982,24 +2028,34 @@ func (*ModelCallStartData) Type() SessionEventType { return SessionEventTypeMode type SessionModelChangeData struct { // Committed Auto preference after the model configuration change, when applicable. AutoTier *AutoTier `json:"autoTier,omitempty"` + // True when autoTier is a managed-policy default. Omitted for user-authored and legacy values. + AutoTierManaged *bool `json:"autoTierManaged,omitempty"` // Reason the change happened, when not user-initiated. `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path, or `"refusal_fallback"` when the active model declined a request (content refusal) and the runtime switched to the configured refusal-fallback model. UI clients can use this to render contextual copy. Cause *string `json:"cause,omitempty"` // Context tier after the model change; null explicitly clears a previously selected tier ContextTier *ContextTier `json:"contextTier,omitempty"` + // True when contextTier is a managed-policy default. Omitted for user-authored and legacy values. + ContextTierManaged *bool `json:"contextTierManaged,omitempty"` // Newly selected model identifier NewModel string `json:"newModel"` // Previously committed Auto preference, when one was explicitly selected. PreviousAutoTier *AutoTier `json:"previousAutoTier,omitempty"` // Model that was previously selected, if any PreviousModel *string `json:"previousModel,omitempty"` + // Provider of previousModel, when known. A provider-only change is a model selection change even when the model identifiers are equal. + PreviousProviderID *string `json:"previousProviderId,omitempty"` // Reasoning effort level before the model change, if applicable PreviousReasoningEffort *string `json:"previousReasoningEffort,omitempty"` // Reasoning summary mode before the model change, if applicable PreviousReasoningSummary *ReasoningSummary `json:"previousReasoningSummary,omitempty"` // Output verbosity level before the model change, if applicable PreviousVerbosity *Verbosity `json:"previousVerbosity,omitempty"` + // Provider selected for newModel. Omitted for legacy or unattributed selections; never inferred from a later selection. + ProviderID *string `json:"providerId,omitempty"` // Reasoning effort level after the model change, if applicable ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // True when the reasoning effort is a managed-policy default bound to reasoningEffortModel. Omitted for agent-authored, user-authored, independent, and legacy effort. + ReasoningEffortManaged *bool `json:"reasoningEffortManaged,omitempty"` // Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. ReasoningEffortModel *string `json:"reasoningEffortModel,omitempty"` // Reasoning summary mode after the model change, if applicable @@ -2523,18 +2579,18 @@ type SubagentConfiguredData struct { func (*SubagentConfiguredData) sessionEventData() {} func (*SubagentConfiguredData) Type() SessionEventType { return SessionEventTypeSubagentConfigured } -// Runtime enforcement of enterprise managed settings: fires when the session blocks or caps a runtime action because enterprise policy governs it, so SDK clients can explain *why* an action was governed. Unlike `session.managed_settings_resolved` (which reports *what* is managed), this reports a concrete governed action — e.g. a user or host tried to turn on a bypass-permissions escalation while policy disables it. Emitted live (not persisted to the session event log) on user/host-initiated attempts only, never for silent policy application. Marked experimental while the managed-settings surface stabilizes. +// Runtime enforcement of enterprise managed settings: fires when the session blocks or caps a runtime action because enterprise policy governs it, so SDK clients can explain *why* an action was governed. Unlike `session.managed_settings_resolved` (which reports *what* is managed), this reports a concrete governed action — e.g. a user or host tried to turn on bypass permissions or Assisted Permissions while the corresponding policy disables it. Emitted live (not persisted to the session event log) on user/host-initiated attempts only, never for silent policy application. Marked experimental while the managed-settings surface stabilizes. // Experimental: SessionManagedSettingsEnforcedData is part of an experimental API and may change or be removed. type SessionManagedSettingsEnforcedData struct { // The category of runtime action that managed policy governed. Action ManagedSettingsEnforcedAction `json:"action"` - // For a `bypass_permissions_blocked` action, which permission-escalation primitive was refused. Absent for actions without a specific escalation primitive. + // For a `bypass_permissions_blocked` action, which permission-mode or escalation primitive was refused. Absent for actions without a specific primitive. Escalation *ManagedSettingsEnforcedEscalation `json:"escalation,omitempty"` // Whether the enforcement was forced by fail-closed handling (managed policy could not be determined) rather than an explicit managed setting. When true, `setting` still names the restriction that was applied. FailClosed bool `json:"failClosed"` // A human-readable explanation of why the action was governed, suitable for surfacing to the user. Message string `json:"message"` - // The managed setting key responsible for the enforcement (e.g. `permissions.disableBypassPermissionsMode`). + // The managed setting key responsible for the enforcement (for example `permissions.disableBypassPermissionsMode` or `permissions.disableAssistedPermissionsMode`). Setting string `json:"setting"` } @@ -2663,10 +2719,14 @@ type SessionStartData struct { AlreadyInUse *bool `json:"alreadyInUse,omitempty"` // Auto routing preference selected at session creation time AutoTier *AutoTier `json:"autoTier,omitempty"` + // True when autoTier is a managed-policy default. Omitted for user-authored and legacy values. + AutoTierManaged *bool `json:"autoTierManaged,omitempty"` // Working directory and git context at session start Context *WorkingDirectoryContext `json:"context,omitempty"` // Context tier selected at session creation time for models with tiered context pricing; null when no tier is selected (e.g., non-tiered model) ContextTier *ContextTier `json:"contextTier,omitempty"` + // True when contextTier is a managed-policy default. Omitted for user-authored and legacy values. + ContextTierManaged *bool `json:"contextTierManaged,omitempty"` // Version string of the Copilot application CopilotVersion string `json:"copilotVersion"` // 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. @@ -2675,8 +2735,12 @@ type SessionStartData struct { GitHubMCPToolConfig *GitHubMCPToolConfig `json:"githubMcpToolConfig,omitempty"` // Identifier of the software producing the events (e.g., "copilot-agent") Producer string `json:"producer"` + // Provider of selectedModel at creation time, when explicitly selected. + ProviderID *string `json:"providerId,omitempty"` // Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // True when the reasoning effort is a managed-policy default bound to reasoningEffortModel. Omitted for agent-authored, user-authored, independent, and legacy effort. + ReasoningEffortManaged *bool `json:"reasoningEffortManaged,omitempty"` // Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. ReasoningEffortModel *string `json:"reasoningEffortModel,omitempty"` // Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") @@ -2745,18 +2809,26 @@ type SessionResumeData struct { AlreadyInUse *bool `json:"alreadyInUse,omitempty"` // Auto routing preference active at resume time AutoTier *AutoTier `json:"autoTier,omitempty"` + // True when autoTier is a managed-policy default. Omitted for user-authored and legacy values. + AutoTierManaged *bool `json:"autoTierManaged,omitempty"` // Updated working directory and git context at resume time Context *WorkingDirectoryContext `json:"context,omitempty"` // Context tier currently selected at resume time; null when no tier is active ContextTier *ContextTier `json:"contextTier,omitempty"` + // True when contextTier is a managed-policy default. Omitted for user-authored and legacy values. + ContextTierManaged *bool `json:"contextTierManaged,omitempty"` // 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 or omitted, pending work is normally marked as interrupted unless the resume passively joined live work owned by another client; sessionWasActive distinguishes that case. ContinuePendingWork *bool `json:"continuePendingWork,omitempty"` // Total number of persisted events in the session at the time of resume EventCount int64 `json:"eventCount"` // On-disk byte size of the session's persisted events.jsonl file at resume time; omitted when the file does not exist or cannot be stat'd EventsFileSizeBytes *int64 `json:"eventsFileSizeBytes,omitempty"` + // Provider of selectedModel at resume time, when explicitly selected. + ProviderID *string `json:"providerId,omitempty"` // Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // True when the reasoning effort is a managed-policy default bound to reasoningEffortModel. Omitted for agent-authored, user-authored, independent, and legacy effort. + ReasoningEffortManaged *bool `json:"reasoningEffortManaged,omitempty"` // Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. ReasoningEffortModel *string `json:"reasoningEffortModel,omitempty"` // Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") @@ -2795,6 +2867,9 @@ func (*SessionSnapshotRewindData) Type() SessionEventType { // Session termination metrics including usage statistics, code changes, and shutdown reason type SessionShutdownData struct { + // Complete accounting state captured atomically with receipt watermarks. + // Internal: AccountingSnapshot is part of the SDK's internal API surface and is not intended for external use. + AccountingSnapshot *UsageGetMetricsResult `json:"accountingSnapshot,omitempty"` // Per-agent usage breakdown, keyed by agent instance identifier. The main conversation uses the stable key `main`. AgentMetrics map[string]ShutdownAgentMetric `json:"agentMetrics,omitzero"` // Aggregate code change metrics for the session @@ -2811,6 +2886,8 @@ type SessionShutdownData struct { EventsFileSizeBytes *int64 `json:"eventsFileSizeBytes,omitempty"` // Per-model usage breakdown, keyed by model identifier ModelMetrics map[string]ShutdownModelMetric `json:"modelMetrics"` + // Provider/model accounting preserved for replay without consulting current provider selection. + ProviderModelMetrics []UsageMetricsProviderModelMetric `json:"providerModelMetrics,omitzero"` // Unix timestamp (milliseconds) when the session started SessionStartTime int64 `json:"sessionStartTime"` // Whether the session ended normally ("routine") or due to a crash/fatal error ("error") @@ -2829,6 +2906,8 @@ type SessionShutdownData struct { // Total number of premium API requests used during the session // Internal: TotalPremiumRequests is part of the SDK's internal API surface and is not intended for external use. TotalPremiumRequests *float64 `json:"totalPremiumRequests,omitempty"` + // Per-source accounting sequence watermarks subsuming preceding usage receipts. + UsageAccountingWatermarks map[string]int64 `json:"usageAccountingWatermarks,omitzero"` } func (*SessionShutdownData) sessionEventData() {} @@ -3679,6 +3758,8 @@ type CommandsChangedCommand struct { // Token usage breakdown for the compaction LLM call (aligned with assistant.usage format) type CompactionCompleteCompactionTokensUsed struct { + // Whether the compaction's credit amount was reported for all, some, or none of its calls. + AiCreditsStatus *AiCreditsStatus `json:"aiCreditsStatus,omitempty"` // Cached input tokens reused in the compaction LLM call CacheReadTokens *int64 `json:"cacheReadTokens,omitempty"` // Tokens written to prompt cache in the compaction LLM call @@ -3692,8 +3773,12 @@ type CompactionCompleteCompactionTokensUsed struct { InputTokens *int64 `json:"inputTokens,omitempty"` // Model identifier used for the compaction LLM call Model *string `json:"model,omitempty"` + // Model display name captured for the compaction call. + ModelDisplayName *string `json:"modelDisplayName,omitempty"` // Output tokens produced by the compaction LLM call OutputTokens *int64 `json:"outputTokens,omitempty"` + // Provider captured for the compaction call; absent when attribution is unknown. + Provider *ModelProviderRef `json:"provider,omitempty"` } // Per-request cost and usage data from the CAPI copilot_usage response field @@ -4086,6 +4171,10 @@ type MCPServersLoadedServer struct { // Content-free structural summary of the failing request for diagnosing malformed 4xx calls type ModelCallFailureRequestFingerprint struct { + // Summed byte length of opaque or encrypted reasoning payloads + EncryptedContentBytes *int64 `json:"encryptedContentBytes,omitempty"` + // Summed byte length of inline image payloads (data URLs and base64 sources) + ImageBytes *int64 `json:"imageBytes,omitempty"` // Total number of image content parts ImagePartCount int64 `json:"imagePartCount"` // Image parts whose media type cannot be determined (rejected by strict providers) @@ -4096,6 +4185,8 @@ type ModelCallFailureRequestFingerprint struct { MessageCount int64 `json:"messageCount"` // Tool calls whose name is missing or empty (rejected by strict providers) NamelessToolCallCount int64 `json:"namelessToolCallCount"` + // Number of messages carrying opaque or encrypted reasoning + ReasoningItemCount *int64 `json:"reasoningItemCount,omitempty"` // Total number of tool calls across assistant messages ToolCallCount int64 `json:"toolCallCount"` // Number of "tool" result messages in the request @@ -4450,6 +4541,8 @@ type PermissionPromptRequestWrite struct { CanOfferSessionApproval bool `json:"canOfferSessionApproval"` // Unified diff showing the proposed changes Diff string `json:"diff"` + // Complete before/after text previews captured while planning this request. Omitted when a complete preview is unavailable, including pre-read sandbox requests and non-UTF-8 files. Missing sides mean creation or deletion, not unavailable content. + FileEdits []PermissionWriteFileEdit `json:"fileEdits,omitzero"` // Path of the file being written to FileName string `json:"fileName"` // Human-readable description of the intended file change @@ -4779,6 +4872,8 @@ type PermissionRequestWrite struct { CanOfferSessionApproval bool `json:"canOfferSessionApproval"` // Unified diff showing the proposed changes Diff string `json:"diff"` + // Complete before/after text previews captured while planning this request. Omitted when a complete preview is unavailable, including pre-read sandbox requests and non-UTF-8 files. Missing sides mean creation or deletion, not unavailable content. + FileEdits []PermissionWriteFileEdit `json:"fileEdits,omitzero"` // Path of the file being written to FileName string `json:"fileName"` // Human-readable description of the intended file change @@ -4977,6 +5072,22 @@ type PermissionSandboxPathGrant struct { RemovedReadonlyPaths []string `json:"removedReadonlyPaths,omitzero"` } +// A named, complete UTF-8 text snapshot for a pending file operation. +type PermissionWriteFileContent struct { + // Complete file contents, including an empty string for an empty file. + Content string `json:"content"` + // Absolute path in the session filesystem namespace. + Path string `json:"path"` +} + +// Complete text snapshots for one proposed file change. At least one side is present. Different before and after paths describe a move. +type PermissionWriteFileEdit struct { + // Complete file content the operation plans to write. Absent only when the file is being deleted. + After *PermissionWriteFileContent `json:"after,omitempty"` + // File content read while planning the operation. Absent only when the file is being created. + Before *PermissionWriteFileContent `json:"before,omitempty"` +} + // A model-facing binary result as persisted: full inline data, a size-omitted marker, or a deduplicated asset reference // Experimental: PersistedBinaryResult is part of an experimental API and may change or be removed. type PersistedBinaryResult interface { @@ -5705,6 +5816,16 @@ type ToolExecutionStartToolDescriptionMetaUI struct { Visibility []ToolExecutionStartToolDescriptionMetaUIVisibility `json:"visibility,omitzero"` } +// Ordered accounting identity assigned under the source session's emission lock. +type UsageAccountingIdentity struct { + // Monotonically increasing sequence within the source session. + Sequence int64 `json:"sequence"` + // Session that assigned this accounting sequence. + SourceSessionID string `json:"sourceSessionId"` + // Existing API call identifier, or a runtime-generated identity when none was supplied. + UsageID string `json:"usageId"` +} + // Internal prompt-cache expiration state for one model // Internal: UsageCheckpointModelCacheState is an internal SDK API and is not part of the public surface. type UsageCheckpointModelCacheState struct { @@ -6207,11 +6328,11 @@ const ( type ManagedSettingsEnforcedAction string const ( - // An attempt to turn on a bypass-permissions ("yolo") escalation was refused or capped because policy disables bypass-permissions mode. + // An attempt to enter a permission mode governed by managed policy was refused or capped. The `setting` and `escalation` fields identify whether this was bypass permissions or Assisted Permissions. ManagedSettingsEnforcedActionBypassPermissionsBlocked ManagedSettingsEnforcedAction = "bypass_permissions_blocked" ) -// For a `bypass_permissions_blocked` action, which permission-escalation primitive was refused +// For a `bypass_permissions_blocked` action, which permission-mode or escalation primitive was refused type ManagedSettingsEnforcedEscalation string const ( @@ -6219,7 +6340,7 @@ const ( ManagedSettingsEnforcedEscalationAllowAll ManagedSettingsEnforcedEscalation = "allow_all" // Automatic approval of all tool permission requests. ManagedSettingsEnforcedEscalationApproveAll ManagedSettingsEnforcedEscalation = "approve_all" - // Assisted mode — keeps normal prompt paths and adds an LLM recommendation, distinct from allow-all. + // Assisted Permissions — uses an LLM review to reduce prompts, distinct from allow-all and not a hard security boundary. ManagedSettingsEnforcedEscalationAssistedApproval ManagedSettingsEnforcedEscalation = "assisted_approval" // A server-wide MCP "Always Allow" (or `--allow-tool `) blanket that would auto-approve every tool from an MCP server. Capped to per-tool approval; each tool still prompts. ManagedSettingsEnforcedEscalationServerWideMCPApproval ManagedSettingsEnforcedEscalation = "server_wide_mcp_approval" @@ -6378,6 +6499,22 @@ const ( ModelCallFinishedOutcomeSuccess ModelCallFinishedOutcome = "success" ) +// Why a WebSocket-capable model call was carried by the HTTP fallback +type ModelCallWebSocketFallbackReason string + +const ( + // The WebSocket returned a retryable API error. + ModelCallWebSocketFallbackReasonAPIError ModelCallWebSocketFallbackReason = "api_error" + // The WebSocket connection could not be established. + ModelCallWebSocketFallbackReasonConnectFailed ModelCallWebSocketFallbackReason = "connect_failed" + // No usable WebSocket connection was available for the request. + ModelCallWebSocketFallbackReasonConnectionUnavailable ModelCallWebSocketFallbackReason = "connection_unavailable" + // Sending the request over the WebSocket failed. + ModelCallWebSocketFallbackReasonSendFailed ModelCallWebSocketFallbackReason = "send_failed" + // The WebSocket transport failed before any output reached the consumer. + ModelCallWebSocketFallbackReasonTransportFailed ModelCallWebSocketFallbackReason = "transport_failed" +) + // Why the session no longer has an explicitly selected model. type ModelDeselectedReason string @@ -6440,6 +6577,14 @@ const ( PermissionApprovalEvaluationReasonCodeArgumentBindingUnreviewable PermissionApprovalEvaluationReasonCode = "argument-binding-unreviewable" // The judge was skipped because authorization extraction could not safely establish a complete recent history. PermissionApprovalEvaluationReasonCodeAuthorizationHistoryIncomplete PermissionApprovalEvaluationReasonCode = "authorization-history-incomplete" + // A code source was excluded from review by content exclusion policy. + PermissionApprovalEvaluationReasonCodeContentExcluded PermissionApprovalEvaluationReasonCode = "content-excluded" + // The shell command used a code source computed at run time. + PermissionApprovalEvaluationReasonCodeDynamicSource PermissionApprovalEvaluationReasonCode = "dynamic-source" + // A code-bearing executable exceeded the binding size limit. + PermissionApprovalEvaluationReasonCodeExecutableTooLarge PermissionApprovalEvaluationReasonCode = "executable-too-large" + // A code-bearing executable could not be inspected. + PermissionApprovalEvaluationReasonCodeExecutableUnavailable PermissionApprovalEvaluationReasonCode = "executable-unavailable" // Assisted approval was inactive for this request. PermissionApprovalEvaluationReasonCodeInactive PermissionApprovalEvaluationReasonCode = "inactive" // The request inherited an outcome from another decision. @@ -6476,6 +6621,8 @@ const ( PermissionApprovalEvaluationReasonCodeShellEnvironmentUnreviewable PermissionApprovalEvaluationReasonCode = "shell-environment-unreviewable" // The script snapshot exceeded the size limit. PermissionApprovalEvaluationReasonCodeTooLarge PermissionApprovalEvaluationReasonCode = "too-large" + // The shell command referenced more code sources than can be reviewed. + PermissionApprovalEvaluationReasonCodeTooManySources PermissionApprovalEvaluationReasonCode = "too-many-sources" // Script review was unavailable. PermissionApprovalEvaluationReasonCodeUnavailable PermissionApprovalEvaluationReasonCode = "unavailable" // Attribution is missing or outside the supported vocabulary. @@ -6486,6 +6633,10 @@ const ( PermissionApprovalEvaluationReasonCodeUnrepresentablePath PermissionApprovalEvaluationReasonCode = "unrepresentable-path" // The script invocation could not be reviewed. PermissionApprovalEvaluationReasonCodeUnreviewableScriptInvocation PermissionApprovalEvaluationReasonCode = "unreviewable-script-invocation" + // The shell command could not be analyzed for execution evidence. + PermissionApprovalEvaluationReasonCodeUnsupportedCommandShape PermissionApprovalEvaluationReasonCode = "unsupported-command-shape" + // The shell command used a code source that cannot be bound for review. + PermissionApprovalEvaluationReasonCodeUnsupportedSource PermissionApprovalEvaluationReasonCode = "unsupported-source" ) // Direction stored in a historical extractor claim. Current runtimes do not apply it. @@ -6644,7 +6795,7 @@ const ( PlanChangedOperationUpdate PlanChangedOperation = "update" ) -// Auto preferences that Copilot API can recommend. +// Enabled Auto preferences that Copilot API can recommend. type RecommendedAutoTier string const ( diff --git a/go/session.go b/go/session.go index 45113a59ac..03ccf43dea 100644 --- a/go/session.go +++ b/go/session.go @@ -62,9 +62,10 @@ type Session struct { transcriptRecovery *TranscriptRecoveryReport client *jsonrpc2.Client clientSessionAPIs *rpc.ClientSessionAPIHandlers - handlers []sessionHandler + handlers []*sessionHandler nextHandlerID uint64 handlerMutex sync.RWMutex + gracefulRetirement bool toolHandlers map[string]ToolHandler toolHandlersM sync.RWMutex setToolsOnce sync.Once @@ -93,6 +94,8 @@ type Session struct { elicitationMu sync.RWMutex canvasHandler CanvasHandler canvasMu sync.RWMutex + skillProvider SkillProvider + skillProviderMu sync.RWMutex bearerTokenProviders map[string]BearerTokenProvider bearerTokenMu sync.RWMutex releaseGitHubTokenProvider func() @@ -221,6 +224,24 @@ func (s *Session) getCanvasHandler() CanvasHandler { return s.canvasHandler } +func (s *Session) registerSkillProvider(provider SkillProvider) { + s.skillProviderMu.Lock() + defer s.skillProviderMu.Unlock() + s.skillProvider = provider +} + +func (s *Session) getSkillProvider() SkillProvider { + s.skillProviderMu.RLock() + defer s.skillProviderMu.RUnlock() + return s.skillProvider +} + +func (s *Session) clearSkillProvider() { + s.skillProviderMu.Lock() + s.skillProvider = nil + s.skillProviderMu.Unlock() +} + // registerBearerTokenProviders installs per-provider [BearerTokenProvider] callbacks // for BYOK providers configured with managed-identity / on-demand bearer-token // auth, keyed by provider name. @@ -406,7 +427,7 @@ func newSession( managedSettings: managedSettings, client: client, clientSessionAPIs: &rpc.ClientSessionAPIHandlers{}, - handlers: make([]sessionHandler, 0), + handlers: make([]*sessionHandler, 0), toolHandlers: make(map[string]ToolHandler), commandHandlers: make(map[string]CommandHandler), eventCh: make(chan SessionEvent, 128), @@ -493,7 +514,7 @@ func (s *Session) SendPrompt(ctx context.Context, prompt string) (string, error) // // Events are still delivered to handlers registered via [Session.On] while waiting. // Synchronous handlers registered before this call finish processing the completing -// root session.idle event before it returns successfully. +// root session.idle event before it returns successfully, unless the session retires. // This does not wait for asynchronous work started by a handler. // Sub-agent events with a non-empty AgentID do not complete the wait or supply its reply. // @@ -503,7 +524,7 @@ func (s *Session) SendPrompt(ctx context.Context, prompt string) (string, error) // Controls how long to wait; does not abort in-flight agent work. // // Returns the final assistant message event, or nil if none was received. -// Returns an error if the timeout is reached or the connection fails. +// Returns an error if the timeout is reached, the session is closed, or the connection fails. // // Example: // @@ -528,34 +549,56 @@ func (s *Session) SendAndWait(ctx context.Context, options MessageOptions) (*Ses defer cancel() } - idleCh := make(chan struct{}, 1) - errCh := make(chan error, 1) + type outcome struct { + event *SessionEvent + err error + } + terminalCh := make(chan outcome, 1) + dispatchedCh := make(chan struct{}, 1) var lastAssistantMessage *SessionEvent - var mu sync.Mutex + completed := false + finish := func(result outcome) { + if !completed { + completed = true + terminalCh <- result + } + } - unsubscribe := s.On(func(event SessionEvent) { - if event.AgentID != nil && *event.AgentID != "" { + // Observe the result before user callbacks can retire the current dispatch. + unobserve := s.on(func(event SessionEvent) { + if completed || (event.AgentID != nil && *event.AgentID != "") { return } switch d := event.Data.(type) { case *AssistantMessageData: - mu.Lock() eventCopy := event lastAssistantMessage = &eventCopy - mu.Unlock() case *SessionIdleData: if d.Mode != nil && *d.Mode == SessionModeAutopilot { break } - select { - case idleCh <- struct{}{}: - default: - } + finish(outcome{event: lastAssistantMessage}) case *SessionErrorData: - select { - case errCh <- fmt.Errorf("session error: %s", d.Message): - default: + finish(outcome{err: fmt.Errorf("session error: %s", d.Message)}) + } + }, true) + defer unobserve() + unsubscribe := s.On(func(event SessionEvent) { + if event.AgentID != nil && *event.AgentID != "" { + return + } + switch data := event.Data.(type) { + case *SessionIdleData: + if data.Mode != nil && *data.Mode == SessionModeAutopilot { + return } + case *SessionErrorData: + default: + return + } + select { + case dispatchedCh <- struct{}{}: + default: } }) defer unsubscribe() @@ -566,16 +609,27 @@ func (s *Session) SendAndWait(ctx context.Context, options MessageOptions) (*Ses } select { - case <-idleCh: - mu.Lock() - result := lastAssistantMessage - mu.Unlock() - return result, nil - case err := <-errCh: - return nil, err + case <-dispatchedCh: + case <-s.eventDone: + s.handlerMutex.RLock() + graceful := s.gracefulRetirement + s.handlerMutex.RUnlock() + if !graceful { + select { + case <-dispatchedCh: + default: + return nil, errors.New("session closed before response completed") + } + } case <-ctx.Done(): return nil, fmt.Errorf("waiting for session.idle: %w", ctx.Err()) } + select { + case result := <-terminalCh: + return result.event, result.err + default: + return nil, errors.New("session closed before response completed") + } } // SendPromptAndWait is a convenience wrapper for [Session.SendAndWait] that @@ -595,6 +649,8 @@ func (s *Session) SendPromptAndWait(ctx context.Context, prompt string) (*Sessio // // The returned function can be called to unsubscribe the handler. It is safe // to call the unsubscribe function multiple times. +// Unsubscribing also removes an entry from dispatch snapshots if it has not started. +// Registration after the session closes is logged and returns a no-op unsubscribe. // // Example: // @@ -610,12 +666,28 @@ func (s *Session) SendPromptAndWait(ctx context.Context, prompt string) (*Sessio // // Later, to stop receiving events: // unsubscribe() func (s *Session) On(handler SessionEventHandler) func() { + return s.on(handler, false) +} + +func (s *Session) on(handler SessionEventHandler, prepend bool) func() { s.handlerMutex.Lock() defer s.handlerMutex.Unlock() + select { + case <-s.eventDone: + log.Printf("Cannot register an event handler on closed session %s", s.SessionID) + return func() {} + default: + } + id := s.nextHandlerID s.nextHandlerID++ - s.handlers = append(s.handlers, sessionHandler{id: id, fn: handler}) + entry := &sessionHandler{id: id, fn: handler} + if prepend { + s.handlers = append([]*sessionHandler{entry}, s.handlers...) + } else { + s.handlers = append(s.handlers, entry) + } // Return unsubscribe function return func() { @@ -624,6 +696,7 @@ func (s *Session) On(handler SessionEventHandler) func() { for i, h := range s.handlers { if h.id == id { + h.fn = nil s.handlers = append(s.handlers[:i], s.handlers[i+1:]...) break } @@ -638,7 +711,9 @@ func (s *Session) On(handler SessionEventHandler) func() { // // This method is internal and typically called when creating a session with tools. func (s *Session) registerTools(tools []Tool) { - s.replaceToolHandlers(buildToolHandlerMap(tools)) + if err := s.replaceToolHandlers(buildToolHandlerMap(tools)); err != nil { + log.Printf("Cannot register tools on session %s: %v", s.SessionID, err) + } } func buildToolHandlerMap(tools []Tool) map[string]ToolHandler { @@ -652,10 +727,16 @@ func buildToolHandlerMap(tools []Tool) map[string]ToolHandler { return handlers } -func (s *Session) replaceToolHandlers(handlers map[string]ToolHandler) { +func (s *Session) replaceToolHandlers(handlers map[string]ToolHandler) error { s.toolHandlersM.Lock() defer s.toolHandlersM.Unlock() + select { + case <-s.eventDone: + return errors.New("session closed before tool handlers could be installed") + default: + } s.toolHandlers = handlers + return nil } // getToolHandler retrieves a registered tool handler by name. @@ -727,14 +808,16 @@ func (s *Session) registerExitPlanModeHandler(handler ExitPlanModeRequestHandler s.exitPlanModeHandler = handler } -func (s *Session) getExitPlanModeHandler() ExitPlanModeRequestHandler { - s.exitPlanModeMu.RLock() - defer s.exitPlanModeMu.RUnlock() - return s.exitPlanModeHandler -} - func (s *Session) handleExitPlanModeRequest(request ExitPlanModeRequest) (ExitPlanModeResult, error) { - handler := s.getExitPlanModeHandler() + s.exitPlanModeMu.RLock() + select { + case <-s.eventDone: + s.exitPlanModeMu.RUnlock() + return ExitPlanModeResult{}, errors.New("session closed before exit-plan-mode request") + default: + } + handler := s.exitPlanModeHandler + s.exitPlanModeMu.RUnlock() if handler == nil { return ExitPlanModeResult{Approved: true}, nil } @@ -748,14 +831,16 @@ func (s *Session) registerAutoModeSwitchHandler(handler AutoModeSwitchRequestHan s.autoModeSwitchHandler = handler } -func (s *Session) getAutoModeSwitchHandler() AutoModeSwitchRequestHandler { - s.autoModeSwitchMu.RLock() - defer s.autoModeSwitchMu.RUnlock() - return s.autoModeSwitchHandler -} - func (s *Session) handleAutoModeSwitchRequest(request AutoModeSwitchRequest) (AutoModeSwitchResponse, error) { - handler := s.getAutoModeSwitchHandler() + s.autoModeSwitchMu.RLock() + select { + case <-s.eventDone: + s.autoModeSwitchMu.RUnlock() + return "", errors.New("session closed before auto-mode-switch request") + default: + } + handler := s.autoModeSwitchHandler + s.autoModeSwitchMu.RUnlock() if handler == nil { return AutoModeSwitchResponseNo, nil } @@ -775,17 +860,18 @@ func (s *Session) registerHooks(hooks *SessionHooks) { s.hooks = hooks } -// getHooks returns the currently registered hooks, or nil. -func (s *Session) getHooks() *SessionHooks { - s.hooksMux.RLock() - defer s.hooksMux.RUnlock() - return s.hooks -} - // handleHooksInvoke handles a hook invocation from the Copilot CLI. // This is an internal method called by the SDK when the CLI invokes a hook. func (s *Session) handleHooksInvoke(hookType string, rawInput json.RawMessage) (any, error) { - hooks := s.getHooks() + s.hooksMux.RLock() + select { + case <-s.eventDone: + s.hooksMux.RUnlock() + return nil, errors.New("session closed before hook invocation") + default: + } + hooks := s.hooks + s.hooksMux.RUnlock() if hooks == nil { return nil, nil @@ -948,6 +1034,12 @@ type systemMessageTransformResponse struct { // This is an internal method called by the SDK when the CLI requests section transforms. func (s *Session) handleSystemMessageTransform(sections map[string]systemMessageTransformSection) (systemMessageTransformResponse, error) { s.transformMu.Lock() + select { + case <-s.eventDone: + s.transformMu.Unlock() + return systemMessageTransformResponse{}, errors.New("session closed before system message transform") + default: + } callbacks := s.transformCallbacks s.transformMu.Unlock() @@ -1489,6 +1581,12 @@ func (s *Session) dispatchEvent(event SessionEvent) { // others from receiving the event. func (s *Session) processEvents() { for { + select { + case <-s.eventDone: + return + default: + } + var event SessionEvent select { case event = <-s.eventCh: @@ -1497,13 +1595,23 @@ func (s *Session) processEvents() { } s.handlerMutex.RLock() - handlers := make([]SessionEventHandler, 0, len(s.handlers)) - for _, h := range s.handlers { - handlers = append(handlers, h.fn) - } + handlers := append([]*sessionHandler(nil), s.handlers...) s.handlerMutex.RUnlock() - for _, handler := range handlers { + for _, entry := range handlers { + s.handlerMutex.RLock() + select { + case <-s.eventDone: + s.handlerMutex.RUnlock() + return + default: + } + handler := entry.fn + s.handlerMutex.RUnlock() + if handler == nil { + continue + } + func() { defer func() { if r := recover(); r != nil { @@ -1516,11 +1624,77 @@ func (s *Session) processEvents() { } } -// stopEventProcessing stops the session event consumer without making an RPC. +// stopEventProcessing signals the consumer to stop without making an RPC or +// waiting for a user handler that may itself be stopping the client. +// It releases registered callbacks, including unadmitted event snapshot entries. // CreateSession/ResumeSession use this when a locally registered session fails // before it can be returned to the caller. func (s *Session) stopEventProcessing() { - s.closeOnce.Do(func() { close(s.eventDone) }) + s.retireEventProcessing(false) +} + +func (s *Session) retireEventProcessing(graceful bool) { + s.closeOnce.Do(func() { + s.handlerMutex.Lock() + s.gracefulRetirement = graceful + close(s.eventDone) + for _, handler := range s.handlers { + handler.fn = nil + } + s.handlers = nil + s.handlerMutex.Unlock() + + s.clearSkillProvider() + s.releaseGitHubTokenProviderRegistration() + + s.toolHandlersM.Lock() + s.toolHandlers = nil + s.toolHandlersM.Unlock() + + s.permissionMux.Lock() + s.permissionHandler = nil + s.permissionMux.Unlock() + + s.mcpAuthMu.Lock() + s.mcpAuthHandler = nil + s.mcpAuthMu.Unlock() + + s.userInputMux.Lock() + s.userInputHandler = nil + s.userInputMux.Unlock() + + s.exitPlanModeMu.Lock() + s.exitPlanModeHandler = nil + s.exitPlanModeMu.Unlock() + + s.autoModeSwitchMu.Lock() + s.autoModeSwitchHandler = nil + s.autoModeSwitchMu.Unlock() + + s.hooksMux.Lock() + s.hooks = nil + s.hooksMux.Unlock() + + s.transformMu.Lock() + s.transformCallbacks = nil + s.transformMu.Unlock() + + s.commandHandlersMu.Lock() + s.commandHandlers = nil + s.commandHandlersMu.Unlock() + + s.elicitationMu.Lock() + s.elicitationHandler = nil + s.elicitationMu.Unlock() + + s.canvasMu.Lock() + s.canvasHandler = nil + s.canvasMu.Unlock() + + s.bearerTokenMu.Lock() + s.bearerTokenProviders = nil + s.bearerTokenMu.Unlock() + }) } // handleBroadcastEvent handles broadcast request events by executing local handlers @@ -1895,6 +2069,7 @@ func (s *Session) GetEvents(ctx context.Context) ([]SessionEvent, error) { // log.Printf("Failed to disconnect session: %v", err) // } func (s *Session) Disconnect() error { + s.clearSkillProvider() s.cancelPendingExternalTools() result, err := s.client.Request(context.Background(), "session.detach", sessionDetachRequest{SessionID: s.SessionID}) if err == nil { @@ -1912,29 +2087,7 @@ func (s *Session) Disconnect() error { // Local cleanup always runs, even if the detach RPC failed, so callers // don't leak in-memory resources (event goroutines, registered // providers/handlers) just because the runtime couldn't be reached. - s.stopEventProcessing() - s.releaseGitHubTokenProviderRegistration() - - // Clear handlers - s.handlerMutex.Lock() - s.handlers = nil - s.handlerMutex.Unlock() - - s.toolHandlersM.Lock() - s.toolHandlers = nil - s.toolHandlersM.Unlock() - - s.permissionMux.Lock() - s.permissionHandler = nil - s.permissionMux.Unlock() - - s.commandHandlersMu.Lock() - s.commandHandlers = nil - s.commandHandlersMu.Unlock() - - s.elicitationMu.Lock() - s.elicitationHandler = nil - s.elicitationMu.Unlock() + s.retireEventProcessing(true) if err != nil { return fmt.Errorf("failed to disconnect session: %w", err) @@ -2123,7 +2276,7 @@ func toolDefinitionsForSetTools(tools []Tool) []rpc.ProtocolExternalToolDefiniti // not block later calls. If ctx is done before the request is sent, for example // while an earlier SetTools call is still in flight, nothing is sent. Once the // request is sent, ctx only bounds the wait: an accepted replacement still -// installs its handlers. +// installs its handlers unless the session has since closed. // // The agent sees the new tools from its next model request, which can fall // within a turn in progress. A model request already in flight was made with the @@ -2161,8 +2314,7 @@ func (s *Session) SetTools(ctx context.Context, tools []Tool) error { done <- fmt.Errorf("failed to set tools: %w", err) return } - s.replaceToolHandlers(handlers) - done <- nil + done <- s.replaceToolHandlers(handlers) }() select { diff --git a/go/session_retirement_test.go b/go/session_retirement_test.go new file mode 100644 index 0000000000..44c646d261 --- /dev/null +++ b/go/session_retirement_test.go @@ -0,0 +1,364 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package copilot + +import ( + "context" + "fmt" + "strings" + "testing" + "time" +) + +func TestSession_RetirementClearsCallbackRoots(t *testing.T) { + session, cleanup := newTestSession() + t.Cleanup(cleanup) + session.On(func(SessionEvent) {}) + entry := session.handlers[0] + session.registerTools([]Tool{{Name: "tool", Handler: func(ToolInvocation) (ToolResult, error) { return ToolResult{}, nil }}}) + session.registerPermissionHandler(PermissionHandler.ApproveAll) + session.registerMCPAuthHandler(func(MCPAuthRequest, MCPAuthInvocation) (*MCPAuthResult, error) { return nil, nil }) + session.registerUserInputHandler(func(UserInputRequest, UserInputInvocation) (UserInputResponse, error) { + return UserInputResponse{}, nil + }) + session.registerExitPlanModeHandler(func(ExitPlanModeRequest, ExitPlanModeInvocation) (ExitPlanModeResult, error) { + return ExitPlanModeResult{}, nil + }) + session.registerAutoModeSwitchHandler(func(AutoModeSwitchRequest, AutoModeSwitchInvocation) (AutoModeSwitchResponse, error) { + return AutoModeSwitchResponseNo, nil + }) + hooks := &SessionHooks{OnSessionEnd: func(SessionEndHookInput, HookInvocation) (*SessionEndHookOutput, error) { + return nil, nil + }} + session.registerHooks(hooks) + transforms := map[string]SectionTransformFn{"instructions": func(value string) (string, error) { return value, nil }} + session.registerTransformCallbacks(transforms) + session.registerCommands([]CommandDefinition{{Name: "command", Handler: func(CommandContext) error { return nil }}}) + session.registerElicitationHandler(func(ElicitationContext) (ElicitationResult, error) { return ElicitationResult{}, nil }) + session.registerCanvasHandler(&recordingCanvasHandler{}) + session.registerBearerTokenProviders(map[string]BearerTokenProvider{ + "provider": func(ProviderTokenArgs) (string, error) { return "token", nil }, + }) + releases := 0 + session.setGitHubTokenProviderRegistrationRelease(func() { releases++ }) + + session.stopEventProcessing() + session.stopEventProcessing() + + for name, retained := range map[string]bool{ + "event registrations": len(session.handlers) != 0, + "retained event entry": entry.fn != nil, + "tools": session.toolHandlers != nil, + "permissions": session.permissionHandler != nil, + "MCP auth": session.mcpAuthHandler != nil, + "user input": session.userInputHandler != nil, + "exit plan mode": session.exitPlanModeHandler != nil, + "auto mode switch": session.autoModeSwitchHandler != nil, + "hooks": session.hooks != nil, + "transforms": session.transformCallbacks != nil, + "commands": session.commandHandlers != nil, + "elicitation": session.elicitationHandler != nil, + "canvas": session.canvasHandler != nil, + "bearer token": session.bearerTokenProviders != nil, + "provider release": session.releaseGitHubTokenProvider != nil, + } { + if retained { + t.Errorf("retired session retained %s", name) + } + } + if releases != 1 { + t.Errorf("provider releases = %d, want exactly one", releases) + } + if hooks.OnSessionEnd == nil || transforms["instructions"] == nil { + t.Fatal("retirement mutated caller-owned hook or transform configuration") + } +} + +func TestSession_RetirementRejectsCallbackRebinding(t *testing.T) { + session, cleanup := newTestSession() + t.Cleanup(cleanup) + session.stopEventProcessing() + unsubscribe := session.On(func(SessionEvent) {}) + if len(session.handlers) != 0 { + t.Fatal("retired session accepted a late event registration") + } + unsubscribe() + unsubscribe() + session.registerTools([]Tool{{Name: "late", Handler: func(ToolInvocation) (ToolResult, error) { return ToolResult{}, nil }}}) + if session.toolHandlers != nil { + t.Fatal("retired session retained late tool handlers") + } +} + +func TestSession_RetirementRejectsEmptyCallbackSuccess(t *testing.T) { + t.Run("hooks", func(t *testing.T) { + session, cleanup := newTestSession() + t.Cleanup(cleanup) + session.SessionID = "retirement-session" + client := &Client{sessions: map[string]*Session{session.SessionID: session}} + request := hooksInvokeRequest{SessionID: session.SessionID, Type: "preToolUse", Input: []byte("{}")} + if _, err := client.handleHooksInvoke(request); err != nil { + t.Fatalf("live session without hooks returned an error: %v", err) + } + session.stopEventProcessing() + _, err := client.handleHooksInvoke(request) + if err == nil || err.Code != -32603 || !strings.Contains(err.Message, "session closed") { + t.Fatalf("retired hook request error = %+v, want closed-session RPC error", err) + } + }) + t.Run("system message transforms", func(t *testing.T) { + session, cleanup := newTestSession() + t.Cleanup(cleanup) + sections := map[string]systemMessageTransformSection{"instructions": {Content: "keep this policy"}} + response, err := session.handleSystemMessageTransform(sections) + if err != nil || response.Sections["instructions"].Content != "keep this policy" { + t.Fatalf("live session without transforms changed content: %+v, %v", response, err) + } + session.stopEventProcessing() + response, err = session.handleSystemMessageTransform(sections) + if err == nil || !strings.Contains(err.Error(), "session closed") { + t.Fatalf("retired transform response = %+v, %v; want closed error", response, err) + } + }) +} + +func TestSession_RetirementRejectsModeRequestDefaults(t *testing.T) { + t.Run("exit plan mode", func(t *testing.T) { + session, cleanup := newTestSession() + t.Cleanup(cleanup) + session.stopEventProcessing() + response, err := session.handleExitPlanModeRequest(ExitPlanModeRequest{}) + if err == nil || !strings.Contains(err.Error(), "session closed") || response.Approved { + t.Fatalf("retired exit-plan-mode response = %+v, %v; want closed error without approval", response, err) + } + }) + t.Run("auto mode switch", func(t *testing.T) { + session, cleanup := newTestSession() + t.Cleanup(cleanup) + session.stopEventProcessing() + response, err := session.handleAutoModeSwitchRequest(AutoModeSwitchRequest{}) + if err == nil || !strings.Contains(err.Error(), "session closed") { + t.Fatalf("retired auto-mode-switch response = %+v, %v; want closed error", response, err) + } + }) +} + +func TestSession_SetToolsAcceptedAfterRetirementDoesNotRebind(t *testing.T) { + session, server := newSetToolsTestSession(t) + session.eventDone = make(chan struct{}) + session.eventCh = make(chan SessionEvent, 2) + processingDone := make(chan struct{}) + go func() { + defer close(processingDone) + session.processEvents() + }() + t.Cleanup(func() { + session.stopEventProcessing() + select { + case <-processingDone: + case <-time.After(2 * time.Second): + t.Error("event processing did not stop after retirement") + } + }) + result := make(chan error, 1) + go func() { + result <- session.SetTools(context.Background(), []Tool{{ + Name: "late", Handler: func(ToolInvocation) (ToolResult, error) { return ToolResult{}, nil }, + }}) + }() + request := server.expectRequest(t, "session.tools.set") + // Keep the transport alive so an accepted late ACK cannot mask local retirement. + session.stopEventProcessing() + server.respond(t, request.ID, map[string]any{}) + err := awaitSetTools(t, result) + if err == nil || !strings.Contains(err.Error(), "session closed") { + t.Fatalf("SetTools error = %v, want session closed", err) + } + + if _, ok := session.getToolHandler("late"); ok { + t.Fatal("accepted late replacement resurrected retired tool handlers") + } +} + +func TestSession_SendAndWaitPreservesTerminalResultBeforeRetirement(t *testing.T) { + for _, terminal := range []string{"idle", "error", "missing", "error-then-idle", "idle-then-error"} { + for iteration := range 32 { + t.Run(fmt.Sprintf("%s-%d", terminal, iteration), func(t *testing.T) { + session, server := newSetToolsTestSession(t) + session.eventDone = make(chan struct{}) + session.eventCh = make(chan SessionEvent, 2) + processingDone := make(chan struct{}) + go func() { + defer close(processingDone) + session.processEvents() + }() + t.Cleanup(func() { + session.stopEventProcessing() + select { + case <-processingDone: + case <-time.After(2 * time.Second): + t.Error("event processing did not stop after retirement") + } + }) + type response struct { + event *SessionEvent + err error + } + result := make(chan response, 1) + go func() { + event, err := session.SendAndWait(context.Background(), MessageOptions{Prompt: "finish"}) + result <- response{event: event, err: err} + }() + request := server.expectRequest(t, "session.send") + terminalObserved := make(chan struct{}, 2) + unsubscribe := session.On(func(event SessionEvent) { + switch event.Data.(type) { + case *SessionIdleData, *SessionErrorData: + terminalObserved <- struct{}{} + } + }) + defer unsubscribe() + switch terminal { + case "idle", "idle-then-error": + session.dispatchEvent(SessionEvent{Data: &AssistantMessageData{Content: "finished"}}) + session.dispatchEvent(SessionEvent{Data: &SessionIdleData{}}) + if terminal == "idle-then-error" { + session.dispatchEvent(SessionEvent{Data: &SessionErrorData{Message: "later provider failure"}}) + } + case "error", "error-then-idle": + session.dispatchEvent(SessionEvent{Data: &SessionErrorData{Message: "provider failed"}}) + if terminal == "error-then-idle" { + session.dispatchEvent(SessionEvent{Data: &SessionIdleData{}}) + } + } + if terminal != "missing" { + count := 1 + if terminal == "error-then-idle" || terminal == "idle-then-error" { + count = 2 + } + for range count { + select { + case <-terminalObserved: + case <-time.After(2 * time.Second): + t.Fatal("the wait did not receive its terminal event before retirement") + } + } + } + // Publish retirement before the send ACK so it can race the terminal result. + session.stopEventProcessing() + server.respond(t, request.ID, map[string]any{"messageId": "message-1"}) + select { + case response := <-result: + switch terminal { + case "idle", "idle-then-error": + if response.err != nil || response.event == nil { + t.Fatalf("completed response = %+v, %v; want assistant message", response.event, response.err) + } + message, ok := response.event.Data.(*AssistantMessageData) + if !ok || message.Content != "finished" { + t.Fatalf("assistant response = %+v, want finished", response.event.Data) + } + case "error", "error-then-idle": + if response.err == nil || response.err.Error() != "session error: provider failed" { + t.Fatalf("terminal error = %v, want provider failure", response.err) + } + case "missing": + if response.err == nil || response.err.Error() != "session closed before response completed" { + t.Fatalf("incomplete response error = %v, want session closure", response.err) + } + } + case <-time.After(2 * time.Second): + t.Fatal("SendAndWait did not observe the acknowledged send and retirement") + } + }) + } + } +} + +func TestSession_SendAndWaitPreservesTerminalResultWhenHandlerDisconnects(t *testing.T) { + for _, terminal := range []string{"idle", "error"} { + t.Run(terminal, func(t *testing.T) { + t.Parallel() + session, server := newSetToolsTestSession(t) + session.eventDone = make(chan struct{}) + session.eventCh = make(chan SessionEvent, 2) + processingDone := make(chan struct{}) + go func() { + defer close(processingDone) + session.processEvents() + }() + t.Cleanup(func() { + session.stopEventProcessing() + select { + case <-processingDone: + case <-time.After(2 * time.Second): + t.Error("event processing did not stop after retirement") + } + }) + + disconnected := make(chan error, 1) + session.On(func(event SessionEvent) { + switch event.Data.(type) { + case *SessionIdleData, *SessionErrorData: + disconnected <- session.Disconnect() + } + }) + lateCallback := make(chan struct{}, 1) + session.On(func(event SessionEvent) { + switch event.Data.(type) { + case *SessionIdleData, *SessionErrorData: + lateCallback <- struct{}{} + } + }) + type response struct { + event *SessionEvent + err error + } + result := make(chan response, 1) + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + go func() { + event, err := session.SendAndWait(ctx, MessageOptions{Prompt: "finish"}) + result <- response{event: event, err: err} + }() + send := server.expectRequest(t, "session.send") + session.dispatchEvent(SessionEvent{Data: &AssistantMessageData{Content: "finished"}}) + if terminal == "idle" { + session.dispatchEvent(SessionEvent{Data: &SessionIdleData{}}) + } else { + session.dispatchEvent(SessionEvent{Data: &SessionErrorData{Message: "provider failed"}}) + } + detach := server.expectRequest(t, "session.detach") + server.respond(t, detach.ID, map[string]any{"success": true}) + select { + case err := <-disconnected: + if err != nil { + t.Fatal("disconnect:", err) + } + case <-ctx.Done(): + t.Fatal("terminal handler did not disconnect") + } + select { + case <-lateCallback: + t.Fatal("retirement admitted a later user callback from the same event snapshot") + default: + } + // Acknowledge only after the earlier handler has retired the session. + server.respond(t, send.ID, map[string]any{"messageId": "message-1"}) + select { + case response := <-result: + if terminal == "error" { + if response.err == nil || response.err.Error() != "session error: provider failed" { + t.Fatalf("terminal error = %v, want provider failure", response.err) + } + } else if response.err != nil || response.event == nil { + t.Fatalf("completed response = %+v, %v; want assistant message", response.event, response.err) + } else if message, ok := response.event.Data.(*AssistantMessageData); !ok || message.Content != "finished" { + t.Fatalf("assistant response = %+v, want finished", response.event.Data) + } + case <-ctx.Done(): + t.Fatal("SendAndWait did not preserve the terminal result") + } + }) + } +} diff --git a/go/session_test.go b/go/session_test.go index b66643544b..f952398a9a 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -22,7 +22,7 @@ import ( // Returns a cleanup function that stops the consumer. func newTestSession() (*Session, func()) { s := &Session{ - handlers: make([]sessionHandler, 0), + handlers: make([]*sessionHandler, 0), commandHandlers: make(map[string]CommandHandler), eventCh: make(chan SessionEvent, 128), eventDone: make(chan struct{}), @@ -1064,7 +1064,7 @@ func checkSendAndWaitSkipsSubagentAndAutopilotContinuationIdle(t *testing.T, roo SessionID: "session-1", client: client, RPC: rpc.NewSessionRPC(client, "session-1"), - handlers: make([]sessionHandler, 0), + handlers: make([]*sessionHandler, 0), eventCh: make(chan SessionEvent, 8), eventDone: make(chan struct{}), } diff --git a/go/skill_provider_test.go b/go/skill_provider_test.go new file mode 100644 index 0000000000..957b99aa8f --- /dev/null +++ b/go/skill_provider_test.go @@ -0,0 +1,700 @@ +package copilot + +import ( + "context" + "encoding/json" + "errors" + "log" + "strings" + "sync" + "testing" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" + "github.com/github/copilot-sdk/go/rpc" +) + +type testSkillProvider struct { + mu sync.Mutex + skills []rpc.SkillProviderDescriptor + markdown map[string]string + listErr error + readErr error + calls []string +} + +func (p *testSkillProvider) ListSkills(context.Context) ([]rpc.SkillProviderDescriptor, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.calls = append(p.calls, "list") + if p.listErr != nil { + return nil, p.listErr + } + return p.skills, nil +} + +func (p *testSkillProvider) ReadSkill(_ context.Context, name string) (string, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.calls = append(p.calls, "read:"+name) + if p.readErr != nil { + return "", p.readErr + } + markdown, ok := p.markdown[name] + if !ok { + return "", ErrSkillNotFound + } + return markdown, nil +} + +func (p *testSkillProvider) snapshotCalls() []string { + p.mu.Lock() + defer p.mu.Unlock() + return append([]string(nil), p.calls...) +} + +var reviewSkillDescriptor = rpc.SkillProviderDescriptor{ + Name: "review", + Description: "Reviews code", +} + +func newTestSkillProvider() *testSkillProvider { + return &testSkillProvider{ + skills: []rpc.SkillProviderDescriptor{reviewSkillDescriptor}, + markdown: map[string]string{"review": "Review carefully."}, + } +} + +func TestSkillProviderSessionPayloads(t *testing.T) { + t.Run("omits flag without provider and sends flag with provider", func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + + createParams := make(chan json.RawMessage, 2) + resumeParams := make(chan json.RawMessage, 2) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + createParams <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil + }) + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + resumeParams <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil + }) + + if _, err := client.CreateSession(t.Context(), &SessionConfig{}); err != nil { + t.Fatalf("CreateSession without provider failed: %v", err) + } + if _, err := client.ResumeSession(t.Context(), "resume-without-provider", &ResumeSessionConfig{}); err != nil { + t.Fatalf("ResumeSession without provider failed: %v", err) + } + provider := newTestSkillProvider() + if _, err := client.CreateSession(t.Context(), &SessionConfig{SkillProvider: provider}); err != nil { + t.Fatalf("CreateSession with provider failed: %v", err) + } + if _, err := client.ResumeSession(t.Context(), "resume-with-provider", &ResumeSessionConfig{SkillProvider: provider}); err != nil { + t.Fatalf("ResumeSession with provider failed: %v", err) + } + + assertSkillProviderFlag(t, <-createParams, false) + assertSkillProviderFlag(t, <-resumeParams, false) + assertSkillProviderFlag(t, <-createParams, true) + assertSkillProviderFlag(t, <-resumeParams, true) + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called during open: %v", got) + } + }) + + t.Run("keeps empty mode enableSkills default with provider", func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + options: ClientOptions{Mode: ModeEmpty}, + } + + createParams := make(chan json.RawMessage, 1) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + createParams <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + server.SetRequestHandler("session.options.update", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{}`), nil + }) + + if _, err := client.CreateSession(t.Context(), &SessionConfig{ + AvailableTools: []string{}, + SkillProvider: newTestSkillProvider(), + }); err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + var payload map[string]any + if err := json.Unmarshal(<-createParams, &payload); err != nil { + t.Fatal(err) + } + if payload["enableSkills"] != false { + t.Fatalf("enableSkills = %v, want false", payload["enableSkills"]) + } + if payload["hasSkillProvider"] != true { + t.Fatalf("hasSkillProvider = %v, want true", payload["hasSkillProvider"]) + } + }) +} + +func TestSkillProviderServesEarlyCallbacksDuringOpen(t *testing.T) { + for _, method := range []string{"session.create", "session.resume"} { + t.Run(method, func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + client.setupNotificationHandler() + + earlyResult := make(chan rpc.SkillProviderListResult, 1) + server.SetRequestHandler(method, func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + raw, err := server.Request(t.Context(), "skillProvider.list", map[string]any{"sessionId": sessionID}) + if err != nil { + if rpcErr, ok := err.(*jsonrpc2.Error); ok { + return nil, rpcErr + } + return nil, &jsonrpc2.Error{Code: -32603, Message: err.Error()} + } + var result rpc.SkillProviderListResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: err.Error()} + } + earlyResult <- result + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + + provider := newTestSkillProvider() + if method == "session.create" { + if _, err := client.CreateSession(t.Context(), &SessionConfig{SkillProvider: provider}); err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + } else if _, err := client.ResumeSession(t.Context(), "early-resume", &ResumeSessionConfig{SkillProvider: provider}); err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + + got := <-earlyResult + if len(got.Skills) != 1 || got.Skills[0].Name != "review" { + t.Fatalf("early list result = %+v", got) + } + }) + } +} + +func TestSkillProviderRejectsCloudBeforeConnect(t *testing.T) { + provider := newTestSkillProvider() + client := &Client{} + + _, err := client.CreateSession(t.Context(), &SessionConfig{ + Cloud: &CloudSessionOptions{}, + SkillProvider: provider, + }) + + if err == nil || err.Error() != "Skill providers are not supported for cloud sessions." { + t.Fatalf("CreateSession error = %v", err) + } + if client.client != nil { + t.Fatal("client connected before rejecting cloud skill provider") + } + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called: %v", got) + } +} + +func TestSkillProviderDispatch(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + client.setupNotificationHandler() + + provider := &testSkillProvider{ + skills: []rpc.SkillProviderDescriptor{ + reviewSkillDescriptor, + { + Name: "deploy", + Description: "Deploys the service", + UserInvocable: Bool(false), + DisableModelInvocation: Bool(true), + ArgumentHint: String("[environment]"), + }, + }, + markdown: map[string]string{"deploy": "# deploy"}, + } + session := newSession("dispatch-session", rpcClient, "", false) + session.registerSkillProvider(provider) + client.sessions[session.SessionID] = session + + listRaw, rpcErr := server.Request(t.Context(), "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + if rpcErr != nil { + t.Fatalf("skillProvider.list failed: %v", rpcErr) + } + var listResult rpc.SkillProviderListResult + if err := json.Unmarshal(listRaw, &listResult); err != nil { + t.Fatal(err) + } + if len(listResult.Skills) != 2 || listResult.Skills[1].Name != "deploy" { + t.Fatalf("list result = %+v", listResult) + } + + readRaw, rpcErr := server.Request(t.Context(), "skillProvider.read", map[string]any{ + "sessionId": session.SessionID, + "name": "deploy", + }) + if rpcErr != nil { + t.Fatalf("skillProvider.read failed: %v", rpcErr) + } + var readResult rpc.SkillProviderReadResult + if err := json.Unmarshal(readRaw, &readResult); err != nil { + t.Fatal(err) + } + if readResult.Markdown == nil { + t.Fatal("markdown = nil, want # deploy") + } + if *readResult.Markdown != "# deploy" { + t.Fatalf("markdown = %q, want # deploy", *readResult.Markdown) + } + if got := provider.snapshotCalls(); strings.Join(got, ",") != "list,read:deploy" { + t.Fatalf("provider calls = %v", got) + } +} + +func TestSkillProviderOmitsUnsetOptionalDescriptorFields(t *testing.T) { + data, err := json.Marshal(rpc.SkillProviderListResult{ + Skills: []rpc.SkillProviderDescriptor{{Name: "review", Description: "Reviews code"}}, + }) + if err != nil { + t.Fatal(err) + } + var decoded map[string][]map[string]any + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + skill := decoded["skills"][0] + for _, key := range []string{"argumentHint", "userInvocable", "disableModelInvocation"} { + if _, ok := skill[key]; ok { + t.Fatalf("%s should be omitted when unset: %s", key, data) + } + } +} + +func TestSkillProviderEmptyListBecomesEmptyArray(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + client.setupNotificationHandler() + session := newSession("empty-list-session", rpcClient, "", false) + session.registerSkillProvider(&testSkillProvider{}) + client.sessions[session.SessionID] = session + + raw, rpcErr := server.Request(t.Context(), "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + if rpcErr != nil { + t.Fatalf("skillProvider.list failed: %v", rpcErr) + } + if string(raw) != `{"skills":[]}` { + t.Fatalf("list response = %s, want empty skills array", raw) + } +} + +func TestSkillProviderErrorEnvelopes(t *testing.T) { + t.Run("read not found returns null markdown", func(t *testing.T) { + _, server, sessionID := skillProviderErrorHarness(t, newTestSkillProvider()) + raw, rpcErr := server.Request(t.Context(), "skillProvider.read", map[string]any{ + "sessionId": sessionID, + "name": "missing", + }) + if rpcErr != nil { + t.Fatalf("skillProvider.read failed: %v", rpcErr) + } + if string(raw) != `{"markdown":null}` { + t.Fatalf("read response = %s, want null markdown", raw) + } + var result rpc.SkillProviderReadResult + if err := json.Unmarshal(raw, &result); err != nil { + t.Fatal(err) + } + if result.Markdown != nil { + t.Fatalf("markdown = %q, want nil", *result.Markdown) + } + }) + + for _, method := range []string{"skillProvider.list", "skillProvider.read"} { + t.Run(method+" provider failure", func(t *testing.T) { + output := captureSkillProviderLog(t) + secret := errors.New("db-password-in-error") + provider := newTestSkillProvider() + operation := "listSkills" + if method == "skillProvider.list" { + provider.listErr = secret + } else { + provider.readErr = secret + operation = "readSkill" + } + _, server, sessionID := skillProviderErrorHarness(t, provider) + params := map[string]any{"sessionId": sessionID} + wantMessage := "Skill provider " + operation + " failed" + if method == "skillProvider.read" { + params["name"] = "review" + } + + rpcErr := requestSkillProviderError(t, server, method, params) + if rpcErr.Code != -32603 || rpcErr.Message != wantMessage { + t.Fatalf("error = %+v, want generic message %q", rpcErr, wantMessage) + } + if rpcErr.Data != nil { + t.Fatalf("generic provider error data = %s, want omitted", rpcErr.Data) + } + if strings.Contains(rpcErr.Message, secret.Error()) { + t.Fatalf("provider error leaked secret: %q", rpcErr.Message) + } + wantLog := "skill provider " + operation + " failed: session_id=" + sessionID + " error=" + secret.Error() + if got := output.text(); !strings.Contains(got, wantLog) { + t.Fatalf("log = %q, want it to contain %q", got, wantLog) + } + }) + } + + t.Run("not-found classification panic", func(t *testing.T) { + captureSkillProviderLog(t) + provider := newTestSkillProvider() + provider.readErr = panickyIsError{} + _, server, sessionID := skillProviderErrorHarness(t, provider) + + rpcErr := requestSkillProviderError(t, server, "skillProvider.read", map[string]any{ + "sessionId": sessionID, + "name": "review", + }) + if rpcErr.Code != -32603 || rpcErr.Message != "Skill provider readSkill failed" || rpcErr.Data != nil { + t.Fatalf("error = %+v, want generic readSkill failure", rpcErr) + } + }) + + t.Run("unknown session", func(t *testing.T) { + _, server, _ := skillProviderErrorHarness(t, newTestSkillProvider()) + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": "missing"}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: missing") + }) + + for _, method := range []string{"skillProvider.list", "skillProvider.read"} { + t.Run(method+" provider panic", func(t *testing.T) { + output := captureSkillProviderLog(t) + _, server, sessionID := skillProviderErrorHarness(t, panickingSkillProvider{}) + params := map[string]any{"sessionId": sessionID} + wantMessage := "Skill provider listSkills failed" + if method == "skillProvider.read" { + params["name"] = "review" + wantMessage = "Skill provider readSkill failed" + } + + rpcErr := requestSkillProviderError(t, server, method, params) + if rpcErr.Code != -32603 || rpcErr.Message != wantMessage || rpcErr.Data != nil { + t.Fatalf("error = %+v, want generic message %q without data", rpcErr, wantMessage) + } + if got := output.text(); !strings.Contains(got, "panic=db-password-in-panic") { + t.Fatalf("log = %q, want the recovered panic value", got) + } + }) + } + + t.Run("no provider", func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + client.setupNotificationHandler() + session := newSession("no-provider-session", rpcClient, "", false) + client.sessions[session.SessionID] = session + + rpcErr := requestSkillProviderError(t, server, "skillProvider.read", map[string]any{ + "sessionId": session.SessionID, + "name": "review", + }) + assertSkillProviderError(t, rpcErr, "No skill provider for session: no-provider-session") + }) +} + +func TestSkillProviderTeardown(t *testing.T) { + t.Run("disconnect clears provider", func(t *testing.T) { + client, server, provider, session := newSkillProviderOpenSession(t, "disconnect-session") + + if err := session.Disconnect(); err != nil { + t.Fatalf("Disconnect failed: %v", err) + } + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: disconnect-session") + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called after disconnect: %v", got) + } + _ = client + }) + + t.Run("delete clears provider", func(t *testing.T) { + client, server, provider, session := newSkillProviderOpenSession(t, "delete-session") + server.SetRequestHandler("session.delete", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil + }) + + if err := client.DeleteSession(t.Context(), session.SessionID); err != nil { + t.Fatalf("DeleteSession failed: %v", err) + } + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: delete-session") + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called after delete: %v", got) + } + }) + + t.Run("connection loss clears provider", func(t *testing.T) { + client, server, provider, session := newSkillProviderOpenSession(t, "closed-session") + + client.handleConnectionClose() + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: closed-session") + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called after connection loss: %v", got) + } + }) + + t.Run("failed create cleanup", func(t *testing.T) { + provider := newTestSkillProvider() + var sessionID string + client, server := newSkillProviderClient(t) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID = sessionIDFromParams(t, params) + return nil, &jsonrpc2.Error{Code: -32000, Message: "create failed"} + }) + + if _, err := client.CreateSession(t.Context(), &SessionConfig{SkillProvider: provider}); err == nil { + t.Fatal("CreateSession succeeded unexpectedly") + } + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": sessionID}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: "+sessionID) + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called after failed create: %v", got) + } + }) + + t.Run("failed resume cleanup", func(t *testing.T) { + provider := newTestSkillProvider() + client, server := newSkillProviderClient(t) + server.SetRequestHandler("session.resume", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return nil, &jsonrpc2.Error{Code: -32000, Message: "resume failed"} + }) + + if _, err := client.ResumeSession(t.Context(), "failed-resume", &ResumeSessionConfig{SkillProvider: provider}); err == nil { + t.Fatal("ResumeSession succeeded unexpectedly") + } + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": "failed-resume"}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: failed-resume") + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called after failed resume: %v", got) + } + }) +} + +func TestSkillProviderResumeRebinding(t *testing.T) { + t.Run("serves replacement provider", func(t *testing.T) { + client, server := newSkillProviderClient(t) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + original := newTestSkillProvider() + replacement := newTestSkillProvider() + replacement.markdown = map[string]string{"review": "replacement"} + + session, err := client.CreateSession(t.Context(), &SessionConfig{ + SessionID: "rebind-session", + SkillProvider: original, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + if _, err := client.ResumeSession(t.Context(), session.SessionID, &ResumeSessionConfig{SkillProvider: replacement}); err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + raw, rpcErr := server.Request(t.Context(), "skillProvider.read", map[string]any{ + "sessionId": session.SessionID, + "name": "review", + }) + if rpcErr != nil { + t.Fatalf("skillProvider.read failed: %v", rpcErr) + } + var result rpc.SkillProviderReadResult + if err := json.Unmarshal(raw, &result); err != nil { + t.Fatal(err) + } + if result.Markdown == nil { + t.Fatal("markdown = nil, want replacement") + } + if *result.Markdown != "replacement" { + t.Fatalf("markdown = %q, want replacement", *result.Markdown) + } + if got := original.snapshotCalls(); len(got) != 0 { + t.Fatalf("original provider was called after resume: %v", got) + } + if got := replacement.snapshotCalls(); strings.Join(got, ",") != "read:review" { + t.Fatalf("replacement provider calls = %v", got) + } + }) + + t.Run("resume without provider unbinds previous provider", func(t *testing.T) { + client, server, original, session := newSkillProviderOpenSession(t, "unbind-session") + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + + if _, err := client.ResumeSession(t.Context(), session.SessionID, &ResumeSessionConfig{}); err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: unbind-session") + if got := original.snapshotCalls(); len(got) != 0 { + t.Fatalf("original provider was called after unbound resume: %v", got) + } + }) +} + +type panickingSkillProvider struct{} + +func (panickingSkillProvider) ListSkills(context.Context) ([]rpc.SkillProviderDescriptor, error) { + panic("db-password-in-panic") +} + +func (panickingSkillProvider) ReadSkill(context.Context, string) (string, error) { + panic("db-password-in-panic") +} + +// panickyIsError panics when errors.Is asks whether it matches a target. +type panickyIsError struct{} + +func (panickyIsError) Error() string { return "panicky" } + +func (panickyIsError) Is(error) bool { panic("classification panicked") } + +func captureSkillProviderLog(t *testing.T) *ahpTestLog { + t.Helper() + output := new(ahpTestLog) + previous := log.Writer() + log.SetOutput(output) + t.Cleanup(func() { log.SetOutput(previous) }) + return output +} + +func newSkillProviderClient(t *testing.T) (*Client, *jsonrpc2.Client) { + t.Helper() + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + client.setupNotificationHandler() + return client, server +} + +func newSkillProviderOpenSession(t *testing.T, sessionID string) (*Client, *jsonrpc2.Client, *testSkillProvider, *Session) { + t.Helper() + client, server := newSkillProviderClient(t) + provider := newTestSkillProvider() + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + gotSessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + gotSessionID + `"}`), nil + }) + server.SetRequestHandler("session.detach", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil + }) + + session, err := client.CreateSession(t.Context(), &SessionConfig{ + SessionID: sessionID, + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + return client, server, provider, session +} + +func skillProviderErrorHarness(t *testing.T, provider SkillProvider) (*Client, *jsonrpc2.Client, string) { + t.Helper() + client, server := newSkillProviderClient(t) + session := newSession("error-session", client.client, "", false) + session.registerSkillProvider(provider) + client.sessions[session.SessionID] = session + return client, server, session.SessionID +} + +func requestSkillProviderError(t *testing.T, server *jsonrpc2.Client, method string, params map[string]any) *jsonrpc2.Error { + t.Helper() + _, err := server.Request(t.Context(), method, params) + if err == nil { + t.Fatalf("%s succeeded unexpectedly", method) + } + rpcErr, ok := err.(*jsonrpc2.Error) + if !ok { + t.Fatalf("%s error = %T %v, want *jsonrpc2.Error", method, err, err) + } + return rpcErr +} + +func assertSkillProviderError(t *testing.T, rpcErr *jsonrpc2.Error, wantMessage string) { + t.Helper() + if rpcErr.Code != -32603 || rpcErr.Message != wantMessage { + t.Fatalf("error = %+v, want code -32603 message %q", rpcErr, wantMessage) + } + if rpcErr.Data != nil { + t.Fatalf("error data = %s, want omitted", rpcErr.Data) + } +} + +func assertSkillProviderFlag(t *testing.T, params json.RawMessage, wantPresent bool) { + t.Helper() + var payload map[string]any + if err := json.Unmarshal(params, &payload); err != nil { + t.Fatalf("failed to decode request params: %v", err) + } + got, present := payload["hasSkillProvider"] + if !wantPresent { + if present { + t.Fatalf("hasSkillProvider = %v, want omitted", got) + } + return + } + if got != true { + t.Fatalf("hasSkillProvider = %v, want true", got) + } + if _, present := payload["skillProvider"]; present { + t.Fatalf("skillProvider callback was serialized: %v", payload["skillProvider"]) + } +} diff --git a/go/types.go b/go/types.go index af4fd783fb..c2f5d97925 100644 --- a/go/types.go +++ b/go/types.go @@ -3,6 +3,7 @@ package copilot import ( "context" "encoding/json" + "errors" "time" "github.com/github/copilot-sdk/go/rpc" @@ -1444,6 +1445,24 @@ const ( AskUserVariantElicitation AskUserVariant = "elicitation" ) +// ErrSkillNotFound is returned by [SkillProvider.ReadSkill] when a listed skill +// is no longer available. It can be wrapped; the SDK checks it with [errors.Is]. +var ErrSkillNotFound = errors.New("skill not found") + +// SkillProvider supplies session-scoped skills from the SDK host. +// +// Experimental: this API may change or be removed in future SDK or CLI +// releases. Implementations must be safe for concurrent calls. +type SkillProvider interface { + // ListSkills returns catalog metadata for skills provided by this session. + // Return nil, nil to publish an empty catalog. + ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) + // ReadSkill returns the SKILL.md markdown for name. Return an error wrapping + // [ErrSkillNotFound] when the skill no longer exists. The markdown is + // ignored whenever a non-nil error is returned. + ReadSkill(ctx context.Context, name string) (string, error) +} + // SessionConfig configures a new session type SessionConfig struct { // SessionID is an optional custom session ID @@ -1643,6 +1662,12 @@ type SessionConfig struct { Agent string // SkillDirectories is a list of directories to load skills from SkillDirectories []string + // SkillProvider supplies ephemeral SDK-hosted skills for this session. The + // provider is never serialized or persisted; re-supply it when resuming. + // + // Experimental: this API may change or be removed in future SDK or CLI + // releases. Implementations must be safe for concurrent calls. + SkillProvider SkillProvider `json:"-"` // PluginDirectories is a list of local filesystem paths to Open Plugins-format // directories (https://open-plugins.com/) to load for this session. // Relative paths resolve against WorkingDirectory (or the runtime cwd if unset). @@ -1778,6 +1803,10 @@ type SessionConfig struct { // be set; if omitted, the runtime is expected to reject session creation // (fail-closed). Unset behaves exactly as before. EnableManagedSettings *bool + // EnforceManagedModelDefaults, when true, enforces locked managed model + // controls for this session. Conflicting changes are rejected; overridable + // managed defaults remain mutable. + EnforceManagedModelDefaults *bool // ManagedSettings supplies host-injected enterprise managed settings for // the session. Unlike EnableManagedSettings (which asks the runtime to // self-fetch account/org and device policy), this provides the managed @@ -1821,6 +1850,9 @@ type ManagedSettingsPermissions struct { // session. See the DisableBypassPermissionsMode constants for known values. // Newer values are forwarded unchanged so runtime policies remain fail-closed. DisableBypassPermissionsMode DisableBypassPermissionsMode `json:"disableBypassPermissionsMode,omitempty"` + // DisableAssistedPermissionsMode prevents Assisted Permissions from being + // activated when true. Nil or false imposes no restriction. + DisableAssistedPermissionsMode *bool `json:"disableAssistedPermissionsMode,omitempty"` // Deny lists operations that must always be denied. Unioned across layers. Deny []string `json:"deny,omitzero"` // Ask lists operations that must prompt for approval. Unioned across layers. @@ -1828,6 +1860,11 @@ type ManagedSettingsPermissions struct { // Allow lists operations permitted without prompting. Every declared allow // list across managed layers must admit an operation for it to be allowed. Allow []string `json:"allow,omitzero"` + // LimitTo is a closed-world host boundary expressed as Domain(hostname), + // Domain(IP), or Domain(*.example.com) rules. Schemes, ports, paths, + // queries, and fragments are rejected. Multiple managed layers intersect + // their lists. A present empty list denies all hosts. + LimitTo []string `json:"limitTo,omitzero"` } // ToolDefer controls whether a tool may be deferred (loaded lazily via tool @@ -2208,6 +2245,13 @@ type ResumeSessionConfig struct { Agent string // SkillDirectories is a list of directories to load skills from SkillDirectories []string + // SkillProvider supplies ephemeral SDK-hosted skills for this resumed + // session. Re-supply it on every resume; omitting it unbinds any previous + // provider. + // + // Experimental: this API may change or be removed in future SDK or CLI + // releases. Implementations must be safe for concurrent calls. + SkillProvider SkillProvider `json:"-"` // PluginDirectories is a list of local filesystem paths to Open Plugins-format // directories (https://open-plugins.com/) to load for this session. // Relative paths resolve against WorkingDirectory (or the runtime cwd if unset). @@ -2323,6 +2367,9 @@ type ResumeSessionConfig struct { // SessionConfig.EnableManagedSettings. Re-supply on resume so the runtime // re-applies the managed-settings self-fetch after a CLI process restart. EnableManagedSettings *bool + // EnforceManagedModelDefaults re-applies managed model lock enforcement on + // resume. See SessionConfig.EnforceManagedModelDefaults. + EnforceManagedModelDefaults *bool // ManagedSettings re-injects host-provided managed settings on resume. See // SessionConfig.ManagedSettings. It must be re-supplied on resume: it // replaces the prior injected layer, and omitting it clears that layer so @@ -2453,6 +2500,8 @@ const ( AutoTierBalance = rpc.AutoTierBalance // AutoTierIntelligence selects the intelligence routing tier. AutoTierIntelligence = rpc.AutoTierIntelligence + // AutoTierFast selects the integrator-only latency preset. + AutoTierFast = rpc.AutoTierFast ) // CapiSessionOptions configures provider-scoped Copilot API (CAPI) session behavior. @@ -2826,6 +2875,7 @@ type createSessionRequest struct { EnableHostGitOperations *bool `json:"enableHostGitOperations,omitempty"` EnableSessionStore *bool `json:"enableSessionStore,omitempty"` EnableSkills *bool `json:"enableSkills,omitempty"` + HasSkillProvider *bool `json:"hasSkillProvider,omitempty"` SkillDirectories []string `json:"skillDirectories,omitempty"` PluginDirectories []string `json:"pluginDirectories,omitempty"` InstructionDirectories []string `json:"instructionDirectories,omitempty"` @@ -2852,6 +2902,7 @@ type createSessionRequest struct { ExpAssignments *CopilotExpAssignmentResponse `json:"expAssignments,omitempty"` FeatureFlags *map[string]bool `json:"featureFlags,omitempty"` EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` + EnforceManagedModelDefaults *bool `json:"enforceManagedModelDefaults,omitempty"` ManagedSettings *ManagedSettings `json:"managedSettings,omitempty"` Traceparent string `json:"traceparent,omitempty"` Tracestate string `json:"tracestate,omitempty"` @@ -2916,6 +2967,7 @@ type resumeSessionRequest struct { EnableHostGitOperations *bool `json:"enableHostGitOperations,omitempty"` EnableSessionStore *bool `json:"enableSessionStore,omitempty"` EnableSkills *bool `json:"enableSkills,omitempty"` + HasSkillProvider *bool `json:"hasSkillProvider,omitempty"` DisableResume *bool `json:"disableResume,omitempty"` ContinuePendingWork *bool `json:"continuePendingWork,omitempty"` AllowTranscriptRecovery *bool `json:"allowTranscriptRecovery,omitempty"` @@ -2956,6 +3008,7 @@ type resumeSessionRequest struct { ExpAssignments *CopilotExpAssignmentResponse `json:"expAssignments,omitempty"` FeatureFlags *map[string]bool `json:"featureFlags,omitempty"` EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` + EnforceManagedModelDefaults *bool `json:"enforceManagedModelDefaults,omitempty"` ManagedSettings *ManagedSettings `json:"managedSettings,omitempty"` Traceparent string `json:"traceparent,omitempty"` Tracestate string `json:"tracestate,omitempty"` diff --git a/go/zsession_events.go b/go/zsession_events.go index 794a765fa8..76cb39be06 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -13,6 +13,7 @@ type ( AgentInterruptedCancelPhase = rpc.AgentInterruptedCancelPhase AgentInterruptedData = rpc.AgentInterruptedData AgentModelPolicy = rpc.AgentModelPolicy + AiCreditsStatus = rpc.AiCreditsStatus AssistantFusionPhaseActivityData = rpc.AssistantFusionPhaseActivityData AssistantFusionPhaseCompletedData = rpc.AssistantFusionPhaseCompletedData AssistantFusionPhaseFailedData = rpc.AssistantFusionPhaseFailedData @@ -190,8 +191,11 @@ type ( ModelCallFinishedData = rpc.ModelCallFinishedData ModelCallFinishedOutcome = rpc.ModelCallFinishedOutcome ModelCallStartData = rpc.ModelCallStartData + ModelCallWebSocketFallbackReason = rpc.ModelCallWebSocketFallbackReason ModelChangeSource = rpc.ModelChangeSource ModelDeselectedReason = rpc.ModelDeselectedReason + ModelProviderKind = rpc.ModelProviderKind + ModelProviderRef = rpc.ModelProviderRef OmittedBinaryOmittedReason = rpc.OmittedBinaryOmittedReason OmittedBinaryResult = rpc.OmittedBinaryResult OmittedBinaryType = rpc.OmittedBinaryType @@ -272,6 +276,8 @@ type ( PermissionRule = rpc.PermissionRule PermissionSandboxPathGrant = rpc.PermissionSandboxPathGrant PermissionSandboxPathGrantAccess = rpc.PermissionSandboxPathGrantAccess + PermissionWriteFileContent = rpc.PermissionWriteFileContent + PermissionWriteFileEdit = rpc.PermissionWriteFileEdit PersistedBinaryImage = rpc.PersistedBinaryImage PersistedBinaryImageType = rpc.PersistedBinaryImageType PersistedBinaryResult = rpc.PersistedBinaryResult @@ -279,6 +285,14 @@ type ( PlanChangedOperation = rpc.PlanChangedOperation PossibleURL = rpc.PossibleURL PromptCacheBreakData = rpc.PromptCacheBreakData + ProviderQuotaAccessState = rpc.ProviderQuotaAccessState + ProviderQuotaAcquisitionStatus = rpc.ProviderQuotaAcquisitionStatus + ProviderQuotaBudgetMetadata = rpc.ProviderQuotaBudgetMetadata + ProviderQuotaCapacityState = rpc.ProviderQuotaCapacityState + ProviderQuotaObservationKind = rpc.ProviderQuotaObservationKind + ProviderQuotaQuantityKind = rpc.ProviderQuotaQuantityKind + ProviderQuotaState = rpc.ProviderQuotaState + ProviderQuotaUnit = rpc.ProviderQuotaUnit RawCitationLocation = rpc.RawCitationLocation RawHumanResponseRecordedResponse = rpc.RawHumanResponseRecordedResponse RawPermissionPromptRequest = rpc.RawPermissionPromptRequest @@ -349,6 +363,7 @@ type ( SessionPermissionRecoveryData = rpc.SessionPermissionRecoveryData SessionPermissionsChangedData = rpc.SessionPermissionsChangedData SessionPlanChangedData = rpc.SessionPlanChangedData + SessionQuotaObservationData = rpc.SessionQuotaObservationData SessionRemoteSteerableChangedData = rpc.SessionRemoteSteerableChangedData SessionResumeData = rpc.SessionResumeData SessionScheduleCancelledData = rpc.SessionScheduleCancelledData @@ -462,6 +477,16 @@ type ( ToolUserRequestedData = rpc.ToolUserRequestedData UIEphemeralQueryData = rpc.UIEphemeralQueryData UIEphemeralQueryPhase = rpc.UIEphemeralQueryPhase + UsageAccountingIdentity = rpc.UsageAccountingIdentity + UsageGetMetricsResult = rpc.UsageGetMetricsResult + UsageMetricsAgentMetric = rpc.UsageMetricsAgentMetric + UsageMetricsCodeChanges = rpc.UsageMetricsCodeChanges + UsageMetricsModelMetric = rpc.UsageMetricsModelMetric + UsageMetricsModelMetricRequests = rpc.UsageMetricsModelMetricRequests + UsageMetricsModelMetricTokenDetail = rpc.UsageMetricsModelMetricTokenDetail + UsageMetricsModelMetricUsage = rpc.UsageMetricsModelMetricUsage + UsageMetricsProviderModelMetric = rpc.UsageMetricsProviderModelMetric + UsageMetricsTokenDetail = rpc.UsageMetricsTokenDetail UserInputCompletedData = rpc.UserInputCompletedData UserInputRequestedData = rpc.UserInputRequestedData UserMessageAgentMode = rpc.UserMessageAgentMode @@ -505,6 +530,9 @@ const ( AgentInterruptedCancelPhasePreFirstToken = rpc.AgentInterruptedCancelPhasePreFirstToken AgentModelPolicyPreferred = rpc.AgentModelPolicyPreferred AgentModelPolicyRequired = rpc.AgentModelPolicyRequired + AiCreditsStatusComplete = rpc.AiCreditsStatusComplete + AiCreditsStatusPartial = rpc.AiCreditsStatusPartial + AiCreditsStatusUnavailable = rpc.AiCreditsStatusUnavailable AssistantMessageToolRequestCallerTypeProgram = rpc.AssistantMessageToolRequestCallerTypeProgram AssistantMessageToolRequestTypeCustom = rpc.AssistantMessageToolRequestTypeCustom AssistantMessageToolRequestTypeFunction = rpc.AssistantMessageToolRequestTypeFunction @@ -554,7 +582,6 @@ const ( AutopilotObjectiveChangedStatusCapReached = rpc.AutopilotObjectiveChangedStatusCapReached AutopilotObjectiveChangedStatusCompleted = rpc.AutopilotObjectiveChangedStatusCompleted AutopilotObjectiveChangedStatusPaused = rpc.AutopilotObjectiveChangedStatusPaused - AutoTierFast = rpc.AutoTierFast AutoTierSwitchFailureReasonPolicyRejected = rpc.AutoTierSwitchFailureReasonPolicyRejected AutoTierSwitchFailureReasonRequestFailed = rpc.AutoTierSwitchFailureReasonRequestFailed AutoTierSwitchFailureReasonSetupFailed = rpc.AutoTierSwitchFailureReasonSetupFailed @@ -667,6 +694,7 @@ const ( MCPOauthRequestReasonRefresh = rpc.MCPOauthRequestReasonRefresh MCPOauthRequestReasonUpscope = rpc.MCPOauthRequestReasonUpscope MCPOauthRequiredStaticClientConfigGrantTypeClientCredentials = rpc.MCPOauthRequiredStaticClientConfigGrantTypeClientCredentials + MCPServerSourceAccount = rpc.MCPServerSourceAccount MCPServerSourceBuiltin = rpc.MCPServerSourceBuiltin MCPServerSourceManaged = rpc.MCPServerSourceManaged MCPServerSourcePlugin = rpc.MCPServerSourcePlugin @@ -704,6 +732,11 @@ const ( ModelCallFinishedOutcomeError = rpc.ModelCallFinishedOutcomeError ModelCallFinishedOutcomeRejected = rpc.ModelCallFinishedOutcomeRejected ModelCallFinishedOutcomeSuccess = rpc.ModelCallFinishedOutcomeSuccess + ModelCallWebSocketFallbackReasonAPIError = rpc.ModelCallWebSocketFallbackReasonAPIError + ModelCallWebSocketFallbackReasonConnectFailed = rpc.ModelCallWebSocketFallbackReasonConnectFailed + ModelCallWebSocketFallbackReasonConnectionUnavailable = rpc.ModelCallWebSocketFallbackReasonConnectionUnavailable + ModelCallWebSocketFallbackReasonSendFailed = rpc.ModelCallWebSocketFallbackReasonSendFailed + ModelCallWebSocketFallbackReasonTransportFailed = rpc.ModelCallWebSocketFallbackReasonTransportFailed ModelChangeSourceAgent = rpc.ModelChangeSourceAgent ModelChangeSourceAutomatic = rpc.ModelChangeSourceAutomatic ModelChangeSourceAutoTierRecommendation = rpc.ModelChangeSourceAutoTierRecommendation @@ -718,6 +751,8 @@ const ( ModelChangeSourceSettingsCommand = rpc.ModelChangeSourceSettingsCommand ModelChangeSourceStartup = rpc.ModelChangeSourceStartup ModelDeselectedReasonProviderWithdrawn = rpc.ModelDeselectedReasonProviderWithdrawn + ModelProviderKindCopilot = rpc.ModelProviderKindCopilot + ModelProviderKindLoki = rpc.ModelProviderKindLoki OmittedBinaryOmittedReasonAssetUnavailable = rpc.OmittedBinaryOmittedReasonAssetUnavailable OmittedBinaryOmittedReasonTooLarge = rpc.OmittedBinaryOmittedReasonTooLarge OmittedBinaryTypeImage = rpc.OmittedBinaryTypeImage @@ -736,6 +771,10 @@ const ( PermissionApprovalEvaluationReasonCodeActionTooLong = rpc.PermissionApprovalEvaluationReasonCodeActionTooLong PermissionApprovalEvaluationReasonCodeArgumentBindingUnreviewable = rpc.PermissionApprovalEvaluationReasonCodeArgumentBindingUnreviewable PermissionApprovalEvaluationReasonCodeAuthorizationHistoryIncomplete = rpc.PermissionApprovalEvaluationReasonCodeAuthorizationHistoryIncomplete + PermissionApprovalEvaluationReasonCodeContentExcluded = rpc.PermissionApprovalEvaluationReasonCodeContentExcluded + PermissionApprovalEvaluationReasonCodeDynamicSource = rpc.PermissionApprovalEvaluationReasonCodeDynamicSource + PermissionApprovalEvaluationReasonCodeExecutableTooLarge = rpc.PermissionApprovalEvaluationReasonCodeExecutableTooLarge + PermissionApprovalEvaluationReasonCodeExecutableUnavailable = rpc.PermissionApprovalEvaluationReasonCodeExecutableUnavailable PermissionApprovalEvaluationReasonCodeInactive = rpc.PermissionApprovalEvaluationReasonCodeInactive PermissionApprovalEvaluationReasonCodeInherited = rpc.PermissionApprovalEvaluationReasonCodeInherited PermissionApprovalEvaluationReasonCodeInterpreterTooLarge = rpc.PermissionApprovalEvaluationReasonCodeInterpreterTooLarge @@ -754,11 +793,14 @@ const ( PermissionApprovalEvaluationReasonCodeSandboxBypass = rpc.PermissionApprovalEvaluationReasonCodeSandboxBypass PermissionApprovalEvaluationReasonCodeShellEnvironmentUnreviewable = rpc.PermissionApprovalEvaluationReasonCodeShellEnvironmentUnreviewable PermissionApprovalEvaluationReasonCodeTooLarge = rpc.PermissionApprovalEvaluationReasonCodeTooLarge + PermissionApprovalEvaluationReasonCodeTooManySources = rpc.PermissionApprovalEvaluationReasonCodeTooManySources PermissionApprovalEvaluationReasonCodeUnavailable = rpc.PermissionApprovalEvaluationReasonCodeUnavailable PermissionApprovalEvaluationReasonCodeUnknown = rpc.PermissionApprovalEvaluationReasonCodeUnknown PermissionApprovalEvaluationReasonCodeUnreadable = rpc.PermissionApprovalEvaluationReasonCodeUnreadable PermissionApprovalEvaluationReasonCodeUnrepresentablePath = rpc.PermissionApprovalEvaluationReasonCodeUnrepresentablePath PermissionApprovalEvaluationReasonCodeUnreviewableScriptInvocation = rpc.PermissionApprovalEvaluationReasonCodeUnreviewableScriptInvocation + PermissionApprovalEvaluationReasonCodeUnsupportedCommandShape = rpc.PermissionApprovalEvaluationReasonCodeUnsupportedCommandShape + PermissionApprovalEvaluationReasonCodeUnsupportedSource = rpc.PermissionApprovalEvaluationReasonCodeUnsupportedSource PermissionDecisionSourceAuthorizationCarryForward = rpc.PermissionDecisionSourceAuthorizationCarryForward PermissionMessageAuthorizationPolarityDenial = rpc.PermissionMessageAuthorizationPolarityDenial PermissionMessageAuthorizationPolarityGrant = rpc.PermissionMessageAuthorizationPolarityGrant @@ -848,6 +890,30 @@ const ( PlanChangedOperationCreate = rpc.PlanChangedOperationCreate PlanChangedOperationDelete = rpc.PlanChangedOperationDelete PlanChangedOperationUpdate = rpc.PlanChangedOperationUpdate + ProviderQuotaAccessStateAllowed = rpc.ProviderQuotaAccessStateAllowed + ProviderQuotaAccessStateDenied = rpc.ProviderQuotaAccessStateDenied + ProviderQuotaAccessStateNotRequired = rpc.ProviderQuotaAccessStateNotRequired + ProviderQuotaAccessStateUnavailable = rpc.ProviderQuotaAccessStateUnavailable + ProviderQuotaAccessStateUnknown = rpc.ProviderQuotaAccessStateUnknown + ProviderQuotaAcquisitionStatusFailed = rpc.ProviderQuotaAcquisitionStatusFailed + ProviderQuotaAcquisitionStatusSucceeded = rpc.ProviderQuotaAcquisitionStatusSucceeded + ProviderQuotaAcquisitionStatusUnavailable = rpc.ProviderQuotaAcquisitionStatusUnavailable + ProviderQuotaCapacityStateAvailable = rpc.ProviderQuotaCapacityStateAvailable + ProviderQuotaCapacityStateExhausted = rpc.ProviderQuotaCapacityStateExhausted + ProviderQuotaCapacityStateNotApplicable = rpc.ProviderQuotaCapacityStateNotApplicable + ProviderQuotaCapacityStateNotRequired = rpc.ProviderQuotaCapacityStateNotRequired + ProviderQuotaCapacityStateUnavailable = rpc.ProviderQuotaCapacityStateUnavailable + ProviderQuotaCapacityStateUnknown = rpc.ProviderQuotaCapacityStateUnknown + ProviderQuotaCapacityStateUnlimited = rpc.ProviderQuotaCapacityStateUnlimited + ProviderQuotaObservationKindAccountSnapshot = rpc.ProviderQuotaObservationKindAccountSnapshot + ProviderQuotaObservationKindAdmissionState = rpc.ProviderQuotaObservationKindAdmissionState + ProviderQuotaQuantityKindAdvisoryBalance = rpc.ProviderQuotaQuantityKindAdvisoryBalance + ProviderQuotaQuantityKindAuthoritativeBudget = rpc.ProviderQuotaQuantityKindAuthoritativeBudget + ProviderQuotaQuantityKindNone = rpc.ProviderQuotaQuantityKindNone + ProviderQuotaUnitAiCredits = rpc.ProviderQuotaUnitAiCredits + ProviderQuotaUnitRequests = rpc.ProviderQuotaUnitRequests + ProviderQuotaUnitTokens = rpc.ProviderQuotaUnitTokens + ProviderQuotaUnitUnknown = rpc.ProviderQuotaUnitUnknown ReasoningSummaryConcise = rpc.ReasoningSummaryConcise ReasoningSummaryDetailed = rpc.ReasoningSummaryDetailed ReasoningSummaryNone = rpc.ReasoningSummaryNone @@ -968,6 +1034,7 @@ const ( SessionEventTypeSessionPermissionRecovery = rpc.SessionEventTypeSessionPermissionRecovery SessionEventTypeSessionPermissionsChanged = rpc.SessionEventTypeSessionPermissionsChanged SessionEventTypeSessionPlanChanged = rpc.SessionEventTypeSessionPlanChanged + SessionEventTypeSessionQuotaObservation = rpc.SessionEventTypeSessionQuotaObservation SessionEventTypeSessionRemoteSteerableChanged = rpc.SessionEventTypeSessionRemoteSteerableChanged SessionEventTypeSessionResume = rpc.SessionEventTypeSessionResume SessionEventTypeSessionScheduleCancelled = rpc.SessionEventTypeSessionScheduleCancelled diff --git a/java/README.md b/java/README.md index cc2c5cf3a4..c470b332f0 100644 --- a/java/README.md +++ b/java/README.md @@ -76,7 +76,7 @@ implementation 'com.github:copilot-sdk-java:1.0.15-preview.1-SNAPSHOT' ## In-process mode (experimental) -The SDK supports running the Copilot runtime **in-process** as a native library instead of spawning a separate CLI process. This eliminates process management overhead and simplifies deployment. In-process mode is currently experimental and supported on **linux-x64** (glibc), **linux-arm64** (glibc), **linuxmusl-x64**, **win32-x64**, **win32-arm64**, **darwin-x64**, and **darwin-arm64**. +The SDK supports running the Copilot runtime **in-process** as a native library instead of spawning a separate CLI process. This eliminates process management overhead and simplifies deployment. In-process mode is currently experimental and supported on **linux-x64** (glibc), **linux-arm64** (glibc), **linuxmusl-x64**, **linuxmusl-arm64**, **win32-x64**, **win32-arm64**, **darwin-x64**, and **darwin-arm64**. Because in-process mode is experimental, see the [Using experimental APIs](#using-experimental-apis) section for how to opt in. @@ -99,7 +99,7 @@ Add both the SDK and the platform-specific native runtime to your project: ${copilot.version} linux-x64 - + net.java.dev.jna @@ -183,6 +183,24 @@ public class CopilotSDK { } ``` +`client.forceStop()` retires tracked sessions locally without `session.detach` +RPCs. Retained session objects reject new work and subscriptions, unused handler +captures are released, and pending plain and structured response waits fail. +Already-admitted event handlers and response continuations are not joined. +Forced retirement also drops callbacks when a graceful `session.close()` is +already awaiting detach. Graceful close fails pending response waits without +joining their continuations; ordinary callbacks remain active until detach completes. +Plain response waits preserve terminal results and errors already received before +retirement, even when a blocked continuation delays forwarding them to the caller. +Graceful owned-process shutdown over stdio waits for the runtime transport and +launcher to exit. Over TCP, it closes the connection and terminates the owned +process after the shutdown acknowledgment, without waiting for peer EOF. +Both preserve intentionally detached services; failed or forced shutdown retains +process-tree cleanup. +For a custom TCP launcher that spawns the runtime as a child, graceful cleanup +terminates only the launcher. The launcher must supervise and reap its runtime +child; a shutdown acknowledgment alone does not prove that child exited. + When targeting MCP tools configured through `setMcpServers(...)`, remember the runtime tool name is `-`. For `setAvailableTools(...)` and `setExcludedTools(...)`, prefer the source-qualified filter form @@ -286,6 +304,51 @@ provider errors, and invalid token responses reject that operation instead of falling back to ambient authentication. Idle sessions refresh only before their next credential-consuming operation; there is no background refresh timer. +### Skill providers (experimental) + +`SessionConfig.setSkillProvider(...)` registers session-scoped skills that the +runtime can list and read on demand. The API is experimental; see +[Using experimental APIs](#using-experimental-apis) before compiling code that +references it. + +```java +import com.github.copilot.SkillProvider; +import com.github.copilot.SkillProviderDescriptor; + +SkillProvider provider = new SkillProvider() { + @Override + public CompletableFuture> listSkills() { + return CompletableFuture.completedFuture(List.of( + new SkillProviderDescriptor("team-plan", "Team planning guidance", true, false, null))); + } + + @Override + public CompletableFuture readSkill(String name) { + if (!name.equals("team-plan")) { + return CompletableFuture.completedFuture(null); // not found + } + return CompletableFuture.completedFuture("# Team plan\nWrite a concise monthly team plan."); + } +}; + +var config = new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setEnableSkills(true) + .setSkillProvider(provider); +``` + +Provider callbacks may be invoked concurrently, so implementations should be +thread-safe. When the runtime cancels a call, for example after its 30-second +limit or when the session disconnects, the SDK calls `cancel(true)` on the +returned future; check `isCancelled()` or attach a completion callback to stop +early. The provider is not persisted with the session; pass it again via +`ResumeSessionConfig.setSkillProvider(...)` on every resume. Resuming without a +provider unbinds any provider the session had. In +`CopilotClientMode.EMPTY`, explicitly set `setEnableSkills(true)` if the +assistant should use skills, because empty mode disables skill loading by +default. Skill providers are local-session only and are rejected for cloud +sessions. + ### Typed MCP installation and removal payloads (breaking change) Three payloads in the experimental MCP installation and removal workflow are now sealed @@ -875,7 +938,7 @@ CI enforces both checks. Spotless runs explicitly in CI; `mvn verify` alone does Run native-runtime Maven commands from the `java` directory. Native packaging requires Node.js in addition to JDK 25 and Maven. In a standalone SDK checkout, `copilot-native/scripts/fetch-native.mjs` retrieves the pinned runtime package from the corresponding GitHub release. When the SDK is nested in `copilot-agent-runtime`, it instead stages the same-checkout artifacts from `dist-cli`; run `pnpm run build:cli` from the runtime repository first. -On a native Linux glibc host, Maven activates `native-linux-x64` or `native-linux-arm64` for the matching architecture when `copilot.native.libc=glibc` is set. On a Linux musl x64 host, Maven activates `native-linuxmusl-x64` when `copilot.native.libc=musl` is set. On Windows x64, Windows ARM64, Intel macOS, and Apple Silicon macOS, Maven activates `native-win32-x64`, `native-win32-arm64`, `native-darwin-x64`, or `native-darwin-arm64` automatically. The matching profile validates the host, runs the native script tests, stages the platform package during `generate-resources`, packages the classifier JAR during `package`, and verifies its native contents. +On a native Linux glibc host, Maven activates `native-linux-x64` or `native-linux-arm64` for the matching architecture when `copilot.native.libc=glibc` is set. On a Linux musl host, Maven activates `native-linuxmusl-x64` or `native-linuxmusl-arm64` for the matching architecture when `copilot.native.libc=musl` is set. On Windows x64, Windows ARM64, Intel macOS, and Apple Silicon macOS, Maven activates `native-win32-x64`, `native-win32-arm64`, `native-darwin-x64`, or `native-darwin-arm64` automatically. The matching profile validates the host, runs the native script tests, stages the platform package during `generate-resources`, packages the classifier JAR during `package`, and verifies its native contents. Before opting in, validate that Node.js reports glibc for the build host: @@ -910,14 +973,14 @@ node copilot-native/scripts/validate-native-host.mjs linux-arm64 mvn -Pinprocess clean verify -Dcopilot.native.libc=glibc ``` -The same command validates in-process mode on Linux musl x64: +The same command validates in-process mode on Linux musl x64 or ARM64: ```bash -node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 +node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 # Use linuxmusl-arm64 on ARM64 mvn -Pinprocess clean verify -Dcopilot.native.libc=musl ``` -On Linux musl ARM64 and other unsupported hosts, do not set `copilot.native.libc`. A normal build produces only the OS-neutral primary, sources, and Javadoc JARs; it does not run native script tests, download or stage native files, or produce a platform classifier JAR. +On unsupported hosts, do not set `copilot.native.libc`. A normal build produces only the OS-neutral primary, sources, and Javadoc JARs; it does not run native script tests, download or stage native files, or produce a platform classifier JAR. To build only the OS-neutral artifacts on any host, or override the glibc opt-in, disable native download and packaging: diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index 2c750e4e47..41af2dd6cb 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -467,6 +467,64 @@ + + native-linuxmusl-arm64 + + + Linux + aarch64 + + + copilot.native.libc + musl + + + + linuxmusl-arm64 + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-native-host + validate + + + fetch-native + generate-resources + + + test-fetch-native + test + + + + + org.apache.maven.plugins + maven-jar-plugin + + + jar-native + package + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + verify-native-jars + package + + + + + + native-win32-x64 @@ -809,6 +867,68 @@ + + + attach-external-linuxmusl-arm64-classifier + + + copilot.native.external.linuxmusl.arm64.classifier.path + + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-external-linuxmusl-arm64-classifier + validate + + exec + + + node + + ${project.basedir}/scripts/validate-native-artifact.mjs + classifier + linuxmusl-arm64 + ${copilot.native.external.linuxmusl.arm64.classifier.path} + ${project.build.finalName}-linuxmusl-arm64.jar + ${copilot.sdk.root} + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-external-linuxmusl-arm64-classifier + package + + attach-artifact + + + + + ${copilot.native.external.linuxmusl.arm64.classifier.path} + jar + linuxmusl-arm64 + + + + + + + + + + jna + ${project.build.directory}/consumer-dependencies + + + + ${testExecutionAgentArgs} ${surefire.jvm.args} --add-opens com.github.copilot.java/com.github.copilot.e2e=ALL-UNNAMED - - 2 + 0 ${copilot.tests.dir} ${copilot.sdk.root} @@ -719,6 +732,22 @@ did not produce the multi-release output. Re-build on JDK 25+ and verify the linuxmusl-x64 + + native-linuxmusl-arm64 + + + Linux + aarch64 + + + copilot.native.libc + musl + + + + linuxmusl-arm64 + + native-win32-x64 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AiCreditsStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/AiCreditsStatus.java new file mode 100644 index 0000000000..f6f95a97a2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AiCreditsStatus.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.generated; + +import javax.annotation.processing.Generated; + +/** + * Whether the accumulated numeric AI-credit subtotal covers the observed calls. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AiCreditsStatus { + /** The {@code complete} variant. */ + COMPLETE("complete"), + /** The {@code partial} variant. */ + PARTIAL("partial"), + /** The {@code unavailable} variant. */ + UNAVAILABLE("unavailable"); + + private final String value; + AiCreditsStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AiCreditsStatus fromValue(String value) { + for (AiCreditsStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AiCreditsStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageEvent.java index 365b945965..87d0e6f29a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageEvent.java @@ -42,6 +42,8 @@ public record AssistantMessageEventData( @JsonProperty("originatingMessageId") String originatingMessageId, /** Model that produced this assistant message, if known */ @JsonProperty("model") String model, + /** Opaque origin provider identity captured at dispatch. Only an exact match with the target provider permits replay of reasoningOpaque, encryptedContent, reasoningBlocks, serverTools, and provider response/item identifiers. Matching model ids or wire protocols is insufficient. Omitted for legacy or unattributed messages; the runtime excludes unproven opaque state when projecting to a known provider. Text content and client tool requests/results remain portable semantic history. */ + @JsonProperty("providerId") String providerId, /** The assistant's text response content */ @JsonProperty("content") String content, /** Tool invocations requested by the assistant in this message */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantUsageEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantUsageEvent.java index 0c02f5d15a..75eb6f53f0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantUsageEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantUsageEvent.java @@ -38,8 +38,16 @@ public final class AssistantUsageEvent extends SessionEvent { @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public record AssistantUsageEventData( + /** Availability of this call's reported AI-credit amount. Missing billing is unavailable, not a zero charge. */ + @JsonProperty("aiCreditsStatus") AiCreditsStatus aiCreditsStatus, /** Model identifier used for this API call */ @JsonProperty("model") String model, + /** Provider identity and product label captured by the actual call's dispatch path. */ + @JsonProperty("provider") ModelProviderRef provider, + /** Model display name captured for this call, not derived from the current selection. */ + @JsonProperty("modelDisplayName") String modelDisplayName, + /** Runtime-owned identity shared with the durable accounting receipt. */ + @JsonProperty("accounting") UsageAccountingIdentity accounting, /** Number of input tokens consumed */ @JsonProperty("inputTokens") Long inputTokens, /** Number of output tokens produced */ @@ -89,6 +97,12 @@ public record AssistantUsageEventData( @JsonProperty("rejectedPredictionTokens") Long rejectedPredictionTokens, /** Transport used for this model call (http or websocket) */ @JsonProperty("transport") AssistantUsageTransport transport, + /** Serialized (uncompressed) byte length of the request body. A content-free size signal. */ + @JsonProperty("requestBodyBytes") Long requestBodyBytes, + /** Why the call was carried by the HTTP fallback of a WebSocket-capable dispatcher; absent when no fallback occurred */ + @JsonProperty("websocketFallbackReason") ModelCallWebSocketFallbackReason websocketFallbackReason, + /** Milliseconds spent on the WebSocket attempt before falling back to HTTP */ + @JsonProperty("websocketFallbackAfterMs") Long websocketFallbackAfterMs, /** 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 */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AutoTier.java b/java/sdk/src/generated/java/com/github/copilot/generated/AutoTier.java index 1c9d81b8ed..b3669208d7 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AutoTier.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AutoTier.java @@ -10,30 +10,38 @@ import javax.annotation.processing.Generated; /** - * Routing preference used when the session model is `auto`. `fast` is an integrator-only latency preset and is not a first-party GitHub Copilot product preference. + * Extensible routing preference for the virtual `auto` model. New identifiers must be advertised and enabled by the provider. `fast` is an integrator-only latency preset. * * @since 1.0.0 */ @javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum AutoTier { +public final class AutoTier { /** The {@code efficiency} variant. */ - EFFICIENCY("efficiency"), + public static final AutoTier EFFICIENCY = new AutoTier("efficiency"); /** The {@code balance} variant. */ - BALANCE("balance"), + public static final AutoTier BALANCE = new AutoTier("balance"); /** The {@code intelligence} variant. */ - INTELLIGENCE("intelligence"), + public static final AutoTier INTELLIGENCE = new AutoTier("intelligence"); /** The {@code fast} variant. */ - FAST("fast"); + public static final AutoTier FAST = new AutoTier("fast"); private final String value; - AutoTier(String value) { this.value = value; } + private AutoTier(String value) { this.value = value; } @com.fasterxml.jackson.annotation.JsonValue public String getValue() { return value; } @com.fasterxml.jackson.annotation.JsonCreator public static AutoTier fromValue(String value) { + if (value == null) return null; + if (value.isEmpty() || value.codePoints().anyMatch(c -> Character.isWhitespace(c) || Character.isISOControl(c))) + throw new IllegalArgumentException("AutoTier requires a routing identifier"); for (AutoTier v : values()) { if (v.value.equals(value)) return v; } - throw new IllegalArgumentException("Unknown AutoTier value: " + value); + return new AutoTier(value); } + /** Returns the known routing preferences. @return known values */ + public static AutoTier[] values() { return new AutoTier[] { EFFICIENCY, BALANCE, INTELLIGENCE, FAST }; } + @Override public boolean equals(Object other) { return other instanceof AutoTier v && value.equals(v.value); } + @Override public int hashCode() { return value.hashCode(); } + @Override public String toString() { return value; } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CompactionCompleteCompactionTokensUsed.java b/java/sdk/src/generated/java/com/github/copilot/generated/CompactionCompleteCompactionTokensUsed.java index ed454deafd..c325857e59 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/CompactionCompleteCompactionTokensUsed.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CompactionCompleteCompactionTokensUsed.java @@ -21,6 +21,12 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record CompactionCompleteCompactionTokensUsed( + /** Whether the compaction's credit amount was reported for all, some, or none of its calls. */ + @JsonProperty("aiCreditsStatus") AiCreditsStatus aiCreditsStatus, + /** Provider captured for the compaction call; absent when attribution is unknown. */ + @JsonProperty("provider") ModelProviderRef provider, + /** Model display name captured for the compaction call. */ + @JsonProperty("modelDisplayName") String modelDisplayName, /** Input tokens consumed by the compaction LLM call */ @JsonProperty("inputTokens") Long inputTokens, /** Output tokens produced by the compaction LLM call */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsEnforcedEscalation.java b/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsEnforcedEscalation.java index 619fb326e1..ff4f36d055 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsEnforcedEscalation.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsEnforcedEscalation.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * For a `bypass_permissions_blocked` action, which permission-escalation primitive was refused + * For a `bypass_permissions_blocked` action, which permission-mode or escalation primitive was refused * * @since 1.0.0 */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java index 5ea2c39f51..2354c95750 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Configuration source: user, workspace, plugin, builtin, or managed + * Configuration source: user, workspace, plugin, builtin, managed, or account * * @since 1.0.0 */ @@ -25,7 +25,9 @@ public enum McpServerSource { /** The {@code builtin} variant. */ BUILTIN("builtin"), /** The {@code managed} variant. */ - MANAGED("managed"); + MANAGED("managed"), + /** The {@code account} variant. */ + ACCOUNT("account"); private final String value; McpServerSource(String value) { this.value = value; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFailureEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFailureEvent.java index 05e8a8371f..717523a2ee 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFailureEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFailureEvent.java @@ -60,6 +60,14 @@ public record ModelCallFailureEventData( @JsonProperty("transport") ModelCallFailureTransport transport, /** Whether the failure originated from an API response or the request transport */ @JsonProperty("failureKind") ModelCallFailureKind failureKind, + /** Serialized (uncompressed) byte length of the failed request body. A content-free size signal. */ + @JsonProperty("requestBodyBytes") Long requestBodyBytes, + /** Zero-based orchestrator retry index of the failed attempt */ + @JsonProperty("retryAttempt") Long retryAttempt, + /** Why the failed call was carried by the HTTP fallback of a WebSocket-capable dispatcher; absent when no fallback occurred */ + @JsonProperty("websocketFallbackReason") ModelCallWebSocketFallbackReason websocketFallbackReason, + /** Milliseconds spent on the WebSocket attempt before falling back to HTTP */ + @JsonProperty("websocketFallbackAfterMs") Long websocketFallbackAfterMs, /** Effective maximum prompt-token limit for the failed call */ @JsonProperty("maxPromptTokens") Long maxPromptTokens, /** Effective maximum output-token limit for the failed call */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFailureRequestFingerprint.java b/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFailureRequestFingerprint.java index b8d0622b6e..6e83338e11 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFailureRequestFingerprint.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFailureRequestFingerprint.java @@ -33,6 +33,12 @@ public record ModelCallFailureRequestFingerprint( @JsonProperty("imagePartCount") Long imagePartCount, /** Image parts whose media type cannot be determined (rejected by strict providers) */ @JsonProperty("imagePartsMissingMediaType") Long imagePartsMissingMediaType, + /** Summed byte length of inline image payloads (data URLs and base64 sources) */ + @JsonProperty("imageBytes") Long imageBytes, + /** Number of messages carrying opaque or encrypted reasoning */ + @JsonProperty("reasoningItemCount") Long reasoningItemCount, + /** Summed byte length of opaque or encrypted reasoning payloads */ + @JsonProperty("encryptedContentBytes") Long encryptedContentBytes, /** Role of the final message in the request */ @JsonProperty("lastMessageRole") String lastMessageRole ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallWebSocketFallbackReason.java b/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallWebSocketFallbackReason.java new file mode 100644 index 0000000000..fc891ae508 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallWebSocketFallbackReason.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Why a WebSocket-capable model call was carried by the HTTP fallback + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelCallWebSocketFallbackReason { + /** The {@code connect_failed} variant. */ + CONNECT_FAILED("connect_failed"), + /** The {@code connection_unavailable} variant. */ + CONNECTION_UNAVAILABLE("connection_unavailable"), + /** The {@code send_failed} variant. */ + SEND_FAILED("send_failed"), + /** The {@code api_error} variant. */ + API_ERROR("api_error"), + /** The {@code transport_failed} variant. */ + TRANSPORT_FAILED("transport_failed"); + + private final String value; + ModelCallWebSocketFallbackReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelCallWebSocketFallbackReason fromValue(String value) { + for (ModelCallWebSocketFallbackReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelCallWebSocketFallbackReason value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ModelProviderKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/ModelProviderKind.java new file mode 100644 index 0000000000..cbef223e2b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ModelProviderKind.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.generated; + +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * The neutral kind of a model provider — the model analog of `AccountKind`. A model provider is the live, entitled source a model came from; central code never branches on this beyond a single dispatch. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelProviderKind { + /** The {@code copilot} variant. */ + COPILOT("copilot"), + /** The {@code loki} variant. */ + LOKI("loki"); + + private final String value; + ModelProviderKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelProviderKind fromValue(String value) { + for (ModelProviderKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelProviderKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ModelProviderRef.java b/java/sdk/src/generated/java/com/github/copilot/generated/ModelProviderRef.java new file mode 100644 index 0000000000..84a0fa8954 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ModelProviderRef.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A neutral reference to the model provider that produced a model: an opaque id, a human-readable label, and the provider kind. Carried on each enumerated Model so consumers can group by provider without reaching into a provider-shaped internal type. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderRef( + /** Opaque, stable id of the provider that produced this model. Matches the enumerated `ModelProviderDescriptor.id`. */ + @JsonProperty("id") String id, + /** Human-readable provider label, owned by the runtime so every consumer renders identical text. */ + @JsonProperty("label") String label, + /** The provider kind. */ + @JsonProperty("kind") ModelProviderKind kind +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaAccessState.java b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaAccessState.java new file mode 100644 index 0000000000..e9d9ef966e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaAccessState.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Access policy reported by the quota service. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ProviderQuotaAccessState { + /** The {@code allowed} variant. */ + ALLOWED("allowed"), + /** The {@code denied} variant. */ + DENIED("denied"), + /** The {@code not_required} variant. */ + NOT_REQUIRED("not_required"), + /** The {@code unknown} variant. */ + UNKNOWN("unknown"), + /** The {@code unavailable} variant. */ + UNAVAILABLE("unavailable"); + + private final String value; + ProviderQuotaAccessState(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ProviderQuotaAccessState fromValue(String value) { + for (ProviderQuotaAccessState v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ProviderQuotaAccessState value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaAcquisitionStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaAcquisitionStatus.java new file mode 100644 index 0000000000..0b2c8aaa51 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaAcquisitionStatus.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.generated; + +import javax.annotation.processing.Generated; + +/** + * Acquisition state, independent of the service's business access/capacity verdict. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ProviderQuotaAcquisitionStatus { + /** The {@code succeeded} variant. */ + SUCCEEDED("succeeded"), + /** The {@code unavailable} variant. */ + UNAVAILABLE("unavailable"), + /** The {@code failed} variant. */ + FAILED("failed"); + + private final String value; + ProviderQuotaAcquisitionStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ProviderQuotaAcquisitionStatus fromValue(String value) { + for (ProviderQuotaAcquisitionStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ProviderQuotaAcquisitionStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaBudgetMetadata.java b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaBudgetMetadata.java new file mode 100644 index 0000000000..c32d10903d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaBudgetMetadata.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Authoritative budget measurements and policy metadata, independent of provider. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ProviderQuotaBudgetMetadata( + /** Whether the budget has unlimited entitlement. */ + @JsonProperty("unlimited") Boolean unlimited, + /** Exact budget entitlement, retaining incumbent fractional-unit compatibility. */ + @JsonProperty("entitlement") Double entitlement, + /** Exact measured consumption in the budget's unit. */ + @JsonProperty("consumed") Double consumed, + /** Service-reported remaining percentage for authoritative budget presentation. */ + @JsonProperty("remainingPercentage") Double remainingPercentage, + /** Whether service policy allows continued usage after exhaustion. */ + @JsonProperty("usageAllowedWhenExhausted") Boolean usageAllowedWhenExhausted, + /** Usage beyond entitlement, in the budget's unit. */ + @JsonProperty("overage") Double overage, + /** Whether additional usage is allowed when the budget is exhausted. */ + @JsonProperty("overageAllowedWhenExhausted") Boolean overageAllowedWhenExhausted, + /** Reset instant in epoch milliseconds when this budget actually defines a window. */ + @JsonProperty("resetAtEpochMs") Double resetAtEpochMs, + /** Whether the reset instant is an estimate. */ + @JsonProperty("resetEstimated") Boolean resetEstimated, + /** Whether this budget uses token-based billing. */ + @JsonProperty("tokenBasedBilling") Boolean tokenBasedBilling, + /** Optional additional-usage budget cap. */ + @JsonProperty("overageLimit") Double overageLimit +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaCapacityState.java b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaCapacityState.java new file mode 100644 index 0000000000..590168d881 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaCapacityState.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Capacity is independent of whether a numeric balance was supplied. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ProviderQuotaCapacityState { + /** The {@code available} variant. */ + AVAILABLE("available"), + /** The {@code exhausted} variant. */ + EXHAUSTED("exhausted"), + /** The {@code unlimited} variant. */ + UNLIMITED("unlimited"), + /** The {@code not_required} variant. */ + NOT_REQUIRED("not_required"), + /** The {@code not_applicable} variant. */ + NOT_APPLICABLE("not_applicable"), + /** The {@code unknown} variant. */ + UNKNOWN("unknown"), + /** The {@code unavailable} variant. */ + UNAVAILABLE("unavailable"); + + private final String value; + ProviderQuotaCapacityState(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ProviderQuotaCapacityState fromValue(String value) { + for (ProviderQuotaCapacityState v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ProviderQuotaCapacityState value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaObservationKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaObservationKind.java new file mode 100644 index 0000000000..adc6826ebe --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaObservationKind.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Timing and purpose of a provider quota observation. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ProviderQuotaObservationKind { + /** The {@code account_snapshot} variant. */ + ACCOUNT_SNAPSHOT("account_snapshot"), + /** The {@code admission_state} variant. */ + ADMISSION_STATE("admission_state"); + + private final String value; + ProviderQuotaObservationKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ProviderQuotaObservationKind fromValue(String value) { + for (ProviderQuotaObservationKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ProviderQuotaObservationKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaQuantityKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaQuantityKind.java new file mode 100644 index 0000000000..44f2509f0c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaQuantityKind.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.generated; + +import javax.annotation.processing.Generated; + +/** + * Interpretation permitted for independently reported quantities. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ProviderQuotaQuantityKind { + /** The {@code authoritative_budget} variant. */ + AUTHORITATIVE_BUDGET("authoritative_budget"), + /** The {@code advisory_balance} variant. */ + ADVISORY_BALANCE("advisory_balance"), + /** The {@code none} variant. */ + NONE("none"); + + private final String value; + ProviderQuotaQuantityKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ProviderQuotaQuantityKind fromValue(String value) { + for (ProviderQuotaQuantityKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ProviderQuotaQuantityKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaState.java b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaState.java new file mode 100644 index 0000000000..ddb7fe9aa9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaState.java @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * An account quota reading. Absence of a quantity is unknown, never zero. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ProviderQuotaState( + /** Provider/account whose service reported this state. */ + @JsonProperty("provider") ModelProviderRef provider, + /** Service-owned quota identifier within this provider. */ + @JsonProperty("quotaId") String quotaId, + /** Unit of entitledQuantity and availableQuantity. */ + @JsonProperty("unit") ProviderQuotaUnit unit, + /** Whether the service permits access, independently of balance. */ + @JsonProperty("accessState") ProviderQuotaAccessState accessState, + /** Service capacity state; missing quantities do not imply exhaustion. */ + @JsonProperty("capacityState") ProviderQuotaCapacityState capacityState, + /** Whether this is a GET account reading or a pre-response admission observation. Observations are never merged across kinds. */ + @JsonProperty("observationKind") ProviderQuotaObservationKind observationKind, + /** Whether quantities are authoritative budget measurements, advisory balances, or absent. Only authoritative budgets support percentage presentation. */ + @JsonProperty("quantityKind") ProviderQuotaQuantityKind quantityKind, + /** Acquisition outcome. Unavailable/failed readings have no service verdict; access/capacity unavailable are compatibility placeholders only. */ + @JsonProperty("acquisitionStatus") ProviderQuotaAcquisitionStatus acquisitionStatus, + /** When the runtime observed this reading, not a charge timestamp or guarantee that consumption has settled. */ + @JsonProperty("observedAt") String observedAt, + /** HTTP status from acquisition, when available. */ + @JsonProperty("httpStatus") Long httpStatus, + /** Service error code or client acquisition category, separate from the business-state reason. */ + @JsonProperty("acquisitionError") String acquisitionError, + /** Optional metadata for an authoritative budget. Advisory balances and admission-only observations do not populate this. */ + @JsonProperty("budgetMetadata") ProviderQuotaBudgetMetadata budgetMetadata, + /** Key for the backwards-compatible snapshots projection, when the authoritative budget supports that contract. */ + @JsonProperty("compatibilityKey") String compatibilityKey, + /** Explicit service admission flag, when reported. */ + @JsonProperty("hasQuota") Boolean hasQuota, + /** Independently reported signed 64-bit entitlement. -1 is an unlimited sentinel, not a capacity-state rewrite; omission and null are preserved. */ + @JsonProperty("entitledQuantity") Long entitledQuantity, + /** Independently reported signed 64-bit available quantity. Zero does not override the service access/capacity verdict; omission and null are preserved. */ + @JsonProperty("availableQuantity") Long availableQuantity, + /** Service-reported explanation for the state. */ + @JsonProperty("reason") String reason, + /** Service-reported quota source. */ + @JsonProperty("source") String source, + /** Service name owning this reading. */ + @JsonProperty("service") String service +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaUnit.java b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaUnit.java new file mode 100644 index 0000000000..d8dc0048d7 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ProviderQuotaUnit.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.generated; + +import javax.annotation.processing.Generated; + +/** + * Units explicitly reported by a quota provider. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ProviderQuotaUnit { + /** The {@code ai_credits} variant. */ + AI_CREDITS("ai_credits"), + /** The {@code requests} variant. */ + REQUESTS("requests"), + /** The {@code tokens} variant. */ + TOKENS("tokens"), + /** The {@code unknown} variant. */ + UNKNOWN("unknown"); + + private final String value; + ProviderQuotaUnit(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ProviderQuotaUnit fromValue(String value) { + for (ProviderQuotaUnit v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ProviderQuotaUnit value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/RecommendedAutoTier.java b/java/sdk/src/generated/java/com/github/copilot/generated/RecommendedAutoTier.java index acccaf5c8e..748395b847 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/RecommendedAutoTier.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/RecommendedAutoTier.java @@ -10,28 +10,36 @@ import javax.annotation.processing.Generated; /** - * Auto preferences that Copilot API can recommend. + * Enabled Auto preferences that Copilot API can recommend. * * @since 1.0.0 */ @javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum RecommendedAutoTier { +public final class RecommendedAutoTier { /** The {@code efficiency} variant. */ - EFFICIENCY("efficiency"), + public static final RecommendedAutoTier EFFICIENCY = new RecommendedAutoTier("efficiency"); /** The {@code balance} variant. */ - BALANCE("balance"), + public static final RecommendedAutoTier BALANCE = new RecommendedAutoTier("balance"); /** The {@code intelligence} variant. */ - INTELLIGENCE("intelligence"); + public static final RecommendedAutoTier INTELLIGENCE = new RecommendedAutoTier("intelligence"); private final String value; - RecommendedAutoTier(String value) { this.value = value; } + private RecommendedAutoTier(String value) { this.value = value; } @com.fasterxml.jackson.annotation.JsonValue public String getValue() { return value; } @com.fasterxml.jackson.annotation.JsonCreator public static RecommendedAutoTier fromValue(String value) { + if (value == null) return null; + if (value.isEmpty() || value.codePoints().anyMatch(c -> Character.isWhitespace(c) || Character.isISOControl(c))) + throw new IllegalArgumentException("RecommendedAutoTier requires a routing identifier"); for (RecommendedAutoTier v : values()) { if (v.value.equals(value)) return v; } - throw new IllegalArgumentException("Unknown RecommendedAutoTier value: " + value); + return new RecommendedAutoTier(value); } + /** Returns the known routing preferences. @return known values */ + public static RecommendedAutoTier[] values() { return new RecommendedAutoTier[] { EFFICIENCY, BALANCE, INTELLIGENCE }; } + @Override public boolean equals(Object other) { return other instanceof RecommendedAutoTier v && value.equals(v.value); } + @Override public int hashCode() { return value.hashCode(); } + @Override public String toString() { return value; } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java index 726dd21975..3d4b35a64d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java @@ -54,6 +54,7 @@ @JsonSubTypes.Type(value = SessionSnapshotRewindEvent.class, name = "session.snapshot_rewind"), @JsonSubTypes.Type(value = SessionShutdownEvent.class, name = "session.shutdown"), @JsonSubTypes.Type(value = SessionUsageCheckpointEvent.class, name = "session.usage_checkpoint"), + @JsonSubTypes.Type(value = SessionQuotaObservationEvent.class, name = "session.quota_observation"), @JsonSubTypes.Type(value = SessionContextChangedEvent.class, name = "session.context_changed"), @JsonSubTypes.Type(value = SessionUsageInfoEvent.class, name = "session.usage_info"), @JsonSubTypes.Type(value = SessionContextClearedEvent.class, name = "session.context_cleared"), @@ -209,6 +210,7 @@ public abstract sealed class SessionEvent permits SessionSnapshotRewindEvent, SessionShutdownEvent, SessionUsageCheckpointEvent, + SessionQuotaObservationEvent, SessionContextChangedEvent, SessionUsageInfoEvent, SessionContextClearedEvent, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsEnforcedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsEnforcedEvent.java index c712a220a9..16f8188571 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsEnforcedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsEnforcedEvent.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Session event "session.managed_settings_enforced". Runtime enforcement of enterprise managed settings: fires when the session blocks or caps a runtime action because enterprise policy governs it, so SDK clients can explain *why* an action was governed. Unlike `session.managed_settings_resolved` (which reports *what* is managed), this reports a concrete governed action — e.g. a user or host tried to turn on a bypass-permissions escalation while policy disables it. Emitted live (not persisted to the session event log) on user/host-initiated attempts only, never for silent policy application. Marked experimental while the managed-settings surface stabilizes. + * Session event "session.managed_settings_enforced". Runtime enforcement of enterprise managed settings: fires when the session blocks or caps a runtime action because enterprise policy governs it, so SDK clients can explain *why* an action was governed. Unlike `session.managed_settings_resolved` (which reports *what* is managed), this reports a concrete governed action — e.g. a user or host tried to turn on bypass permissions or Assisted Permissions while the corresponding policy disables it. Emitted live (not persisted to the session event log) on user/host-initiated attempts only, never for silent policy application. Marked experimental while the managed-settings surface stabilizes. * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) @@ -36,9 +36,9 @@ public final class SessionManagedSettingsEnforcedEvent extends SessionEvent { public record SessionManagedSettingsEnforcedEventData( /** The category of runtime action that managed policy governed. */ @JsonProperty("action") ManagedSettingsEnforcedAction action, - /** For a `bypass_permissions_blocked` action, which permission-escalation primitive was refused. Absent for actions without a specific escalation primitive. */ + /** For a `bypass_permissions_blocked` action, which permission-mode or escalation primitive was refused. Absent for actions without a specific primitive. */ @JsonProperty("escalation") ManagedSettingsEnforcedEscalation escalation, - /** The managed setting key responsible for the enforcement (e.g. `permissions.disableBypassPermissionsMode`). */ + /** The managed setting key responsible for the enforcement (for example `permissions.disableBypassPermissionsMode` or `permissions.disableAssistedPermissionsMode`). */ @JsonProperty("setting") String setting, /** Whether the enforcement was forced by fail-closed handling (managed policy could not be determined) rather than an explicit managed setting. When true, `setting` still names the restriction that was applied. */ @JsonProperty("failClosed") Boolean failClosed, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java index 09e39c584d..c6de1f0246 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java @@ -38,12 +38,18 @@ public record SessionModelChangeEventData( @JsonProperty("previousModel") String previousModel, /** Newly selected model identifier */ @JsonProperty("newModel") String newModel, + /** Provider selected for newModel. Omitted for legacy or unattributed selections; never inferred from a later selection. */ + @JsonProperty("providerId") String providerId, + /** Provider of previousModel, when known. A provider-only change is a model selection change even when the model identifiers are equal. */ + @JsonProperty("previousProviderId") String previousProviderId, /** 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, /** Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. */ @JsonProperty("reasoningEffortModel") String reasoningEffortModel, + /** True when the reasoning effort is a managed-policy default bound to reasoningEffortModel. Omitted for agent-authored, user-authored, independent, and legacy effort. */ + @JsonProperty("reasoningEffortManaged") Boolean reasoningEffortManaged, /** Reasoning summary mode before the model change, if applicable */ @JsonProperty("previousReasoningSummary") ReasoningSummary previousReasoningSummary, /** Reasoning summary mode after the model change, if applicable */ @@ -54,6 +60,8 @@ public record SessionModelChangeEventData( @JsonProperty("verbosity") Verbosity verbosity, /** Context tier after the model change; null explicitly clears a previously selected tier */ @JsonProperty("contextTier") ContextTier contextTier, + /** True when contextTier is a managed-policy default. Omitted for user-authored and legacy values. */ + @JsonProperty("contextTierManaged") Boolean contextTierManaged, /** Reason the change happened, when not user-initiated. `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path, or `"refusal_fallback"` when the active model declined a request (content refusal) and the runtime switched to the configured refusal-fallback model. UI clients can use this to render contextual copy. */ @JsonProperty("cause") String cause, /** Origin of the effective model change, when known. */ @@ -61,7 +69,31 @@ public record SessionModelChangeEventData( /** Previously committed Auto preference, when one was explicitly selected. */ @JsonProperty("previousAutoTier") AutoTier previousAutoTier, /** Committed Auto preference after the model configuration change, when applicable. */ - @JsonProperty("autoTier") AutoTier autoTier + @JsonProperty("autoTier") AutoTier autoTier, + /** True when autoTier is a managed-policy default. Omitted for user-authored and legacy values. */ + @JsonProperty("autoTierManaged") Boolean autoTierManaged ) { + + /** + * Creates event data with the components it had before later optional fields were added. + */ + public SessionModelChangeEventData( + String previousModel, + String newModel, + String previousReasoningEffort, + String reasoningEffort, + String reasoningEffortModel, + ReasoningSummary previousReasoningSummary, + ReasoningSummary reasoningSummary, + Verbosity previousVerbosity, + Verbosity verbosity, + ContextTier contextTier, + String cause, + ModelChangeSource source, + AutoTier previousAutoTier, + AutoTier autoTier + ) { + this(previousModel, newModel, null, null, previousReasoningEffort, reasoningEffort, reasoningEffortModel, null, previousReasoningSummary, reasoningSummary, previousVerbosity, verbosity, contextTier, null, cause, source, previousAutoTier, autoTier, null); + } } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionQuotaObservationEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionQuotaObservationEvent.java new file mode 100644 index 0000000000..5a95a7bdf2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionQuotaObservationEvent.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.quota_observation". A provider-owned quota observation, distinct from per-call usage and charge accounting. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionQuotaObservationEvent extends SessionEvent { + + @Override + public String getType() { return "session.quota_observation"; } + + @JsonProperty("data") + private SessionQuotaObservationEventData data; + + public SessionQuotaObservationEventData getData() { return data; } + public void setData(SessionQuotaObservationEventData data) { this.data = data; } + + /** Data payload for {@link SessionQuotaObservationEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionQuotaObservationEventData( + /** The admitted provider's state observation. Admission observations never contain quantities or reset/percentage semantics. */ + @JsonProperty("observation") ProviderQuotaState observation + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java index 4120d9e7c8..1020ae8195 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java @@ -43,18 +43,26 @@ public record SessionResumeEventData( @JsonProperty("eventsFileSizeBytes") Long eventsFileSizeBytes, /** Model currently selected at resume time */ @JsonProperty("selectedModel") String selectedModel, + /** Provider of selectedModel at resume time, when explicitly selected. */ + @JsonProperty("providerId") String providerId, /** Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") */ @JsonProperty("reasoningEffort") String reasoningEffort, /** Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. */ @JsonProperty("reasoningEffortModel") String reasoningEffortModel, + /** True when the reasoning effort is a managed-policy default bound to reasoningEffortModel. Omitted for agent-authored, user-authored, independent, and legacy effort. */ + @JsonProperty("reasoningEffortManaged") Boolean reasoningEffortManaged, /** Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") */ @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, /** Output verbosity level used for model calls, if applicable (e.g. "low", "medium", "high") */ @JsonProperty("verbosity") Verbosity verbosity, /** Context tier currently selected at resume time; null when no tier is active */ @JsonProperty("contextTier") ContextTier contextTier, + /** True when contextTier is a managed-policy default. Omitted for user-authored and legacy values. */ + @JsonProperty("contextTierManaged") Boolean contextTierManaged, /** Auto routing preference active at resume time */ @JsonProperty("autoTier") AutoTier autoTier, + /** True when autoTier is a managed-policy default. Omitted for user-authored and legacy values. */ + @JsonProperty("autoTierManaged") Boolean autoTierManaged, /** Session limits currently configured at resume time; null when no limits are active */ @JsonProperty("sessionLimits") SessionLimitsConfig sessionLimits, /** Updated working directory and git context at resume time */ @@ -68,5 +76,29 @@ public record SessionResumeEventData( /** 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 or omitted, pending work is normally marked as interrupted unless the resume passively joined live work owned by another client; sessionWasActive distinguishes that case. */ @JsonProperty("continuePendingWork") Boolean continuePendingWork ) { + + /** + * Creates event data with the components it had before later optional fields were added. + */ + public SessionResumeEventData( + OffsetDateTime resumeTime, + Long eventCount, + Long eventsFileSizeBytes, + String selectedModel, + String reasoningEffort, + String reasoningEffortModel, + ReasoningSummary reasoningSummary, + Verbosity verbosity, + ContextTier contextTier, + AutoTier autoTier, + SessionLimitsConfig sessionLimits, + WorkingDirectoryContext context, + Boolean alreadyInUse, + Boolean sessionWasActive, + Boolean remoteSteerable, + Boolean continuePendingWork + ) { + this(resumeTime, eventCount, eventsFileSizeBytes, selectedModel, null, reasoningEffort, reasoningEffortModel, null, reasoningSummary, verbosity, contextTier, null, autoTier, null, sessionLimits, context, alreadyInUse, sessionWasActive, remoteSteerable, continuePendingWork); + } } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionShutdownEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionShutdownEvent.java index ab97f6b192..59f09bc9c2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionShutdownEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionShutdownEvent.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.copilot.CopilotExperimental; +import java.util.List; import java.util.Map; import javax.annotation.processing.Generated; @@ -36,6 +37,12 @@ public final class SessionShutdownEvent extends SessionEvent { @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public record SessionShutdownEventData( + /** Complete accounting state captured atomically with receipt watermarks. */ + @JsonProperty("accountingSnapshot") UsageGetMetricsResult accountingSnapshot, + /** Provider/model accounting preserved for replay without consulting current provider selection. */ + @JsonProperty("providerModelMetrics") List providerModelMetrics, + /** Per-source accounting sequence watermarks subsuming preceding usage receipts. */ + @JsonProperty("usageAccountingWatermarks") Map usageAccountingWatermarks, /** Whether the session ended normally ("routine") or due to a crash/fatal error ("error") */ @JsonProperty("shutdownType") ShutdownType shutdownType, /** Error description when shutdownType is "error" */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java index db7336ce68..e3ea9d31e6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java @@ -47,18 +47,26 @@ public record SessionStartEventData( @JsonProperty("startTime") OffsetDateTime startTime, /** Model selected at session creation time, if any */ @JsonProperty("selectedModel") String selectedModel, + /** Provider of selectedModel at creation time, when explicitly selected. */ + @JsonProperty("providerId") String providerId, /** Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") */ @JsonProperty("reasoningEffort") String reasoningEffort, /** Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. */ @JsonProperty("reasoningEffortModel") String reasoningEffortModel, + /** True when the reasoning effort is a managed-policy default bound to reasoningEffortModel. Omitted for agent-authored, user-authored, independent, and legacy effort. */ + @JsonProperty("reasoningEffortManaged") Boolean reasoningEffortManaged, /** Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") */ @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, /** Output verbosity level used for model calls, if applicable (e.g. "low", "medium", "high") */ @JsonProperty("verbosity") Verbosity verbosity, /** Context tier selected at session creation time for models with tiered context pricing; null when no tier is selected (e.g., non-tiered model) */ @JsonProperty("contextTier") ContextTier contextTier, + /** True when contextTier is a managed-policy default. Omitted for user-authored and legacy values. */ + @JsonProperty("contextTierManaged") Boolean contextTierManaged, /** Auto routing preference selected at session creation time */ @JsonProperty("autoTier") AutoTier autoTier, + /** True when autoTier is a managed-policy default. Omitted for user-authored and legacy values. */ + @JsonProperty("autoTierManaged") Boolean autoTierManaged, /** Session limits configured at session creation time, if any */ @JsonProperty("sessionLimits") SessionLimitsConfig sessionLimits, /** Working directory and git context at session start */ @@ -72,5 +80,31 @@ public record SessionStartEventData( /** 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 ) { + + /** + * Creates event data with the components it had before later optional fields were added. + */ + public SessionStartEventData( + String sessionId, + Long version, + String producer, + String copilotVersion, + OffsetDateTime startTime, + String selectedModel, + String reasoningEffort, + String reasoningEffortModel, + ReasoningSummary reasoningSummary, + Verbosity verbosity, + ContextTier contextTier, + AutoTier autoTier, + SessionLimitsConfig sessionLimits, + WorkingDirectoryContext context, + GitHubMcpToolConfig gitHubMcpToolConfig, + Boolean alreadyInUse, + Boolean remoteSteerable, + String detachedFromSpawningParentSessionId + ) { + this(sessionId, version, producer, copilotVersion, startTime, selectedModel, null, reasoningEffort, reasoningEffortModel, null, reasoningSummary, verbosity, contextTier, null, autoTier, null, sessionLimits, context, gitHubMcpToolConfig, alreadyInUse, remoteSteerable, detachedFromSpawningParentSessionId); + } } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionUsageCheckpointEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionUsageCheckpointEvent.java index bdbc1add95..e0fd312afc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionUsageCheckpointEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionUsageCheckpointEvent.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; +import java.util.Map; import javax.annotation.processing.Generated; /** @@ -37,6 +38,12 @@ public final class SessionUsageCheckpointEvent extends SessionEvent { public record SessionUsageCheckpointEventData( /** Session-wide accumulated nano-AI units cost at checkpoint time */ @JsonProperty("totalNanoAiu") Double totalNanoAiu, + /** Complete accounting state captured atomically with the receipt watermarks. */ + @JsonProperty("accountingSnapshot") UsageGetMetricsResult accountingSnapshot, + /** Provider/model accounting snapshot that subsumes preceding usage receipts. */ + @JsonProperty("providerModelMetrics") List providerModelMetrics, + /** Highest accounted sequence per source session; no per-call deduplication history is retained. */ + @JsonProperty("usageAccountingWatermarks") Map usageAccountingWatermarks, /** Total number of premium API requests used at checkpoint time */ @JsonProperty("totalPremiumRequests") Double totalPremiumRequests, /** Internal per-model prompt-cache state used to restore expiration tracking on resume */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/UsageAccountingIdentity.java b/java/sdk/src/generated/java/com/github/copilot/generated/UsageAccountingIdentity.java new file mode 100644 index 0000000000..317aefc28c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/UsageAccountingIdentity.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Ordered accounting identity assigned under the source session's emission lock. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageAccountingIdentity( + /** Session that assigned this accounting sequence. */ + @JsonProperty("sourceSessionId") String sourceSessionId, + /** Monotonically increasing sequence within the source session. */ + @JsonProperty("sequence") Long sequence, + /** Existing API call identifier, or a runtime-generated identity when none was supplied. */ + @JsonProperty("usageId") String usageId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/UsageGetMetricsResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/UsageGetMetricsResult.java new file mode 100644 index 0000000000..9142a777e9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/UsageGetMetricsResult.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Accumulated session usage metrics, including premium request cost, token counts, model breakdown, and code-change totals. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageGetMetricsResult( + /** Completeness of totalNanoAiu. Numeric totals remain the reported subtotal; zero is not a claim of free usage when unavailable. */ + @JsonProperty("aiCreditsStatus") AiCreditsStatus aiCreditsStatus, + /** Total user-initiated premium request cost across all models (may be fractional due to multipliers) */ + @JsonProperty("totalPremiumRequestCost") Double totalPremiumRequestCost, + /** Raw count of user-initiated API requests */ + @JsonProperty("totalUserRequests") Long totalUserRequests, + /** Session-wide accumulated nano-AI units cost */ + @JsonProperty("totalNanoAiu") Double totalNanoAiu, + /** Session-wide per-token-type accumulated token counts */ + @JsonProperty("tokenDetails") Map tokenDetails, + /** Total time spent in model API calls (milliseconds) */ + @JsonProperty("totalApiDurationMs") Long totalApiDurationMs, + /** ISO 8601 timestamp when the session started */ + @JsonProperty("sessionStartTime") OffsetDateTime sessionStartTime, + /** Aggregated code change metrics */ + @JsonProperty("codeChanges") UsageMetricsCodeChanges codeChanges, + /** Per-model token and request metrics, keyed by model identifier */ + @JsonProperty("modelMetrics") Map modelMetrics, + /** Provider-attributed model metrics, ordered by first observed provider and model. +Null attribution preserves historical usage whose provider or model was not recorded. */ + @JsonProperty("providerModelMetrics") List providerModelMetrics, + /** Per-agent usage metrics, keyed by agent instance identifier. The main conversation uses the stable key `main`. */ + @JsonProperty("agentMetrics") Map agentMetrics, + /** Currently active model identifier */ + @JsonProperty("currentModel") String currentModel, + /** Input tokens from the most recent main-agent API call */ + @JsonProperty("lastCallInputTokens") Long lastCallInputTokens, + /** Output tokens from the most recent main-agent API call */ + @JsonProperty("lastCallOutputTokens") Long lastCallOutputTokens +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsAgentMetric.java b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsAgentMetric.java new file mode 100644 index 0000000000..a0e9f07d60 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsAgentMetric.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.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; + +/** + * Usage attributed to one agent instance, including its identity, API duration, AI units, and per-model breakdown. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsAgentMetric( + /** Configured agent name, when this is a subagent */ + @JsonProperty("agentName") String agentName, + /** Human-readable label for this subagent invocation, copied from the originating `subagent.started` event. For task-tool subagents this is the invocation's task description rather than the agent's configured display name, so group by `agentName` for stable per-agent labels. */ + @JsonProperty("agentDisplayName") String agentDisplayName, + /** Time spent in model API calls by this agent, in milliseconds */ + @JsonProperty("totalApiDurationMs") Long totalApiDurationMs, + /** Accumulated nano-AI units cost for this agent */ + @JsonProperty("totalNanoAiu") Double totalNanoAiu, + /** Per-model usage for this agent, keyed by model identifier */ + @JsonProperty("modelMetrics") Map modelMetrics +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsCodeChanges.java b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsCodeChanges.java new file mode 100644 index 0000000000..92f117b26b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsCodeChanges.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +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; + +/** + * Aggregated code change metrics + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsCodeChanges( + /** Total lines of code added */ + @JsonProperty("linesAdded") Long linesAdded, + /** Total lines of code removed */ + @JsonProperty("linesRemoved") Long linesRemoved, + /** Number of distinct files modified */ + @JsonProperty("filesModifiedCount") Long filesModifiedCount, + /** Distinct file paths modified during the session */ + @JsonProperty("filesModified") List filesModified +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsModelMetric.java b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsModelMetric.java new file mode 100644 index 0000000000..e55b06e3e9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsModelMetric.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.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Per-model usage metrics, including request counts/costs, token usage, nano-AI units, and per-token-type details. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsModelMetric( + /** Completeness of this model's reported AI-credit subtotal. Explicitly reported zero is complete, not unavailable. */ + @JsonProperty("aiCreditsStatus") AiCreditsStatus aiCreditsStatus, + /** Request count and cost metrics for this model */ + @JsonProperty("requests") UsageMetricsModelMetricRequests requests, + /** Token usage metrics for this model */ + @JsonProperty("usage") UsageMetricsModelMetricUsage usage, + /** Latest known prompt-cache expiration for this model. A timestamp in the past indicates that the observed cache has expired. */ + @JsonProperty("cacheExpiresAt") OffsetDateTime cacheExpiresAt, + /** Accumulated nano-AI units cost for this model */ + @JsonProperty("totalNanoAiu") Double totalNanoAiu, + /** Token count details per type */ + @JsonProperty("tokenDetails") Map tokenDetails +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsModelMetricRequests.java b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsModelMetricRequests.java new file mode 100644 index 0000000000..56951bf8ee --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsModelMetricRequests.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request count and cost metrics for this model + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsModelMetricRequests( + /** Number of API requests made with this model */ + @JsonProperty("count") Long count, + /** User-initiated premium request cost (with multiplier applied) */ + @JsonProperty("cost") Double cost +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsModelMetricTokenDetail.java b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsModelMetricTokenDetail.java new file mode 100644 index 0000000000..64bdc0c797 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsModelMetricTokenDetail.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Per-model token-detail entry containing the accumulated token count for one token type. + * + * @since 1.0.0 + */ +@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/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsModelMetricUsage.java b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsModelMetricUsage.java new file mode 100644 index 0000000000..e984a1b823 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsModelMetricUsage.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Token usage metrics for this model + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsModelMetricUsage( + /** Total input tokens consumed */ + @JsonProperty("inputTokens") Long inputTokens, + /** Total output tokens produced */ + @JsonProperty("outputTokens") Long outputTokens, + /** Total tokens read from prompt cache */ + @JsonProperty("cacheReadTokens") Long cacheReadTokens, + /** Total tokens written to prompt cache */ + @JsonProperty("cacheWriteTokens") Long cacheWriteTokens, + /** Total output tokens used for reasoning */ + @JsonProperty("reasoningTokens") Long reasoningTokens +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsProviderModelMetric.java b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsProviderModelMetric.java new file mode 100644 index 0000000000..27e17e6ba6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsProviderModelMetric.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Usage for one recorded provider and model, without merging identical model IDs across providers. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsProviderModelMetric( + /** Provider identity and product label captured when the call was dispatched; null when unknown. */ + @JsonProperty("provider") ModelProviderRef provider, + /** Model identity, or null for legacy aggregate-only usage. */ + @JsonProperty("modelId") String modelId, + /** Model display name captured at call time, when known. */ + @JsonProperty("modelDisplayName") String modelDisplayName, + /** Request, token, and cost totals for this provider/model. */ + @JsonProperty("metrics") UsageMetricsModelMetric metrics +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsTokenDetail.java b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsTokenDetail.java new file mode 100644 index 0000000000..2e30d0bf90 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/UsageMetricsTokenDetail.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session-wide token-detail entry containing the accumulated token count for one token type. + * + * @since 1.0.0 + */ +@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/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountAuthInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountAuthInfo.java new file mode 100644 index 0000000000..b6a93fb206 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountAuthInfo.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.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; + +/** + * An interactive account whose model provider owns its credentials. It carries no GitHub credential. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AccountAuthInfo extends AuthInfo { + + @JsonProperty("type") + private final String type = "account"; + + @Override + public String getType() { return type; } + + /** Host coordinate owned by the account's model provider. */ + @JsonProperty("host") + private String host; + + /** Login identifying the provider-owned account. */ + @JsonProperty("login") + private String login; + + public String getHost() { return host; } + public void setHost(String host) { this.host = host; } + + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsCustomAgentInitialModelDecisionParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsCustomAgentInitialModelDecisionParams.java new file mode 100644 index 0000000000..1555b36663 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsCustomAgentInitialModelDecisionParams.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * The models a custom agent asks for, and the models actually available. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record AgentsCustomAgentInitialModelDecisionParams( + /** The agent's declared `model:` entry, serialized. A single name or an ordered list of acceptable names. */ + @JsonProperty("agentModelsJson") String agentModelsJson, + /** The models available to this session, serialized in the shape the model list carries. */ + @JsonProperty("availableModelsJson") String availableModelsJson +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsCustomAgentInitialModelDecisionResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsCustomAgentInitialModelDecisionResult.java new file mode 100644 index 0000000000..afd26e66af --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsCustomAgentInitialModelDecisionResult.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.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; + +/** + * The model to switch to, and the warning to show when the agent's preference could not be met. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record AgentsCustomAgentInitialModelDecisionResult( + /** The first available model that matches the agent's preferences. Absent when none of the requested models is available. */ + @JsonProperty("targetModel") String targetModel, + /** The reasoning effort attached to the selected model preference. Absent when that preference does not specify an effort. */ + @JsonProperty("reasoningEffort") String reasoningEffort, + /** What to tell the user about an unmet preference. Absent when the preference was met. A warning with no `targetModel` means the agent's models are all unavailable. */ + @JsonProperty("warning") String warning +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetAvailableBuiltinsParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetAvailableBuiltinsParams.java new file mode 100644 index 0000000000..fee8d13df9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetAvailableBuiltinsParams.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.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * The feature flags to evaluate shipped agents against. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record AgentsGetAvailableBuiltinsParams( + /** Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or pass null for no flags. */ + @JsonProperty("featureFlags") Map featureFlags, + /** Flag overrides keyed by name. A null entry uses the corresponding base flag; false explicitly disables it. Omit or pass null for no overrides. */ + @JsonProperty("overrides") Map overrides, + /** The surface asking, which gates agents that only apply to one client. Omit or pass null to apply no client filter. */ + @JsonProperty("context") String context +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetAvailableBuiltinsResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetAvailableBuiltinsResult.java new file mode 100644 index 0000000000..14667e1269 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetAvailableBuiltinsResult.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.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; + +/** + * The shipped agents available under the requested flags. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record AgentsGetAvailableBuiltinsResult( + /** Available shipped agents, in the runtime's own order. */ + @JsonProperty("agents") List agents +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinDefinitionParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinDefinitionParams.java new file mode 100644 index 0000000000..cc69529078 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinDefinitionParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * The shipped agent whose definition to load. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record AgentsGetBuiltinDefinitionParams( + /** The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside that list is special-cased in code and has no definition, and is reported as an error rather than as an empty definition. */ + @JsonProperty("name") String name +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinDefinitionResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinDefinitionResult.java new file mode 100644 index 0000000000..2fd4d55fa3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinDefinitionResult.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.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; + +/** + * One shipped agent's definition. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record AgentsGetBuiltinDefinitionResult( + /** The agent's definition, serialized as JSON. It carries the authored keys plus the runtime's projected `__nativeCustomAgent` view of the same agent. It is a string rather than an object because the runtime parses it with the agent schema's tolerant shape, which accepts keys this contract does not name. */ + @JsonProperty("definitionJson") String definitionJson +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinListingDefinitionParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinListingDefinitionParams.java new file mode 100644 index 0000000000..9258f87d1b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinListingDefinitionParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * The shipped agent whose listing entry to load. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record AgentsGetBuiltinListingDefinitionParams( + /** The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here too, from its in-code definition. */ + @JsonProperty("name") String name +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinListingDefinitionResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinListingDefinitionResult.java new file mode 100644 index 0000000000..f49d37d53b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinListingDefinitionResult.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.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; + +/** + * One shipped agent, projected for a listing. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record AgentsGetBuiltinListingDefinitionResult( + /** The agent projected as a custom agent, serialized as JSON. It is a string rather than an object for the same reason as `getBuiltinDefinition`: the runtime parses the underlying definition with the agent schema's tolerant shape, which accepts keys this contract does not name. */ + @JsonProperty("definitionJson") String definitionJson +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinsResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinsResult.java new file mode 100644 index 0000000000..d0148ac6d7 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentsGetBuiltinsResult.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.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; + +/** + * The agents this runtime ships, named so a consumer can tell them apart from authored ones. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record AgentsGetBuiltinsResult( + /** Every agent name this runtime ships. */ + @JsonProperty("names") List names, + /** The subset of `names` a user is allowed to turn off. A shipped agent outside this list is always active and a client should not offer a toggle for it. */ + @JsonProperty("disableableNames") List disableableNames, + /** The subset of `names` defined by a shipped YAML definition. The remainder are special-cased in code and have no definition to load. */ + @JsonProperty("yamlBasedNames") List yamlBasedNames +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AiCreditsStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AiCreditsStatus.java new file mode 100644 index 0000000000..cc13ac67a4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AiCreditsStatus.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.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Whether the accumulated numeric AI-credit subtotal covers the observed calls. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AiCreditsStatus { + /** The {@code complete} variant. */ + COMPLETE("complete"), + /** The {@code partial} variant. */ + PARTIAL("partial"), + /** The {@code unavailable} variant. */ + UNAVAILABLE("unavailable"); + + private final String value; + AiCreditsStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AiCreditsStatus fromValue(String value) { + for (AiCreditsStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AiCreditsStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthIdentity.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthIdentity.java index dfeb3c023a..7768741926 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthIdentity.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthIdentity.java @@ -20,7 +20,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -record AuthIdentity( +public record AuthIdentity( /** Authentication type */ @JsonProperty("type") AuthInfoType type, /** Authentication host */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfo.java index 5d82a47f85..7b03a6a29a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfo.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfo.java @@ -25,6 +25,7 @@ @JsonSubTypes.Type(value = TokenProviderAuthInfo.class, name = "token-provider"), @JsonSubTypes.Type(value = CopilotApiTokenAuthInfo.class, name = "copilot-api-token"), @JsonSubTypes.Type(value = UserAuthInfo.class, name = "user"), + @JsonSubTypes.Type(value = AccountAuthInfo.class, name = "account"), @JsonSubTypes.Type(value = GhCliAuthInfo.class, name = "gh-cli"), @JsonSubTypes.Type(value = ApiKeyAuthInfo.class, name = "api-key") }) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfoType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfoType.java index 5f81c8cf84..1be60446c1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfoType.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfoType.java @@ -22,6 +22,8 @@ public enum AuthInfoType { ENV("env"), /** The {@code user} variant. */ USER("user"), + /** The {@code account} variant. */ + ACCOUNT("account"), /** The {@code gh-cli} variant. */ GH_CLI("gh-cli"), /** The {@code api-key} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginAccount.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginAccount.java new file mode 100644 index 0000000000..87405999ed --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginAccount.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.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A credential-free account choice after sign-in. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AuthLoginAccount( + /** Opaque identifier supplied to the next login step to select this account. */ + @JsonProperty("selectionId") String selectionId, + /** Host coordinate owned by the selected account's provider. */ + @JsonProperty("host") String host, + /** Human-readable login for the account choice. */ + @JsonProperty("login") String login, + /** Provider kind that owns this account choice. */ + @JsonProperty("kind") AccountKind kind +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultDto.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultDto.java index cbcffb6e54..ecbb106f12 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultDto.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultDto.java @@ -11,10 +11,11 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.copilot.CopilotExperimental; +import java.util.List; import javax.annotation.processing.Generated; /** - * Terminal result of an interactive login flow. + * Result of an interactive login flow. Pending consent or account selection is not terminal. * * @apiNote This type is experimental and may change in a future version. * @@ -25,11 +26,13 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record AuthLoginResultDto( - /** Terminal disposition of the login. */ + /** Current disposition of the login, including pending user decisions. */ @JsonProperty("status") AuthLoginResultStatus status, /** Host that was signed in, when completed. */ @JsonProperty("host") String host, /** Login that was signed in, when completed. */ - @JsonProperty("login") String login + @JsonProperty("login") String login, + /** Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 first. */ + @JsonProperty("accounts") List accounts ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultStatus.java index 9e36ad0027..07becf2fb9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultStatus.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultStatus.java @@ -11,7 +11,7 @@ import javax.annotation.processing.Generated; /** - * Terminal disposition of a login persistence attempt. + * Disposition of a login attempt, including pending user decisions. * * @apiNote This type is experimental and may change in a future version. * @@ -24,6 +24,8 @@ public enum AuthLoginResultStatus { COMPLETED("completed"), /** The {@code needs-plaintext-consent} variant. */ NEEDS_PLAINTEXT_CONSENT("needs-plaintext-consent"), + /** The {@code needs-account-selection} variant. */ + NEEDS_ACCOUNT_SELECTION("needs-account-selection"), /** The {@code declined} variant. */ DECLINED("declined"); diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepCompleted.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepCompleted.java index b1fb524643..a8ea53e123 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepCompleted.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepCompleted.java @@ -32,7 +32,7 @@ public final class AuthLoginStepCompleted extends AuthLoginStep { @Override public String getKind() { return kind; } - /** The terminal login result. */ + /** Login result. When status is needs-plaintext-consent or needs-account-selection, advance with the user's decision to continue. */ @JsonProperty("result") private AuthLoginResultDto result; diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValueActiveAccount.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValueActiveAccount.java index 8ee2fb9416..e10579d0e6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValueActiveAccount.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthReadValueActiveAccount.java @@ -36,6 +36,13 @@ public final class AuthReadValueActiveAccount extends AuthReadValue { @JsonProperty("account") private AccountStatus account; + /** Credential-free identity metadata for the active account, including resolved Copilot user information when available. */ + @JsonProperty("authInfo") + private AuthIdentity authInfo; + public AccountStatus getAccount() { return account; } public void setAccount(AccountStatus account) { this.account = account; } + + public AuthIdentity getAuthInfo() { return authInfo; } + public void setAuthInfo(AuthIdentity authInfo) { this.authInfo = authInfo; } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTier.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTier.java index 00b18bcc94..cc568096a9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTier.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTier.java @@ -10,30 +10,38 @@ import javax.annotation.processing.Generated; /** - * Routing preference used when the session model is `auto`. `fast` is an integrator-only latency preset and is not a first-party GitHub Copilot product preference. + * Extensible routing preference for the virtual `auto` model. New identifiers must be advertised and enabled by the provider. `fast` is an integrator-only latency preset. * * @since 1.0.0 */ @javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum AutoTier { +public final class AutoTier { /** The {@code efficiency} variant. */ - EFFICIENCY("efficiency"), + public static final AutoTier EFFICIENCY = new AutoTier("efficiency"); /** The {@code balance} variant. */ - BALANCE("balance"), + public static final AutoTier BALANCE = new AutoTier("balance"); /** The {@code intelligence} variant. */ - INTELLIGENCE("intelligence"), + public static final AutoTier INTELLIGENCE = new AutoTier("intelligence"); /** The {@code fast} variant. */ - FAST("fast"); + public static final AutoTier FAST = new AutoTier("fast"); private final String value; - AutoTier(String value) { this.value = value; } + private AutoTier(String value) { this.value = value; } @com.fasterxml.jackson.annotation.JsonValue public String getValue() { return value; } @com.fasterxml.jackson.annotation.JsonCreator public static AutoTier fromValue(String value) { + if (value == null) return null; + if (value.isEmpty() || value.codePoints().anyMatch(c -> Character.isWhitespace(c) || Character.isISOControl(c))) + throw new IllegalArgumentException("AutoTier requires a routing identifier"); for (AutoTier v : values()) { if (v.value.equals(value)) return v; } - throw new IllegalArgumentException("Unknown AutoTier value: " + value); + return new AutoTier(value); } + /** Returns the known routing preferences. @return known values */ + public static AutoTier[] values() { return new AutoTier[] { EFFICIENCY, BALANCE, INTELLIGENCE, FAST }; } + @Override public boolean equals(Object other) { return other instanceof AutoTier v && value.equals(v.value); } + @Override public int hashCode() { return value.hashCode(); } + @Override public String toString() { return value; } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTierDescriptor.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTierDescriptor.java new file mode 100644 index 0000000000..49616a82a6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTierDescriptor.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.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; + +/** + * A server-advertised routing preference. Identifiers and execution types are extensible. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AutoTierDescriptor( + /** Opaque routing identifier transmitted unchanged to the provider. */ + @JsonProperty("id") String id, + /** Human-readable label, not a routing identifier. */ + @JsonProperty("displayName") String displayName, + /** Description displayed beside the preference. */ + @JsonProperty("description") String description, + /** Execution kind; this client supports `auto` preferences on the Auto model. */ + @JsonProperty("type") String type, + /** Current account-specific availability. */ + @JsonProperty("status") AutoTierStatus status +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTierMetadata.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTierMetadata.java new file mode 100644 index 0000000000..347bf55666 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTierMetadata.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.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; + +/** + * Account-bound discovery metadata for the virtual `auto` model. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AutoTierMetadata( + /** Provider-default preference, used only when no explicit preference exists. */ + @JsonProperty("defaultTier") String defaultTier, + /** Routing preferences in the server's presentation order. */ + @JsonProperty("tiers") List tiers, + /** Provider that supplied this metadata, when the catalog is provider-attributed. */ + @JsonProperty("providerId") String providerId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTierStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTierStatus.java new file mode 100644 index 0000000000..8bf78312b6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTierStatus.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.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; + +/** + * Availability of a server-advertised routing preference. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AutoTierStatus( + /** Whether the provider permits selecting this preference. */ + @JsonProperty("enabled") Boolean enabled, + /** Extensible machine-readable unavailability reason. */ + @JsonProperty("reason") String reason, + /** Human-readable explanation of availability. */ + @JsonProperty("message") String message +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinAgentSummary.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinAgentSummary.java new file mode 100644 index 0000000000..65e75ff6f7 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinAgentSummary.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.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; + +/** + * A shipped agent, named and described. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record BuiltinAgentSummary( + /** The agent name, as it appears in `getBuiltins`. */ + @JsonProperty("name") String name, + /** One-line description of what the agent does. */ + @JsonProperty("description") String description +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAccount.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAccount.java new file mode 100644 index 0000000000..f41fd3dd79 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAccount.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.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Eligible account. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectorDiscoveryAccount( + /** Opaque account ID. */ + @JsonProperty("accountId") String accountId, + /** Account metadata. */ + @JsonProperty("authInfo") ConnectorDiscoveryAuthInfo authInfo +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAuthInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAuthInfo.java new file mode 100644 index 0000000000..3857e88ff1 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAuthInfo.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.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Account metadata. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectorDiscoveryAuthInfo( + /** Authentication type. */ + @JsonProperty("type") AuthInfoType type, + /** Host. */ + @JsonProperty("host") String host, + /** Login. */ + @JsonProperty("login") String login +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAvailability.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAvailability.java new file mode 100644 index 0000000000..00162fc078 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryAvailability.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Availability. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ConnectorDiscoveryAvailability { + /** The {@code enabled} variant. */ + ENABLED("enabled"), + /** The {@code disabled} variant. */ + DISABLED("disabled"), + /** The {@code unavailable} variant. */ + UNAVAILABLE("unavailable"); + + private final String value; + ConnectorDiscoveryAvailability(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ConnectorDiscoveryAvailability fromValue(String value) { + for (ConnectorDiscoveryAvailability v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ConnectorDiscoveryAvailability value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryCatalogEntry.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryCatalogEntry.java new file mode 100644 index 0000000000..76c137c5e2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorDiscoveryCatalogEntry.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Entry. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectorDiscoveryCatalogEntry( + /** Name. */ + @JsonProperty("name") String name, + /** Display name. */ + @JsonProperty("displayName") String displayName, + /** Description. */ + @JsonProperty("description") String description, + /** Logo. */ + @JsonProperty("logo") String logo, + /** Tier. */ + @JsonProperty("tier") String tier, + /** Release tag. */ + @JsonProperty("releaseTag") String releaseTag, + /** Status. */ + @JsonProperty("status") ConnectorCatalogStatus status +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UserSettingsSetResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsGetAccountsResult.java similarity index 72% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/UserSettingsSetResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsGetAccountsResult.java index c5ab98621a..1334b673fa 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UserSettingsSetResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsGetAccountsResult.java @@ -15,7 +15,7 @@ import javax.annotation.processing.Generated; /** - * Outcome of writing user settings. + * Eligible accounts. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -24,8 +24,10 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record UserSettingsSetResult( - /** Top-level keys whose write landed in settings.json but is shadowed by a value still present in the legacy config.json (config.json wins on read). The write does not take effect until the legacy value is removed. */ - @JsonProperty("shadowedKeys") List shadowedKeys +public record ConnectorsGetAccountsResult( + /** Availability. */ + @JsonProperty("availability") ConnectorDiscoveryAvailability availability, + /** Eligible accounts. */ + @JsonProperty("accounts") List accounts ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsGetCapabilitiesResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsGetCapabilitiesResult.java new file mode 100644 index 0000000000..dc1b256e27 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsGetCapabilitiesResult.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Feature availability. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectorsGetCapabilitiesResult( + /** API version. */ + @JsonProperty("apiVersion") Long apiVersion, + /** Availability. */ + @JsonProperty("availability") ConnectorDiscoveryAvailability availability, + /** Whether accounts are selected by opaque ID. */ + @JsonProperty("opaqueAccountSelection") Boolean opaqueAccountSelection, + /** Whether results are cached. */ + @JsonProperty("conditionalCache") Boolean conditionalCache +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsListParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsListParams.java new file mode 100644 index 0000000000..39a2c4a966 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsListParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Selected account. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectorsListParams( + /** Opaque account ID. */ + @JsonProperty("accountId") String accountId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsListResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsListResult.java new file mode 100644 index 0000000000..ccba6073d4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsListResult.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.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Entries for the selected account. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectorsListResult( + /** Opaque account ID. */ + @JsonProperty("accountId") String accountId, + /** Revision. */ + @JsonProperty("revision") Long revision, + /** Refresh time in Unix epoch milliseconds. */ + @JsonProperty("refreshedAtMs") Long refreshedAtMs, + /** Entries. */ + @JsonProperty("connectors") List connectors +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsRefreshParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsRefreshParams.java new file mode 100644 index 0000000000..e5384ba3f2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsRefreshParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Selected account. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectorsRefreshParams( + /** Opaque account ID. */ + @JsonProperty("accountId") String accountId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsRefreshResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsRefreshResult.java new file mode 100644 index 0000000000..a14c50fd28 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorsRefreshResult.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.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Entries for the selected account. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectorsRefreshResult( + /** Opaque account ID. */ + @JsonProperty("accountId") String accountId, + /** Revision. */ + @JsonProperty("revision") Long revision, + /** Refresh time in Unix epoch milliseconds. */ + @JsonProperty("refreshedAtMs") Long refreshedAtMs, + /** Entries. */ + @JsonProperty("connectors") List connectors +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java index 3088672b69..6fa6843944 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java @@ -23,8 +23,12 @@ public record CurrentModel( /** Currently active model identifier */ @JsonProperty("modelId") String modelId, + /** Provider selected for this model. Together with modelId, identifies a catalog entry even when providers serve the same model. Omitted for unattributed legacy selections. */ + @JsonProperty("providerId") String providerId, /** Captured base model to restore when leaving plan mode. Omitted outside plan mode or when no plan override has captured a base model. Persistent agent model requirements apply to this model rather than the temporary plan model. */ @JsonProperty("planBaseModelId") String planBaseModelId, + /** Provider of planBaseModelId, when that saved selection is provider-qualified. */ + @JsonProperty("planBaseProviderId") String planBaseProviderId, /** Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. */ @JsonProperty("reasoningEffort") String reasoningEffort, /** Context tier for models that support multiple context-window sizes. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitCurrentBranchRemoteParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitCurrentBranchRemoteParams.java new file mode 100644 index 0000000000..67f7b64880 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitCurrentBranchRemoteParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Working-tree path a git query applies to. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitCurrentBranchRemoteParams( + /** Absolute path to a directory inside the git working tree to query. */ + @JsonProperty("cwd") String cwd +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitCurrentBranchRemoteResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitCurrentBranchRemoteResult.java new file mode 100644 index 0000000000..faa7e52b3d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitCurrentBranchRemoteResult.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.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; + +/** + * The remote the checked-out branch tracks. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitCurrentBranchRemoteResult( + /** Name of the tracked remote. Reports `origin` whenever the working tree has no tracking configuration to read, including on a detached HEAD, so this is never null and never empty. */ + @JsonProperty("remote") String remote +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnerOption.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnerOption.java new file mode 100644 index 0000000000..43e3d2d01d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnerOption.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.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; + +/** + * A GitHub login the authenticated user may act as: their own account, or an organization they belong to. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitHubOwnerOption( + /** The owner's GitHub login. */ + @JsonProperty("login") String login, + /** Which kind of owner this is. The authenticated user's own account is always reported as `user`. */ + @JsonProperty("type") String type +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersCancelParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersCancelParams.java new file mode 100644 index 0000000000..1ea9251095 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersCancelParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * The owner listing to abandon. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitHubOwnersCancelParams( + /** Request id the listing was started with. */ + @JsonProperty("requestId") Long requestId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersCancelResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersCancelResult.java new file mode 100644 index 0000000000..d70a718261 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersCancelResult.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.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Whether the id named a running owner listing. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitHubOwnersCancelResult( + /** True when a listing with the id was running and the cancel stopped it. False when the id was never registered, was registered but unused, was released after being abandoned, or its listing had ended. An unused id is released and cannot start a later listing. */ + @JsonProperty("canceled") Boolean canceled +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersListParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersListParams.java new file mode 100644 index 0000000000..224b85f6dc --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersListParams.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Credential to list owners under, and the request id that makes the listing cancellable. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitHubOwnersListParams( + /** Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled before use, released after being abandoned, or already used is refused rather than silently running uncancellable. */ + @JsonProperty("requestId") Long requestId, + /** The credential the listing runs under, carried opaquely because its shape is the host's own and the runtime only resolves a token and a GitHub host from it. No credential travels: this selects one the runtime already holds. */ + @JsonProperty("authInfo") Object authInfo +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersListResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersListResult.java new file mode 100644 index 0000000000..02f844bced --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersListResult.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitHubOwnersListResult( + /** The owners, on success: the authenticated user first, then the organizations they belong to. */ + @JsonProperty("owners") List owners, + /** Why no owners could be listed, phrased for a user. Present when the listing failed in a way the caller should render rather than raise. */ + @JsonProperty("message") String message, + /** A line the caller should log. Present only alongside `message`, and only for failures worth recording. */ + @JsonProperty("warning") String warning, + /** A malformed request or an unreadable credential, which the caller raises instead of rendering. Kept a field rather than a dispatch error so it stays distinct from `message`, which the caller renders. */ + @JsonProperty("throwError") String throwError +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersRequestIdResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersRequestIdResult.java new file mode 100644 index 0000000000..8b5d180b05 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubOwnersRequestIdResult.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.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitHubOwnersRequestIdResult( + /** Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`. */ + @JsonProperty("requestId") Long requestId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryAtPathParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryAtPathParams.java new file mode 100644 index 0000000000..6a896bff33 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryAtPathParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Working-tree path whose owning GitHub repository should be resolved. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitHubRepositoryAtPathParams( + /** Absolute path to a directory inside the git working tree to resolve. */ + @JsonProperty("path") String path +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryAtPathResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryAtPathResult.java new file mode 100644 index 0000000000..f32a3eafc0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryAtPathResult.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.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; + +/** + * The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitHubRepositoryAtPathResult( + /** Resolved repository identity, or null when the selected remote resolves to no GitHub host. */ + @JsonProperty("repository") GitHubRepositoryIdentity repository +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryIdentity.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryIdentity.java new file mode 100644 index 0000000000..a1d8c3acc3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubRepositoryIdentity.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.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; + +/** + * Owner, name, and host of a GitHub repository, as resolved from a git remote URL. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitHubRepositoryIdentity( + /** Repository owner login (user or organization). */ + @JsonProperty("owner") String owner, + /** Repository name, without the owner prefix or the `.git` suffix. */ + @JsonProperty("name") String name, + /** Host the remote points at, for example `github.com` or a GitHub Enterprise hostname. */ + @JsonProperty("host") String host +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitRemoteRepository.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitRemoteRepository.java new file mode 100644 index 0000000000..936e0f8159 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitRemoteRepository.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.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; + +/** + * A GitHub repository one of a working tree's remotes points at. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitRemoteRepository( + /** Account or organization owning the repository. */ + @JsonProperty("owner") String owner, + /** Repository name, without the owner. */ + @JsonProperty("name") String name, + /** GitHub host serving the repository, which is not `github.com` for a GitHub Enterprise remote. */ + @JsonProperty("host") String host, + /** Name of the first remote that produced this distinct repository entry, such as `origin` or `upstream`. */ + @JsonProperty("remoteName") String remoteName +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitReposFromRemotesParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitReposFromRemotesParams.java new file mode 100644 index 0000000000..f9c351550f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitReposFromRemotesParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Git working tree whose GitHub remotes should be listed. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitReposFromRemotesParams( + /** Absolute path to the root of the git working tree. */ + @JsonProperty("gitRoot") String gitRoot +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitReposFromRemotesResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitReposFromRemotesResult.java new file mode 100644 index 0000000000..cc62acf984 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitReposFromRemotesResult.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.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; + +/** + * The GitHub repositories a working tree's remotes point at. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitReposFromRemotesResult( + /** One entry per distinct GitHub repository, in the order git reports the first remote for each repository. Empty when no remote points at a GitHub host, which a caller should read as `not connected to GitHub`. Failing to read the remotes is an error, not an empty list. */ + @JsonProperty("repositories") List repositories +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitWorkingDirectoryContextParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitWorkingDirectoryContextParams.java new file mode 100644 index 0000000000..17acdb64c3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitWorkingDirectoryContextParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Working-tree path a git query applies to. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GitWorkingDirectoryContextParams( + /** Absolute path to a directory inside the git working tree to query. */ + @JsonProperty("cwd") String cwd +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateLoadForConfigDirParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateLoadForConfigDirParams.java new file mode 100644 index 0000000000..a056698c92 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateLoadForConfigDirParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Selects the configuration directory whose machine-wide state to read. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GlobalStateLoadForConfigDirParams( + /** Copilot configuration directory to read the state document from, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to read the directory the server resolved for itself. */ + @JsonProperty("configDir") String configDir +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateLoadResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateLoadResult.java new file mode 100644 index 0000000000..0b6dbb9d53 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateLoadResult.java @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.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; + +/** + * The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GlobalStateLoadResult( + /** Plugins installed on this machine. */ + @JsonProperty("installedPlugins") List installedPlugins, + /** Models the user selected recently, most recent first. */ + @JsonProperty("recentModelIds") List recentModelIds, + /** When the host first ran on this machine. */ + @JsonProperty("firstLaunchAt") String firstLaunchAt, + /** Terminals the user has already been asked to set up, so the host does not ask twice. */ + @JsonProperty("askedSetupTerminals") List askedSetupTerminals, + /** Folders where the user declined the init prompt, so it stays hidden there. */ + @JsonProperty("suppressInitFolders") List suppressInitFolders, + /** Whether the sandbox onboarding has been shown. */ + @JsonProperty("sandboxOnboardingShown") Boolean sandboxOnboardingShown, + /** Whether the user declined to trust the sandbox credential proxy CA. */ + @JsonProperty("sandboxCredentialProxyCaDeclined") Boolean sandboxCredentialProxyCaDeclined, + /** Whether the app tip has been shown. */ + @JsonProperty("appTipShown") Boolean appTipShown, + /** Whether the one-off cleanup of stored reasoning summaries has run. */ + @JsonProperty("reasoningSummariesCleanupDone") Boolean reasoningSummariesCleanupDone, + /** Account used for the most recent sign-in. */ + @JsonProperty("lastLoggedInUser") LoggedInUser lastLoggedInUser, + /** Every account the host has signed in to on this machine. */ + @JsonProperty("loggedInUsers") List loggedInUsers, + /** Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only behavior. */ + @JsonProperty("staff") Boolean staff, + /** Whether the user was recognized as GitHub staff. */ + @JsonProperty("staffGithub") Boolean staffGitHub, + /** Whether the user was recognized as Microsoft staff. */ + @JsonProperty("staffMicrosoft") Boolean staffMicrosoft, + /** When the staff-only model reset last ran. */ + @JsonProperty("staffModelResetAt") String staffModelResetAt, + /** When the staff-only log level migration last ran. */ + @JsonProperty("staffLogLevelMigrationAt") String staffLogLevelMigrationAt, + /** When the staff-only update channel migration last ran. */ + @JsonProperty("staffUpdateChannelMigrationAt") String staffUpdateChannelMigrationAt, + /** Folders the user has marked as trusted. */ + @JsonProperty("trustedFolders") List trustedFolders, + /** Whether the user has answered the prompt suggesting they install the desktop app. */ + @JsonProperty("appInstallNudgeResponded") Boolean appInstallNudgeResponded, + /** When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the once-per-day cap for non-staff users across restarts. */ + @JsonProperty("autoFeedbackLastPromptedAt") String autoFeedbackLastPromptedAt +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateWriteKeyParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateWriteKeyParams.java new file mode 100644 index 0000000000..f6ee53a57b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateWriteKeyParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A single top-level key to record in the host's machine-wide state. The write replaces only that key and leaves the rest of the document untouched, so two writers recording different one-off flags do not overwrite each other. The stored credential keys cannot be written through this method. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record GlobalStateWriteKeyParams( + /** Copilot configuration directory to write the state document in, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to write the directory the server resolved for itself. Mirrors `globalState.loadForConfigDir`, so a caller can read and write the same directory. */ + @JsonProperty("configDir") String configDir, + /** Top-level key to write, named as it appears in the result of `globalState.load`. It must be one of the writable keys that `globalState.writeKey` lists. */ + @JsonProperty("key") String key, + /** Value to store for the key. Omit it, or pass null, to remove the key instead. */ + @JsonProperty("value") Object value +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/LoggedInUser.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/LoggedInUser.java new file mode 100644 index 0000000000..ebd6526dc4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/LoggedInUser.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.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; + +/** + * An account the host has signed in to, identified by the server it lives on and the login it uses there. The same person can appear more than once when they use both github.com and an Enterprise server. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record LoggedInUser( + /** Host the account belongs to, such as `github.com` or an Enterprise server. */ + @JsonProperty("host") String host, + /** Account login on that host. */ + @JsonProperty("login") String login, + /** Account kind, when the host recorded one. Consumers must tolerate new strings. */ + @JsonProperty("kind") String kind, + /** Source account this account was derived from, when one was recorded. */ + @JsonProperty("derivedFrom") String derivedFrom +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingMeta.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingMeta.java index 46967ea441..3a2bf1fd0b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingMeta.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingMeta.java @@ -28,6 +28,8 @@ public record ManagedSettingMeta( /** Whether users and repositories may choose a different value. `false` means policy locks the value. */ @JsonProperty("overridable") Boolean overridable, /** Channel that supplied this scalar value, matching a `layers[].source`: `device`, `server`, or `policyHelper`. These scalar defaults select one winning channel, not a mixed source. Treat unknown values as additional channels; more may be added. */ - @JsonProperty("source") String source + @JsonProperty("source") String source, + /** Original managed value when the runtime adjusted it to a supported effective value. Omitted when no adjustment was needed. */ + @JsonProperty("requested") String requested ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsMeta.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsMeta.java index 7348141749..d98d081e63 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsMeta.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsMeta.java @@ -28,6 +28,10 @@ public record ManagedSettingsMeta( /** Lock state and provenance of `values.model`. */ @JsonProperty("model") ManagedSettingMeta model, /** Lock state and provenance of `values.autoTier`. */ - @JsonProperty("autoTier") ManagedSettingMeta autoTier + @JsonProperty("autoTier") ManagedSettingMeta autoTier, + /** Lock state and provenance of `values.effortLevel`. */ + @JsonProperty("effortLevel") ManagedSettingMeta effortLevel, + /** Lock state and provenance of `values.contextTier`. */ + @JsonProperty("contextTier") ManagedSettingMeta contextTier ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsResolveParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsResolveParams.java index ffb5af2b18..1ecc14a56c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsResolveParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsResolveParams.java @@ -29,6 +29,23 @@ public record ManagedSettingsResolveParams( /** GitHub token to resolve instead of the current account. The call fails when the token cannot be resolved. */ @JsonProperty("gitHubToken") String gitHubToken, /** Embedding client identity for server policy requests, as in session creation. Omit for the CLI identity. */ - @JsonProperty("clientName") String clientName + @JsonProperty("clientName") String clientName, + /** Working directory used to run an organization policy helper. When omitted, sessionless resolution does not run the helper. */ + @JsonProperty("workingDirectory") String workingDirectory ) { + + /** + * Creates managed-settings resolution parameters without a working directory. + * + * @param selectionId Opaque account identifier returned by `account.getAllUsers`. When omitted, the current account is used, or device policy only when no account is signed in. + * @param gitHubToken GitHub token to resolve instead of the current account. The call fails when the token cannot be resolved. + * @param clientName Embedding client identity for server policy requests, as in session creation. Omit for the CLI identity. + */ + public ManagedSettingsResolveParams( + String selectionId, + String gitHubToken, + String clientName + ) { + this(selectionId, gitHubToken, clientName, null); + } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsValues.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsValues.java index 931294b19c..a9094de7ca 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsValues.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsValues.java @@ -25,9 +25,13 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record ManagedSettingsValues( - /** Managed default model identifier, as configured. New sessions start with it; it can name a model the account cannot use, so hosts match it against the listed models. */ + /** Managed default model identifier. When model availability was resolved, aliases and family names are projected to a concrete available model ID; otherwise the configured value is returned. */ @JsonProperty("model") String model, /** Managed Auto routing preference, used when the selected model is `auto`. */ - @JsonProperty("autoTier") AutoTier autoTier + @JsonProperty("autoTier") AutoTier autoTier, + /** Managed reasoning-effort default for the managed concrete model. The runtime clamps it to an entitled effort when model availability is known. */ + @JsonProperty("effortLevel") String effortLevel, + /** Managed context-tier default for the managed concrete model. */ + @JsonProperty("contextTier") ContextTier contextTier ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServer.java new file mode 100644 index 0000000000..4e37d915f8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServer.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Effective MCP configuration entry. Configuration enablement is distinct from the optional live observation. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpConfiguredServer( + /** Server name (config key) */ + @JsonProperty("name") String name, + /** Whether this configured server is enabled after session configuration and policy filtering. */ + @JsonProperty("enabled") Boolean enabled, + /** Configuration provenance: user, workspace, plugin, builtin, or managed. */ + @JsonProperty("source") McpServerSource source, + /** Plugin name that provided this server, when source is plugin. */ + @JsonProperty("sourcePlugin") String sourcePlugin, + /** Plugin version that provided this server, when source is plugin. */ + @JsonProperty("sourcePluginVersion") String sourcePluginVersion, + /** Human-readable display name supplied by configuration. */ + @JsonProperty("displayName") String displayName, + /** Observed state from an already materialized matching server. Omitted when no live graph has this configured server; it never determines configuration enablement. */ + @JsonProperty("live") McpConfiguredServerState live +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServerState.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServerState.java new file mode 100644 index 0000000000..0087f2546b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServerState.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.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Observational state for a matching already materialized MCP server. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpConfiguredServerState( + /** Observed connection status. This is not a configuration or readiness guarantee. */ + @JsonProperty("status") McpServerStatus status, + /** Observed connection error, when the materialized server failed. */ + @JsonProperty("error") String error +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java index f2917ced22..9b93d39830 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java @@ -27,6 +27,8 @@ public record McpServer( @JsonProperty("status") McpServerStatus status, /** Configuration source: user, workspace, plugin, builtin, or managed */ @JsonProperty("source") McpServerSource source, + /** Configured URL for an HTTP/SSE server, regardless of configuration source. Omitted for local and in-memory servers. */ + @JsonProperty("url") String url, /** Plugin name that provided this server, when source is plugin. */ @JsonProperty("sourcePlugin") String sourcePlugin, /** Plugin version that provided this server, when source is plugin. */ @@ -63,6 +65,6 @@ public McpServer( String error, McpServerMetadata serverMetadata ) { - this(name, status, source, sourcePlugin, sourcePluginVersion, displayName, error, serverMetadata, null); + this(name, status, source, null, sourcePlugin, sourcePluginVersion, displayName, error, serverMetadata, null); } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java index 72b1503d59..0902ef8812 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Configuration source: user, workspace, plugin, builtin, or managed + * Configuration source: user, workspace, plugin, builtin, managed, or account * * @since 1.0.0 */ @@ -25,7 +25,9 @@ public enum McpServerSource { /** The {@code builtin} variant. */ BUILTIN("builtin"), /** The {@code managed} variant. */ - MANAGED("managed"); + MANAGED("managed"), + /** The {@code account} variant. */ + ACCOUNT("account"); private final String value; McpServerSource(String value) { this.value = value; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java index 81ffc46a36..63fbd2937a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java @@ -31,6 +31,8 @@ public record Model( @JsonProperty("capabilities") ModelCapabilities capabilities, /** Provider-supplied model metadata. Keys and JSON-compatible values are preserved unchanged. This is factual metadata published by the model provider; it carries no picker or UX semantics. */ @JsonProperty("metadata") Map metadata, + /** Model vendor as the Copilot API reports it, for example "Anthropic" or "Azure OpenAI". Open vocabulary, passed through unchanged. It can name the vendor that serves the model instead of the one that built it, or a label that is not a vendor, such as "Experimental". Absent when the Copilot API reports no vendor. */ + @JsonProperty("vendor") String vendor, /** Policy state (if applicable) */ @JsonProperty("policy") ModelPolicy policy, /** Billing information */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelClearStartupSeedResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelClearStartupSeedResult.java new file mode 100644 index 0000000000..d905811186 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelClearStartupSeedResult.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Data type {@code ModelClearStartupSeedResult}. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record ModelClearStartupSeedResult( + @JsonProperty("cleared") Boolean cleared +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PendingExternalToolRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PendingExternalToolRequest.java new file mode 100644 index 0000000000..6529827e05 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PendingExternalToolRequest.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.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; + +/** + * External tool call of the session or one of its sub-agents that is still waiting for session.tools.handlePendingToolCall. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record PendingExternalToolRequest( + /** Request ID to pass to session.tools.handlePendingToolCall */ + @JsonProperty("requestId") String requestId, + /** Tool call ID assigned to this external tool invocation */ + @JsonProperty("toolCallId") String toolCallId, + /** Name of the external tool to invoke */ + @JsonProperty("toolName") String toolName, + /** Arguments to pass to the external tool */ + @JsonProperty("arguments") Object arguments, + /** Stable identity of the provider that offered the tool, for hosts that route extension-owned tools by provider */ + @JsonProperty("providerId") String providerId, + /** Sub-agent instance identifier (the envelope agentId of its events) of the agent that issued the call; absent for calls issued by the root agent */ + @JsonProperty("agentId") String agentId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaAccessState.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaAccessState.java new file mode 100644 index 0000000000..d567cb75b3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaAccessState.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Access policy reported by the quota service. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ProviderQuotaAccessState { + /** The {@code allowed} variant. */ + ALLOWED("allowed"), + /** The {@code denied} variant. */ + DENIED("denied"), + /** The {@code not_required} variant. */ + NOT_REQUIRED("not_required"), + /** The {@code unknown} variant. */ + UNKNOWN("unknown"), + /** The {@code unavailable} variant. */ + UNAVAILABLE("unavailable"); + + private final String value; + ProviderQuotaAccessState(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ProviderQuotaAccessState fromValue(String value) { + for (ProviderQuotaAccessState v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ProviderQuotaAccessState value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaAcquisitionStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaAcquisitionStatus.java new file mode 100644 index 0000000000..575227472b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaAcquisitionStatus.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.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Acquisition state, independent of the service's business access/capacity verdict. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ProviderQuotaAcquisitionStatus { + /** The {@code succeeded} variant. */ + SUCCEEDED("succeeded"), + /** The {@code unavailable} variant. */ + UNAVAILABLE("unavailable"), + /** The {@code failed} variant. */ + FAILED("failed"); + + private final String value; + ProviderQuotaAcquisitionStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ProviderQuotaAcquisitionStatus fromValue(String value) { + for (ProviderQuotaAcquisitionStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ProviderQuotaAcquisitionStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaBudgetMetadata.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaBudgetMetadata.java new file mode 100644 index 0000000000..2697dbdf48 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaBudgetMetadata.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.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; + +/** + * Authoritative budget measurements and policy metadata, independent of provider. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ProviderQuotaBudgetMetadata( + /** Whether the budget has unlimited entitlement. */ + @JsonProperty("unlimited") Boolean unlimited, + /** Exact budget entitlement, retaining incumbent fractional-unit compatibility. */ + @JsonProperty("entitlement") Double entitlement, + /** Exact measured consumption in the budget's unit. */ + @JsonProperty("consumed") Double consumed, + /** Service-reported remaining percentage for authoritative budget presentation. */ + @JsonProperty("remainingPercentage") Double remainingPercentage, + /** Whether service policy allows continued usage after exhaustion. */ + @JsonProperty("usageAllowedWhenExhausted") Boolean usageAllowedWhenExhausted, + /** Usage beyond entitlement, in the budget's unit. */ + @JsonProperty("overage") Double overage, + /** Whether additional usage is allowed when the budget is exhausted. */ + @JsonProperty("overageAllowedWhenExhausted") Boolean overageAllowedWhenExhausted, + /** Reset instant in epoch milliseconds when this budget actually defines a window. */ + @JsonProperty("resetAtEpochMs") Double resetAtEpochMs, + /** Whether the reset instant is an estimate. */ + @JsonProperty("resetEstimated") Boolean resetEstimated, + /** Whether this budget uses token-based billing. */ + @JsonProperty("tokenBasedBilling") Boolean tokenBasedBilling, + /** Optional additional-usage budget cap. */ + @JsonProperty("overageLimit") Double overageLimit +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaCapacityState.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaCapacityState.java new file mode 100644 index 0000000000..45e9bf6a98 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaCapacityState.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Capacity is independent of whether a numeric balance was supplied. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ProviderQuotaCapacityState { + /** The {@code available} variant. */ + AVAILABLE("available"), + /** The {@code exhausted} variant. */ + EXHAUSTED("exhausted"), + /** The {@code unlimited} variant. */ + UNLIMITED("unlimited"), + /** The {@code not_required} variant. */ + NOT_REQUIRED("not_required"), + /** The {@code not_applicable} variant. */ + NOT_APPLICABLE("not_applicable"), + /** The {@code unknown} variant. */ + UNKNOWN("unknown"), + /** The {@code unavailable} variant. */ + UNAVAILABLE("unavailable"); + + private final String value; + ProviderQuotaCapacityState(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ProviderQuotaCapacityState fromValue(String value) { + for (ProviderQuotaCapacityState v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ProviderQuotaCapacityState value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaObservationKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaObservationKind.java new file mode 100644 index 0000000000..ca1406ed70 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaObservationKind.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.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Timing and purpose of a provider quota observation. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ProviderQuotaObservationKind { + /** The {@code account_snapshot} variant. */ + ACCOUNT_SNAPSHOT("account_snapshot"), + /** The {@code admission_state} variant. */ + ADMISSION_STATE("admission_state"); + + private final String value; + ProviderQuotaObservationKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ProviderQuotaObservationKind fromValue(String value) { + for (ProviderQuotaObservationKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ProviderQuotaObservationKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaQuantityKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaQuantityKind.java new file mode 100644 index 0000000000..1282854ba8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaQuantityKind.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.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Interpretation permitted for independently reported quantities. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ProviderQuotaQuantityKind { + /** The {@code authoritative_budget} variant. */ + AUTHORITATIVE_BUDGET("authoritative_budget"), + /** The {@code advisory_balance} variant. */ + ADVISORY_BALANCE("advisory_balance"), + /** The {@code none} variant. */ + NONE("none"); + + private final String value; + ProviderQuotaQuantityKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ProviderQuotaQuantityKind fromValue(String value) { + for (ProviderQuotaQuantityKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ProviderQuotaQuantityKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaState.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaState.java new file mode 100644 index 0000000000..253d64bbe8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaState.java @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * An account quota reading. Absence of a quantity is unknown, never zero. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ProviderQuotaState( + /** Provider/account whose service reported this state. */ + @JsonProperty("provider") ModelProviderRef provider, + /** Service-owned quota identifier within this provider. */ + @JsonProperty("quotaId") String quotaId, + /** Unit of entitledQuantity and availableQuantity. */ + @JsonProperty("unit") ProviderQuotaUnit unit, + /** Whether the service permits access, independently of balance. */ + @JsonProperty("accessState") ProviderQuotaAccessState accessState, + /** Service capacity state; missing quantities do not imply exhaustion. */ + @JsonProperty("capacityState") ProviderQuotaCapacityState capacityState, + /** Whether this is a GET account reading or a pre-response admission observation. Observations are never merged across kinds. */ + @JsonProperty("observationKind") ProviderQuotaObservationKind observationKind, + /** Whether quantities are authoritative budget measurements, advisory balances, or absent. Only authoritative budgets support percentage presentation. */ + @JsonProperty("quantityKind") ProviderQuotaQuantityKind quantityKind, + /** Acquisition outcome. Unavailable/failed readings have no service verdict; access/capacity unavailable are compatibility placeholders only. */ + @JsonProperty("acquisitionStatus") ProviderQuotaAcquisitionStatus acquisitionStatus, + /** When the runtime observed this reading, not a charge timestamp or guarantee that consumption has settled. */ + @JsonProperty("observedAt") String observedAt, + /** HTTP status from acquisition, when available. */ + @JsonProperty("httpStatus") Long httpStatus, + /** Service error code or client acquisition category, separate from the business-state reason. */ + @JsonProperty("acquisitionError") String acquisitionError, + /** Optional metadata for an authoritative budget. Advisory balances and admission-only observations do not populate this. */ + @JsonProperty("budgetMetadata") ProviderQuotaBudgetMetadata budgetMetadata, + /** Key for the backwards-compatible snapshots projection, when the authoritative budget supports that contract. */ + @JsonProperty("compatibilityKey") String compatibilityKey, + /** Explicit service admission flag, when reported. */ + @JsonProperty("hasQuota") Boolean hasQuota, + /** Independently reported signed 64-bit entitlement. -1 is an unlimited sentinel, not a capacity-state rewrite; omission and null are preserved. */ + @JsonProperty("entitledQuantity") Long entitledQuantity, + /** Independently reported signed 64-bit available quantity. Zero does not override the service access/capacity verdict; omission and null are preserved. */ + @JsonProperty("availableQuantity") Long availableQuantity, + /** Service-reported explanation for the state. */ + @JsonProperty("reason") String reason, + /** Service-reported quota source. */ + @JsonProperty("source") String source, + /** Service name owning this reading. */ + @JsonProperty("service") String service +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaUnit.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaUnit.java new file mode 100644 index 0000000000..9c9e7f3d3e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderQuotaUnit.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.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Units explicitly reported by a quota provider. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ProviderQuotaUnit { + /** The {@code ai_credits} variant. */ + AI_CREDITS("ai_credits"), + /** The {@code requests} variant. */ + REQUESTS("requests"), + /** The {@code tokens} variant. */ + TOKENS("tokens"), + /** The {@code unknown} variant. */ + UNKNOWN("unknown"); + + private final String value; + ProviderQuotaUnit(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ProviderQuotaUnit fromValue(String value) { + for (ProviderQuotaUnit v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ProviderQuotaUnit value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QuotaWarningProjection.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QuotaWarningProjection.java new file mode 100644 index 0000000000..dad4961084 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QuotaWarningProjection.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.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; + +/** + * A pending session quota warning, consumed once by takeWarnings. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record QuotaWarningProjection( + /** Existing warning category. */ + @JsonProperty("warningType") String warningType, + /** User-facing warning text. */ + @JsonProperty("message") String message, + /** Related account or upgrade link. */ + @JsonProperty("url") String url +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxHostCapability.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxHostCapability.java index e3efc94513..2c4925f402 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxHostCapability.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxHostCapability.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record SandboxHostCapability( - /** The policy feature, as an extensible string: ignore names you do not recognize. Known values: `network` (sandboxed commands can reach the network; on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns), `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`), `denied_paths` (native enforcement of `filesystem.deniedPaths`), `shell` (shell commands inside the sandbox), and `filesystem_enumeration` (enumerate-only filesystem grants; on Windows this needs Process Security Environment 1.1 filesystem enumeration support, and without it sandboxed PowerShell still runs but cannot resolve its current location; other platforms always report it). */ + /** The policy feature, as an extensible string: ignore names you do not recognize. Known values: `network` (sandboxed commands can reach the network; on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns), `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support or MXC's PSEC 1.0-only proxy-loopback compatibility capability, and a policy that uses it must also set `network.allowLocalNetwork`; compatibility applies only to an explicit identity-less runtime proxy, not general host-loopback access, and other policy restrictions still apply), `denied_paths` (native enforcement of `filesystem.deniedPaths`), `shell` (shell commands inside the sandbox), and `filesystem_enumeration` (enumerate-only filesystem grants; on Windows this needs Process Security Environment 1.1 filesystem enumeration support, and without it sandboxed PowerShell still runs but cannot resolve its current location; other platforms always report it). */ @JsonProperty("name") String name, /** Whether this host can run the feature. */ @JsonProperty("supported") Boolean supported, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountApi.java index 0adbf18092..714ef4ebb6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountApi.java @@ -28,7 +28,7 @@ public final class ServerAccountApi { } /** - * Optional opaque account selection or compatibility GitHub token used to look up quota. + * Gets Copilot quota usage for the current or opaquely selected authenticated user. *

* Invokes the method with no params, applying the runtime defaults. * @@ -41,7 +41,7 @@ public CompletableFuture getQuota() { } /** - * Optional opaque account selection or compatibility GitHub token used to look up quota. + * Gets Copilot quota usage for the current or opaquely selected authenticated user. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -52,7 +52,7 @@ public CompletableFuture getQuota(AccountGetQuotaParams p } /** - * Current authentication state + * Gets the currently active authentication credentials from the global auth manager. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -63,7 +63,7 @@ public CompletableFuture getCurrentAuth() { } /** - * List of all authenticated users + * Gets all authenticated users available for account switching. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -74,7 +74,7 @@ public CompletableFuture> getAllUsers() { } /** - * Credentials to validate and store. Omit login to resolve the authenticated user from the token. + * Validates and stores authentication credentials. When login is omitted, resolves the authenticated user from the token before persistence. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -85,7 +85,7 @@ public CompletableFuture login(AccountLoginParams params) { } /** - * User to log out + * Removes user authentication from keychain and persisted state. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentRegistryApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentRegistryApi.java index f398fb2dcb..a96ce680d7 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentRegistryApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentRegistryApi.java @@ -27,7 +27,7 @@ public final class ServerAgentRegistryApi { } /** - * Inputs to spawn a managed-server child via the controller's spawn delegate. + * Spawns a managed-server child with the supplied configuration and returns a discriminated-union result. The caller (typically the CLI controller) is responsible for attaching to the spawned child and sending any follow-up prompt. When the controller-local spawn gate is closed the server returns JSON-RPC MethodNotFound. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentsApi.java index 0c621a3624..ddff436555 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentsApi.java @@ -27,7 +27,7 @@ public final class ServerAgentsApi { } /** - * Optional project paths to include in agent discovery. + * Discovers custom agents across user, project, plugin, and remote sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture discover(AgentsDiscoverParams par } /** - * Optional project paths to include when enumerating agent discovery directories. + * Returns the canonical directories where a client may create custom agents that the runtime will recognize, including ones that do not exist yet. Project directories become active once created. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -48,4 +48,59 @@ public CompletableFuture getDiscoveryPaths(Agents return caller.invoke("agents.getDiscoveryPaths", params, AgentsGetDiscoveryPathsResult.class); } + /** + * Lists the agents this runtime ships, by name. A consumer separating shipped agents from ones the user or a plugin authored should compare against these names rather than against `AgentInfo.source`: an authored agent may carry the `builtin` source while not being one of these, and the runtime treats the two as separate questions. `disableableNames` is the subset a user may turn off, which a client needs to decide whether to offer a toggle. `yamlBasedNames` is the subset backed by a shipped YAML definition, which a client needs before asking the runtime to load one. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture getBuiltins() { + return caller.invoke("agents.getBuiltins", java.util.Map.of(), AgentsGetBuiltinsResult.class); + } + + /** + * Lists the shipped agents a client should offer right now, filtered by the feature flags it passes. `getBuiltins` names every agent the runtime knows about; some of those are gated, so a client rendering a picker wants this narrower list together with the description to show beside each name. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture getAvailableBuiltins(AgentsGetAvailableBuiltinsParams params) { + return caller.invoke("agents.getAvailableBuiltins", params, AgentsGetAvailableBuiltinsResult.class); + } + + /** + * Loads one shipped agent's YAML definition, for a client that needs what the agent declares rather than only its name. `getBuiltins` reports which names have a definition to load: a name outside its `yamlBasedNames` is special-cased in code and has none. The definition crosses as its own JSON rather than as contract-typed fields, because the runtime parses it with the agent schema's tolerant shape and re-typing it here would drop the keys that shape accepts and this one does not. The projected `__nativeCustomAgent` view the runtime derives is included, so a caller reading the declared model and a caller rendering the agent see the same definition. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture getBuiltinDefinition(AgentsGetBuiltinDefinitionParams params) { + return caller.invoke("agents.getBuiltinDefinition", params, AgentsGetBuiltinDefinitionResult.class); + } + + /** + * Projects one shipped agent the way a picker lists it, reading only the metadata at the head of the definition file and stopping before the prompt body. `getBuiltinDefinition` answers the whole definition instead, so a client listing every shipped agent should prefer this one: the cost of a listing grows with the number of agents, and the prompt body is the part a listing never shows. The two also differ in shape. This returns the projected custom agent on its own, whereas `getBuiltinDefinition` returns the authored definition with that projection nested under `__nativeCustomAgent`. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture getBuiltinListingDefinition(AgentsGetBuiltinListingDefinitionParams params) { + return caller.invoke("agents.getBuiltinListingDefinition", params, AgentsGetBuiltinListingDefinitionResult.class); + } + + /** + * Resolves the model a custom agent asks for against the models actually available, and answers both the model to switch to and the warning a user should see when the agent's preference cannot be met. A custom agent may name several acceptable models in preference order, so the decision is a match rather than a lookup, and an agent whose preference is unavailable is a normal outcome that produces a warning rather than an error. A host must call this rather than pick the first available name itself, because the preference order and the wording of the warning are what keep one installation's agent selection the same as another's. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture customAgentInitialModelDecision(AgentsCustomAgentInitialModelDecisionParams params) { + return caller.invoke("agents.customAgentInitialModelDecision", params, AgentsCustomAgentInitialModelDecisionResult.class); + } + } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCatalogApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCatalogApi.java index 32e7963e35..f7d7c2fe1b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCatalogApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCatalogApi.java @@ -28,7 +28,7 @@ public final class ServerCatalogApi { } /** - * A bounded catalog search. Both the query length and the result count are capped by the schema so a caller cannot request an unbounded scan. + * Requests a bounded catalog search. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with search available returns inert candidate summaries, each with an opaque single-use handle scoped to this runtime instance; a runtime without it returns the typed search-unavailable result. Public authorities may be searched anonymously, while an authority that requires credentials yields the typed authentication-required result. All returned text, URLs, and package metadata are untrusted external data and can never trigger instructions, tools, or installation. Read-only: nothing is installed, configured, or persisted. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -39,7 +39,7 @@ public CompletableFuture search(CatalogSearchParams params) } /** - * A bounded catalog search. Both the query length and the result count are capped by the schema so a caller cannot request an unbounded scan. + * Requests a bounded catalog search. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with search available returns inert candidate summaries, each with an opaque single-use handle scoped to this runtime instance; a runtime without it returns the typed search-unavailable result. Public authorities may be searched anonymously, while an authority that requires credentials yields the typed authentication-required result. All returned text, URLs, and package metadata are untrusted external data and can never trigger instructions, tools, or installation. Read-only: nothing is installed, configured, or persisted. *

* Accepts the extensible request, including inputs added after the params record. * @@ -52,7 +52,7 @@ public CompletableFuture search(CatalogSearchRequest reques } /** - * Terminates one retained catalog selection group through an opaque reference previously returned by the model-safe search projection. + * Terminates one retained catalog selection group. A selected outcome returns the native host a fresh single-use candidate handle plus the original searchId for a later explicit mcp.planInstall call; non-selected outcomes release the group without producing a planning input. Candidate state, cards, URLs, credentials and private identifiers remain inside the runtime. The model-facing catalog_select tool projects the result separately and never exposes the candidate handle or searchId. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCommandsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCommandsApi.java index efa5317ea4..82c5fad8c4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCommandsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCommandsApi.java @@ -27,7 +27,7 @@ public final class ServerCommandsApi { } /** - * Slash commands available in the session, after applying any include/exclude filters. + * Lists the well-known built-in slash commands that work as the first message in a new session (e.g. /plan, /env), without requiring an active session. Commands that depend on session state, authentication, or a synced session are omitted. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerConnectorsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerConnectorsApi.java new file mode 100644 index 0000000000..5b74d83721 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerConnectorsApi.java @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code connectors} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerConnectorsApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerConnectorsApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Returns feature availability. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getCapabilities() { + return caller.invoke("connectors.getCapabilities", java.util.Map.of(), ConnectorsGetCapabilitiesResult.class); + } + + /** + * Returns eligible accounts. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getAccounts() { + return caller.invoke("connectors.getAccounts", java.util.Map.of(), ConnectorsGetAccountsResult.class); + } + + /** + * Lists entries for the selected account. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture list(ConnectorsListParams params) { + return caller.invoke("connectors.list", params, ConnectorsListResult.class); + } + + /** + * Refreshes entries for the selected account. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture refresh(ConnectorsRefreshParams params) { + return caller.invoke("connectors.refresh", params, ConnectorsRefreshResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerEnvironmentsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerEnvironmentsApi.java index 9d52f2f1ea..a8220ef6fc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerEnvironmentsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerEnvironmentsApi.java @@ -27,7 +27,7 @@ public final class ServerEnvironmentsApi { } /** - * Optional discovery filters supported by GitHub Mission Control. + * Lists GitHub Mission Control environments visible to the authenticated identity. Does not require a running host and excludes host relay credentials. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture list(EnvironmentsListParams par } /** - * Identify a Mission Control environment to retrieve. + * Gets safe discovery information for a GitHub Mission Control environment without requiring a running host. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture get(EnvironmentsGetParams params } /** - * Identify a user-managed Mission Control environment to delete. + * Deletes a user-managed GitHub Mission Control environment. GitHub-managed environments cannot be deleted. Does not stop a running host, which may register again. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerExtensionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerExtensionsApi.java index 7bc74b441f..385fc3bbc2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerExtensionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerExtensionsApi.java @@ -27,7 +27,7 @@ public final class ServerExtensionsApi { } /** - * Extensions discovered from persisted Copilot home state and their effective loading mode. Launch-scoped additional plugins are not included. + * Discovers user and enabled installed-plugin extensions from persisted Copilot home state, including enablement preferences. Launch-scoped additional plugins are not included. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture discover() { } /** - * Source-qualified extension identifiers to persistently enable for future sessions. + * Persistently enables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.enable to update them. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture enable(ExtensionsEnableParams params) { } /** - * Source-qualified extension identifiers to persistently disable for future sessions. + * Persistently disables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.disable to update them. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitApi.java new file mode 100644 index 0000000000..da512566d0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitApi.java @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code git} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +final class ServerGitApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerGitApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Reads the remote that the branch checked out in a working tree tracks, as `branch..remote` configures it. Reports `origin` rather than failing whenever there is no tracking configuration to read — on a detached HEAD, on a branch with no upstream, or when git itself fails — because a caller asking which remote to talk to needs an answer it can act on, not an error. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture currentBranchRemote(GitCurrentBranchRemoteParams params) { + return caller.invoke("git.currentBranchRemote", params, GitCurrentBranchRemoteResult.class); + } + + /** + * Collects the repository context of a working directory in one call: working tree root, repository identifier and host, current branch, and the HEAD and base commits. Every repository field is omitted when the path is not inside a git working tree, and the requested path is echoed back as `cwd`. The answer is the same `SessionWorkingDirectoryContext` that `session.metadata.recordContextChange` accepts, so a caller polling for a context change can forward the result unchanged. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. It can become public once an SDK consumer needs to derive session context from a directory itself. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture workingDirectoryContext(GitWorkingDirectoryContextParams params) { + return caller.invoke("git.workingDirectoryContext", params, SessionWorkingDirectoryContext.class); + } + + /** + * Lists the GitHub repositories a working tree's remotes point at, one entry per distinct repository, so a caller can resolve a base and head repository without parsing remote URLs itself. When several remotes name the same repository, only the first is listed, and the entry keeps that remote name. Remotes pointing at no GitHub host are left out, so an empty list means the tree reaches GitHub through no remote. Failing to read the remotes is reported as an error rather than as an empty list, because the two mean different things to a caller. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture reposFromRemotes(GitReposFromRemotesParams params) { + return caller.invoke("git.reposFromRemotes", params, GitReposFromRemotesResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitHubOwnersApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitHubOwnersApi.java new file mode 100644 index 0000000000..175c41400a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitHubOwnersApi.java @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code gitHubOwners} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +final class ServerGitHubOwnersApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerGitHubOwnersApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Registers a cancellable owner listing and returns its request id. Separate from `gitHubOwners.list` so the id exists before the listing starts: a caller that abandons the listing the moment it begins would otherwise have nothing to name in `gitHubOwners.cancel`. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture nextRequestId() { + return caller.invoke("gitHubOwners.nextRequestId", java.util.Map.of(), GitHubOwnersRequestIdResult.class); + } + + /** + * Lists the logins the authenticated user may act as — their own account first, then the organizations they belong to — by asking the GitHub API under the supplied credential. No credential travels in the request: `authInfo` selects one the runtime already holds, and the runtime resolves the token and the GitHub host from it. A failure the caller should render arrives as `message`; one it should raise arrives as `throwError`. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture list(GitHubOwnersListParams params) { + return caller.invoke("gitHubOwners.list", params, GitHubOwnersListResult.class); + } + + /** + * Abandons an owner listing started with the given request id. Answers `canceled: true` while a listing with that id is running. Answers `canceled: false` when the id was never registered, was registered but not used, was released after being abandoned, or its listing has ended. Canceling an unused id releases it, and a later `list` with that id is refused. The cancel acts only on owner listings and never reaches another request of the host. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture cancel(GitHubOwnersCancelParams params) { + return caller.invoke("gitHubOwners.cancel", params, GitHubOwnersCancelResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitHubRepositoryApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitHubRepositoryApi.java new file mode 100644 index 0000000000..d3eb8f0bd0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGitHubRepositoryApi.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code gitHubRepository} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +final class ServerGitHubRepositoryApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerGitHubRepositoryApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Resolves the GitHub repository that owns a working-tree path by reading the selected git remote configured for it, preferring `origin`. Returns a null `repository` when the path is inside a git working tree but that selected remote does not resolve to a GitHub host. Fails when the path is not inside a git working tree at all, so a caller can tell 'not a repository' apart from 'a repository with no GitHub remote'. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture atPath(GitHubRepositoryAtPathParams params) { + return caller.invoke("gitHubRepository.atPath", params, GitHubRepositoryAtPathResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGlobalStateApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGlobalStateApi.java new file mode 100644 index 0000000000..3c215395b8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerGlobalStateApi.java @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code globalState} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +final class ServerGlobalStateApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerGlobalStateApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Reads the host's machine-wide state: which plugins are installed and the one-off flags and timestamps that record what the user has already been shown or migrated. This is the state that outlives a single session and a single workspace, so a host reads it to decide whether to run a first-launch step, offer an onboarding prompt, or skip one it has already completed. The stored credentials are deliberately not part of this result; a caller that needs an authenticated identity asks the account methods for it instead. Reading is non-destructive and every field is optional, because a fresh install has recorded nothing yet. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture load() { + return caller.invoke("globalState.load", java.util.Map.of(), GlobalStateLoadResult.class); + } + + /** + * Reads the host's machine-wide state exactly as `globalState.load` does, but from a caller-supplied configuration directory instead of the one the server resolved for itself. Use this when a consumer scopes a session to its own Copilot home — the SDK's per-session `configDir` override — so the state read matches the directory that session actually uses. An absent or empty `configDir` resolves the server's own home, making this identical to `globalState.load`. The stored credentials are omitted here for the same reason they are omitted from `globalState.load`: a caller that needs an authenticated identity asks the account methods instead, so pointing this at another directory cannot be used to read the credentials kept in it. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture loadForConfigDir(GlobalStateLoadForConfigDirParams params) { + return caller.invoke("globalState.loadForConfigDir", params, GlobalStateLoadResult.class); + } + + /** + * Records one top-level key in the host's machine-wide state, the counterpart to `globalState.load`. A host calls this to remember that it has shown an onboarding step, asked a one-off question, or completed a migration, so the next run can skip it. Only the named key is replaced and the rest of the document is preserved, which lets two writers record different flags without overwriting each other; passing no value removes the key instead. Only the keys a host records itself are writable: `appInstallNudgeResponded`, `appTipShown`, `askedSetupTerminals`, `autoFeedbackLastPromptedAt`, `firstLaunchAt`, `recentModelIds`, `sandboxCredentialProxyCaDeclined` and `sandboxOnboardingShown`. Every other key is refused, including `installedPlugins`, the stored credentials, `trustedFolders`, the staff flags and the signed-in accounts. Plugin enablement must use the plugin APIs, which apply repository and managed-policy checks. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture writeKey(GlobalStateWriteKeyParams params) { + return caller.invoke("globalState.writeKey", params, Void.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java index b6f69bf7dc..f41fbd63ac 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java @@ -27,7 +27,7 @@ public final class ServerHooksApi { } /** - * Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHostApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHostApi.java index 2cb0c99c37..a921cdf945 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHostApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHostApi.java @@ -27,7 +27,7 @@ public final class ServerHostApi { } /** - * Publishes a resident session attached to the listener's owning connection. + * Publishes an attached resident session for this listener's lifetime without copying it. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture publishSession(HostPublishSes } /** - * One application-owned session handoff, requested by the supervised hosting participant. + * Requests app-owned materialization over the owning SDK participant. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ CompletableFuture createSession(HostCreateSessionParams } /** - * Ends one participation, not the application's session lifetime. + * Releases app ownership retention after AHP detaches. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ CompletableFuture releaseSession(HostReleaseSessionParams params) { } /** - * Starts a supervised AHP host with at least one explicitly selected transport. + * Starts a connection-owned AHP host with explicit localServer and/or githubEnvironment transports as a supervised SDK participant. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -71,7 +71,7 @@ public CompletableFuture start(HostStartParams params) { } /** - * Stops a connection-owned listener and joins its teardown. + * Stops a listener owned by this SDK connection and joins its cleanup without deleting sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -82,7 +82,7 @@ public CompletableFuture dispose(HostDisposeParams params) { } /** - * Empty acknowledgement for a completed host lifecycle operation. + * Resolves current authenticated credentials and remote-control policy only for the runtime-owned Mission Control hosting participant. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -93,7 +93,7 @@ CompletableFuture getEnvironmentCredentials() { } /** - * Empty acknowledgement for a completed host lifecycle operation. + * Returns listener settings only to the supervised hosting participant over its SDK connection. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -104,7 +104,7 @@ CompletableFuture getConfiguration() { } /** - * Readiness reported by the supervised hosting participant on its own SDK connection. + * Reports a supervised hosting participant's bound AHP endpoint after its SDK handshake. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerInstructionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerInstructionsApi.java index 70eb5b0211..23d8c7e2de 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerInstructionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerInstructionsApi.java @@ -27,7 +27,7 @@ public final class ServerInstructionsApi { } /** - * Optional project paths to include in instruction discovery. + * Discovers instruction sources across user, repository, and plugin sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture discover(InstructionsDiscov } /** - * Optional project paths to include when enumerating instruction discovery targets. + * Returns the canonical files and directories where a client may create custom instructions that the runtime will recognize, including ones that do not exist yet. Repository targets become active once created. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerLlmInferenceApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerLlmInferenceApi.java index 4b7663f1f3..753eccb9f3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerLlmInferenceApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerLlmInferenceApi.java @@ -27,7 +27,7 @@ public final class ServerLlmInferenceApi { } /** - * Indicates whether the calling client was registered as the LLM inference provider. + * Registers an SDK client as the LLM inference callback provider. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture setProvider() { } /** - * Response head. + * Delivers the response head (status + headers) for an in-flight request, correlated by the requestId the runtime supplied in httpRequestStart. Must be called exactly once per request before any httpResponseChunk frames. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture httpResponseStart( } /** - * A response body chunk or terminal error. + * Delivers a body byte range (or a terminal transport error) for an in-flight response, correlated by requestId. Set `end` true on the last chunk. When `error` is set the response terminates with a transport-level failure and the runtime raises an APIConnectionError. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java index 4f9cd612bf..a686df708c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java @@ -27,7 +27,7 @@ public final class ServerManagedSettingsApi { } /** - * Validated device-managed settings discovered before a session exists. + * Discovers device-managed settings from production MDM and managed-file sources, validates them against the runtime-owned managed-settings schema, and returns the canonical JSON without requiring a session. `managedSettings.resolve` returns the same device settings together with the account's server policy. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture read() { } /** - * Invokes {@code managedSettings.clearCache}. + * Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing "sync account policy" should call `managedSettings.resolve` or start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture clearCache() { } /** - * Optional opaque account selection or GitHub token whose managed settings are resolved. + * Resolves the effective enterprise managed settings without a session, from the device channel and, when an account is available, the account's server policy through the same per-account cache sessions use. A cached server policy less than an hour old is used without a fetch; otherwise the policy is fetched, and when the fetch fails a cached policy up to 24 hours old is used instead, unless `forceRemoteSettingsRefresh` requires a live fetch. With no account requested or signed in, it reports device policy only; signing out removes the account's cached policy. It can fetch server policy over the network when the cache is stale, so call it off latency-critical paths such as startup rather than before listing models. The policy helper is not run. `layers` lists each channel's document before merging, and `values` and `meta` carry typed effective values and their lock state for the keys typed so far. *

* Invokes the method with no params, applying the runtime defaults. * @@ -62,7 +62,7 @@ public CompletableFuture resolve() { } /** - * Optional opaque account selection or GitHub token whose managed settings are resolved. + * Resolves the effective enterprise managed settings without a session, from the device channel and, when an account is available, the account's server policy through the same per-account cache sessions use. A cached server policy less than an hour old is used without a fetch; otherwise the policy is fetched, and when the fetch fails a cached policy up to 24 hours old is used instead, unless `forceRemoteSettingsRefresh` requires a live fetch. With no account requested or signed in, it reports device policy only; signing out removes the account's cached policy. It can fetch server policy over the network when the cache is stale, so call it off latency-critical paths such as startup rather than before listing models. The policy helper is not run. `layers` lists each channel's document before merging, and `values` and `meta` carry typed effective values and their lock state for the keys typed so far. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -73,7 +73,7 @@ public CompletableFuture resolve(ManagedSettingsRe } /** - * The authoring JSON schema for managed settings recognized by this runtime. + * Returns the managed-settings authoring JSON schema with descriptive `x-composition` annotations aligned with the shared settings-engine vocabulary. These annotations are not a complete runtime composition contract: model, effortLevel, and contextTier remain coupled. Use `managedSettings.compose` for the runtime's effective result. Performs no I/O. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -84,7 +84,7 @@ public CompletableFuture schema() { } /** - * A candidate managed-settings document to validate without applying it. + * Validates a candidate managed-settings document the way the runtime validates delivered policy, without applying it. Reports errors that would reject the document, warnings for content the runtime ignores, and the canonical document it would apply. Document text nested more than 64 levels deep is rejected. Performs no I/O. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -95,7 +95,7 @@ public CompletableFuture validate(ManagedSettings } /** - * Candidate managed-settings documents to merge without applying them. + * Merges candidate managed-settings documents for the device, server, and policy-helper channels into the effective settings the runtime would enforce on this host, using the same precedence and composition rules as live resolution, without applying them. Like live resolution, a server's advisory sandbox force-enable is declined on a host that cannot run the sandbox. Does not fetch policy or read policy files, but may perform blocking OS or subprocess probes for sandbox support. Preview documents are limited to 1 MiB and 64 levels of nesting. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpApi.java index 9557a2f4ac..553a644214 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpApi.java @@ -35,7 +35,7 @@ public final class ServerMcpApi { } /** - * Optional working directory used as context for MCP server discovery. + * Discovers MCP servers from user, workspace, plugin, and builtin sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -46,7 +46,7 @@ public CompletableFuture discover(McpDiscoverParams params) { } /** - * A side-effect-free request for an MCP install plan. Computing a plan never writes configuration, stores a secret, or reloads MCP servers. + * Requests a side-effect-free MCP install plan from a catalog candidate handle or a caller-supplied card. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with planning available returns a normalised plan and opaque single-use plan handle; a runtime without it returns the typed planning-unavailable result. A completed plan reports resource identity, provenance, eligible transport choices, the user-scope target, required typed values and secret placeholders, the policy result, the configuration changes installing would make, and whether a reload would be needed. Planning never writes configuration, stores a secret, or reloads MCP servers, so abandoning a plan needs no call and leaves nothing behind. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -57,7 +57,7 @@ public CompletableFuture planInstall(McpPlanInstallParams } /** - * A side-effect-free request for an MCP install plan. Computing a plan never writes configuration, stores a secret, or reloads MCP servers. + * Requests a side-effect-free MCP install plan from a catalog candidate handle or a caller-supplied card. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with planning available returns a normalised plan and opaque single-use plan handle; a runtime without it returns the typed planning-unavailable result. A completed plan reports resource identity, provenance, eligible transport choices, the user-scope target, required typed values and secret placeholders, the policy result, the configuration changes installing would make, and whether a reload would be needed. Planning never writes configuration, stores a secret, or reloads MCP servers, so abandoning a plan needs no call and leaves nothing behind. *

* Accepts the extensible request, including inputs added after the params record. * @@ -70,7 +70,7 @@ public CompletableFuture planInstall(McpPlanInstallRequest } /** - * Side-effect-free preparation of one original bound remote MCP choice. + * Consumes a bound catalogue plan and retains one exact fully resolved personal remote MCP operation requiring no supplied values or configured secrets. Returns its runtime operation ID and original expiry before any confirmation, activation, writer initialisation or installation effect. Register the original connection, operation and selected-session binding before calling applyInstall. Missing lower owned admission is unavailable, never a raw-config fallback. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -81,7 +81,7 @@ public CompletableFuture prepareInstall(McpPrep } /** - * Applies exactly one previously prepared operation on its original connection. + * Consumes a retained prepared MCP operation once, revalidates its original authority, requests explicit human consent through installations.confirm on the original connection, then revalidates source and applies the sealed transaction. An uncertain result requires original-operation inspection or recovery, never replay. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -92,7 +92,7 @@ public CompletableFuture applyInstall(McpApplyInstallPara } /** - * Read-only preparation of one owned removal under fresh selected-session authority. + * Prepares a read-only removal plan for an exact owned receipt under the selected existing session. Returns the original operation ID before confirmation; neither planning nor abandonment changes configuration or shared OAuth credentials. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -103,7 +103,7 @@ public CompletableFuture planUninstall(McpPlanU } /** - * One-use application of the exact retained removal plan. + * Consumes the original owned-removal plan once and requests fresh exact human confirmation on its original connection. Drift is refused; unrelated manual configuration and shared OAuth credentials are preserved. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpConfigApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpConfigApi.java index 6d3510f515..7271e94e29 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpConfigApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpConfigApi.java @@ -27,7 +27,7 @@ public final class ServerMcpConfigApi { } /** - * User-configured MCP servers, keyed by server name. + * Lists MCP servers from user configuration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture list() { } /** - * MCP server name and configuration to add to user configuration. + * Adds an MCP server to user configuration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture add(McpConfigAddParams params) { } /** - * MCP server name and replacement configuration to write to user configuration. + * Updates an MCP server in user configuration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture update(McpConfigUpdateParams params) { } /** - * MCP server name to remove from user configuration. + * Removes an MCP server from user configuration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -71,7 +71,7 @@ public CompletableFuture remove(McpConfigRemoveParams params) { } /** - * MCP server names to enable for new sessions. + * Enables MCP servers in user configuration for new sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -82,7 +82,7 @@ public CompletableFuture enable(McpConfigEnableParams params) { } /** - * MCP server names to disable for new sessions. + * Disables MCP servers in user configuration for new sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -93,7 +93,7 @@ public CompletableFuture disable(McpConfigDisableParams params) { } /** - * Invokes {@code mcp.config.reload}. + * Drops this runtime process's in-memory MCP server-definition cache so the next MCP config read observes disk. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpInstallationsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpInstallationsApi.java index ec99b766e9..6055d0fd3f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpInstallationsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpInstallationsApi.java @@ -27,7 +27,7 @@ public final class ServerMcpInstallationsApi { } /** - * New-work inventory or recovery request under an explicitly selected existing session. + * Reads receipt-owned MCP inventory for the selected account and host without activating servers or reconstructing missing ownership. Configuration ownership does not prove session-specific usability. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture list(McpInstallationsL } /** - * New-work inventory or recovery request under an explicitly selected existing session. + * Reconciles already-confirmed durable MCP transactions, then inspects owned inventory. Does not replay apply or reconstruct deleted ownership metadata; unresolved or unsafe evidence remains an explicit refusal. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture recover(McpInstallatio } /** - * Existing-operation control. A new session selector is deliberately not accepted. + * Inspects a known operation only on its original connection. Remains available after account or selected-session loss; does not acquire new authority or rebind an operation. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture status(McpInstallation } /** - * Existing-operation control. A new session selector is deliberately not accepted. + * Requests cancellation of a known operation on its original connection, including before apply or confirmation. Already-started effects retain their transaction lease and report an honest terminal or recovery outcome. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerModelsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerModelsApi.java index e4fd3cbc53..491340551d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerModelsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerModelsApi.java @@ -27,7 +27,7 @@ public final class ServerModelsApi { } /** - * Optional opaque account selection or compatibility GitHub token used to list models. + * Lists Copilot models available to the authenticated user. *

* Invokes the method with no params, applying the runtime defaults. * @@ -40,7 +40,7 @@ public CompletableFuture list() { } /** - * Optional opaque account selection or compatibility GitHub token used to list models. + * Lists Copilot models available to the authenticated user. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -51,7 +51,7 @@ public CompletableFuture list(ModelsListParams params) { } /** - * The running runtime's complete catalog of well-known built-in model IDs, including supported models and additional IDs with built-in metadata. + * Returns the running runtime's complete catalog of well-known built-in model IDs without authentication or network access. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsApi.java index da5ac6c0e8..4fd1a839b0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsApi.java @@ -34,7 +34,7 @@ public final class ServerPluginsApi { } /** - * Plugins installed in user/global state. + * Lists plugins installed in user/global state. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -45,7 +45,7 @@ public CompletableFuture list() { } /** - * Plugin source and optional working directory for relative-path resolution. + * Installs a plugin from a marketplace, GitHub repo, URL, or local path. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -56,7 +56,7 @@ public CompletableFuture install(PluginsInstallParams para } /** - * Name (or spec) of the plugin to uninstall. + * Uninstalls an installed plugin. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -67,7 +67,7 @@ public CompletableFuture uninstall(PluginsUninstallParams params) { } /** - * Name (or spec) of the plugin to update. + * Updates an installed plugin to its latest published version. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -78,7 +78,7 @@ public CompletableFuture update(PluginsUpdateParams params) } /** - * Result of updating all installed plugins. + * Updates every installed plugin to its latest published version. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -89,7 +89,7 @@ public CompletableFuture updateAll() { } /** - * Plugin names (or specs) to enable, plus the optional working directory the repository-controlled guard is evaluated against. + * Enables installed plugins for new sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -100,7 +100,7 @@ public CompletableFuture enable(PluginsEnableParams params) { } /** - * Plugin names (or specs) to disable, plus the optional working directory the repository-controlled guard is evaluated against. + * Disables installed plugins for new sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsBuiltinApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsBuiltinApi.java index 0610fcb28e..b35a77a817 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsBuiltinApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsBuiltinApi.java @@ -27,7 +27,7 @@ public final class ServerPluginsBuiltinApi { } /** - * Trusted built-in plugin directories to use for this runtime process. + * Replaces this server's trusted built-in plugin directories while no sessions are active. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java index e01bbeeec7..93adce4997 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java @@ -27,7 +27,7 @@ public final class ServerPluginsMarketplacesApi { } /** - * All registered marketplaces, including built-in defaults. + * Lists all registered marketplaces (defaults + user-added). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture list() { } /** - * Marketplace source and optional working directory for relative-path resolution. + * Registers a new marketplace from a source (owner/repo, URL, or local path). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture add(PluginsMarketplacesAd } /** - * Name of the marketplace to remove and an optional force flag. + * Removes a previously-registered marketplace. When the marketplace has dependent plugins and `force` is not set, the marketplace is left intact and the result lists the dependents so the caller can decide whether to retry with `force=true`. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture remove(PluginsMarketpl } /** - * Name of the marketplace whose plugin catalog to fetch. + * Lists plugins advertised by a registered marketplace. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -71,7 +71,7 @@ public CompletableFuture browse(PluginsMarketpl } /** - * Optional marketplace name; omit to refresh all. + * Re-fetches one or all registered marketplace catalogs. *

* Invokes the method with no params, applying the runtime defaults. * @@ -84,7 +84,7 @@ public CompletableFuture refresh() { } /** - * Optional marketplace name; omit to refresh all. + * Re-fetches one or all registered marketplace catalogs. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java index 7eb9aa0118..99f65a5f89 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java @@ -55,10 +55,18 @@ public final class ServerRpc { public final ServerAgentsApi agents; /** API methods for the {@code instructions} namespace. */ public final ServerInstructionsApi instructions; + /** API methods for the {@code globalState} namespace. */ + final ServerGlobalStateApi globalState; /** API methods for the {@code commands} namespace. */ public final ServerCommandsApi commands; /** API methods for the {@code user} namespace. */ public final ServerUserApi user; + /** API methods for the {@code gitHubRepository} namespace. */ + final ServerGitHubRepositoryApi gitHubRepository; + /** API methods for the {@code gitHubOwners} namespace. */ + final ServerGitHubOwnersApi gitHubOwners; + /** API methods for the {@code git} namespace. */ + final ServerGitApi git; /** API methods for the {@code managedSettings} namespace. */ public final ServerManagedSettingsApi managedSettings; /** API methods for the {@code runtime} namespace. */ @@ -71,6 +79,8 @@ public final class ServerRpc { public final ServerSessionsApi sessions; /** API methods for the {@code agentRegistry} namespace. */ public final ServerAgentRegistryApi agentRegistry; + /** API methods for the {@code connectors} namespace. */ + public final ServerConnectorsApi connectors; /** * Creates a new server RPC client. @@ -94,18 +104,23 @@ public ServerRpc(RpcCaller caller) { this.plugins = new ServerPluginsApi(caller); this.agents = new ServerAgentsApi(caller); this.instructions = new ServerInstructionsApi(caller); + this.globalState = new ServerGlobalStateApi(caller); this.commands = new ServerCommandsApi(caller); this.user = new ServerUserApi(caller); + this.gitHubRepository = new ServerGitHubRepositoryApi(caller); + this.gitHubOwners = new ServerGitHubOwnersApi(caller); + this.git = new ServerGitApi(caller); this.managedSettings = new ServerManagedSettingsApi(caller); this.runtime = new ServerRuntimeApi(caller); this.sessionFs = new ServerSessionFsApi(caller); this.llmInference = new ServerLlmInferenceApi(caller); this.sessions = new ServerSessionsApi(caller); this.agentRegistry = new ServerAgentRegistryApi(caller); + this.connectors = new ServerConnectorsApi(caller); } /** - * Optional message to echo back to the caller. + * Checks server responsiveness and returns protocol information. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -116,7 +131,7 @@ public CompletableFuture ping(PingParams params) { } /** - * Connection-level opt-ins for the `server.connect` handshake. Transport authentication is consumed by the native protocol boundary before dispatch. + * Performs the SDK server connection handshake and validates the optional connection token. Marked internal because this is JSON-RPC transport plumbing invoked automatically by an SDK client's own `connect()` wrapper, not a user-facing method. Stays internal as long as the SDK client owns the handshake; would only become public if the SDK ever exposed the raw schema surface to consumers without a connection wrapper. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -127,7 +142,7 @@ CompletableFuture connect(ConnectParams params) { } /** - * Invokes {@code registerExtensionLaunchProvider}. + * Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime uses its built-in extension launcher. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRuntimeApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRuntimeApi.java index e57db70946..d09f784d8d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRuntimeApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRuntimeApi.java @@ -27,7 +27,7 @@ public final class ServerRuntimeApi { } /** - * Invokes {@code runtime.shutdown}. + * Gracefully shuts down an SDK-owned runtime. The response is sent only after cleanup completes; callers may then terminate the owned runtime process. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java index 2634955e4a..1484140f6a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java @@ -31,7 +31,7 @@ public final class ServerSandboxApi { } /** - * Whether the host running this runtime can run the command sandbox. The runtime checks `supported` once per process. A capability answer can change while the process runs, for example after the user installs a missing package. + * Reports whether the host running this runtime can run the command sandbox, without starting a session or spawning a sandboxed command. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxProxyCaApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxProxyCaApi.java index 0ffe1733d5..0cc59738e9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxProxyCaApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxProxyCaApi.java @@ -27,7 +27,7 @@ public final class ServerSandboxProxyCaApi { } /** - * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * Reports whether the persistent certificate authority of the sandbox credential proxy exists, whether OS trust includes it, and whether it must be rotated. Changes nothing. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture getStatus(SandboxProxyCa } /** - * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * Creates the persistent certificate authority of the sandbox credential proxy if none is stored, without changing OS trust, and returns the path of its public certificate. Keeps an existing certificate authority, even one that must be rotated. Fails where OS trust is unsupported. Trust it with sandbox.proxyCa.trust: the CLI trusts only the hosts in the saved user settings, so it refuses a certificate authority that also covers hosts from sandboxConfig. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture create(SandboxProxyCaCreate } /** - * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * Replaces the persistent certificate authority of the sandbox credential proxy with a new one for the current credential hosts. If OS trust included the old one, removes it and trusts the new one, which can show an OS authentication prompt. Running sandboxed tools keep the old certificate authority until they restart. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture rotate(SandboxProxyCaRotate } /** - * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * Adds the persistent certificate authority of the sandbox credential proxy to OS trust, so sandboxed clients that read only OS trust accept the proxy. Call create first. Refuses a certificate authority that is not constrained to the current credential hosts. Can show an OS authentication prompt. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -71,7 +71,7 @@ public CompletableFuture trust(SandboxProxyCaTrustPar } /** - * Status of the persistent certificate authority of the sandbox credential proxy. + * Removes the persistent certificate authority of the sandbox credential proxy from OS trust. Keeps the stored certificate authority. Can show an OS authentication prompt. Sandboxed clients that read only OS trust then reject the proxy; clients that read the per-process certificate bundle continue to work. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSecretsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSecretsApi.java index 7f17687818..ebdcac5e9e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSecretsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSecretsApi.java @@ -27,7 +27,7 @@ public final class ServerSecretsApi { } /** - * Secret values to add to the redaction filter. + * Registers secret values for redaction in session logs and exports. The SDK calls this to inject dynamically generated secret values (e.g., OIDC tokens). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionFsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionFsApi.java index 872702997a..ff48cdfb76 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionFsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionFsApi.java @@ -27,7 +27,7 @@ public final class ServerSessionFsApi { } /** - * Initial working directory, session-state path layout, and path conventions used to register the calling SDK client as the session filesystem provider. A registered provider is authoritative for path interpretation and filesystem facts used by workspace permission validation. Paths are interpreted lexically; home-relative paths (`~` and `~/...`) and Windows drive-relative paths such as `C:foo` are unsupported. Until provider-side canonicalization is supported, providers must not expose symlinks inside allowed roots that escape those roots. + * Registers an SDK client as the session filesystem provider. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java index 0b7f099245..fd963c0cbd 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java @@ -28,7 +28,7 @@ public final class ServerSessionsApi { } /** - * Open a session by creating, resuming, attaching, connecting to a remote, or handing off. + * Creates or resumes a local session and returns the opened session ID. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -39,7 +39,7 @@ public CompletableFuture open(SessionsOpenParams params) { } /** - * Source session identifier to fork from, optional event-ID boundary, and optional friendly name for the new session. + * Creates a new session by forking persisted history from an existing session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -50,7 +50,7 @@ public CompletableFuture fork(SessionsForkParams params) { } /** - * Remote session connection parameters. + * Connects to an existing remote session and exposes it as an SDK session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -61,7 +61,7 @@ public CompletableFuture connect(SessionsConnectParams pa } /** - * Optional source filter, metadata-load limit, and context filter applied to the returned sessions. + * Lists sessions, optionally filtered by source and working-directory context. Returned entries are discriminated by `isRemote`: local entries carry only the lightweight `LocalSessionMetadataValue` shape; remote entries carry the full `RemoteSessionMetadataValue` shape (repository, PR number, taskType, etc.). *

* Invokes the method with no params, applying the runtime defaults. * @@ -74,7 +74,7 @@ public CompletableFuture list() { } /** - * Optional source filter, metadata-load limit, and context filter applied to the returned sessions. + * Lists sessions, optionally filtered by source and working-directory context. Returned entries are discriminated by `isRemote`: local entries carry only the lightweight `LocalSessionMetadataValue` shape; remote entries carry the full `RemoteSessionMetadataValue` shape (repository, PR number, taskType, etc.). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -85,7 +85,7 @@ public CompletableFuture list(SessionsListParams params) { } /** - * Session ID whose persisted metadata should be read. + * Reads lightweight persisted metadata for one local session without opening it. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -96,7 +96,7 @@ CompletableFuture getMetadata(SessionsGetMetadataPara } /** - * Bounded batch request for client-owned metadata from persisted local sessions. + * Reads client-owned metadata for multiple persisted local sessions without opening them. Results preserve request order and report missing, corrupt, unsupported, or temporarily unavailable sessions independently. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -107,7 +107,7 @@ public CompletableFuture> getClientMetadata(SessionsGetClientMetada } /** - * Pagination options for reading an inactive or active local session's persisted event journal. + * Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The first read pins the currently opened journal generation and its byte-length boundary; opaque cursor continuations remain on that generation across runtime-owned compaction, truncation, and rewrite operations, which replace the live path atomically, and events appended after the boundary are excluded. For cold hydration, await the first successful page before activation and establish lossless live-event buffering before resume; merge subsequent live events by ID, preserving persisted order and letting live payloads win. Continuations are process-local, single-use capabilities bound to the originating session and storage context and must be paged sequentially; concurrent or repeated use of the same cursor expires that duplicate read rather than reading the generation twice. A complete snapshot has cursorStatus 'ok' and hasMore false. Snapshots expire after five idle minutes, with at most eight retained per process and idle-only eviction under pressure; completion and cancelled-worker exit release their handles. No transcript copy is created, but retained handles may keep replaced files' disk blocks alive until release. Pages have a soft 1 MiB serialized event-array budget including resolved binary assets; one oversized event is returned alone to guarantee progress. Working memory also includes a record/lookahead and asset resolution; resolving the first binary reference may scan the full pinned generation to build a bounded offset index. If the snapshot expires, is evicted, is cancelled before a continuation is established, or becomes unreadable after an observable unsupported in-place shortening, the continuation returns cursorStatus 'expired' with an empty terminal page and never falls back to a different generation. A missing or initially unreadable journal is an RPC error. Persisted history excludes ephemeral events and may omit payloads that are reconstructed only for an active session; use the active session event stream for post-resume live events. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -118,7 +118,7 @@ public CompletableFuture readPersistedEvents( } /** - * Limit for non-empty local session IDs. + * Lists recent local session IDs that contain user-visible history, omitting housekeeping-only sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -129,7 +129,7 @@ CompletableFuture listNonEmptySessionIds(S } /** - * GitHub task ID to look up. + * Finds the local session bound to a GitHub task ID, if any. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -140,7 +140,7 @@ public CompletableFuture findByTaskId(SessionsFindBy } /** - * UUID prefix to resolve to a unique session ID. + * Resolves a UUID prefix to a unique session ID, if exactly one session matches. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -151,7 +151,7 @@ public CompletableFuture findByPrefix(SessionsFindBy } /** - * Optional working-directory context used to score session relevance. + * Returns the most-relevant prior session for a given working-directory context. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -162,7 +162,7 @@ public CompletableFuture getLastForContext(Sess } /** - * Session ID whose event-log file path to compute. + * Computes the absolute path to a session's persisted events.jsonl file. Internal: filesystem paths are only meaningful in-process (CLI and runtime share a filesystem). Currently used by the CLI's contribution-graph feature to read historical events directly. Remote SDK consumers must not depend on this; a proper event-query API would replace it if the contribution graph ever needed to work over the wire. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -173,7 +173,7 @@ CompletableFuture getEventFilePath(SessionsGetEv } /** - * Map of sessionId -> on-disk size in bytes for each session's workspace directory. + * Returns the on-disk byte size of each session's workspace directory. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -184,7 +184,7 @@ public CompletableFuture getSizes() { } /** - * Session IDs to test for live in-use locks. + * Returns the subset of the supplied session IDs that are currently held by another running process. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -195,7 +195,7 @@ public CompletableFuture checkInUse(SessionsCheckInUse } /** - * Session ID to look up the persisted remote-steerable flag for. + * Returns a session's persisted remote-steerable flag, if any has been recorded. Internal: this is CLI-specific book-keeping used by `--continue` / `--resume` to inherit the prior session's remote-steerable preference. SDK consumers that want similar behavior should manage their own persistence around start/stop calls rather than relying on this runtime-side flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -206,7 +206,7 @@ CompletableFuture getPersistedRemoteS } /** - * Session ID to close. + * Closes a session: emits shutdown, flushes pending events, releases the in-use lock, and disposes the active session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -217,7 +217,7 @@ public CompletableFuture close(SessionsCloseParams params) { } /** - * Session IDs to close, deactivate, and delete from disk. + * Closes, deactivates, and deletes a set of sessions, returning the bytes freed per session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -228,7 +228,7 @@ public CompletableFuture bulkDelete(SessionsBulkDelete } /** - * Session ID to delete from disk. + * Deletes one local session from disk after running the same lifecycle hooks as the session manager. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -239,7 +239,7 @@ CompletableFuture delete(SessionsDeleteParams params) { } /** - * Age threshold and optional flags controlling which old sessions are pruned (or simulated when dryRun is true). + * Deletes sessions older than the given threshold, with optional dry-run and exclusion list. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -250,7 +250,7 @@ public CompletableFuture pruneOld(SessionsPruneOldParams } /** - * Session ID whose pending events should be flushed to disk. + * Flushes a session's pending events to disk. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -261,7 +261,7 @@ public CompletableFuture save(SessionsSaveParams params) { } /** - * Session ID whose in-use lock should be released. + * Releases the in-use lock held by this process for a session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -272,7 +272,7 @@ public CompletableFuture releaseLock(SessionsReleaseLockParams params) { } /** - * Session metadata records to enrich with summary and context information. + * Backfills missing summary and context fields on the supplied session metadata records. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -283,7 +283,40 @@ public CompletableFuture enrichMetadata(SessionsEn } /** - * Active session ID and an optional flag for deferring repo-level hooks until folder trust. + * Creates the workspace record for a session that has not been opened yet. A host that hands a session off to another application — writing the record and then launching that application against the session ID — needs the record on disk before any session exists to carry it, which the session-scoped workspace methods cannot do. Replaces any existing record and resets the checkpoint index. When writing to the local filesystem, a stored `fork_count` survives on disk. Returns the record it built, so a surviving stored `fork_count` can differ from the answer. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture createWorkspace(SessionsCreateWorkspaceParams params) { + return caller.invoke("sessions.createWorkspace", params, SessionsCreateWorkspaceResult.class); + } + + /** + * Reads a session's workspace record straight from disk, without opening the session. Resuming by session ID has to know where the session lives before it can connect, so the lookup cannot come from the session-scoped workspace methods, which resolve their location from a live session's context. Returns no record when the file is absent. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture loadWorkspace(SessionsLoadWorkspaceParams params) { + return caller.invoke("sessions.loadWorkspace", params, SessionsLoadWorkspaceResult.class); + } + + /** + * Merges fields into a session's workspace record on disk, creating the record when it is absent. The counterpart to `sessions.loadWorkspace`, for the same before-the-session-exists case. It preserves stored workspace-schema fields the request does not supply, does not preserve stored keys outside the workspace schema, and never replaces a stored `fork_count`. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture updateWorkspaceFields(SessionsUpdateWorkspaceFieldsParams params) { + return caller.invoke("sessions.updateWorkspaceFields", params, Void.class); + } + + /** + * Reloads user, plugin, and (optionally) repo hooks on the active session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -294,7 +327,7 @@ public CompletableFuture reloadPluginHooks(SessionsReloadPluginHooksParams } /** - * Active session ID whose deferred repo-level hooks should be loaded. + * Loads previously-deferred repo-level hooks on the active session, returning queued startup prompts. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -305,7 +338,7 @@ public CompletableFuture loadDeferredRepoHo } /** - * Manager-wide additional plugins to register; replaces any previously-configured set. + * Replaces the manager-wide additional plugins registered with the session manager. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -316,7 +349,7 @@ public CompletableFuture setAdditionalPlugins(SessionsSetAdditionalPlugins } /** - * Session ID whose board entry count should be returned. + * Gets the dynamic-context board entry count associated with a session, when available. Internal: this exists solely so CLI telemetry events (`rem_spawn_gate`, `rem_consolidation_complete`) can pair START / END board counts around the detached rem-agent spawn. "Dynamic context board" is a runtime-internal concept that is not part of the public SDK contract; the long-term plan is to relocate the telemetry emission into the runtime so this method can be deleted entirely. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -327,7 +360,7 @@ CompletableFuture getBoardEntryCount(SessionsG } /** - * Parameters for attaching the remote-control singleton to a session. + * Attaches the runtime-managed remote-control singleton to a session, awaiting initial setup. If remote control is already attached to a different session, the singleton is transferred (preserving the underlying Mission Control connection). Returns the final status. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -338,7 +371,7 @@ public CompletableFuture startRemoteControl(Se } /** - * Parameters for atomically rebinding the remote-control singleton. + * Atomically rebinds the remote-control singleton to a different session, preserving the underlying Mission Control connection. When `expectedFromSessionId` is provided and does not match the singleton's current `attachedSessionId`, the transfer is rejected with `transferred: false` and the current status is returned unchanged. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -349,7 +382,7 @@ public CompletableFuture transferRemoteCont } /** - * Patch for the singleton's steering state. + * Patches the steering state of the active remote-control singleton. When remote control is off, this is a no-op and the off status is returned. Today only `enabled: true` is actionable on the underlying exporter; passing `false` is reserved for future use. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -360,7 +393,7 @@ public CompletableFuture setRemoteContro } /** - * Parameters for stopping the remote-control singleton. + * Stops the remote-control singleton. When `expectedSessionId` is provided and does not match the singleton's current `attachedSessionId`, the stop is rejected with `stopped: false` and the current status is returned unchanged (unless `force` is set, in which case the singleton is unconditionally torn down). *

* Invokes the method with no params, applying the runtime defaults. * @@ -373,7 +406,7 @@ public CompletableFuture stopRemoteControl() { } /** - * Parameters for stopping the remote-control singleton. + * Stops the remote-control singleton. When `expectedSessionId` is provided and does not match the singleton's current `attachedSessionId`, the stop is rejected with `stopped: false` and the current status is returned unchanged (unless `force` is set, in which case the singleton is unconditionally torn down). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -384,7 +417,7 @@ public CompletableFuture stopRemoteControl(Sess } /** - * Wrapper for the singleton's current status. + * Returns the current state of the remote-control singleton, including the attached session id and frontend URL when active. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -395,7 +428,7 @@ public CompletableFuture getRemoteControlS } /** - * Params to attach or detach an in-process ExtensionController delegate. + * Attaches (or detaches) an in-process ExtensionController delegate for the given session in a local host adapter. Pass `controller: undefined` to detach. Internal because the controller cannot cross the JSON-RPC boundary; the runtime manages its own session extension service. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsApi.java index 2f77e8da88..30b35346bb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsApi.java @@ -34,7 +34,7 @@ public final class ServerSkillsApi { } /** - * Side-effect-free planning of one verified Agent Finder Skill candidate. + * Plans installation of a verified Agent Finder Skill candidate without writing files. The returned review is safe to present to a user and installing always leaves the Skill disabled until separately enabled. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -45,7 +45,7 @@ public CompletableFuture planInstall(SkillsPl } /** - * Applies exactly one retained verified Skill installation plan. + * Consumes one verified Skill installation plan, requests explicit human consent through installations.confirm on the original connection, then revalidates and installs the Skill disabled. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -56,7 +56,7 @@ public CompletableFuture applyInstall(SkillsApplyInstal } /** - * Read-only preparation of one owned Skill removal under fresh selected-session authority. + * Prepares a read-only removal plan for an owned verified Agent Finder Skill installation. Uninstall planning is never gated by the Skill-install feature flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -67,7 +67,7 @@ public CompletableFuture planUninstall(Skills } /** - * One-use application of the exact retained Skill removal plan. + * Consumes an owned Skill removal plan, requests explicit human consent through installations.confirm, refuses drift, and removes the exact owned files through quarantine. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -78,7 +78,7 @@ public CompletableFuture applyUninstall(SkillsApplyUnin } /** - * Optional project paths and additional skill directories to include in discovery. + * Discovers skills across global and project sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -89,7 +89,7 @@ public CompletableFuture discover(SkillsDiscoverParams par } /** - * Optional project paths to enumerate. + * Returns the canonical directories where a client may create skills that the runtime will recognize, including ones that do not exist yet. Project directories become active once created. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsConfigApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsConfigApi.java index c29770bf14..35cf2e2ef1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsConfigApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsConfigApi.java @@ -27,7 +27,7 @@ public final class ServerSkillsConfigApi { } /** - * Skill names to mark as disabled in global configuration, replacing any previous list. + * Replaces the global list of disabled skills. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture setDisabledSkills(SkillsConfigSetDisabledSkillsPa } /** - * Adds or removes a single skill from the global disabled list, leaving every other entry untouched. + * Atomically adds or removes one skill from the disabled list. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsInstallationsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsInstallationsApi.java index e0d0afe72a..51029bc363 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsInstallationsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsInstallationsApi.java @@ -27,7 +27,7 @@ public final class ServerSkillsInstallationsApi { } /** - * Inventory request under an explicitly selected existing session. + * Lists owned verified Agent Finder Skill installations for the selected existing session. Listing is never gated by the Skill-install feature flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture list(SkillsInstallat } /** - * Inventory request under an explicitly selected existing session. + * Reconciles interrupted owned Skill installation work for the selected existing session, then inspects owned inventory. Recovery is never gated by the Skill-install feature flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture recover(SkillsInstal } /** - * Existing-operation control. A new session selector is deliberately not accepted. + * Inspects a known Skill installation operation on its original runtime connection. Status is never gated by the Skill-install feature flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture status(SkillsInstall } /** - * Existing-operation control. A new session selector is deliberately not accepted. + * Requests cancellation of a known Skill installation operation before commit. Already-started durable work requires recovery instead of silent replay. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -71,7 +71,7 @@ public CompletableFuture cancel(SkillsInstall } /** - * Persisted enablement update for one owned Skill installation. + * Atomically persists enablement for one owned Agent Finder Skill and reconciles the selected bound session. Enablement is installation-scoped by receipt identity and is never gated by the Skill-install feature flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerToolsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerToolsApi.java index 2938010010..6f3b3b8759 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerToolsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerToolsApi.java @@ -27,7 +27,7 @@ public final class ServerToolsApi { } /** - * Optional model identifier whose tool overrides should be applied to the listing. + * Lists built-in tools available for a model. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerUserSettingsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerUserSettingsApi.java index 665cfb107a..9c5656ff14 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerUserSettingsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerUserSettingsApi.java @@ -27,18 +27,7 @@ public final class ServerUserSettingsApi { } /** - * Invokes {@code user.settings.reload}. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ - @CopilotExperimental - public CompletableFuture reload() { - return caller.invoke("user.settings.reload", java.util.Map.of(), Void.class); - } - - /** - * Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. + * Lists every known user setting from settings.json, each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,14 +38,14 @@ public CompletableFuture get() { } /** - * Partial user settings to write to settings.json. Each top-level key is written individually, replacing the existing value; a key whose value is null is removed. + * Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture set(UserSettingsSetParams params) { - return caller.invoke("user.settings.set", params, UserSettingsSetResult.class); + public CompletableFuture set(UserSettingsSetParams params) { + return caller.invoke("user.settings.set", params, Void.class); } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsApi.java index 76b3fdc7e0..2b4f752fdf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsApi.java @@ -35,7 +35,7 @@ public final class SessionAccountsApi { } /** - * Enumerate request carrying the typed collection query. + * Enumerate a typed accounts collection: the signed-in accounts, or the providers offered for interactive login. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -51,7 +51,7 @@ public CompletableFuture enumerate(SessionAccountsEnumerateP } /** - * Read request carrying the typed datum query. + * Read one typed accounts datum: the active account, a neutral status summary, or the last authentication errors. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -67,7 +67,7 @@ public CompletableFuture get(SessionAccountsGetParams params) { } /** - * Mutation request carrying the typed write command. + * Apply one non-interactive accounts mutation: switch the active account, log an account out, or set credentials from a token. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginApi.java index 5537a0dad2..24e3c6a642 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginApi.java @@ -31,7 +31,7 @@ public final class SessionAccountsLoginApi { } /** - * Begin an interactive login flow for a provider kind. Dispatch is kind-only. + * Begin an interactive login flow for a provider kind (dispatch is kind-only) and return its opaque flow id and first step. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture begin(SessionAccountsL } /** - * Advance an in-flight login flow, optionally fulfilling an input-required step. + * Advance an in-flight login flow, optionally fulfilling an input-required step, and return the next step. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture advance(SessionAccountsLoginAdvanceParam } /** - * Cancel an in-flight login flow. + * Cancel an in-flight login flow and release its resources. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAgentApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAgentApi.java index d2499fe3a5..0030c622d0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAgentApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAgentApi.java @@ -31,7 +31,7 @@ public final class SessionAgentApi { } /** - * Controls whether built-in agents and authored prompt text are included. + * Lists agents available to the session. Defaults to custom agents only; pass includeBuiltInAgents to include the effective built-in agents. *

* Invokes the method with no params, applying the runtime defaults. * @@ -44,7 +44,7 @@ public CompletableFuture list() { } /** - * Controls whether built-in agents and authored prompt text are included. + * Lists agents available to the session. Defaults to custom agents only; pass includeBuiltInAgents to include the effective built-in agents. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -60,7 +60,7 @@ public CompletableFuture list(SessionAgentListParams par } /** - * An in-memory authored prompt override for an available agent. + * Sets an in-memory authored prompt override for an available agent. For built-in agents, this replaces only the static base prompt while preserving runtime-owned dynamic prompt composition and behavior. The special `general-purpose` agent is not overrideable. Overrides are not persisted; resumed and forked sessions start without them, so the host must re-apply them. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture setPrompt(SessionAgentSetPromptParams params) { } /** - * Identifies the target session. + * Gets the currently selected custom agent for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -87,7 +87,7 @@ public CompletableFuture getCurrent() { } /** - * Name of the custom agent to select for subsequent turns. + * Selects a custom agent for subsequent turns in the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -103,7 +103,7 @@ public CompletableFuture select(SessionAgentSelectPara } /** - * Identifies the target session. + * Clears the selected custom agent and returns the session to the default agent. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -114,7 +114,7 @@ public CompletableFuture deselect() { } /** - * Identifies the target session. + * Reloads custom agent definitions and returns the refreshed list. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java index ab4058e6b6..8edc125d7e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java @@ -29,7 +29,7 @@ public final class SessionAutopilotObjectiveApi { } /** - * Identifies the target session. + * Reads the current canonical autopilot objective state for this session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasActionApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasActionApi.java index 8c6ce4b79e..3e41100db6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasActionApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasActionApi.java @@ -31,7 +31,7 @@ public final class SessionCanvasActionApi { } /** - * Canvas action invocation parameters. + * Invokes an action on an open canvas instance. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasApi.java index 2fce433e5c..fdcee24883 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasApi.java @@ -38,7 +38,7 @@ public final class SessionCanvasApi { } /** - * Identifies the target session. + * Lists canvases declared for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture list() { } /** - * Identifies the target session. + * Lists currently open canvas instances for the live session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture listOpen() { } /** - * Canvas open parameters. + * Opens or focuses a canvas instance. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture open(SessionCanvasOpenParams p } /** - * Canvas close parameters. + * Closes an open canvas instance. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderApi.java index c68c050f75..90a80dfce0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderApi.java @@ -31,7 +31,7 @@ final class SessionCanvasProviderApi { } /** - * Internal canvas provider registration parameters. + * Registers an internal canvas provider connection and its contributions. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ CompletableFuture register(SessionCanvasProviderRegisterParams params) { } /** - * Internal canvas provider unregistration parameters. + * Unregisters an internal canvas provider connection. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsApi.java index 6be3682233..55cd139ae5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsApi.java @@ -31,7 +31,7 @@ public final class SessionCommandsApi { } /** - * Optional filters controlling which command sources to include in the listing. + * Lists slash commands available in the session. *

* Invokes the method with no params, applying the runtime defaults. * @@ -44,7 +44,7 @@ public CompletableFuture list() { } /** - * Optional filters controlling which command sources to include in the listing. + * Lists slash commands available in the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -60,7 +60,7 @@ public CompletableFuture list(SessionCommandsListPara } /** - * Slash command name and optional raw input string to invoke. + * Invokes a slash command in the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture invoke(SessionCommandsInv } /** - * The pending slash-command invocation effect to finalize, plus whether the host applied or cancelled it. + * Finalizes persistence associated with a client-applied slash-command effect. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -92,7 +92,7 @@ CompletableFuture finalizeInvocationEffe } /** - * Pending command request ID and an optional error if the client handler failed. + * Reports completion of a pending client-handled slash command. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -108,7 +108,7 @@ public CompletableFuture handlePendin } /** - * Slash command name and argument string to execute synchronously. + * Executes a slash command synchronously and returns any error. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -124,7 +124,7 @@ public CompletableFuture execute(SessionCommandsEx } /** - * Slash-prefixed command string to enqueue for FIFO processing. + * Enqueues a slash command for FIFO processing on the local session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -140,7 +140,7 @@ public CompletableFuture enqueue(SessionCommandsEnqueuePar } /** - * Queued-command request ID and the result indicating whether the host executed it (and whether to stop processing further queued commands). + * Reports whether the host actually executed a queued command and whether to continue processing. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCompletionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCompletionsApi.java index 6b9d8aa252..f9fa5d1dae 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCompletionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCompletionsApi.java @@ -31,7 +31,7 @@ public final class SessionCompletionsApi { } /** - * Identifies the target session. + * Gets the characters that should trigger host-driven completions for the session. Empty disables host-driven completions (e.g. local sessions, or a relay host that does not advertise them). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture getTrigge } /** - * Request host-driven completions for the current composer input. + * Requests host-driven completion items for the current composer input. Returns an empty list when the host has no items or does not support completions. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectedIdeInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectedIdeInfo.java new file mode 100644 index 0000000000..9a2f0bce74 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectedIdeInfo.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.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * The IDE a host is connected to, as reported to the session. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionConnectedIdeInfo( + /** Display name of the connected IDE, for example `VS Code`. */ + @JsonProperty("ideName") String ideName, + /** Absolute path of the workspace folder the IDE has open. */ + @JsonProperty("workspaceFolder") String workspaceFolder +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java index e192274a87..e69fc59d23 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java @@ -32,7 +32,7 @@ public final class SessionConnectorsApi { } /** - * Identifies the target session. + * Returns feature availability and bounded polling limits for the EXPERIMENTAL session connector API. This method never performs a Connector service request. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -43,7 +43,7 @@ public CompletableFuture getCapabilities } /** - * Identifies the target session. + * Returns the session account selection, or null. * * @return a future that completes with the {@code ConnectorSessionAccount} value, * or {@code null} when the result is absent. Callers must handle the @@ -58,7 +58,7 @@ public CompletableFuture getAccount() { } /** - * Identifies the target session. + * Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -69,7 +69,7 @@ public CompletableFuture getStatus() { } /** - * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + * Returns the cached Connector catalog for the pinned opaque account selection, fetching it only when this session has no cached catalog. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -85,7 +85,7 @@ public CompletableFuture list(SessionConnectorsList } /** - * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + * Refreshes and validates the Connector catalog for the pinned opaque account selection. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -101,7 +101,7 @@ public CompletableFuture refresh(SessionConnecto } /** - * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * Initiates an idempotent Connector connection request without opening a browser. Returns connected when the service is immediately authoritative, consent_required with a validated URL, or pending with an opaque continuation ID. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -117,7 +117,7 @@ public CompletableFuture connect(SessionConnectorsConnec } /** - * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * Re-initiates an idempotent Connector connection request without browser or UI effects, with the same typed outcomes as connect. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -133,7 +133,7 @@ public CompletableFuture reconnect(SessionConnectorsReco } /** - * Explicitly bounded continuation of a pending Connector connection. + * Continues a pending Connector connection with caller-supplied attempt, interval, and deadline bounds. The runtime never opens the returned consent URL. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -149,7 +149,7 @@ public CompletableFuture continueConnection(SessionConne } /** - * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * Disconnects one Connector for the pinned opaque account selection, refreshes the authoritative catalog, and removes its session-owned MCP projection. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -165,7 +165,7 @@ public CompletableFuture disconnect(SessionCo } /** - * Requests authoritative Connector-to-MCP reconciliation for the pinned account. + * Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -181,7 +181,7 @@ public CompletableFuture reconcile(SessionConn } /** - * Requests authoritative Connector-to-MCP reconciliation for the pinned account. + * Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. *

* Accepts the extensible request, including inputs added after the params record. * @@ -196,7 +196,7 @@ public CompletableFuture reconcile(SessionConn } /** - * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + * Reconciles the authoritative Connector catalog into the session MCP projection during startup with a bounded deadline and fail-closed cleanup. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -212,7 +212,7 @@ CompletableFuture reconcileForStartup(SessionConnectorsReconcil } /** - * Identifies the target session. + * Removes the runtime-owned Connector MCP projection without changing service-side connections. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionContentExclusionApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionContentExclusionApi.java index eb621e6b49..afddaed971 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionContentExclusionApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionContentExclusionApi.java @@ -31,7 +31,7 @@ public final class SessionContentExclusionApi { } /** - * Local file system absolute paths within the session working directory to check against its content-exclusion policy. + * Checks local file system absolute paths within the session working directory against its content-exclusion policy. Results preserve input order. Unsupported paths/filesystems and unavailable policy evaluation return available false, and callers must treat every requested path as excluded. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java index ee9134f803..b0183065b0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java @@ -29,7 +29,7 @@ public final class SessionCustomizationsApi { } /** - * Identifies the target session. + * For local sessions, reconciles repository context and discovered instructions, plugins, skills, agents, hooks, MCP servers, and extensions after files appear or change under the working directory. Independent component failures are returned in outcomes and errors; a rejected call can have partially applied earlier steps. Remote sessions must reload on their agent host instead. The model-facing context is rebuilt on the next turn. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDebugApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDebugApi.java index b2cd030183..ed06730c49 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDebugApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDebugApi.java @@ -31,7 +31,7 @@ public final class SessionDebugApi { } /** - * Options for collecting a session debug bundle with configurable redaction. + * Collects a session debug log bundle into a local archive or staging directory. Logs are redacted by default; redaction can be configured per caller-provided diagnostic entry. The runtime includes session-owned logs by default and accepts caller-provided diagnostic entries so host applications can add their own files without changing this API shape. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDiagnosticsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDiagnosticsApi.java index af4c57837b..d263853aec 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDiagnosticsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDiagnosticsApi.java @@ -31,7 +31,7 @@ public final class SessionDiagnosticsApi { } /** - * Patch session diagnostic thresholds for explicitly supplied sources. + * Patches configured session diagnostic sources without restarting their producers. Setting a source level to off clears its retained diagnostics and invalidates cursors selecting that source. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture configure(SessionDia } /** - * Cursor-based request for session diagnostics. The default limit is 100 (maximum 500); the default waitMs is zero (maximum 30000). + * Reads a bounded batch of retained session diagnostics for the selected sources. Records are never consumed and each reader advances independently through its opaque cursor. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogApi.java index 8ad9b3b1f5..87ad200719 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogApi.java @@ -31,7 +31,7 @@ public final class SessionEventLogApi { } /** - * Cursor, batch size, and optional long-poll/filter parameters for reading session events. + * Reads a batch of session events from a cursor, optionally waiting for new events. Supports tail-first reads via `direction: backward`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture read(SessionEventLogReadPara } /** - * Identifies the target session. + * Returns a snapshot of the current tail cursor without consuming events. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -58,7 +58,7 @@ public CompletableFuture tail() { } /** - * Event type to register consumer interest for, used by runtime gating logic. + * Registers consumer interest in an event type for runtime gating purposes. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture registerInterest } /** - * Opaque handle previously returned by `registerInterest` to release. + * Releases a consumer's previously-registered interest in an event type. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsApi.java index e21c109684..d4d5837dcc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsApi.java @@ -31,7 +31,7 @@ public final class SessionExtensionsApi { } /** - * Identifies the target session. + * Lists extensions discovered for the session and their current status. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture list() { } /** - * Source-qualified extension identifier to enable for the session. + * Enables an extension for the session and persists the preference when the session has a settings store. Hosts synchronizing effective membership should use extensions.reconcile instead. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture enable(SessionExtensionsEnableParams params) { } /** - * Source-qualified extension identifier to disable for the session. + * Disables an extension for the session and persists the preference when the session has a settings store. Hosts synchronizing effective membership should use extensions.reconcile instead. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture disable(SessionExtensionsDisableParams params) { } /** - * Identifies the target session. + * Reloads extension definitions and processes for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -85,7 +85,18 @@ public CompletableFuture reload() { } /** - * Parameters for session.extensions.sendAttachmentsToMessage. + * Host-only reconciliation of authoritative session-effective extension membership and enablement. Refreshes runtime-owned discovery and preferences without persisting settings, installing plugins, or restarting unchanged activations. Returns ExtensionList only after required starts and process/contribution cleanup settle. Takes no caller inventory or overrides. Missing controllers, unready/incomplete discovery, unavailable workspaces, superseded inputs, and lifecycle failures are errors, not empty membership. Independently proven revocations may be applied before an error; retry converges without restarting healthy activations. Error data contains lifecycleChangesApplied and code: extension_reconciliation_host_required, extension_reconciliation_unavailable, extension_reconciliation_not_ready, extension_reconciliation_discovery_failed, extension_reconciliation_workspace_unavailable, extension_reconciliation_superseded, or extension_reconciliation_lifecycle_failed. Mark host reconciliation state applied only on success. On older runtimes, method-not-found must not fall back to global discovery and persistent extension disables. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture reconcile() { + return caller.invoke("session.extensions.reconcile", java.util.Map.of("sessionId", this.sessionId), SessionExtensionsReconcileResult.class); + } + + /** + * Push attachments into the next user-message turn from an extension. The host should surface them as composer pills and forward them via the next session.send call. Callable only by extension-owned connections. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsReconcileParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsReconcileParams.java new file mode 100644 index 0000000000..5e6ac8ef1b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsReconcileParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionExtensionsReconcileParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsReconcileResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsReconcileResult.java new file mode 100644 index 0000000000..7d1472c947 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsReconcileResult.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.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Extensions discovered for the session, with their current status. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionExtensionsReconcileResult( + /** Discovered extensions and their current status */ + @JsonProperty("extensions") List extensions +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFleetApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFleetApi.java index ca855b70a9..1c5730cd88 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFleetApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFleetApi.java @@ -31,7 +31,7 @@ public final class SessionFleetApi { } /** - * Parameters for starting fleet orchestration: an optional user prompt combined with the fleet instructions, plus the send options forwarded to the resulting turn. + * Starts fleet mode by submitting the fleet orchestration prompt to the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthApi.java index 94a3b8e6d0..388576154a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthApi.java @@ -32,7 +32,7 @@ public final class SessionGitHubAuthApi { } /** - * Identifies the target session. + * Gets authentication status and account metadata for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -43,7 +43,7 @@ public CompletableFuture getStatus() { } /** - * New auth credentials to install on the session. Omit to leave credentials unchanged. + * Updates the session's auth credentials used for outbound model and API requests. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -59,7 +59,7 @@ public CompletableFuture setCredentials(S } /** - * Identifies the target session. + * Gets the current authentication information for internal session hosts. * * @return a future that completes with the {@code AuthIdentity} value, * or {@code null} when the result is absent. Callers must handle the @@ -74,7 +74,7 @@ CompletableFuture getCurrentAuthInfo() { } /** - * Identifies the target session. + * Gets all authentication accounts available to the internal session host. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -85,7 +85,7 @@ CompletableFuture> getAllAuthAvailable() { } /** - * Identifies the target session. + * Refreshes Copilot account metadata for the current authentication. * * @return a future that completes with the {@code AuthIdentity} value, * or {@code null} when the result is absent. Callers must handle the @@ -100,7 +100,7 @@ CompletableFuture refreshCopilotUser() { } /** - * Internal GitHub login parameters. + * Logs in a GitHub user through the internal session host. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -116,7 +116,7 @@ CompletableFuture login(SessionGitHubAuthLoginParams params) { } /** - * Parameters for switching the session's active authentication. + * Switches the session to another available authentication. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -132,7 +132,7 @@ CompletableFuture switchToAuth(SessionGitHubAuthSwitchToAuthParams params) } /** - * Identifies the target session. + * Logs out the session's current GitHub authentication. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -143,7 +143,7 @@ CompletableFuture logout() { } /** - * Parameters identifying a GitHub authentication to log out. + * Logs out a specific GitHub authentication. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -159,7 +159,7 @@ CompletableFuture logoutUser(SessionGitHubAuthLogoutUserParams params) { } /** - * Identifies the target session. + * Gets validation errors from the most recent authentication attempt. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionHistoryApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionHistoryApi.java index ad44d864de..ccfbfbd17f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionHistoryApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionHistoryApi.java @@ -31,7 +31,7 @@ public final class SessionHistoryApi { } /** - * Optional compaction parameters. + * Compacts the session history to reduce context usage. *

* Invokes the method with no params, applying the runtime defaults. * @@ -44,7 +44,7 @@ public CompletableFuture compact() { } /** - * Optional compaction parameters. + * Compacts the session history to reduce context usage. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -60,7 +60,7 @@ public CompletableFuture compact(SessionHistoryComp } /** - * Identifier of the event to truncate to; this event and all later events are removed. + * Truncates persisted session history to a specific event. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture truncate(SessionHistoryTr } /** - * Identifies the target session. + * Lists the user turns that the session can rewind to. Never rejects for a busy session: rewind reads need the session's file-change captures to be settled, so a session that still holds active work answers with `unavailableReason: "session-busy"` and no points, which the caller can retry. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -87,7 +87,7 @@ public CompletableFuture listRewindPoints( } /** - * Event boundary to preview for conversation-and-files rewind. + * Previews the files that a conversation-and-files rewind would restore. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -103,7 +103,7 @@ public CompletableFuture previewRewind(Sessio } /** - * Boundary and mode for rewinding session history. + * Rewinds the session conversation, optionally restoring files changed by the discarded turns. Not crash-atomic: file restore and conversation truncation are separate stores, applied in that order, so a process crash between them can leave the workspace rewound while the conversation still contains the discarded turns. There is no recovery journal; re-running the same rewind is the recovery path for a crash before truncation lands, since file restore is idempotent (already-restored files are reported as skipped) and truncation is re-derived from the still-retained boundary event. After truncation lands that boundary no longer exists, so the same request is rejected; the only stage that can still be outstanding is snapshot pruning, whose failure leaves orphan snapshots the capture store tolerates. The reverse inconsistency cannot occur, because truncation is never applied before file restore succeeds. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -119,7 +119,7 @@ public CompletableFuture rewind(SessionHistoryRewind } /** - * Identifies the target session. + * Cancels any in-progress background compaction on a local session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -130,7 +130,7 @@ public CompletableFuture cancelB } /** - * Identifies the target session. + * Aborts any in-progress manual compaction on a local session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -141,7 +141,7 @@ public CompletableFuture abortManualC } /** - * Identifies the target session. + * Produces a markdown summary of the session's conversation context for hand-off scenarios. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -152,7 +152,7 @@ public CompletableFuture summarizeForHa } /** - * Parameters for clearing the conversation and seeding the window that replaces it. + * Clears the session's conversation history, keeping only system and developer messages, and seeds the fresh context window with a first user message. Must be called from inside a tool handler: the clear has to drop the results of the tool calls its wipe orphans, and it rejects when no tool call is in flight. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java index 0a1cf9ba21..1252fa8f59 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java @@ -29,7 +29,7 @@ public final class SessionInstructionsApi { } /** - * Identifies the target session. + * Gets instruction sources loaded for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -40,7 +40,7 @@ public CompletableFuture getSources() { } /** - * Identifies the target session. + * For local sessions, invalidates instruction discovery and the model-facing prompt, then returns freshly discovered sources. The updated prompt takes effect on the next turn. Remote sessions must reload on their agent host instead. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLimitPredictionApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLimitPredictionApi.java index a64f82afae..b28b54bd37 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLimitPredictionApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLimitPredictionApi.java @@ -31,7 +31,7 @@ public final class SessionLimitPredictionApi { } /** - * Parameters for predicting an AI-credit session limit. Omitting `modelId` uses the session's currently selected model. + * Predicts an AI-credit session limit for the session's resolved model. Returns an unavailable result instead of falling back when the current model is unresolved auto. *

* Invokes the method with no params, applying the runtime defaults. * @@ -44,7 +44,7 @@ public CompletableFuture predict() { } /** - * Parameters for predicting an AI-credit session limit. Omitting `modelId` uses the session's currently selected model. + * Predicts an AI-credit session limit for the session's resolved model. Returns an unavailable result instead of falling back when the current model is unresolved auto. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLspApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLspApi.java index c965007d43..2b20bd44e3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLspApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLspApi.java @@ -31,7 +31,7 @@ public final class SessionLspApi { } /** - * Parameters for (re)loading the merged LSP configuration set. + * Loads the merged LSP configuration set for the session's working directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedPermissions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedPermissions.java index 6bf568c985..c8193260d1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedPermissions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedPermissions.java @@ -24,11 +24,15 @@ public record SessionManagedPermissions( /** When set to `disable`, prevents bypass/allow-all permission modes. Advisory auto-approval remains available because normal prompt paths stay active. Any other value is accepted rather than failing the session, but is enforced as `disable`: the key is only present to restrict something, so a mode this runtime cannot interpret fails closed to the most restrictive one it knows. Omit the key entirely to impose no restriction. */ @JsonProperty("disableBypassPermissionsMode") String disableBypassPermissionsMode, + /** When true, prevents Assisted Permissions from being activated. An actively Assisted session falls back to Manual Approval while the policy is in force. Omit the key or set it to false to impose no restriction. */ + @JsonProperty("disableAssistedPermissionsMode") Boolean disableAssistedPermissionsMode, /** Permission rules that block matching operations. Deny has highest precedence. */ @JsonProperty("deny") List deny, /** Permission rules that require explicit human approval. */ @JsonProperty("ask") List ask, /** Permission rules that allow matching operations unless another managed source, deny, or ask rule restricts them. */ - @JsonProperty("allow") List allow + @JsonProperty("allow") List allow, + /** Closed-world host boundary expressed as `Domain(hostname)`, `Domain(IP)`, or `Domain(*.example.com)` rules. Schemes, ports, paths, queries, and fragments are rejected because every network request must be enforceable at host-level egress. Multiple managed sources intersect their lists; an empty list denies all hosts. */ + @JsonProperty("limitTo") List limitTo ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsApi.java index adf8568aa2..e2e4a67fe3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsApi.java @@ -29,7 +29,7 @@ public final class SessionManagedSettingsApi { } /** - * Identifies the target session. + * Waits for the live session's in-flight managed-settings application, then returns the retained effective snapshot used by runtime enforcement and by `session.managed_settings_resolved`. It does not perform another account, device, or server resolution, and rejects when resolution has not produced a snapshot. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java index 4960d36f02..5f805354c4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java @@ -48,7 +48,23 @@ public final class SessionMcpApi { } /** - * Identifies the target session. + * Records the IDE the host is connected to, so the agent's system prompt can name it and its workspace folder. Null or an omitted `ide` clears the recorded value, which is how a host reports that it is disconnected; there is no separate clear method. Both `ideName` and `workspaceFolder` are required together, because half a state cannot be attributed to a project. + *

+ * 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 + */ + @CopilotExperimental + CompletableFuture setConnectedIdeInfo(SessionMcpSetConnectedIdeInfoParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.setConnectedIdeInfo", _p, Void.class); + } + + /** + * Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -59,7 +75,18 @@ public CompletableFuture list() { } /** - * Server name whose tool list should be returned. + * Lists effective MCP configuration without starting, restarting, authenticating, or waiting for servers. An optional live observation is from an already materialized matching server; this is not a readiness guarantee. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture listConfigured() { + return caller.invoke("session.mcp.listConfigured", java.util.Map.of("sessionId", this.sessionId), SessionMcpListConfiguredResult.class); + } + + /** + * Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -75,7 +102,7 @@ public CompletableFuture listTools(SessionMcpListTool } /** - * Name of the MCP server to enable for the session. + * Enables an MCP server for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -91,7 +118,7 @@ public CompletableFuture enable(SessionMcpEnableParams params) { } /** - * Name of the MCP server to enable for the session. + * Enables an MCP server for the session. *

* Accepts the extensible request, including inputs added after the params record. * @@ -106,7 +133,7 @@ public CompletableFuture enable(SessionMcpEnableRequest request) { } /** - * Name of the MCP server to disable for the session. + * Disables an MCP server for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -122,7 +149,7 @@ public CompletableFuture disable(SessionMcpDisableParams params) { } /** - * Name of the MCP server to disable for the session. + * Disables an MCP server for the session. *

* Accepts the extensible request, including inputs added after the params record. * @@ -137,7 +164,7 @@ public CompletableFuture disable(SessionMcpDisableRequest request) { } /** - * Identifies the target session. + * Reloads MCP server connections for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -148,7 +175,7 @@ public CompletableFuture reload() { } /** - * Identifies the target session. + * Releases any turns waiting on an in-flight MCP load without cancelling the load, letting the agent proceed while MCP servers finish connecting in the background. No-op when no MCP load is in flight or waiting turns were already released. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -159,7 +186,7 @@ public CompletableFuture moveLoadingToB } /** - * Opaque MCP reload configuration. + * Reloads MCP server connections for the session with an explicit host-provided configuration. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -175,7 +202,7 @@ CompletableFuture reloadWithConfig(SessionMcpReloadWithCo } /** - * Identifiers and raw MCP CreateMessageRequest params used to run a sampling inference. + * Runs an MCP sampling inference on behalf of an MCP server. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -191,7 +218,7 @@ public CompletableFuture executeSampling(Sessio } /** - * The requestId previously passed to executeSampling that should be cancelled. + * Cancels an in-flight MCP sampling execution by request ID. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -207,7 +234,7 @@ public CompletableFuture cancelSampling } /** - * Mode controlling how MCP server env values are resolved (`direct` or `indirect`). + * Sets how environment-variable values supplied to MCP servers are resolved (direct or indirect). *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -223,7 +250,7 @@ public CompletableFuture setEnvValueMode(Sessio } /** - * Identifies the target session. + * Removes the auto-managed `github` MCP server when present. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -234,7 +261,7 @@ public CompletableFuture removeGitHub() { } /** - * Credential-free authentication identity used to configure GitHub MCP. + * Configures the built-in GitHub MCP server for the session's current auth context. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -250,7 +277,7 @@ CompletableFuture configureGitHub(SessionMcpConfigureG } /** - * Server name and optional configuration for an individual MCP server start. Omit `config` for a config-free start-by-name of an already-configured server. + * Starts an individual MCP server on the live session. Omit `config` for a config-free start-by-name of an already-configured server (reuses the server's already-registered configuration); supply `config` to start from a caller-supplied configuration. Session-scoped and ephemeral: the server is added to this session's running set only and is reaped when the session ends. Does NOT modify persistent user configuration (`mcp.config.*`), so it does not affect future sessions. The server surfaces through `session.mcp.list` and the `session.mcp_servers_loaded` / `session.mcp_server_status_changed` events like any other server. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -266,7 +293,7 @@ public CompletableFuture startServer(SessionMcpStartServerParams params) { } /** - * Server name and optional configuration for an individual MCP server start. Omit `config` for a config-free start-by-name of an already-configured server. + * Starts an individual MCP server on the live session. Omit `config` for a config-free start-by-name of an already-configured server (reuses the server's already-registered configuration); supply `config` to start from a caller-supplied configuration. Session-scoped and ephemeral: the server is added to this session's running set only and is reaped when the session ends. Does NOT modify persistent user configuration (`mcp.config.*`), so it does not affect future sessions. The server surfaces through `session.mcp.list` and the `session.mcp_servers_loaded` / `session.mcp_server_status_changed` events like any other server. *

* Accepts the extensible request, including inputs added after the params record. * @@ -281,7 +308,7 @@ public CompletableFuture startServer(SessionMcpStartServerRequest request) } /** - * Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. + * Restarts an individual MCP server on the live session (stops then starts). Omit `config` for a config-free restart-by-name of an already-configured server; supply `config` to restart with a replacement configuration. Session-scoped and ephemeral: does NOT modify persistent user configuration (`mcp.config.*`). *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -297,7 +324,7 @@ public CompletableFuture restartServer(SessionMcpRestartServerParams param } /** - * Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. + * Restarts an individual MCP server on the live session (stops then starts). Omit `config` for a config-free restart-by-name of an already-configured server; supply `config` to restart with a replacement configuration. Session-scoped and ephemeral: does NOT modify persistent user configuration (`mcp.config.*`). *

* Accepts the extensible request, including inputs added after the params record. * @@ -312,7 +339,7 @@ public CompletableFuture restartServer(SessionMcpRestartServerRequest requ } /** - * Server name for an individual MCP server stop. + * Stops an individual MCP server on the session's host. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -328,7 +355,7 @@ public CompletableFuture stopServer(SessionMcpStopServerParams params) { } /** - * Server name for an individual MCP server stop. + * Stops an individual MCP server on the session's host. *

* Accepts the extensible request, including inputs added after the params record. * @@ -343,7 +370,7 @@ public CompletableFuture stopServer(SessionMcpStopServerRequest request) { } /** - * Registration parameters for an external MCP client. + * Registers a pre-connected external MCP client (e.g. IDE) on the session's host. The caller retains lifecycle ownership of the client and transport. Marked internal because the `client` and `transport` arguments are in-process MCP SDK instances that cannot be serialized across the JSON-RPC boundary; once the CLI moves on top of the SDK, external clients will be expressed as transport configs the runtime can construct itself. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -359,7 +386,7 @@ CompletableFuture registerExternalClient(SessionMcpRegisterExternalClientP } /** - * Server name identifying the external client to remove. + * Unregisters a previously registered external MCP client by server name. Marked internal as the paired companion of `registerExternalClient`: only in-process callers that registered a client this way can meaningfully unregister it. Disappears alongside `registerExternalClient`: once external clients are described to the runtime as config rather than handed in as instances, lifecycle (including deregistration) is owned entirely by the runtime. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -375,7 +402,7 @@ CompletableFuture unregisterExternalClient(SessionMcpUnregisterExternalCli } /** - * Server name to check running status for. + * Checks whether a named MCP server is currently running on the session's host. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java index 6b932855b2..35cd4c57a3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java @@ -32,7 +32,7 @@ public final class SessionMcpAppsApi { } /** - * MCP server and resource URI to fetch. + * Fetch an MCP resource (typically a `ui://` MCP App bundle, per SEP-1865) from a connected server. Requires the `mcp-apps` session capability. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -48,7 +48,7 @@ public CompletableFuture readResource(SessionM } /** - * MCP server to list app-callable tools for. + * List tools that an MCP App view is allowed to call (SEP-1865 visibility filter). Returns tools whose `_meta.ui.visibility` is unset (default `["model","app"]`) or includes `"app"`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -64,7 +64,7 @@ public CompletableFuture listTools(SessionMcpApps } /** - * MCP server, tool name, and arguments to invoke from an MCP App view. + * Call an MCP tool from an MCP App view (SEP-1865). Enforces the visibility check that prevents an app iframe from invoking model-only tools. Returns the standard MCP `CallToolResult`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -80,7 +80,7 @@ public CompletableFuture callTool(SessionMcpAppsCallToolParams params) } /** - * Host context to advertise to MCP App guests. + * Replace the host context returned to MCP App guests on `ui/initialize`. Hosts use this to advertise theme, locale, or other metadata to the guest UI. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -96,7 +96,7 @@ public CompletableFuture setHostContext(SessionMcpAppsSetHostContextParams } /** - * Identifies the target session. + * Read the current host context advertised to MCP App guests. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -107,7 +107,7 @@ public CompletableFuture getHostContext() { } /** - * MCP server to diagnose MCP Apps wiring for. + * Diagnose MCP Apps wiring for a specific MCP server. Reports the session capability, feature-flag state, advertised extension, and how many tools have `_meta.ui` populated. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java index 45679f83a4..b6c940462e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java @@ -31,7 +31,7 @@ public final class SessionMcpHeadersApi { } /** - * MCP headers refresh request id and the host response. + * Responds to a pending MCP dynamic headers refresh request. Hosts that subscribe to `mcp.headers_refresh_required` use this to provide short-lived per-server headers or to indicate that no dynamic headers are available for this refresh. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredParams.java new file mode 100644 index 0000000000..d4d886b5b0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpListConfiguredParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredResult.java new file mode 100644 index 0000000000..b54b5b238c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredResult.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.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Effective MCP configuration with optional live observations from matching already materialized servers. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpListConfiguredResult( + /** Effective configured MCP servers. */ + @JsonProperty("servers") List servers +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthApi.java index 8e90316975..3910d287eb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthApi.java @@ -32,7 +32,7 @@ public final class SessionMcpOauthApi { } /** - * Pending MCP OAuth request ID and host-provided token or cancellation response. + * Resolves a pending MCP OAuth request with a host-provided token or cancellation. The pending request is emitted as mcp.oauth_required with the data necessary to authorize the request. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -48,7 +48,7 @@ public CompletableFuture handlePendin } /** - * Identifies the MCP server whose persisted OAuth credentials were updated. + * Notifies the session that MCP OAuth authentication succeeded and updated credentials were persisted, so cached tool definitions can be refreshed. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -64,7 +64,7 @@ public CompletableFuture authenticationStateChanged(SessionMcpOauthAuthent } /** - * Effect-free preparation bound to the existing local session, requester and installation, with frozen options. + * Prepares an inert, expiring owned OAuth login bound to the original session requester and exact installation. Does not activate, connect, read credentials or open a browser. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -80,7 +80,7 @@ public CompletableFuture prepareLogin(Session } /** - * Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback handling, and static OAuth client selection. + * Starts OAuth authentication for a remote MCP server. Owned servers require the original one-use prepareLogin handle and exact installation ID; manual servers retain the existing direct login behaviour. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -96,7 +96,7 @@ public CompletableFuture login(SessionMcpOauthLoginP } /** - * Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback handling, and static OAuth client selection. + * Starts OAuth authentication for a remote MCP server. Owned servers require the original one-use prepareLogin handle and exact installation ID; manual servers retain the existing direct login behaviour. *

* Accepts the extensible request, including inputs added after the params record. * @@ -111,7 +111,7 @@ public CompletableFuture login(SessionMcpOauthLoginR } /** - * Host-delivered callback for a runtime-managed MCP OAuth login. + * Completes a runtime-managed MCP OAuth login after the authorization server redirects to a host-managed callback URL. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -127,7 +127,7 @@ public CompletableFuture complete(SessionMcpOauthCompleteParams params) { } /** - * Remote MCP server name for a passive OAuth status probe. + * Passively probes a configured remote MCP server to classify whether OAuth is required or a cached/override token is accepted. Does not start OAuth, emit pending OAuth requests, or mutate MCP connection state. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -143,7 +143,7 @@ public CompletableFuture probe(SessionMcpOauthProbeParams p } /** - * Remote MCP server name for a passive OAuth status probe. + * Passively probes a configured remote MCP server to classify whether OAuth is required or a cached/override token is accepted. Does not start OAuth, emit pending OAuth requests, or mutate MCP connection state. *

* Accepts the extensible request, including inputs added after the params record. * @@ -158,7 +158,7 @@ public CompletableFuture probe(SessionMcpOauthProbeRequest } /** - * Targets only the original prepared/applying owned login on this exact session requester. + * Cancels the exact owned OAuth login issued to this original session requester, without clearing shared credentials. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -174,7 +174,7 @@ public CompletableFuture cancelLogin(SessionMc } /** - * Pending MCP OAuth request id to respond to. + * Responds to a pending MCP OAuth authorization request by its request id. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsApi.java index 7b47b74a21..ec60f552ca 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsApi.java @@ -31,7 +31,7 @@ public final class SessionMcpPromptsApi { } /** - * MCP server whose prompts to enumerate. + * Enumerate one page of prompts a connected MCP server exposes (proxies MCP `prompts/list`). Pass `cursor` to continue from a prior result's `nextCursor`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture list(SessionMcpPromptsList } /** - * MCP server, prompt name, and optional string-valued arguments. + * Get a prompt's messages from a connected MCP server (proxies MCP `prompts/get`). Content is preserved as opaque JSON. Does not send messages to the model, execute tools, or fetch referenced resources. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesApi.java index c1a30e135d..e81f50eb80 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesApi.java @@ -31,7 +31,7 @@ public final class SessionMcpResourcesApi { } /** - * MCP server and resource URI to fetch. + * Fetch an MCP resource from a connected server by URI (proxies MCP `resources/read`). *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture read(SessionMcpResources } /** - * MCP server whose resources to enumerate. + * Enumerate one page of resources a connected MCP server exposes (proxies MCP `resources/list`). Pass `cursor` to continue from a prior result's `nextCursor`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture list(SessionMcpResources } /** - * MCP server whose resource templates to enumerate. + * Enumerate one page of resource templates a connected MCP server exposes (proxies MCP `resources/templates/list`). Pass `cursor` to continue from a prior result's `nextCursor`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpSetConnectedIdeInfoParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpSetConnectedIdeInfoParams.java new file mode 100644 index 0000000000..38e53b55d6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpSetConnectedIdeInfoParams.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Records which IDE the host is connected to, or clears it. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionMcpSetConnectedIdeInfoParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports that it is disconnected. */ + @JsonProperty("ide") SessionConnectedIdeInfo ide +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java index 8d7f3dd753..bf97a0ec39 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java @@ -31,7 +31,7 @@ public final class SessionMetadataApi { } /** - * Identifies the target session. + * Returns a snapshot of the session's identifying metadata, mode, agent, and remote info. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture snapshot() { } /** - * Identifies the target session. + * Returns the client-owned string metadata persisted with this local session. The metadata is not included in model context, events, telemetry, snapshots, or remote exports. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -53,7 +53,7 @@ public CompletableFuture getClientMetadata() { } /** - * Atomic patch for client-owned session metadata. Operations apply in clear, remove, then set order. The resulting bag must satisfy the ClientMetadata entry and serialized-size limits. Local storage coordinates concurrent runtime processes; custom SessionFs providers must serialize writers that access the same session from multiple processes. + * Atomically patches the client-owned string metadata persisted with this local session and returns the committed bag. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -69,7 +69,7 @@ public CompletableFuture updateClientMetadata(SessionMetadataUpd } /** - * Identifies the target session. + * Reports whether the local session is currently processing user/agent messages. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -80,7 +80,7 @@ public CompletableFuture isProcessing() { } /** - * Identifies the target session. + * Returns a snapshot of activity flags for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -91,7 +91,7 @@ public CompletableFuture activity() { } /** - * Model identifier and token limits used to compute the context-info breakdown. + * Returns the token breakdown for the session's current context window for a given model. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -107,7 +107,7 @@ public CompletableFuture contextInfo(SessionMe } /** - * Identifies the target session. + * Returns the experimental per-source attribution breakdown of the session's current context window as a flat list of entries (skills, subagents, MCP servers, built-in tools, plugin rollups, system/tool-definition costs, with nesting via parentId), plus the successful compaction count. The heaviest individual messages are available separately via `metadata.getContextHeaviestMessages`. Returns null until the session has initialized its system prompt and tool metadata. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -118,7 +118,7 @@ public CompletableFuture getContextA } /** - * Parameters for the heaviest-messages query. + * Returns the largest individual messages currently in the session's context window, most-expensive first. Companion to `metadata.getContextAttribution`. Returns an empty list until the session has initialized. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -134,7 +134,7 @@ public CompletableFuture getCon } /** - * Updated working-directory/git context to record on the session. + * Records a working-directory/git context change and emits a `session.context_changed` event. For a local session, a report whose `cwd` diverges from the session's current working directory is ignored (the call still succeeds but records nothing and emits no event): a local session's working directory is authoritative and is moved via `metadata.setWorkingDirectory` (or an SDK `session.resume` that supplies a `workingDirectory`), not by this method. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -150,7 +150,7 @@ public CompletableFuture recordContextChange(SessionMetadataRecordContextC } /** - * Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is. + * Updates the session's working directory. For local sessions the target is validated first (an absolute path that exists on disk) and the permission primary directory is re-based; a rejected validation fails the call before any session state changes. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -166,7 +166,7 @@ public CompletableFuture setWorkingDir } /** - * Model identifier to use when re-tokenizing the session's existing messages. + * Re-tokenizes the session's existing messages against a model and returns aggregate token totals. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataContextInfoParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataContextInfoParams.java index 0c5b909db7..a190c0d194 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataContextInfoParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataContextInfoParams.java @@ -26,11 +26,13 @@ public record SessionMetadataContextInfoParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, - /** Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. */ + /** Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from the session. */ @JsonProperty("promptTokenLimit") Long promptTokenLimit, - /** Maximum output tokens allowed by the target model. Pass 0 if unknown. */ + /** Requested output allowance to reserve against the combined context ceiling. Pass 0 to resolve the session's request cap, falling back to the model's advertised output limit. */ @JsonProperty("outputTokenLimit") Long outputTokenLimit, /** Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. */ - @JsonProperty("selectedModel") String selectedModel + @JsonProperty("selectedModel") String selectedModel, + /** Provider owning selectedModel. Omit to use the current provider for the current model, or deterministic legacy lookup for a different model. */ + @JsonProperty("providerId") String providerId ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataContextInfoResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataContextInfoResult.java index 6472956026..4415e14096 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataContextInfoResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataContextInfoResult.java @@ -34,6 +34,10 @@ public record SessionMetadataContextInfoResult( public record SessionMetadataContextInfoResultContextInfo( /** The model used for token counting */ @JsonProperty("modelName") String modelName, + /** Provider whose catalog supplies this context window. Omitted when provider identity is unavailable. */ + @JsonProperty("provider") ModelProviderRef provider, + /** Provider-owned model display label, or the Auto routing label. Presentation only; modelName remains the tokenization identity. */ + @JsonProperty("displayModelName") String displayModelName, /** Tokens consumed by the system prompt */ @JsonProperty("systemTokens") Long systemTokens, /** Tokens consumed by user/assistant/tool messages */ @@ -44,13 +48,13 @@ public record SessionMetadataContextInfoResultContextInfo( @JsonProperty("mcpToolsTokens") Long mcpToolsTokens, /** Sum of system, conversation and tool-definition tokens */ @JsonProperty("totalTokens") Long totalTokens, - /** Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) */ + /** Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified. */ @JsonProperty("promptTokenLimit") Long promptTokenLimit, /** Token count at which background compaction starts (configurable percentage of promptTokenLimit) */ @JsonProperty("compactionThreshold") Long compactionThreshold, - /** Prompt token limit plus the model's full output token limit. */ + /** Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays. */ @JsonProperty("limit") Long limit, - /** Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) */ + /** Output reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%). */ @JsonProperty("bufferTokens") Long bufferTokens ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataGetContextAttributionResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataGetContextAttributionResult.java index c27f37afb0..44713111cf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataGetContextAttributionResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataGetContextAttributionResult.java @@ -40,11 +40,11 @@ public record SessionMetadataGetContextAttributionResultContextAttribution( @JsonProperty("modelId") String modelId, /** How `modelId` was chosen. Not a closed set — tolerate unknown values. Known values today: `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected model), `default` (a fallback before any model is known). */ @JsonProperty("modelSource") String modelSource, - /** Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. */ + /** Effective input budget after reserving requested output against the combined context ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. */ @JsonProperty("promptTokenLimit") Long promptTokenLimit, - /** Prompt limit plus the model's output reserve: the full context window `categories.freeSpace` and `categories.buffer` are measured against. Mirrors `SessionContextInfo.limit`. */ + /** Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`. */ @JsonProperty("limit") Long limit, - /** Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. */ + /** Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. */ @JsonProperty("bufferTokens") Long bufferTokens, /** Token count at which background compaction starts. Mirrors `SessionContextInfo.compactionThreshold`. */ @JsonProperty("compactionThreshold") Long compactionThreshold, @@ -72,7 +72,7 @@ public record SessionMetadataGetContextAttributionResultContextAttributionCatego @JsonProperty("messages") Long messages, /** Remaining unused window capacity (clamped at 0). */ @JsonProperty("freeSpace") Long freeSpace, - /** Output reserve plus post-blocking-threshold buffer. */ + /** Overlapping output reservation plus post-blocking-threshold buffer. */ @JsonProperty("buffer") Long buffer ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeApi.java index 0f4989a920..8c2c2d66da 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeApi.java @@ -31,7 +31,7 @@ public final class SessionModeApi { } /** - * Identifies the target session. + * Gets the current agent interaction mode. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture get() { } /** - * Agent interaction mode to apply to the session. + * Sets the current agent interaction mode. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeSetParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeSetParams.java index 9a20862a13..9c11577700 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeSetParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeSetParams.java @@ -36,6 +36,8 @@ public record SessionModeSetParams( @JsonProperty("planModelConfigured") Boolean planModelConfigured, /** Dedicated model to use in plan mode, when configured. */ @JsonProperty("planModel") String planModel, + /** Provider owning planModel. Omit for legacy bare-model selection. */ + @JsonProperty("planModelProviderId") String planModelProviderId, /** Reasoning effort to use with the dedicated plan model. */ @JsonProperty("planReasoningEffort") String planReasoningEffort, /** Context tier to use with the dedicated plan model. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java index 8d47c9c716..1c5c1c3d39 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java @@ -8,6 +8,7 @@ package com.github.copilot.generated.rpc; import com.github.copilot.CopilotExperimental; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import javax.annotation.processing.Generated; @@ -31,7 +32,7 @@ public final class SessionModelApi { } /** - * Identifies the target session. + * Gets the session's authoritative model snapshot, including the committed Auto preference and any newer unclaimed Auto preference waiting for a future user turn. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +43,7 @@ public CompletableFuture getCurrent() { } /** - * Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. + * Switches the session to a model and optional reasoning configuration. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +59,22 @@ public CompletableFuture switchTo(SessionModelSwitch } /** - * An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. + * Switches the session to a model and optional reasoning configuration. + *

+ * Accepts the extensible request, including inputs added after the params record. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture switchTo(SessionModelSwitchToRequest request) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(Objects.requireNonNull(request, "request")); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.model.switchTo", _p, SessionModelSwitchToResult.class); + } + + /** + * Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +90,7 @@ public CompletableFuture switchAutoTier(Sessio } /** - * Managed, repository, and CLI model overrides to overlay onto the session at startup. + * Resolves and applies organization-managed and repository model overlays. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -90,7 +106,23 @@ CompletableFuture applyStartupOverlay(SessionModelApplyStar } /** - * Host-supplied exact model selection IDs to allow for this running session. CAPI IDs are intersected with repository `.github/allowed_models.txt` policy; provider-qualified IDs remain exempt from repository-only policy but are restricted by this host list. Omit or pass null to clear the host restriction; an explicit empty or disjoint list is rejected. Validation and pre-selection fallback failures preserve the previous restriction. Failures after a fallback selection commits retain the new restriction and selected model; callers should inspect current session state after such an error. + * Clears an unchanged startup model/provider seed before default-model resolution. Never clears a resumed or user-selected model. + *

+ * 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 + */ + @CopilotExperimental + CompletableFuture clearStartupSeed(SessionModelClearStartupSeedParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.model.clearStartupSeed", _p, ModelClearStartupSeedResult.class); + } + + /** + * Replaces or clears the host-supplied model allowlist for a running session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -106,7 +138,7 @@ public CompletableFuture setAllowedModels(Se } /** - * Reasoning effort level to apply to the currently selected model. + * Updates the session's reasoning effort without changing the selected model. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -122,7 +154,7 @@ public CompletableFuture setReasoningEffor } /** - * Optional listing options. + * Lists models available to this session using its own auth and integration context. Connected hosts (CLI TUI, GitHub App) should call this through the session client so remote sessions return the remote CLI's available models rather than the caller's. *

* Invokes the method with no params, applying the runtime defaults. * @@ -135,7 +167,7 @@ public CompletableFuture list() { } /** - * Optional listing options. + * Lists models available to this session using its own auth and integration context. Connected hosts (CLI TUI, GitHub App) should call this through the session client so remote sessions return the remote CLI's available models rather than the caller's. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java index ad25a718a2..e4aadb58f1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java @@ -36,6 +36,8 @@ record SessionModelApplyStartupOverlayParams( @JsonProperty("autoTier") AutoTier autoTier, /** Model selected by repository settings, when configured. */ @JsonProperty("repoModel") String repoModel, + /** Provider owning the repository-selected model. Omitted repository provider identity retains legacy bare-model behavior. */ + @JsonProperty("repoModelProviderId") String repoModelProviderId, /** Reasoning effort selected by repository settings, when configured. */ @JsonProperty("repoReasoningEffort") String repoReasoningEffort, /** Context tier selected by repository settings, when configured. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelClearStartupSeedParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelClearStartupSeedParams.java new file mode 100644 index 0000000000..d927c746ca --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelClearStartupSeedParams.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.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.model.clearStartupSeed} RPC method. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionModelClearStartupSeedParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + @JsonProperty("expectedModel") String expectedModel, + /** Provider captured with the startup seed; omission matches only an unqualified seed. */ + @JsonProperty("expectedProviderId") String expectedProviderId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java index 6d6e79a26a..28ef368715 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java @@ -26,8 +26,12 @@ public record SessionModelGetCurrentResult( /** Currently active model identifier */ @JsonProperty("modelId") String modelId, + /** Provider selected for this model. Together with modelId, identifies a catalog entry even when providers serve the same model. Omitted for unattributed legacy selections. */ + @JsonProperty("providerId") String providerId, /** Captured base model to restore when leaving plan mode. Omitted outside plan mode or when no plan override has captured a base model. Persistent agent model requirements apply to this model rather than the temporary plan model. */ @JsonProperty("planBaseModelId") String planBaseModelId, + /** Provider of planBaseModelId, when that saved selection is provider-qualified. */ + @JsonProperty("planBaseProviderId") String planBaseProviderId, /** Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. */ @JsonProperty("reasoningEffort") String reasoningEffort, /** Context tier for models that support multiple context-window sizes. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelListResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelListResult.java index 1c73b4b22f..3a18d77488 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelListResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelListResult.java @@ -33,6 +33,8 @@ public record SessionModelListResult( /** Cost categories for the full CAPI catalog, including picker-disabled models that Auto may select. Metadata only; entries absent from `list` are not manually selectable. */ @JsonProperty("modelPriceCategories") List modelPriceCategories, /** Per-quota snapshots returned alongside the model list, keyed by quota type. */ - @JsonProperty("quotaSnapshots") Map quotaSnapshots + @JsonProperty("quotaSnapshots") Map quotaSnapshots, + /** Ordered Auto routing preferences discovered for this session's account. */ + @JsonProperty("auto") AutoTierMetadata auto ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java index a37eebd2f6..0f30a514ca 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java @@ -26,7 +26,7 @@ public record SessionModelSwitchToParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, - /** Model selection id to switch to, as returned by `list`. A bare id (e.g. `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. */ + /** Model id to switch to, as returned by `list`. Include providerId to select an exact catalog entry when providers share the id. Without providerId, a bare id retains incumbent selection behavior; a registry-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a BYOK model. */ @JsonProperty("modelId") String modelId, /** Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. */ @JsonProperty("autoTier") AutoTier autoTier, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToRequest.java new file mode 100644 index 0000000000..09168fcb52 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToRequest.java @@ -0,0 +1,410 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.Objects; +import javax.annotation.processing.Generated; + +/** + * Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. + *

+ * Required inputs are constructor arguments. Optional inputs have fluent setters. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class SessionModelSwitchToRequest { + + /** Model id to switch to, as returned by `list`. Include providerId to select an exact catalog entry when providers share the id. Without providerId, a bare id retains incumbent selection behavior; a registry-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a BYOK model. */ + @JsonProperty("modelId") + private final String modelId; + + /** Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. */ + @JsonProperty("autoTier") + private AutoTier autoTier; + + /** Reasoning effort level to use for the model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. "none" disables reasoning. Pass null to clear any session effort override and fall back to the model's default. When omitted, the session's current effort is kept. */ + @JsonProperty("reasoningEffort") + private String reasoningEffort; + + /** Reasoning summary mode to request for supported model clients */ + @JsonProperty("reasoningSummary") + private ReasoningSummary reasoningSummary; + + /** Output verbosity level to request for supported models */ + @JsonProperty("verbosity") + private Verbosity verbosity; + + /** Override individual model capabilities resolved by the runtime */ + @JsonProperty("modelCapabilities") + private ModelCapabilitiesOverride modelCapabilities; + + /** Explicit context tier for the selected model. `"default"` / `"long_context"` apply the requested tier; omit this field to use normal model behavior with no explicit tier. */ + @JsonProperty("contextTier") + private ContextTier contextTier; + + /** Origin to record on the effective `session.model_change` event for trusted in-process calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. */ + @JsonProperty("source") + private ModelChangeSource source; + + /** When true, defer this switch (enqueue it) if another model change is already queued, even when no turn is active — so it drains last (FIFO) and wins over the already-queued change. Intended for genuine user-initiated model selections; internal restore/reapply switches omit it and apply immediately when no turn is active. When no other model change is queued this has no effect (a switch still applies immediately unless a turn is active). */ + @JsonProperty("deferIfModelChangeQueued") + private Boolean deferIfModelChangeQueued; + + /** Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. */ + @JsonProperty("compactionDecision") + private String compactionDecision; + + /** When true, evaluate context-window compaction policy before applying the switch. */ + @JsonProperty("runCompactionPreflight") + private Boolean runCompactionPreflight; + + /** Optional repository settings scope to persist after the switch commits. */ + @JsonProperty("repoScope") + private String repoScope; + + /** Settings scope used when persisting the selected model. */ + @JsonProperty("modelChangeScope") + private String modelChangeScope; + + /** Require the target to be currently available and enabled before applying the switch. */ + @JsonProperty("requireAvailable") + private Boolean requireAvailable; + + /** Optional settings context and explicit-override flags used to persist a picker selection. */ + @JsonProperty("pickerPersistence") + private ModelPickerPersistenceRequest pickerPersistence; + + /** Provider id from the selected list entry's provider reference. Selects this exact provider/model pair; an unavailable pair fails rather than using another provider. Omit for deterministic legacy bare-model selection. */ + @JsonProperty("providerId") + private String providerId; + + /** + * Creates a request with its required inputs. + * + * @param modelId Model id to switch to, as returned by `list`. Include providerId to select an exact catalog entry when providers share the id. Without providerId, a bare id retains incumbent selection behavior; a registry-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a BYOK model. + */ + public SessionModelSwitchToRequest(String modelId) { + this.modelId = Objects.requireNonNull(modelId, "modelId"); + } + + /** + * Returns the {@code modelId} property. + * + * @return Model id to switch to, as returned by `list`. Include providerId to select an exact catalog entry when providers share the id. Without providerId, a bare id retains incumbent selection behavior; a registry-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a BYOK model. + */ + public String getModelId() { + return modelId; + } + + /** + * Returns the {@code autoTier} property. + * + * @return Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. + */ + public AutoTier getAutoTier() { + return autoTier; + } + + /** + * Returns the {@code reasoningEffort} property. + * + * @return Reasoning effort level to use for the model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. "none" disables reasoning. Pass null to clear any session effort override and fall back to the model's default. When omitted, the session's current effort is kept. + */ + public String getReasoningEffort() { + return reasoningEffort; + } + + /** + * Returns the {@code reasoningSummary} property. + * + * @return Reasoning summary mode to request for supported model clients + */ + public ReasoningSummary getReasoningSummary() { + return reasoningSummary; + } + + /** + * Returns the {@code verbosity} property. + * + * @return Output verbosity level to request for supported models + */ + public Verbosity getVerbosity() { + return verbosity; + } + + /** + * Returns the {@code modelCapabilities} property. + * + * @return Override individual model capabilities resolved by the runtime + */ + public ModelCapabilitiesOverride getModelCapabilities() { + return modelCapabilities; + } + + /** + * Returns the {@code contextTier} property. + * + * @return Explicit context tier for the selected model. `"default"` / `"long_context"` apply the requested tier; omit this field to use normal model behavior with no explicit tier. + */ + public ContextTier getContextTier() { + return contextTier; + } + + /** + * Returns the {@code source} property. + * + * @return Origin to record on the effective `session.model_change` event for trusted in-process calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. + */ + public ModelChangeSource getSource() { + return source; + } + + /** + * Returns the {@code deferIfModelChangeQueued} property. + * + * @return When true, defer this switch (enqueue it) if another model change is already queued, even when no turn is active — so it drains last (FIFO) and wins over the already-queued change. Intended for genuine user-initiated model selections; internal restore/reapply switches omit it and apply immediately when no turn is active. When no other model change is queued this has no effect (a switch still applies immediately unless a turn is active). + */ + public Boolean getDeferIfModelChangeQueued() { + return deferIfModelChangeQueued; + } + + /** + * Returns the {@code compactionDecision} property. + * + * @return Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. + */ + public String getCompactionDecision() { + return compactionDecision; + } + + /** + * Returns the {@code runCompactionPreflight} property. + * + * @return When true, evaluate context-window compaction policy before applying the switch. + */ + public Boolean getRunCompactionPreflight() { + return runCompactionPreflight; + } + + /** + * Returns the {@code repoScope} property. + * + * @return Optional repository settings scope to persist after the switch commits. + */ + public String getRepoScope() { + return repoScope; + } + + /** + * Returns the {@code modelChangeScope} property. + * + * @return Settings scope used when persisting the selected model. + */ + public String getModelChangeScope() { + return modelChangeScope; + } + + /** + * Returns the {@code requireAvailable} property. + * + * @return Require the target to be currently available and enabled before applying the switch. + */ + public Boolean getRequireAvailable() { + return requireAvailable; + } + + /** + * Returns the {@code pickerPersistence} property. + * + * @return Optional settings context and explicit-override flags used to persist a picker selection. + */ + public ModelPickerPersistenceRequest getPickerPersistence() { + return pickerPersistence; + } + + /** + * Returns the {@code providerId} property. + * + * @return Provider id from the selected list entry's provider reference. Selects this exact provider/model pair; an unavailable pair fails rather than using another provider. Omit for deterministic legacy bare-model selection. + */ + public String getProviderId() { + return providerId; + } + + /** + * Sets the {@code autoTier} property. + * + * @param value Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. + * @return this request + */ + public SessionModelSwitchToRequest setAutoTier(AutoTier value) { + this.autoTier = value; + return this; + } + + /** + * Sets the {@code reasoningEffort} property. + * + * @param value Reasoning effort level to use for the model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. "none" disables reasoning. Pass null to clear any session effort override and fall back to the model's default. When omitted, the session's current effort is kept. + * @return this request + */ + public SessionModelSwitchToRequest setReasoningEffort(String value) { + this.reasoningEffort = value; + return this; + } + + /** + * Sets the {@code reasoningSummary} property. + * + * @param value Reasoning summary mode to request for supported model clients + * @return this request + */ + public SessionModelSwitchToRequest setReasoningSummary(ReasoningSummary value) { + this.reasoningSummary = value; + return this; + } + + /** + * Sets the {@code verbosity} property. + * + * @param value Output verbosity level to request for supported models + * @return this request + */ + public SessionModelSwitchToRequest setVerbosity(Verbosity value) { + this.verbosity = value; + return this; + } + + /** + * Sets the {@code modelCapabilities} property. + * + * @param value Override individual model capabilities resolved by the runtime + * @return this request + */ + public SessionModelSwitchToRequest setModelCapabilities(ModelCapabilitiesOverride value) { + this.modelCapabilities = value; + return this; + } + + /** + * Sets the {@code contextTier} property. + * + * @param value Explicit context tier for the selected model. `"default"` / `"long_context"` apply the requested tier; omit this field to use normal model behavior with no explicit tier. + * @return this request + */ + public SessionModelSwitchToRequest setContextTier(ContextTier value) { + this.contextTier = value; + return this; + } + + /** + * Sets the {@code source} property. + * + * @param value Origin to record on the effective `session.model_change` event for trusted in-process calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. + * @return this request + */ + public SessionModelSwitchToRequest setSource(ModelChangeSource value) { + this.source = value; + return this; + } + + /** + * Sets the {@code deferIfModelChangeQueued} property. + * + * @param value When true, defer this switch (enqueue it) if another model change is already queued, even when no turn is active — so it drains last (FIFO) and wins over the already-queued change. Intended for genuine user-initiated model selections; internal restore/reapply switches omit it and apply immediately when no turn is active. When no other model change is queued this has no effect (a switch still applies immediately unless a turn is active). + * @return this request + */ + public SessionModelSwitchToRequest setDeferIfModelChangeQueued(Boolean value) { + this.deferIfModelChangeQueued = value; + return this; + } + + /** + * Sets the {@code compactionDecision} property. + * + * @param value Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. + * @return this request + */ + public SessionModelSwitchToRequest setCompactionDecision(String value) { + this.compactionDecision = value; + return this; + } + + /** + * Sets the {@code runCompactionPreflight} property. + * + * @param value When true, evaluate context-window compaction policy before applying the switch. + * @return this request + */ + public SessionModelSwitchToRequest setRunCompactionPreflight(Boolean value) { + this.runCompactionPreflight = value; + return this; + } + + /** + * Sets the {@code repoScope} property. + * + * @param value Optional repository settings scope to persist after the switch commits. + * @return this request + */ + public SessionModelSwitchToRequest setRepoScope(String value) { + this.repoScope = value; + return this; + } + + /** + * Sets the {@code modelChangeScope} property. + * + * @param value Settings scope used when persisting the selected model. + * @return this request + */ + public SessionModelSwitchToRequest setModelChangeScope(String value) { + this.modelChangeScope = value; + return this; + } + + /** + * Sets the {@code requireAvailable} property. + * + * @param value Require the target to be currently available and enabled before applying the switch. + * @return this request + */ + public SessionModelSwitchToRequest setRequireAvailable(Boolean value) { + this.requireAvailable = value; + return this; + } + + /** + * Sets the {@code pickerPersistence} property. + * + * @param value Optional settings context and explicit-override flags used to persist a picker selection. + * @return this request + */ + public SessionModelSwitchToRequest setPickerPersistence(ModelPickerPersistenceRequest value) { + this.pickerPersistence = value; + return this; + } + + /** + * Sets the {@code providerId} property. + * + * @param value Provider id from the selected list entry's provider reference. Selects this exact provider/model pair; an unavailable pair fails rather than using another provider. Omit for deterministic legacy bare-model selection. + * @return this request + */ + public SessionModelSwitchToRequest setProviderId(String value) { + this.providerId = value; + return this; + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionNameApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionNameApi.java index 9bfc5bea59..aeefa0df31 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionNameApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionNameApi.java @@ -31,7 +31,7 @@ public final class SessionNameApi { } /** - * Identifies the target session. + * Gets the session's friendly name. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture get() { } /** - * New friendly name to apply to the session. + * Sets the session's friendly name. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture set(SessionNameSetParams params) { } /** - * Auto-generated session summary to apply as the session's name when no user-set name exists. + * Persists an auto-generated session summary as the session's name when no user-set name exists. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java index dbb3080cdb..5d14a1458a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java @@ -49,6 +49,8 @@ public record SessionOpenOptions( @JsonProperty("expAssignments") Object expAssignments, /** Opt-in: self-fetch and enforce enterprise managed settings, including managed hook policies, at session bootstrap. */ @JsonProperty("enableManagedSettings") Boolean enableManagedSettings, + /** Opt in to enforcing non-overridable managed model controls on session model, Auto-tier, reasoning-effort, and context-tier changes. Managed defaults still apply when omitted; this option only turns conflicting changes into errors. */ + @JsonProperty("enforceManagedModelDefaults") Boolean enforceManagedModelDefaults, /** Permissions-only enterprise policy injected by the SDK host at session create or resume. Composes restrictively with self-fetched and device policy and is not persisted. */ @JsonProperty("managedSettings") SessionManagedSettings managedSettings, /** Opt in to capturing file changes for session rewind and session diff. Capture cannot reconstruct changes made before it was enabled. On create it starts capture from the first turn. It is also honored on resume: for a session that already has tracked prior turns, tracking continues automatically even if this is omitted; passing it on resume additionally enables tracking for an eligible session that has no prior root turn yet. Resuming a session whose prior root turns were never tracked has no restorable baseline, so tracking stays disabled for it and rewind reports file change tracking as unavailable; the resume itself still succeeds, so sessions that predate tracking remain loadable. The opt-in is only rejected when the session can never track (a subagent session, or one without local session storage). It is intentionally absent from the mutable options update because enabling it after edits have occurred would create an incomplete, misleading baseline. Subagents share the parent session's capture store and are not tracked as separate rewind points: a file a subagent writes is attributed to whichever root user turn was open when the capture was staged, just before the tool body ran. A turn cannot open while a staged capture is still in flight, so a subagent tool that staged under the spawning turn stays attributed to it however late the write lands, while a capture it stages after the user's next message belongs to that later turn. Attribution decides which turn's rewind point counts and file preview include that write; it does not narrow which rewinds revert it, because a rewind restores every capture from the selected turn onward, so the earlier spawning turn reverts it as well. */ @@ -63,6 +65,8 @@ public record SessionOpenOptions( @JsonProperty("provider") ProviderConfig provider, /** Options scoped to the built-in CAPI (Copilot API) provider. */ @JsonProperty("capi") CapiSessionOptions capi, + /** Whether a CLI host explicitly requested the initial Auto preference. False preserves a settings-derived preference without validating availability during creation; execution still validates it. Defaults to true and is ignored for non-CLI callers. */ + @JsonProperty("autoTierIsExplicit") Boolean autoTierIsExplicit, /** Named BYOK provider connections, additive to CAPI auth. Combining with `provider` is rejected. */ @JsonProperty("providers") List providers, /** BYOK model definitions added to the selectable model list, each referencing a provider name. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsApi.java index 32d4b76908..06986da2ff 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsApi.java @@ -31,7 +31,7 @@ public final class SessionOptionsApi { } /** - * Patch of mutable session options to apply to the running session. + * Patches the genuinely-mutable subset of session options. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsApi.java index aed2de53fb..eb4cc591e8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsApi.java @@ -44,7 +44,7 @@ public final class SessionPermissionsApi { } /** - * Patch of permission policy fields to apply (omit a field to leave it unchanged). + * Replaces selected permission policy fields (rules, paths, URLs, exclusions, allow-all flags) on the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -60,7 +60,7 @@ public CompletableFuture configure(SessionPer } /** - * Pending permission request ID and the decision to apply (approve/reject and scope). + * Provides a decision for a pending tool permission request. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture } /** - * No parameters; returns currently-pending permission requests for the session. + * Reconstructs the set of pending tool permission requests from the session's event history. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -87,7 +87,7 @@ public CompletableFuture pendingRequest } /** - * Allow-all toggle for tool permission requests, with an optional telemetry source. + * Enables or disables automatic approval of tool permission requests for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -103,7 +103,7 @@ public CompletableFuture setApproveAll(Se } /** - * Permission mode to apply for the session. + * Sets the permission mode for the session. `manual` follows the normal approval flow, `assisted` attaches LLM safety recommendations, and `allow-all` automatically approves permission requests. The result returns the authoritative post-mutation mode so callers can update local state without racing the `session.permissions_changed` notification. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -119,7 +119,7 @@ public CompletableFuture setMode(SessionPermiss } /** - * No parameters. + * Returns the current permission mode for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -130,7 +130,7 @@ public CompletableFuture getMode() { } /** - * Scope and add/remove instructions for modifying session- or location-scoped permission rules. + * Adds or removes session-scoped or location-scoped permission rules. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -146,7 +146,7 @@ public CompletableFuture modifyRules(Sessio } /** - * Toggles whether permission prompts should be bridged into session events for this client. + * Sets whether the client wants permission prompts bridged into session events. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -162,7 +162,7 @@ public CompletableFuture setRequired(Sessio } /** - * Clears session-scoped tool approvals and optionally clears location-scoped approvals and exact session-approved paths. + * Clears session-scoped tool approvals and, for full resets, exact session-approved paths. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -178,7 +178,7 @@ public CompletableFuture resetSes } /** - * Notification payload describing the permission prompt that the client just rendered. + * Notifies the runtime that a permission prompt UI has been shown to the user. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsFolderTrustApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsFolderTrustApi.java index 55bac08941..741e4ed737 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsFolderTrustApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsFolderTrustApi.java @@ -31,7 +31,7 @@ public final class SessionPermissionsFolderTrustApi { } /** - * Folder path to check for trust. + * Reports whether a folder is trusted according to the user's folder trust state. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture isTrusted } /** - * Folder path to add to trusted folders. + * Adds a folder to the user's trusted folders list. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsLocationsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsLocationsApi.java index 46ce8ef4d3..37f16bf40c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsLocationsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsLocationsApi.java @@ -31,7 +31,7 @@ public final class SessionPermissionsLocationsApi { } /** - * Working directory to resolve into a location-permissions key. + * Resolves the permission location key and type for a working directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture resolve(Sessi } /** - * Working directory to load persisted location permissions for. + * Applies persisted location-scoped tool approvals and allowed directories for a working directory to this session's permission service. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture apply(SessionPe } /** - * Location-scoped tool approval to persist. + * Persists a tool approval for a permission location and applies its rules to this session's live permission service. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsPathsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsPathsApi.java index ecf9d81b52..62ae0a193b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsPathsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsPathsApi.java @@ -31,7 +31,7 @@ public final class SessionPermissionsPathsApi { } /** - * No parameters; returns the session's recursive directory grants and exact session-approved paths. + * Returns the session's recursive directory grants, exact session-approved paths, and primary working directory. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture list() { } /** - * Directory path to add to the session's allowed directories. + * Adds a directory to the session's allow-list and activates conventional skill and agent definitions under it. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture add(SessionPermission } /** - * Directory path to set as the session's new primary working directory. + * Updates the session's primary working directory used by the permission policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture updatePrima } /** - * Path to evaluate against the session's allowed directories. + * Reports whether a path falls within any of the session's allowed directories. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -90,7 +90,7 @@ public CompletableFuture * Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsUrlsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsUrlsApi.java index 5ca15960d2..3261b07033 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsUrlsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsUrlsApi.java @@ -31,7 +31,7 @@ public final class SessionPermissionsUrlsApi { } /** - * Whether the URL-permission policy should run in unrestricted mode. + * Toggles the runtime's URL-permission policy between unrestricted and restricted modes. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPlanApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPlanApi.java index 7805183ddf..daf85af7db 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPlanApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPlanApi.java @@ -31,7 +31,7 @@ public final class SessionPlanApi { } /** - * Identifies the target session. + * Reads the session plan file from the workspace. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture read() { } /** - * Replacement contents to write to the session plan file. + * Writes new content to the session plan file. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture update(SessionPlanUpdateParams params) { } /** - * Identifies the target session. + * Deletes the session plan file from the workspace. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -69,7 +69,7 @@ public CompletableFuture delete() { } /** - * Identifies the target session. + * Reads todo rows from the session SQL database for plan rendering. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -80,7 +80,7 @@ public CompletableFuture readSqlTodos() { } /** - * Identifies the target session. + * Reads todo rows AND dependency edges from the session SQL database for structured progress UI. Same defensive behavior as readSqlTodos — returns empty arrays when the database, tables, or columns aren't available. Clients should call this on session start and after every `session.todos_changed` event to refresh structured-UI rendering. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsApi.java index 6a49785b30..e041a149c1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsApi.java @@ -35,7 +35,7 @@ public final class SessionPluginsApi { } /** - * Identifies the target session. + * Lists globally installed, live, built-in, and enterprise-managed desired plugins using the live session's authoritative account, working directory, and retained managed policy. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -46,7 +46,7 @@ public CompletableFuture list() { } /** - * Plugin source resolved relative to the session's authoritative working directory. + * Installs a plugin using the live session's authoritative account, working directory, and retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -62,7 +62,7 @@ public CompletableFuture install(SessionPluginsInst } /** - * Name (or spec) of the plugin to uninstall. + * Uninstalls a plugin when permitted by the live session's retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -78,7 +78,7 @@ public CompletableFuture uninstall(SessionPluginsUninstallParams params) { } /** - * Name (or spec) of the plugin to update. + * Updates an installed plugin using the live session's authoritative account, working directory, and retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -94,7 +94,7 @@ public CompletableFuture update(SessionPluginsUpdate } /** - * Plugin names (or specs) to enable in the session's authoritative working directory. + * Enables installed plugins when permitted by the live session's retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -110,7 +110,7 @@ public CompletableFuture enable(SessionPluginsEnableParams params) { } /** - * Plugin names (or specs) to disable in the session's authoritative working directory. + * Disables installed plugins when permitted by the live session's retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -126,7 +126,7 @@ public CompletableFuture disable(SessionPluginsDisableParams params) { } /** - * Optional flags controlling which side effects the reload performs. + * Reloads the session's plugin set, refreshing MCP servers, custom agents, hooks, and skills cache so SDK-driven changes via `server.plugins.*` take effect immediately. *

* Invokes the method with no params, applying the runtime defaults. * @@ -139,7 +139,7 @@ public CompletableFuture reload() { } /** - * Optional flags controlling which side effects the reload performs. + * Reloads the session's plugin set, refreshing MCP servers, custom agents, hooks, and skills cache so SDK-driven changes via `server.plugins.*` take effect immediately. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsMarketplacesApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsMarketplacesApi.java index 3ec14c5cf7..c9682c21f8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsMarketplacesApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsMarketplacesApi.java @@ -31,7 +31,7 @@ public final class SessionPluginsMarketplacesApi { } /** - * Identifies the target session. + * Lists registered and enterprise-managed desired marketplaces using the live session's retained policy. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture list() { } /** - * Marketplace source and optional working directory for relative-path resolution. + * Adds a marketplace when permitted by the live session's retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture add(SessionPlugins } /** - * Name of the marketplace to remove and an optional force flag. + * Removes a marketplace when permitted by the live session's retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture remove(SessionP } /** - * Name of the marketplace whose plugin catalog to fetch. + * Browses a marketplace resolved through the live session's working directory and retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -90,7 +90,7 @@ public CompletableFuture browse(SessionP } /** - * Optional marketplace name; omit to refresh all. + * Refreshes marketplaces resolved through the live session's working directory and retained managed policy. *

* Invokes the method with no params, applying the runtime defaults. * @@ -103,7 +103,7 @@ public CompletableFuture refresh() { } /** - * Optional marketplace name; omit to refresh all. + * Refreshes marketplaces resolved through the live session's working directory and retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderApi.java index 26ebee8489..8d3b283405 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderApi.java @@ -31,7 +31,7 @@ public final class SessionProviderApi { } /** - * Optional model identifier to scope the endpoint snapshot to. + * Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. *

* Invokes the method with no params, applying the runtime defaults. * @@ -44,7 +44,7 @@ public CompletableFuture getEndpoint() { } /** - * Optional model identifier to scope the endpoint snapshot to. + * Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -60,7 +60,7 @@ public CompletableFuture getEndpoint(SessionPr } /** - * BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. + * Adds BYOK providers and/or models to the session's registry at runtime, extending the additive registry built from the session's `providers`/`models` options. Both fields are optional, so a call may add providers only, models only, or both. Within a single call providers are registered before models, so a model may reference a provider added in the same call; across calls a model may reference any provider already registered (from session creation or a prior add). A model whose referenced provider is not registered by the end of the call is rejected. Newly added models become selectable via `model.list` / `model.switchTo` and are inherited by sub-agents spawned afterwards. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture add(SessionProviderAddParams } /** - * Authoritative BYOK provider and model registry snapshot to apply atomically to the session. + * Atomically updates the session's BYOK provider and model registry by applying the supplied snapshot, replacing existing entries, updating models, or removing entries absent from the snapshot. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -92,7 +92,7 @@ public CompletableFuture sync(SessionProviderSyncPara } /** - * Host-managed model selection ids to withdraw from the session's BYOK registry. + * Withdraws named host-managed models from the session's BYOK registry, leaving every other entry untouched. The scoped counterpart to `provider.sync`: a snapshot can only describe entries the caller knows about, so using it to remove one model silently withdraws rows registered by another source, such as a plugin calling `provider.add` at runtime. Naming what to remove leaves unrelated entries alone. Selection ids that are not registered are ignored, so withdrawal is idempotent. A provider is removed only when one of the withdrawn models was the last entry referencing it; a provider that simply has no models, which is the normal state while its rows are supplied by catalog discovery, is left in place. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersApi.java index 6f6dc8b5e5..f7dde45999 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersApi.java @@ -35,7 +35,7 @@ public final class SessionProvidersApi { } /** - * Identifies the target session. + * Returns adapter definitions and supported operations in this session's effective provider catalog, without running discovery. Does not list provider instances or select inference models. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -46,7 +46,7 @@ public CompletableFuture getCatalog() { } /** - * Provider discovery parameters. + * Discovers reachable instances using an adapter from this session's effective provider catalog and provider-specific discovery input. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -62,7 +62,7 @@ public CompletableFuture discover(SessionProvide } /** - * Provider status request parameters. + * Gets current health and version information for a discovered model-provider instance. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsApi.java index b309fc7c12..3f5855d9a8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsApi.java @@ -31,7 +31,7 @@ public final class SessionProvidersModelsApi { } /** - * Provider model inventory request parameters. + * Lists models installed or otherwise available from a discovered model-provider instance. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture list(SessionProviders } /** - * A discovered instance and one of its models to translate into provider configuration. Pass back the instance and model as returned by `session.providers.discover` and `session.providers.models.list`. + * Translates a discovered model into the provider and model configuration needed to use it, and reports whether each is already registered in this session. Prepares only: it registers nothing, writes nothing, and performs no provider requests. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueApi.java index 101d1726f2..e38110708f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueApi.java @@ -31,7 +31,7 @@ public final class SessionQueueApi { } /** - * Identifies the target session. + * Returns the local session's pending user-facing queued items and steering messages. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture pendingItems() { } /** - * Identifies the target session. + * Returns the internal native queue snapshot for in-process session orchestration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -53,7 +53,7 @@ CompletableFuture snapshot() { } /** - * Parameters for moving a queued item by stable id. + * Moves an addressable queued item to a public visible position. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -69,7 +69,7 @@ public CompletableFuture moveItem(SessionQueueMoveIt } /** - * Parameters for inserting a queued message at a public visible position. + * Inserts a new queued message at a public visible position. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -85,7 +85,7 @@ public CompletableFuture insertAt(SessionQueueInsert } /** - * Parameters for removing a queued item by stable id. + * Removes an addressable queued item by its stable id. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -101,7 +101,7 @@ public CompletableFuture removeAt(SessionQueueRemove } /** - * Parameters for editing a single queued message. + * Updates the text of an addressable single-message queue item. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -117,7 +117,7 @@ public CompletableFuture updateText(SessionQueueUp } /** - * Conditional withdrawal of a single user message, from its queue or from the running turn it started. + * Atomically withdraws an unchanged user message of a local session: from the queued or steering lane while unconsumed, or from the running turn it started while the model has not answered it and nothing the user sent after it is pending. Withdrawing from the running turn interrupts that turn and removes its events from history. A client retaining the original draft may restore it only when removed is true. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -133,7 +133,7 @@ public CompletableFuture withdrawMessage(Sess } /** - * Append to one pending steering message without changing its identity or delivery position. + * Atomically appends text and attachments to an unchanged, unconsumed local steering message. Returns updated=false if delivery or withdrawal already claimed the message. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -149,7 +149,7 @@ public CompletableFuture appendSteering(Sessio } /** - * Parameters for duplicating a queued item. + * Duplicates an addressable queued item immediately after its source. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -165,7 +165,7 @@ public CompletableFuture duplicateAt(SessionQueue } /** - * Parameters for acquiring or releasing the queued-lane drain pause. Acquisition is exclusive and non-idempotent: `paused: true` against an already-paused session fails with `queue_already_paused`. The pause is never released automatically — it is not tied to the caller's lifetime, so a client that exits without sending `paused: false` leaves the lane frozen. Release is unowned: `paused: false` clears the pause for any caller, including one that never acquired it. + * Acquires or releases the queued-lane drain pause. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -181,7 +181,7 @@ public CompletableFuture setDrainPaused(SessionQueueSetDrainPausedParams p } /** - * Parameters for steering a queued message into a live turn. + * Moves an addressable queued message into the live turn's steering lane. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -197,7 +197,7 @@ public CompletableFuture sendNow(SessionQueueSendNowP } /** - * Identifies the target session. + * Reports whether the local session has native queued work pending. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -208,7 +208,7 @@ CompletableFuture hasPending() { } /** - * Inputs for starting a deferred-idle drain. + * Begins a native deferred-idle drain when background work has quiesced. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -224,7 +224,7 @@ CompletableFuture beginDeferredIdleDrain(Sess } /** - * Inputs for completing a deferred-idle drain. + * Finishes a native deferred-idle drain and reports whether to drain queue work or emit idle. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -240,7 +240,7 @@ CompletableFuture finishDeferredIdleDrain(Se } /** - * Inputs for marking session.idle deferred in native state. + * Marks session.idle as deferred by native background work state. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -256,7 +256,7 @@ CompletableFuture deferSessionIdle(SessionQueueDeferSessionIdleParams para } /** - * Identifies the target session. + * Removes the most recently queued user-facing item (LIFO). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -267,7 +267,7 @@ public CompletableFuture removeMostRecent() } /** - * Identifies the target session. + * Clears all pending queued items on the local session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -278,7 +278,7 @@ public CompletableFuture clear() { } /** - * Internal filter for consuming queued system notifications. + * Consumes queued native system notifications matching an internal filter. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -294,7 +294,7 @@ CompletableFuture consumeSystemNotifications(Sessio } /** - * Identifies the target session. + * Enqueues the internal resume-pending wake item when orphan handling needs a follow-up turn. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -305,7 +305,7 @@ CompletableFuture enqueueResumePending() { } /** - * Identifies the target session. + * Drains the native local-session work queue for in-process session orchestration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaApi.java new file mode 100644 index 0000000000..15ecde4f84 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaApi.java @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code quota} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionQuotaApi { + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionQuotaApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Gets the session's current quota and account projection without making a network request. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture get() { + return caller.invoke("session.quota.get", java.util.Map.of("sessionId", this.sessionId), SessionQuotaGetResult.class); + } + + /** + * Refreshes the session's provider model catalog bypassing its cache, folds quota snapshots into session state, and returns the updated projection. Failures leave the last known quota intact. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture refresh() { + return caller.invoke("session.quota.refresh", java.util.Map.of("sessionId", this.sessionId), SessionQuotaRefreshResult.class); + } + + /** + * Returns and clears the session's pending quota warnings. Reading or refreshing quota does not drain warnings. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture> takeWarnings() { + return caller.invoke("session.quota.takeWarnings", java.util.Map.of("sessionId", this.sessionId), RpcMapper.INSTANCE.getTypeFactory().constructCollectionType(List.class, QuotaWarningProjection.class)); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaDelegateWarning.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaDelegateWarning.java new file mode 100644 index 0000000000..ed6331a96f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaDelegateWarning.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.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; + +/** + * Existing delegation guidance for an account without delegation access. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionQuotaDelegateWarning( + /** User-facing guidance. */ + @JsonProperty("text") String text, + /** Link to upgrade the account. */ + @JsonProperty("url") String url +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaGetParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaGetParams.java new file mode 100644 index 0000000000..1c66851a3d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaGetParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionQuotaGetParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaGetResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaGetResult.java new file mode 100644 index 0000000000..cc92c0199f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaGetResult.java @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * The incumbent session-owned quota and account projection. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionQuotaGetResult( + /** Known quota snapshots, keyed by the provider's quota type. */ + @JsonProperty("snapshots") Map snapshots, + /** Provider-owned account quota state, including snapshots without numeric balances. */ + @JsonProperty("providerQuotas") List providerQuotas, + /** Whether the account uses the free limited Copilot plan. */ + @JsonProperty("isFreeUser") Boolean isFreeUser, + /** Whether the account uses token-based billing. */ + @JsonProperty("isTbbUser") Boolean isTbbUser, + /** Resolved Copilot plan tier. */ + @JsonProperty("planTier") SessionQuotaPlanTier planTier, + /** Whether premium-request costs are billable. */ + @JsonProperty("premiumRequestsBillable") Boolean premiumRequestsBillable, + /** Whether model cost columns should be shown. */ + @JsonProperty("modelCostColumnVisible") Boolean modelCostColumnVisible, + /** Whether the account can delegate tasks to GitHub. */ + @JsonProperty("delegateAvailable") Boolean delegateAvailable, + /** Whether the account can sign up for Copilot Free. */ + @JsonProperty("canSignupForCopilotFree") Boolean canSignupForCopilotFree, + /** Whether dynamic workflows are enabled for the active account. */ + @JsonProperty("dynamicWorkflowsEnabled") Boolean dynamicWorkflowsEnabled, + /** Whether dynamic workflows are visible under the session's feature flags. */ + @JsonProperty("dynamicWorkflowsUiVisible") Boolean dynamicWorkflowsUiVisible, + /** Upgrade link for a free account. */ + @JsonProperty("upgradeUrl") String upgradeUrl, + /** Existing delegation warning for a free account. */ + @JsonProperty("delegateWarning") SessionQuotaDelegateWarning delegateWarning +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaPlanTier.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaPlanTier.java new file mode 100644 index 0000000000..fc2e5c53c9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaPlanTier.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Copilot plan tier used by the session quota projection. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SessionQuotaPlanTier { + /** The {@code free} variant. */ + FREE("free"), + /** The {@code edu} variant. */ + EDU("edu"), + /** The {@code pro} variant. */ + PRO("pro"), + /** The {@code pro_plus} variant. */ + PRO_PLUS("pro_plus"), + /** The {@code business} variant. */ + BUSINESS("business"), + /** The {@code enterprise} variant. */ + ENTERPRISE("enterprise"), + /** The {@code max} variant. */ + MAX("max"), + /** The {@code unknown} variant. */ + UNKNOWN("unknown"); + + private final String value; + SessionQuotaPlanTier(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SessionQuotaPlanTier fromValue(String value) { + for (SessionQuotaPlanTier v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SessionQuotaPlanTier value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaRefreshParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaRefreshParams.java new file mode 100644 index 0000000000..ac51e99123 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaRefreshParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionQuotaRefreshParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaRefreshResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaRefreshResult.java new file mode 100644 index 0000000000..0f0892d284 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaRefreshResult.java @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * The incumbent session-owned quota and account projection. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionQuotaRefreshResult( + /** Known quota snapshots, keyed by the provider's quota type. */ + @JsonProperty("snapshots") Map snapshots, + /** Provider-owned account quota state, including snapshots without numeric balances. */ + @JsonProperty("providerQuotas") List providerQuotas, + /** Whether the account uses the free limited Copilot plan. */ + @JsonProperty("isFreeUser") Boolean isFreeUser, + /** Whether the account uses token-based billing. */ + @JsonProperty("isTbbUser") Boolean isTbbUser, + /** Resolved Copilot plan tier. */ + @JsonProperty("planTier") SessionQuotaPlanTier planTier, + /** Whether premium-request costs are billable. */ + @JsonProperty("premiumRequestsBillable") Boolean premiumRequestsBillable, + /** Whether model cost columns should be shown. */ + @JsonProperty("modelCostColumnVisible") Boolean modelCostColumnVisible, + /** Whether the account can delegate tasks to GitHub. */ + @JsonProperty("delegateAvailable") Boolean delegateAvailable, + /** Whether the account can sign up for Copilot Free. */ + @JsonProperty("canSignupForCopilotFree") Boolean canSignupForCopilotFree, + /** Whether dynamic workflows are enabled for the active account. */ + @JsonProperty("dynamicWorkflowsEnabled") Boolean dynamicWorkflowsEnabled, + /** Whether dynamic workflows are visible under the session's feature flags. */ + @JsonProperty("dynamicWorkflowsUiVisible") Boolean dynamicWorkflowsUiVisible, + /** Upgrade link for a free account. */ + @JsonProperty("upgradeUrl") String upgradeUrl, + /** Existing delegation warning for a free account. */ + @JsonProperty("delegateWarning") SessionQuotaDelegateWarning delegateWarning +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaSnapshot.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaSnapshot.java new file mode 100644 index 0000000000..996bdf7227 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaSnapshot.java @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.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; + +/** + * Session quota snapshot, preserving the incumbent numeric units and reset metadata. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionQuotaSnapshot( + /** Whether the entitlement is unlimited. */ + @JsonProperty("isUnlimitedEntitlement") Boolean isUnlimitedEntitlement, + /** Entitled quantity in this quota's units. */ + @JsonProperty("entitlementRequests") Double entitlementRequests, + /** Consumed quantity in this quota's units. */ + @JsonProperty("usedRequests") Double usedRequests, + /** Whether usage is allowed after quota exhaustion. */ + @JsonProperty("usageAllowedWithExhaustedQuota") Boolean usageAllowedWithExhaustedQuota, + /** Additional usage in this quota's units. */ + @JsonProperty("overage") Double overage, + /** Whether additional usage is allowed after quota exhaustion. */ + @JsonProperty("overageAllowedWithExhaustedQuota") Boolean overageAllowedWithExhaustedQuota, + /** Percentage of the entitlement remaining. */ + @JsonProperty("remainingPercentage") Double remainingPercentage, + /** Quota reset time in milliseconds since the Unix epoch, when known. */ + @JsonProperty("resetDateEpochMs") Double resetDateEpochMs, + /** Whether the reset time is estimated. */ + @JsonProperty("resetDateEstimated") Boolean resetDateEstimated, + /** Whether the provider reports available quota. */ + @JsonProperty("hasQuota") Boolean hasQuota, + /** Whether this quota uses token-based billing. */ + @JsonProperty("tokenBasedBilling") Boolean tokenBasedBilling, + /** Additional-usage budget cap, when provided. */ + @JsonProperty("overageEntitlement") Double overageEntitlement +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaTakeWarningsParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaTakeWarningsParams.java new file mode 100644 index 0000000000..2a96fdd5a2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQuotaTakeWarningsParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionQuotaTakeWarningsParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRemoteApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRemoteApi.java index bb1cfa0e25..015c69f830 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRemoteApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRemoteApi.java @@ -31,7 +31,7 @@ public final class SessionRemoteApi { } /** - * Optional remote session mode ("off", "export", or "on"); defaults to enabling both export and remote steering. + * Enables remote session export or steering. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture enable(SessionRemoteEnablePa } /** - * Identifies the target session. + * Disables remote session export and steering. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -58,7 +58,7 @@ public CompletableFuture disable() { } /** - * New remote-steerability state to persist as a `session.remote_steerable_changed` event. + * Persists a remote-steerability change emitted by the host as a session event. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java index 9c235ef2e3..21050a5317 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java @@ -113,6 +113,8 @@ public final class SessionRpc { public final SessionEventLogApi eventLog; /** API methods for the {@code usage} namespace. */ public final SessionUsageApi usage; + /** API methods for the {@code quota} namespace. */ + public final SessionQuotaApi quota; /** API methods for the {@code limitPrediction} namespace. */ public final SessionLimitPredictionApi limitPrediction; /** API methods for the {@code remote} namespace. */ @@ -173,6 +175,7 @@ public SessionRpc(RpcCaller caller, String sessionId) { this.queue = new SessionQueueApi(caller, sessionId); this.eventLog = new SessionEventLogApi(caller, sessionId); this.usage = new SessionUsageApi(caller, sessionId); + this.quota = new SessionQuotaApi(caller, sessionId); this.limitPrediction = new SessionLimitPredictionApi(caller, sessionId); this.remote = new SessionRemoteApi(caller, sessionId); this.visibility = new SessionVisibilityApi(caller, sessionId); @@ -180,7 +183,7 @@ public SessionRpc(RpcCaller caller, String sessionId) { } /** - * Identifies the target session. + * Suspends the session while preserving persisted state for later resume. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -191,7 +194,7 @@ public CompletableFuture suspend() { } /** - * Parameters for sending a user message to the session + * Sends a user message to the session and returns its message ID. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -207,7 +210,7 @@ public CompletableFuture send(SessionSendParams params) { } /** - * Parameters for sending zero or more user messages to the session in a single turn. Remote-backed (Mission Control) sessions do not support this method and will return an error. + * Sends zero or more user messages to the session in a single turn and returns their message IDs. All provided messages are appended to the conversation in order, then exactly one agent turn runs over the resulting history. When the list is empty, one turn runs over the existing history with no new user message. Remote-backed (Mission Control) sessions do not support this method and will return an error. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -223,7 +226,7 @@ public CompletableFuture sendMessages(SessionSendMess } /** - * Internal request for sending a system notification. + * Queues or sends an internal system notification to the session according to its passive policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -239,7 +242,7 @@ CompletableFuture sendSystemNotification(SessionSendSystemNotificationPara } /** - * Parameters for aborting the current turn + * Aborts the current agent turn. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -255,7 +258,7 @@ public CompletableFuture abort(SessionAbortParams params) { } /** - * Parameters for interrupting the main agent turn. + * Interrupts the current main agent turn while leaving running background work (subagents, sidekicks, and promoted attached shells) alive. No-op when the main loop is not processing. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -271,7 +274,7 @@ public CompletableFuture interruptMainTurn(Sessi } /** - * Identifies the target session. + * Cancels every running background agent (task-registry subagents plus sidekick agents) without interrupting the main agent loop. Promoted attached shells are left running. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -282,7 +285,7 @@ public CompletableFuture cancelAllBackgroundAgents() { } /** - * Parameters for shutting down the session + * Shuts down the session and persists its final state. Awaits any deferred sessionEnd hooks before resolving so user-supplied hook scripts complete before the runtime tears down. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -298,7 +301,7 @@ public CompletableFuture shutdown(SessionShutdownParams params) { } /** - * Message text, optional severity level, persistence flag, optional follow-up URL, and optional tip. + * Emits a user-visible session log event. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java index a13ef559ef..2ace81bb8e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java @@ -31,7 +31,7 @@ public final class SessionSandboxApi { } /** - * Identifies the target session. + * Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture getEnforcemen } /** - * Request to disable sandboxing for the current session while resolving an active sandbox-bypass permission prompt. + * Disables sandboxing for the remainder of the current session and approves the referenced pending sandbox-bypass permission request. The request is rejected unless the exact request is still pending and the effective sandbox policy permits bypass. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture disableForSessio } /** - * Request to accept the sandbox path grant offered on an active sandbox escalation permission prompt. + * Adds the path offered by a pending sandbox escalation permission request's sandboxPathGrant to the session's sandbox policy and approves the request, so the blocked operation re-runs inside the sandbox rather than outside it. The request is rejected unless the exact request is still pending, carries a sandboxPathGrant, and the grant still takes effect under the current managed policy. Does not persist the path; hosts that store sandbox settings save it themselves. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleApi.java index a6e8ad88d7..0b6a90aca9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleApi.java @@ -31,7 +31,7 @@ public final class SessionScheduleApi { } /** - * Identifies the target session. + * Lists the session's currently active scheduled prompts. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture list() { } /** - * Identifies the target session. + * Hydrates the native schedule registry from persisted session events. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -53,7 +53,7 @@ CompletableFuture hydrate() { } /** - * Identifies the target session. + * Reports whether the session has an active self-paced scheduled prompt. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -64,7 +64,7 @@ CompletableFuture hasSelfPaced() { } /** - * Register a relative-interval scheduled prompt. + * Registers a relative-interval scheduled prompt. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -80,7 +80,7 @@ CompletableFuture add(SessionScheduleAddParams params) { } /** - * Register a cron scheduled prompt. + * Registers a recurring cron scheduled prompt. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -96,7 +96,7 @@ CompletableFuture addCron(SessionScheduleAddCronParams params } /** - * Register an absolute-time scheduled prompt. + * Registers an absolute-time scheduled prompt. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -112,7 +112,7 @@ CompletableFuture addAt(SessionScheduleAddAtParams params) { } /** - * Register a self-paced scheduled prompt. + * Registers a self-paced scheduled prompt. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -128,7 +128,7 @@ CompletableFuture addSelfPaced(SessionScheduleAddSelfPacedPar } /** - * Re-arm a self-paced scheduled prompt. + * Re-arms an active self-paced scheduled prompt. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -144,7 +144,7 @@ CompletableFuture rearmSelfPaced(SessionScheduleRearmSelfPace } /** - * Identifier of the scheduled prompt to remove. + * Removes a scheduled prompt by id. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsApi.java index 213b7de0a4..f07d028f93 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsApi.java @@ -31,7 +31,7 @@ final class SessionSettingsApi { } /** - * Identifies the target session. + * Returns a redacted snapshot of session runtime settings, with secrets and raw feature flags excluded. Internal: the runtime settings shape is a runtime-internal surface and is deliberately kept out of the public SDK, because consumers should not depend on the runtime's internal settings layout. It remains callable in-process and is expected to be reworked as the runtime internals are consolidated. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ CompletableFuture snapshot() { } /** - * Named Rust-owned settings predicate to evaluate for this session. + * Evaluates a named Rust-owned settings predicate without exposing raw feature flags. Internal: the raw feature-flag names and composition are runtime-internal, so this predicate-evaluation helper is kept out of the public SDK surface and is callable in-process only. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellApi.java index ebfd1a9c7d..6d3187ef75 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellApi.java @@ -31,7 +31,7 @@ public final class SessionShellApi { } /** - * Shell command to run, with optional working directory and timeout in milliseconds. Spawn failures return an RPC error. + * Starts a shell command, returning an RPC error if it cannot be spawned. The command runs as the leader of its own process group (POSIX) or in a dedicated job object (Windows), so a forced termination — via "shell.kill", the request timeout, or session disposal — signals that whole group/job rather than only the direct child. Two gaps are worth planning for: a command that exits on its own does not trigger that teardown, and on POSIX a descendant that moves itself into a new session or process group (for example via "setsid") leaves the signalled group, so either can leave a background process running. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture exec(SessionShellExecParams par } /** - * Identifier of a process previously returned by "shell.exec" and the signal to send. + * Sends a signal to a shell process previously started via "shell.exec". The signal targets the command's whole process group (POSIX) or job object (Windows), so descendants still in that group are signalled too, not just the direct child. On POSIX a descendant that moved itself into a new session or process group (for example via "setsid") is no longer in the signalled group and survives. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture kill(SessionShellKillParams par } /** - * User-requested shell command and cancellation handle. + * Executes a user-requested shell command through the session runtime. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -79,7 +79,7 @@ public CompletableFuture executeUserRequ } /** - * User-requested shell execution cancellation handle. + * Cancels a user-requested shell command by request ID. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSkillsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSkillsApi.java index f6cceb98b9..e21c294b31 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSkillsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSkillsApi.java @@ -31,7 +31,7 @@ public final class SessionSkillsApi { } /** - * Identifies the target session. + * Lists skills available to the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture list() { } /** - * Identifies the target session. + * Returns the skills that have been invoked during this session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -53,7 +53,7 @@ public CompletableFuture getInvoked() { } /** - * Name of the skill to enable for the session. + * Enables a skill for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -69,7 +69,7 @@ public CompletableFuture enable(SessionSkillsEnableParams params) { } /** - * Name of the skill to disable for the session. + * Disables a skill for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -85,7 +85,7 @@ public CompletableFuture disable(SessionSkillsDisableParams params) { } /** - * Identifies the target session. + * Reloads skill definitions for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -96,7 +96,7 @@ public CompletableFuture reload() { } /** - * Identifies the target session. + * Ensures the session's skill definitions have been loaded from disk. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java index c6ca1335de..9e78691586 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java @@ -31,7 +31,7 @@ public final class SessionTasksApi { } /** - * Agent type, prompt, name, and optional description and model override for the new task. + * Starts a background agent task in the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture startAgent(SessionTasksSt } /** - * Identifies the target session. + * Lists background tasks tracked by the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -58,7 +58,7 @@ public CompletableFuture list() { } /** - * Registers or reclaims a client-owned task. + * Registers a client-owned task, or reclaims an orphaned task belonging to the same extension principal. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture register(SessionTasksRegist } /** - * Updates a client-owned task. + * Publishes generic progress or a terminal outcome for a client-owned task. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -90,7 +90,7 @@ public CompletableFuture update(SessionTasksUpdatePara } /** - * Identifies the target session. + * Refreshes metadata for any detached background shells the runtime knows about. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -101,7 +101,7 @@ public CompletableFuture refresh() { } /** - * Identifies the target session. + * Waits for all in-flight background tasks and any follow-up turns to settle. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -112,7 +112,7 @@ public CompletableFuture waitForPending() { } /** - * Identifier of the background task to fetch progress for. + * Returns progress information for a background task by ID. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -128,7 +128,7 @@ public CompletableFuture getProgress(SessionTasks } /** - * Identifies the target session. + * Returns the first sync-waiting task that can currently be promoted to background mode. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -139,7 +139,7 @@ public CompletableFuture getCurrentPromo } /** - * Identifier of the task to promote to background mode. + * Promotes an eligible synchronously-waited task so it continues running in the background. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -155,7 +155,7 @@ public CompletableFuture promoteToBackgro } /** - * Identifies the target session. + * Atomically promotes the first promotable sync-waiting task to background mode and returns it. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -166,7 +166,7 @@ public CompletableFuture promoteCu } /** - * Identifier of the background task to cancel. + * Cancels a background task. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -182,7 +182,7 @@ public CompletableFuture cancel(SessionTasksCancelPara } /** - * Identifier of the completed or cancelled task to remove from tracking. + * Removes a completed or cancelled background task from tracking. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -198,7 +198,7 @@ public CompletableFuture remove(SessionTasksRemovePara } /** - * Identifier of the target agent task, message content, and optional sender agent ID. + * Sends a message to a background agent task. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTelemetryApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTelemetryApi.java index 42589661ef..d74f8366da 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTelemetryApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTelemetryApi.java @@ -31,7 +31,7 @@ public final class SessionTelemetryApi { } /** - * Identifies the target session. + * Gets the telemetry engagement ID currently associated with the session, when available. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture getEngagementId( } /** - * Feature override key/value pairs to attach to subsequent telemetry events from this session. + * Sets feature override key/value pairs to attach to subsequent telemetry events for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsApi.java index 62593a3588..250478499b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsApi.java @@ -31,7 +31,7 @@ public final class SessionToolsApi { } /** - * A tool name and arguments to execute through the session's native invocation pipeline. + * Executes one tool from the session's currently offered tool set through the native invocation pipeline. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture execute(SessionToolsExecuteParams params) { } /** - * Options controlling how Rust-owned built-in tool descriptors are materialized. + * Returns the Rust-owned built-in tool descriptors used to construct the session's offered tool set. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture getBuiltinDesc } /** - * Task-completion tool arguments and final result used to build a label-safe session event payload. + * Projects a completed task_complete tool call into its label-safe session event payload. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -79,7 +79,7 @@ public CompletableFuture taskCompleteEv } /** - * Pending external tool call request ID, with the tool result or an error describing why it failed. + * Provides the result for a pending external tool call. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -95,7 +95,18 @@ public CompletableFuture handlePendingT } /** - * Identifies the target session. + * Lists the external tool calls of the session and its sub-agents that are still waiting for session.tools.handlePendingToolCall. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture listPendingRequests() { + return caller.invoke("session.tools.listPendingRequests", java.util.Map.of("sessionId", this.sessionId), SessionToolsListPendingRequestsResult.class); + } + + /** + * Resolves, builds, and validates the runtime tool list for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -106,7 +117,7 @@ public CompletableFuture initializeAndValidate() { } /** - * Identifies the target session. + * Returns lightweight metadata for the session's currently initialized tools. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -117,7 +128,7 @@ public CompletableFuture getCurrentMetadat } /** - * Complete externally implemented tool list for the calling connection. An empty list removes every tool previously supplied by that connection. + * Atomically replaces the complete externally implemented tool list supplied by the calling connection. Built-in, MCP/plugin, extension-discovered, subagent, and tools supplied by other connections remain unchanged. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -133,7 +144,7 @@ public CompletableFuture set(SessionToolsSetParams params) { } /** - * Subagent settings to apply to the current session + * Sets the current session's live subagent settings override, which takes precedence over persisted user settings until cleared. Persisted user settings remain the source of truth for future sessions. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsListPendingRequestsParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsListPendingRequestsParams.java new file mode 100644 index 0000000000..8a10167180 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsListPendingRequestsParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * No parameters; returns the external tool calls of the session and its sub-agents that are still waiting for a result. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionToolsListPendingRequestsParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsListPendingRequestsResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsListPendingRequestsResult.java new file mode 100644 index 0000000000..c7b17b4928 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsListPendingRequestsResult.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.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * External tool calls still waiting for a result. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionToolsListPendingRequestsResult( + /** External tool calls of the session and its sub-agents that session.tools.handlePendingToolCall would still accept, in the order they were requested. Includes calls that were pending when the session was last suspended and resumed with pending work continued, so a host that did not observe the original external_tool.requested event can answer them. */ + @JsonProperty("items") List items +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java index 7feccbd56a..5187b51774 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java @@ -31,7 +31,7 @@ public final class SessionUiApi { } /** - * Transient question to answer without adding it to conversation history. + * Runs a transient no-tools model query against the current conversation context. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture ephemeralQuery(SessionUi } /** - * Prompt message and JSON schema describing the form fields to elicit from the user. + * Requests structured input from a UI-capable client. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture elicitation(SessionUiElicit } /** - * Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + * Provides the user response for a pending elicitation request. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -79,7 +79,7 @@ public CompletableFuture handlePendingE } /** - * Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + * Resolves a pending elicitation request after direct interaction in the trusted in-process client. Only an accepted response to the built-in ask_user tool can become trusted human evidence. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -95,7 +95,7 @@ CompletableFuture handleHumanAskUser(SessionUiHandleHumanAs } /** - * Request ID of a pending `user_input.requested` event and the user's response. + * Resolves a pending `user_input.requested` event with the user's response. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -111,7 +111,7 @@ public CompletableFuture handlePendingUse } /** - * Request ID of a pending `user_input.requested` event and the user's response. + * Resolves a pending `user_input.requested` event after direct interaction in the trusted in-process client. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -127,7 +127,7 @@ CompletableFuture handleHumanUserInput(SessionUiHandleHum } /** - * Request ID of a pending `sampling.requested` event and an optional sampling result payload (omit to reject). + * Resolves a pending `sampling.requested` event with a sampling result, or rejects it. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -143,7 +143,7 @@ public CompletableFuture handlePendingSamp } /** - * Request ID of a pending `auto_mode_switch.requested` event and the user's response. + * Resolves a pending `auto_mode_switch.requested` event with the user's accept/decline decision. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -159,7 +159,7 @@ public CompletableFuture handlePendi } /** - * Request ID of a pending `session_limits_exhausted.requested` event and the user's selected limit action. + * Resolves a pending `session_limits_exhausted.requested` event with the user's selected limit action. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -175,7 +175,7 @@ public CompletableFuture han } /** - * Request ID of a pending `exit_plan_mode.requested` event and the user's response. + * Resolves a pending `exit_plan_mode.requested` event with the user's response. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -191,7 +191,7 @@ public CompletableFuture handlePending } /** - * Request ID of a pending `exit_plan_mode.requested` event and the user's response. + * Resolves a pending `exit_plan_mode.requested` event after direct interaction in the trusted in-process client. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -207,7 +207,7 @@ CompletableFuture handleHumanExitPlanMode(SessionUiHandle } /** - * Identifies the target session. + * Registers an in-process handler for auto-mode-switch requests so the server bridge skips dispatch. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -218,7 +218,7 @@ public CompletableFuture reg } /** - * Opaque handle previously returned by `registerDirectAutoModeSwitchHandler` to release. + * Unregisters a previously-registered in-process auto-mode-switch handler by its opaque handle. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageApi.java index 16ded9d6b6..75a19eadb1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageApi.java @@ -19,6 +19,8 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") public final class SessionUsageApi { + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + private final RpcCaller caller; private final String sessionId; @@ -29,7 +31,7 @@ public final class SessionUsageApi { } /** - * Identifies the target session. + * Gets accumulated usage metrics for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -39,4 +41,20 @@ public CompletableFuture getMetrics() { return caller.invoke("session.usage.getMetrics", java.util.Map.of("sessionId", this.sessionId), SessionUsageGetMetricsResult.class); } + /** + * Internal host-only replacement of absolute code-change totals reported by a relay host. + *

+ * 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 + */ + @CopilotExperimental + CompletableFuture setCodeChanges(SessionUsageSetCodeChangesParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.usage.setCodeChanges", _p, Void.class); + } + } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageGetMetricsResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageGetMetricsResult.java index a38afd7042..1a771a239a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageGetMetricsResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageGetMetricsResult.java @@ -12,6 +12,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.github.copilot.CopilotExperimental; import java.time.OffsetDateTime; +import java.util.List; import java.util.Map; import javax.annotation.processing.Generated; @@ -26,6 +27,8 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record SessionUsageGetMetricsResult( + /** Completeness of totalNanoAiu. Numeric totals remain the reported subtotal; zero is not a claim of free usage when unavailable. */ + @JsonProperty("aiCreditsStatus") AiCreditsStatus aiCreditsStatus, /** Total user-initiated premium request cost across all models (may be fractional due to multipliers) */ @JsonProperty("totalPremiumRequestCost") Double totalPremiumRequestCost, /** Raw count of user-initiated API requests */ @@ -42,6 +45,9 @@ public record SessionUsageGetMetricsResult( @JsonProperty("codeChanges") UsageMetricsCodeChanges codeChanges, /** Per-model token and request metrics, keyed by model identifier */ @JsonProperty("modelMetrics") Map modelMetrics, + /** Provider-attributed model metrics, ordered by first observed provider and model. +Null attribution preserves historical usage whose provider or model was not recorded. */ + @JsonProperty("providerModelMetrics") List providerModelMetrics, /** Per-agent usage metrics, keyed by agent instance identifier. The main conversation uses the stable key `main`. */ @JsonProperty("agentMetrics") Map agentMetrics, /** Currently active model identifier */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageSetCodeChangesParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageSetCodeChangesParams.java new file mode 100644 index 0000000000..d8a963a75a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageSetCodeChangesParams.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Internal absolute code-change totals reported by the owning host. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionUsageSetCodeChangesParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Absolute added-line total, replacing the previous reading. */ + @JsonProperty("linesAdded") Double linesAdded, + /** Absolute removed-line total, replacing the previous reading. */ + @JsonProperty("linesRemoved") Double linesRemoved, + /** Absolute changed-file count; omission preserves the previous count. */ + @JsonProperty("filesCount") Double filesCount +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionVisibilityApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionVisibilityApi.java index 54f38c2614..b942828e10 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionVisibilityApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionVisibilityApi.java @@ -31,7 +31,7 @@ public final class SessionVisibilityApi { } /** - * Identifies the target session. + * Returns the session's current Mission Control sharing status and shareable GitHub URL. Reflects whether the synced session is visible to repository readers ("repo") or restricted to its creator and collaborators ("unshared"). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture get() { } /** - * Desired sharing status for the session. + * Sets the session's Mission Control sharing status, controlling whether the synced session is visible to repository readers. Returns the effective status and shareable GitHub URL after the change. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowApi.java index 2b9e81e42f..e746033b39 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowApi.java @@ -35,7 +35,7 @@ public final class SessionWorkflowApi { } /** - * Parameters for invoking a registered workflow. + * Runs a registered dynamic workflow by name at the top level. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -51,7 +51,7 @@ public CompletableFuture run(SessionWorkflowRunParams } /** - * Parameters for resuming a workflow run from its persisted identity. + * Resumes a dynamic workflow run using its persisted name, arguments, journal, and accounting. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -67,7 +67,7 @@ public CompletableFuture resume(SessionWorkflowResu } /** - * Internal parameters for invoking a registered workflow from a tool. + * Internal tool-originated dynamic workflow invocation. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -83,7 +83,7 @@ CompletableFuture runFromTool(SessionWorkflowRunFromToolParam } /** - * Internal parameters for resuming a workflow run from a tool. + * Internal tool-originated dynamic workflow resume. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -99,7 +99,7 @@ CompletableFuture resumeFromTool(SessionWorkflowResumeFrom } /** - * Parameters for retrieving a workflow run. + * Gets the current or settled envelope for a dynamic workflow run. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -115,7 +115,7 @@ public CompletableFuture getRun(SessionWorkflowGetR } /** - * Parameters for paging workflow runs. + * Lists durable dynamic workflow runs for this session in creation order. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -131,7 +131,7 @@ public CompletableFuture listRuns(SessionWorkflow } /** - * Parameters for retrieving a workflow run. + * Gets durable and live observability detail for one dynamic workflow run. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -147,7 +147,7 @@ public CompletableFuture getRunDetail(Session } /** - * Parameters for paging workflow progress. + * Pages durable progress for one dynamic workflow run. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -163,7 +163,7 @@ public CompletableFuture getRunProgress(Ses } /** - * Parameters for cancelling a workflow run. + * Requests cancellation of a dynamic workflow run and returns its run envelope. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -179,7 +179,7 @@ public CompletableFuture cancel(SessionWorkflowCanc } /** - * Parameters for pausing a running workflow. + * Pauses a running dynamic workflow and returns its settled run envelope. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -195,7 +195,7 @@ public CompletableFuture pause(SessionWorkflowPauseP } /** - * Parameters for an owned durable pause checkpoint. + * Atomically pauses an owned dynamic workflow attempt at a durable checkpoint. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -211,7 +211,7 @@ CompletableFuture pauseAtCheckpoint(SessionWorkfl } /** - * Parameters for recording workflow progress. + * Records a batch of ordered dynamic workflow progress lines. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -227,7 +227,7 @@ public CompletableFuture log(SessionWorkflowLogParams params) { } /** - * Parameters for one workflow-scoped subagent call. + * Runs one dynamic-workflow-scoped subagent and returns its result. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowJournalApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowJournalApi.java index 0909931439..70bc1b47e2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowJournalApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowJournalApi.java @@ -31,7 +31,7 @@ public final class SessionWorkflowJournalApi { } /** - * Parameters for reading a workflow journal entry. + * Reads a memoized dynamic workflow journal entry. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture get(SessionWorkflowJou } /** - * Parameters for storing a workflow journal entry. + * Stores a memoized dynamic workflow journal entry. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkingDirectoryContextWithClient.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkingDirectoryContextWithClient.java new file mode 100644 index 0000000000..d909895e2d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkingDirectoryContextWithClient.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.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; + +/** + * A working-directory context together with the client that produced it. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionWorkingDirectoryContextWithClient( + /** Current working directory path */ + @JsonProperty("cwd") String cwd, + /** Root directory of the git repository */ + @JsonProperty("gitRoot") String gitRoot, + /** Repository identifier derived from the git remote URL */ + @JsonProperty("repository") String repository, + /** Hosting platform type of the repository */ + @JsonProperty("hostType") String hostType, + /** Current git branch name */ + @JsonProperty("branch") String branch, + /** Raw host string from the git remote URL */ + @JsonProperty("repositoryHost") String repositoryHost, + /** Head commit of the current git branch */ + @JsonProperty("headCommit") String headCommit, + /** Merge-base commit SHA */ + @JsonProperty("baseCommit") String baseCommit, + /** Name of the client that created the session */ + @JsonProperty("clientName") String clientName +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkspacesApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkspacesApi.java index 71a3d1ea1b..c65bd3854d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkspacesApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkspacesApi.java @@ -8,6 +8,7 @@ package com.github.copilot.generated.rpc; import com.github.copilot.CopilotExperimental; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import javax.annotation.processing.Generated; @@ -31,7 +32,7 @@ public final class SessionWorkspacesApi { } /** - * Identifies the target session. + * Gets current workspace metadata for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +43,7 @@ public CompletableFuture getWorkspace() { } /** - * Workspace metadata fields to update. + * Updates workspace metadata for a local session and returns the refreshed workspace. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +59,7 @@ public CompletableFuture updateMetadata(S } /** - * Optional session context used when creating a local workspace. + * Ensures a local session workspace exists and returns it. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +75,7 @@ public CompletableFuture ensure(SessionWorkspaces } /** - * Identifies the target session. + * Lists files stored in the session workspace files directory. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -85,7 +86,7 @@ public CompletableFuture listFiles() { } /** - * Relative path of the workspace file to read. + * Reads a file from the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -101,7 +102,7 @@ public CompletableFuture readFile(SessionWorksp } /** - * Relative path and UTF-8 content for the workspace file to create or overwrite. + * Creates or overwrites a file in the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -117,7 +118,7 @@ public CompletableFuture createFile(SessionWorkspacesCreateFileParams para } /** - * Relative path of the workspace file or directory to inspect. + * Returns metadata for a file or directory in the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -133,7 +134,7 @@ public CompletableFuture statFile(SessionWorksp } /** - * Directory to create within the session workspace files directory. + * Creates a directory in the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -149,7 +150,7 @@ public CompletableFuture createDirectory(SessionWorkspacesCreateDirectoryP } /** - * File or directory to remove from the session workspace files directory. + * Removes a file or directory from the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -165,7 +166,7 @@ public CompletableFuture removePath(SessionWorkspacesRemovePathParams para } /** - * Source and destination paths for a rename within the session workspace files directory. + * Renames a file or directory within the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -181,7 +182,7 @@ public CompletableFuture renamePath(SessionWorkspacesRenamePathParams para } /** - * Identifies the target session. + * Lists workspace checkpoints in chronological order. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -192,7 +193,7 @@ public CompletableFuture listCheckpoints } /** - * Checkpoint number to read. + * Reads the content of a workspace checkpoint by number. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -208,7 +209,7 @@ public CompletableFuture readCheckpoint(S } /** - * Compaction summary checkpoint to persist. + * Adds a compaction summary checkpoint to the local session workspace. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -224,7 +225,7 @@ public CompletableFuture addSummary(SessionWo } /** - * Rollback point for local workspace summaries. + * Truncates local workspace compaction summaries after a rollback. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -240,7 +241,7 @@ public CompletableFuture truncateSumma } /** - * Identifies the target session. + * Reads the autopilot objective state file from the local session workspace. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -251,7 +252,7 @@ public CompletableFuture readAuto } /** - * Autopilot objective file content to persist. + * Writes the autopilot objective state file in the local session workspace. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -267,7 +268,7 @@ public CompletableFuture writeAu } /** - * Identifies the target session. + * Deletes the autopilot objective state file from the local session workspace. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -278,7 +279,7 @@ public CompletableFuture delete } /** - * Identifies the target session. + * Checks whether the local session workspace has an autopilot objective state file. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -289,7 +290,7 @@ public CompletableFuture autopi } /** - * Pasted content to save as a UTF-8 file in the session workspace. + * Saves pasted content as a UTF-8 file in the session workspace. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -305,7 +306,7 @@ public CompletableFuture saveLargePaste(S } /** - * Parameters for computing a workspace diff. + * Computes a diff for the session workspace. Never rejects for a busy session: a `session`-mode diff that cannot read the session's file-change captures falls back to an unstaged git diff with `isFallback: true` and reports why in `unavailableReason`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -320,4 +321,19 @@ public CompletableFuture diff(SessionWorkspacesDiff return caller.invoke("session.workspaces.diff", _p, SessionWorkspacesDiffResult.class); } + /** + * Computes a diff for the session workspace. Never rejects for a busy session: a `session`-mode diff that cannot read the session's file-change captures falls back to an unstaged git diff with `isFallback: true` and reports why in `unavailableReason`. + *

+ * Accepts the extensible request, including inputs added after the params record. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture diff(SessionWorkspacesDiffRequest request) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(Objects.requireNonNull(request, "request")); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.workspaces.diff", _p, SessionWorkspacesDiffResult.class); + } + } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkspacesDiffRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkspacesDiffRequest.java new file mode 100644 index 0000000000..cf12c01e63 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkspacesDiffRequest.java @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.Objects; +import javax.annotation.processing.Generated; + +/** + * Parameters for computing a workspace diff. + *

+ * Required inputs are constructor arguments. Optional inputs have fluent setters. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class SessionWorkspacesDiffRequest { + + /** Diff mode requested by the client. */ + @JsonProperty("mode") + private final WorkspaceDiffMode mode; + + /** When true, ignore whitespace-only changes (git `--ignore-all-space`). Defaults to false. */ + @JsonProperty("ignoreWhitespace") + private Boolean ignoreWhitespace; + + /** Include the full before/after text used to compute each session diff. Defaults to false; true is accepted only for session mode. Existing capture/read limits still apply, and binary or unavailable contents are not returned. This can substantially increase response size. */ + @JsonProperty("includeContents") + private Boolean includeContents; + + /** + * Creates a request with its required inputs. + * + * @param mode Diff mode requested by the client. + */ + public SessionWorkspacesDiffRequest(WorkspaceDiffMode mode) { + this.mode = Objects.requireNonNull(mode, "mode"); + } + + /** + * Returns the {@code mode} property. + * + * @return Diff mode requested by the client. + */ + public WorkspaceDiffMode getMode() { + return mode; + } + + /** + * Returns the {@code ignoreWhitespace} property. + * + * @return When true, ignore whitespace-only changes (git `--ignore-all-space`). Defaults to false. + */ + public Boolean getIgnoreWhitespace() { + return ignoreWhitespace; + } + + /** + * Returns the {@code includeContents} property. + * + * @return Include the full before/after text used to compute each session diff. Defaults to false; true is accepted only for session mode. Existing capture/read limits still apply, and binary or unavailable contents are not returned. This can substantially increase response size. + */ + public Boolean getIncludeContents() { + return includeContents; + } + + /** + * Sets the {@code ignoreWhitespace} property. + * + * @param value When true, ignore whitespace-only changes (git `--ignore-all-space`). Defaults to false. + * @return this request + */ + public SessionWorkspacesDiffRequest setIgnoreWhitespace(Boolean value) { + this.ignoreWhitespace = value; + return this; + } + + /** + * Sets the {@code includeContents} property. + * + * @param value Include the full before/after text used to compute each session diff. Defaults to false; true is accepted only for session mode. Existing capture/read limits still apply, and binary or unavailable contents are not returned. This can substantially increase response size. + * @return this request + */ + public SessionWorkspacesDiffRequest setIncludeContents(Boolean value) { + this.includeContents = value; + return this; + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsCreateWorkspaceParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsCreateWorkspaceParams.java new file mode 100644 index 0000000000..04c54ddedb --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsCreateWorkspaceParams.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identity, state location and starting context for a workspace record. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionsCreateWorkspaceParams( + /** Session ID the workspace record belongs to */ + @JsonProperty("sessionId") String sessionId, + /** Directory the session's state is written under when no session filesystem provider is configured. Ignored when a provider is configured; the provider's session state path is used instead. */ + @JsonProperty("sessionStatePath") String sessionStatePath, + /** `windows` (any letter case) selects Windows path rules. Any other value selects POSIX path rules. */ + @JsonProperty("convention") String convention, + /** Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. */ + @JsonProperty("context") SessionWorkingDirectoryContextWithClient context, + /** User-supplied display name for the workspace */ + @JsonProperty("name") String name +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsCreateWorkspaceResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsCreateWorkspaceResult.java new file mode 100644 index 0000000000..b2cbe17721 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsCreateWorkspaceResult.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.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; + +/** + * The workspace record that was written. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionsCreateWorkspaceResult( + /** The created workspace record, as JSON */ + @JsonProperty("workspaceJson") String workspaceJson +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsLoadWorkspaceParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsLoadWorkspaceParams.java new file mode 100644 index 0000000000..bb4b83da26 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsLoadWorkspaceParams.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Where the session's state lives, as a root directory and the session ID under it. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionsLoadWorkspaceParams( + /** Root directory every session's state directory sits under */ + @JsonProperty("sessionsHome") String sessionsHome, + /** Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsLoadWorkspaceResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsLoadWorkspaceResult.java new file mode 100644 index 0000000000..f17c2c85d6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsLoadWorkspaceResult.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.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; + +/** + * The workspace record on disk, omitted when the session has none. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionsLoadWorkspaceResult( + /** The workspace record, as JSON. Omitted when the record does not exist. */ + @JsonProperty("workspaceJson") String workspaceJson +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsUpdateWorkspaceFieldsParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsUpdateWorkspaceFieldsParams.java new file mode 100644 index 0000000000..770e8269be --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsUpdateWorkspaceFieldsParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Where the session's state lives, plus workspace-schema fields to merge into its workspace record. Stored keys outside the schema are not preserved, and a stored `fork_count` is never replaced. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionsUpdateWorkspaceFieldsParams( + /** Root directory every session's state directory sits under */ + @JsonProperty("sessionsHome") String sessionsHome, + /** Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. */ + @JsonProperty("sessionId") String sessionId, + /** Workspace-schema fields to merge into the record, as a JSON object. Fields the object omits keep their stored values, except stored keys outside the schema are not preserved and a stored `fork_count` is never replaced. */ + @JsonProperty("fieldsJson") String fieldsJson +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.java index 659da543b3..64a287661b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily. + * Authoritative catalog metadata for one SDK-provided skill. The skill's SKILL.md text is fetched separately and lazily. * * @since 1.0.0 */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.java index 96bb95206b..056b65f811 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported. + * Text-only SKILL.md content returned by an SDK session's skill provider. YAML frontmatter is optional: fields it omits come from the catalog descriptor, fields it declares must match the descriptor, and `allowed-tools` is read only from frontmatter. Related files and assets are not supported. * * @since 1.0.0 */ @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) record SkillProviderReadResult( - /** Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. */ + /** SKILL.md text, with or without YAML frontmatter, or null when the provider has no skill with the requested name. The runtime enforces a 1 MiB UTF-8 byte limit. */ @JsonProperty("markdown") String markdown ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UsageMetricsModelMetric.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UsageMetricsModelMetric.java index ed5f093054..879402975b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UsageMetricsModelMetric.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UsageMetricsModelMetric.java @@ -23,6 +23,8 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record UsageMetricsModelMetric( + /** Completeness of this model's reported AI-credit subtotal. Explicitly reported zero is complete, not unavailable. */ + @JsonProperty("aiCreditsStatus") AiCreditsStatus aiCreditsStatus, /** Request count and cost metrics for this model */ @JsonProperty("requests") UsageMetricsModelMetricRequests requests, /** Token usage metrics for this model */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UsageMetricsProviderModelMetric.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UsageMetricsProviderModelMetric.java new file mode 100644 index 0000000000..b4955e8c8d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UsageMetricsProviderModelMetric.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.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; + +/** + * Usage for one recorded provider and model, without merging identical model IDs across providers. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsProviderModelMetric( + /** Provider identity and product label captured when the call was dispatched; null when unknown. */ + @JsonProperty("provider") ModelProviderRef provider, + /** Model identity, or null for legacy aggregate-only usage. */ + @JsonProperty("modelId") String modelId, + /** Model display name captured at call time, when known. */ + @JsonProperty("modelDisplayName") String modelDisplayName, + /** Request, token, and cost totals for this provider/model. */ + @JsonProperty("metrics") UsageMetricsModelMetric metrics +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UserSettingsGetResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UserSettingsGetResult.java index c94e90fcc6..ac7b3cf933 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UserSettingsGetResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UserSettingsGetResult.java @@ -15,7 +15,7 @@ import javax.annotation.processing.Generated; /** - * Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. + * Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkspaceDiffContents.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkspaceDiffContents.java new file mode 100644 index 0000000000..7ffdaa1862 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkspaceDiffContents.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.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; + +/** + * Complete text used to generate one session diff. These are display contents, with the same text decoding as the patch, not a file-restore contract. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkspaceDiffContents( + /** Complete text before the session first changed the file. Omitted when the file did not exist; an empty string represents an existing empty file. */ + @JsonProperty("before") String before, + /** Complete current text read when computing the diff. Omitted for a deleted file; an empty string represents an existing empty file. */ + @JsonProperty("after") String after +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkspaceDiffFileChange.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkspaceDiffFileChange.java index e63b92b569..5ac0ec4efc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkspaceDiffFileChange.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkspaceDiffFileChange.java @@ -30,6 +30,27 @@ public record WorkspaceDiffFileChange( /** Original file path for renamed files. */ @JsonProperty("oldPath") String oldPath, /** Whether the diff content was omitted because it exceeded the per-file size limit. */ - @JsonProperty("isTruncated") Boolean isTruncated + @JsonProperty("isTruncated") Boolean isTruncated, + /** Full text used for this patch, only when includeContents was requested for session mode. Omitted for binary, oversized or unavailable contents, and for fallback results. Read isFallback and isTruncated before treating an absent value as a missing file. */ + @JsonProperty("contents") WorkspaceDiffContents contents ) { + + /** + * Creates a record with the components it had before later optional fields were added. + * + * @param path Path to the changed file, relative to the workspace root when the file lives under it. A file changed outside the workspace root keeps a `../`-relative path, or an absolute path when no relative path exists (for example a different Windows drive). + * @param diff Unified diff content for the file. Empty when the diff was truncated. + * @param changeType Type of change represented by this file diff. + * @param oldPath Original file path for renamed files. + * @param isTruncated Whether the diff content was omitted because it exceeded the per-file size limit. + */ + public WorkspaceDiffFileChange( + String path, + String diff, + WorkspaceDiffFileChangeType changeType, + String oldPath, + Boolean isTruncated + ) { + this(path, diff, changeType, oldPath, isTruncated, null); + } } diff --git a/java/sdk/src/main/java/com/github/copilot/CliServerManager.java b/java/sdk/src/main/java/com/github/copilot/CliServerManager.java index e742b242e5..92203ab755 100644 --- a/java/sdk/src/main/java/com/github/copilot/CliServerManager.java +++ b/java/sdk/src/main/java/com/github/copilot/CliServerManager.java @@ -14,7 +14,12 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.List; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; @@ -34,6 +39,7 @@ final class CliServerManager { private static final Logger LOG = Logger.getLogger(CliServerManager.class.getName()); private static final int STDERR_READER_JOIN_TIMEOUT_MS = 5000; + static final int FORCE_KILL_TIMEOUT_SECONDS = 10; private final CopilotClientOptions options; private final StringBuilder stderrBuffer = new StringBuilder(); @@ -132,12 +138,20 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { // Forward stderr to logger in background startStderrReader(process); - Integer detectedPort = null; - if (!options.isUseStdio()) { - detectedPort = waitForPortAnnouncement(process); + try { + Integer detectedPort = null; + if (!options.isUseStdio()) { + detectedPort = waitForPortAnnouncement(process); + } + return new ProcessInfo(process, detectedPort); + } catch (IOException | RuntimeException error) { + try { + cleanupCliProcess(process, captureDescendants(process), true); + } catch (RuntimeException cleanupError) { + error.addSuppressed(cleanupError); + } + throw error; } - - return new ProcessInfo(process, detectedPort); } /** @@ -206,11 +220,98 @@ private Integer waitForPortAnnouncement(Process process) throws IOException { } } - process.destroyForcibly(); throw new IOException("Timeout waiting for CLI to announce port"); } } + static void cleanupCliProcess(Process process, List descendants, boolean forceImmediately) { + if (!process.isAlive() && descendants.stream().noneMatch(ProcessHandle::isAlive)) { + return; + } + try { + if (terminateProcessTree(process, descendants, forceImmediately)) { + return; + } + if (!forceImmediately) { + if (terminateProcessTree(process, descendants, true)) { + return; + } + } + throw new IllegalStateException("CLI process tree did not terminate after shutdown: " + process.pid()); + } catch (InterruptedException error) { + Thread.currentThread().interrupt(); + throw new CompletionException("Interrupted while terminating the CLI process tree", error); + } catch (ExecutionException error) { + throw new CompletionException("Failed to await CLI process tree termination", error.getCause()); + } + } + + static List captureDescendants(Process process) { + return process.descendants().sorted(Comparator.comparingInt(CliServerManager::processDepth).reversed()) + .toList(); + } + + private static int processDepth(ProcessHandle process) { + int depth = 0; + var parent = process.parent(); + while (parent.isPresent()) { + depth++; + parent = parent.get().parent(); + } + return depth; + } + + private static boolean terminateProcessTree(Process process, List descendants, boolean force) + throws InterruptedException, ExecutionException { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(FORCE_KILL_TIMEOUT_SECONDS); + // Reap deepest children while their parents can still collect their exit. + // Killing the launcher first can orphan zombies in containers without an init. + for (var child : descendants) { + if (child.isAlive()) { + if (force) { + child.destroyForcibly(); + } else { + child.destroy(); + } + waitForProcessExit(child, deadline); + } + } + if (process.isAlive()) { + if (force) { + process.destroyForcibly(); + } else { + process.destroy(); + } + } + return waitForProcessTreeExit(process, descendants, deadline); + } + + private static boolean waitForProcessTreeExit(Process process, List descendants, long deadline) + throws InterruptedException, ExecutionException { + if (!process.waitFor(Math.max(0, deadline - System.nanoTime()), TimeUnit.NANOSECONDS)) { + return false; + } + for (var child : descendants) { + if (!waitForProcessExit(child, deadline)) { + return false; + } + } + return true; + } + + private static boolean waitForProcessExit(ProcessHandle process, long deadline) + throws InterruptedException, ExecutionException { + if (!process.isAlive()) { + return true; + } + try { + process.onExit().get(Math.max(0, deadline - System.nanoTime()), TimeUnit.NANOSECONDS); + return true; + } catch (TimeoutException error) { + return false; + } + } + String getStderrOutput() { synchronized (stderrBuffer) { return stderrBuffer.toString().trim(); diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java index 4a6d33c8ce..4e15259aeb 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java @@ -99,7 +99,7 @@ public final class CopilotClient implements AutoCloseable { public static final int AUTOCLOSEABLE_TIMEOUT_SECONDS = 10; private static final int RUNTIME_SHUTDOWN_TIMEOUT_SECONDS = 10; private static final int PROCESS_EXIT_TIMEOUT_SECONDS = 10; - private static final int FORCE_KILL_TIMEOUT_SECONDS = 10; + private static final int FORCE_KILL_TIMEOUT_SECONDS = CliServerManager.FORCE_KILL_TIMEOUT_SECONDS; /** * One-shot dispatcher used to run the owned-executor shutdown off any caller @@ -585,9 +585,21 @@ private Connection startCoreBody() { installationConfirmationAdapter.registerHandlers(connectedRpc); } InstallationConfirmationAdapter connectedInstallationConfirmationAdapter = installationConfirmationAdapter; + // JsonRpcClient keeps one handler per method, so this one serves every request + // that honors cancellation. + connectedRpc.registerMethodHandler("$/cancelRequest", (rpcId, params) -> { + dispatcher.handleCancelRequest(params); + if (connectedInstallationConfirmationAdapter != null) { + connectedInstallationConfirmationAdapter.handleCancel(connectedRpc, params); + } + }); connectedRpc.setCloseHandler(() -> { ahpHosts.disconnect(connection.serverRpc().host); - sessions.values().forEach(CopilotSession::cancelPendingExternalTools); + sessions.values().forEach(session -> { + session.cancelPendingExternalTools(); + session.clearSkillProvider(); + }); + dispatcher.cancelSkillProviderCalls(); if (connectedLlmAdapter != null) { connectedLlmAdapter.cancelPending(); } @@ -652,7 +664,11 @@ private Connection startCoreBody() { if (drainBeforeCleanup) { serverManager.awaitStderrReader(); } - cleanupCliProcess(process, true); + try { + CliServerManager.cleanupCliProcess(process, CliServerManager.captureDescendants(process), true); + } catch (RuntimeException cleanupError) { + e.addSuppressed(cleanupError); + } if (!drainBeforeCleanup) { serverManager.awaitStderrReader(); } @@ -753,7 +769,7 @@ private static boolean isUnsupportedConnectMethod(JsonRpcException ex) { *

  • Requests runtime shutdown for SDK-owned CLI processes
  • *
  • Closes stdin for an owned stdio process and waits for its host * cleanup
  • - *
  • Closes the JSON-RPC connection, terminating an owned process if + *
  • Closes the JSON-RPC connection, terminating an owned process tree if * needed
  • * *

    @@ -793,7 +809,8 @@ public CompletableFuture stop() { } /** - * Forces an immediate stop of the client without graceful cleanup. + * Immediately stops the client, retires sessions and pending waits, and drops + * unadmitted callbacks. * * @return A future that completes when the client is stopped */ @@ -801,8 +818,8 @@ public CompletableFuture forceStop() { ahpHosts.disconnect(); disposed = true; var activeSessions = new ArrayList<>(sessions.values()); + activeSessions.forEach(CopilotSession::forceStopLocally); sessions.clear(); - activeSessions.forEach(CopilotSession::cancelPendingExternalTools); gitHubTokenProviders.clear(); // Dispatch the blocking shutdownOwnedExecutor() on a dedicated thread: // cleanupConnection() is chained off async work running on the owned @@ -830,6 +847,10 @@ private CompletableFuture cleanupConnection(boolean gracefulRuntimeShutdow return CompletableFuture.completedFuture(null); } + // Retain children before EOF or shutdown can let their launcher exit. + List descendants = connection.process == null + ? List.of() + : CliServerManager.captureDescendants(connection.process); CompletableFuture shutdownFuture = CompletableFuture.completedFuture(null); if (gracefulRuntimeShutdown && (connection.process != null || connection.runtimeHost != null)) { long runtimeShutdownStartNanos = System.nanoTime(); @@ -849,8 +870,10 @@ private CompletableFuture cleanupConnection(boolean gracefulRuntimeShutdow } return shutdownFuture.handleAsync((ignored, error) -> { + boolean preserveDetachedServices = gracefulRuntimeShutdown && error == null && !options.isUseStdio(); if (gracefulRuntimeShutdown && connection.process != null && options.isUseStdio()) { - awaitStdioProcessExit(connection.process); + boolean exitObserved = awaitOwnedRuntimeExit(connection); + preserveDetachedServices = exitObserved && error == null; } try { connection.rpc.close(); @@ -859,7 +882,11 @@ private CompletableFuture cleanupConnection(boolean gracefulRuntimeShutdow } if (connection.process != null) { - cleanupCliProcess(connection.process, !gracefulRuntimeShutdown || error != null); + // Successful shutdown leaves intentionally detached services alive. + // Keep the captured tree only for failed or forced shutdown. + CliServerManager.cleanupCliProcess(connection.process, + preserveDetachedServices ? List.of() : descendants, + !gracefulRuntimeShutdown || error != null); } if (connection.runtimeHost != null) { closeRuntimeHost(connection.runtimeHost); @@ -869,54 +896,24 @@ private CompletableFuture cleanupConnection(boolean gracefulRuntimeShutdow }).thenCompose(result -> result); } - private static void awaitStdioProcessExit(Process process) { + private static boolean awaitOwnedRuntimeExit(Connection connection) { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(PROCESS_EXIT_TIMEOUT_SECONDS); try { - // Host telemetry flushes after stdio EOF, not the shutdown RPC response. - // Keep the reader draining stdout until the child has finished. - process.getOutputStream().close(); - if (!process.waitFor(PROCESS_EXIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - LOG.fine("Process did not exit after stdin EOF within graceful shutdown timeout; terminating"); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - LOG.log(Level.FINE, "Interrupted while waiting for process exit", e); - } catch (IOException e) { - LOG.log(Level.FINE, "Error closing process stdin", e); - } - } - - /** - * Returns true only when the child had already exited and no streams were - * destroyed. - */ - private static boolean cleanupCliProcess(Process process, boolean forceImmediately) { - try { - if (process.isAlive()) { - if (forceImmediately) { - process.destroyForcibly(); - if (!process.waitFor(FORCE_KILL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - LOG.fine("Process did not terminate within force kill timeout"); - } - return false; - } - - process.destroy(); - if (process.waitFor(FORCE_KILL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - return false; - } - - process.destroyForcibly(); - if (!process.waitFor(FORCE_KILL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - LOG.fine("Process did not terminate within force kill timeout"); - } + // Host telemetry flushes after stdin EOF, not the shutdown response. + connection.process.getOutputStream().close(); + if (!connection.rpc.awaitPeerEof(deadline - System.nanoTime(), TimeUnit.NANOSECONDS)) { + LOG.fine("Runtime transport did not reach EOF within graceful shutdown timeout; terminating tree"); return false; } + if (!connection.process.waitFor(Math.max(0, deadline - System.nanoTime()), TimeUnit.NANOSECONDS)) { + LOG.fine("Launcher did not exit after runtime EOF; terminating launcher"); + } return true; } 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); + LOG.log(Level.FINE, "Interrupted while waiting for process exit", e); + } catch (IOException e) { + LOG.log(Level.FINE, "Error awaiting runtime exit after closing stdin", e); } return false; } @@ -961,6 +958,10 @@ public CompletableFuture createSession(SessionConfig config) { return CompletableFuture.failedFuture( new IllegalArgumentException("gitHubToken and gitHubTokenProvider are mutually exclusive")); } + if (config.getCloud() != null && config.getSkillProvider() != null) { + return CompletableFuture.failedFuture( + new IllegalArgumentException("Skill providers are not supported for cloud sessions.")); + } return ensureConnected().thenCompose(connection -> { long totalNanos = System.nanoTime(); // For cloud sessions, let the CLI/server assign the session id @@ -1029,7 +1030,7 @@ public CompletableFuture createSession(SessionConfig config) { if (options.getMode() == CopilotClientMode.EMPTY) { if (config.getAvailableTools() == null) { if (registeredIdHolder[0] != null) { - sessions.remove(registeredIdHolder[0]); + sessions.remove(registeredIdHolder[0], preRegisteredSessionHolder[0]); } throw new IllegalArgumentException( "CopilotClient is in Mode = EMPTY but the session config did not specify " @@ -1129,9 +1130,10 @@ public CompletableFuture createSession(SessionConfig config) { }).exceptionally(ex -> { if (preRegisteredSessionHolder[0] != null) { preRegisteredSessionHolder[0].cancelPendingExternalTools(); + preRegisteredSessionHolder[0].clearSkillProvider(); } if (registeredIdHolder[0] != null) { - sessions.remove(registeredIdHolder[0]); + sessions.remove(registeredIdHolder[0], preRegisteredSessionHolder[0]); } if (tokenRegistration != null) { tokenRegistration.close(); @@ -1189,6 +1191,14 @@ public CompletableFuture resumeSession(String sessionId, ResumeS } return ensureConnected().thenCompose(connection -> { long totalNanos = System.nanoTime(); + // Validate before registering so a rejected resume leaves any resident + // session and its skill provider in place. + if (options.getMode() == CopilotClientMode.EMPTY && config.getAvailableTools() == null) { + throw new IllegalArgumentException( + "CopilotClient is in Mode = EMPTY but the resume session config did not specify " + + "availableTools. Empty mode requires every session to explicitly opt into " + + "the tools it wants — e.g. setAvailableTools(new ToolSet().addBuiltIn(BuiltInTools.ISOLATED))."); + } // Register the session before the RPC call to avoid missing early events. long setupNanos = System.nanoTime(); CopilotSession replacedSession = sessions.get(sessionId); @@ -1217,15 +1227,8 @@ public CompletableFuture resumeSession(String sessionId, ResumeS request.setEnableGitHubTelemetryForwarding(true); } - // Empty mode: validate availableTools and set toolFilterPrecedence for resume - // path + // Empty mode: set toolFilterPrecedence and isolation defaults for resume path if (options.getMode() == CopilotClientMode.EMPTY) { - if (config.getAvailableTools() == null) { - throw new IllegalArgumentException( - "CopilotClient is in Mode = EMPTY but the resume session config did not specify " - + "availableTools. Empty mode requires every session to explicitly opt into " - + "the tools it wants — e.g. setAvailableTools(new ToolSet().addBuiltIn(BuiltInTools.ISOLATED))."); - } request.setToolFilterPrecedence("excluded"); if (request.getSkipEmbeddingRetrieval() == null) { request.setSkipEmbeddingRetrieval(true); @@ -1290,7 +1293,7 @@ public CompletableFuture resumeSession(String sessionId, ResumeS // re-key. String returnedId = response.sessionId(); if (returnedId != null && !returnedId.equals(sessionId)) { - sessions.remove(sessionId); + sessions.remove(sessionId, session); session.setActiveSessionId(returnedId); sessions.put(returnedId, session); } @@ -1313,15 +1316,11 @@ public CompletableFuture resumeSession(String sessionId, ResumeS }); }).exceptionally(ex -> { session.cancelPendingExternalTools(); - sessions.remove(sessionId); - // Also remove the re-keyed entry if the server returned a different ID - String activeId = session.getSessionId(); - if (!sessionId.equals(activeId)) { - sessions.remove(activeId); - } - if (replacedSession != null) { - sessions.putIfAbsent(sessionId, replacedSession); - } + session.clearSkillProvider(); + // A concurrent resume may own the ID now; undo only this attempt. + sessions.compute(sessionId, + (id, current) -> current == null || current == session ? replacedSession : current); + sessions.remove(session.getSessionId(), session); if (tokenRegistration != null) { tokenRegistration.close(); } @@ -1667,6 +1666,7 @@ public CompletableFuture deleteSession(String sessionId) { } CopilotSession session = sessions.remove(sessionId); if (session != null) { + session.clearSkillProvider(); session.releaseGitHubTokenProviderRegistration(); } })); diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java index ddd46aa262..923ae88401 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java @@ -16,6 +16,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -172,6 +173,7 @@ public final class CopilotSession implements AutoCloseable { private static final Logger LOG = Logger.getLogger(CopilotSession.class.getName()); private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); private final java.util.Set> structuredWaits = ConcurrentHashMap.newKeySet(); + private final Map, CompletableFuture> plainWaits = new ConcurrentHashMap<>(); /** * Fixed name of the runtime's built-in tool-search tool. A client can replace @@ -195,7 +197,7 @@ public final class CopilotSession implements AutoCloseable { private final SessionUiApi ui; private final JsonRpcClient rpc; private volatile SessionRpc sessionRpc; - private final Set> eventHandlers = new CopyOnWriteArraySet<>(); + private final Set eventHandlers = new CopyOnWriteArraySet<>(); private final AtomicReference> toolHandlers = new AtomicReference<>(Map.of()); private final Object setToolsLock = new Object(); private CompletableFuture setToolsTail = CompletableFuture.completedFuture(null); @@ -211,6 +213,7 @@ public final class CopilotSession implements AutoCloseable { private final AtomicReference exitPlanModeHandler = new AtomicReference<>(); private final AtomicReference autoModeSwitchHandler = new AtomicReference<>(); private final AtomicReference hooksHandler = new AtomicReference<>(); + private final AtomicReference skillProvider = new AtomicReference<>(); private volatile EventErrorHandler eventErrorHandler; private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS; private volatile Map>> transformCallbacks; @@ -218,8 +221,17 @@ public final class CopilotSession implements AutoCloseable { private volatile Executor executor; private volatile GitHubTokenProviderRegistry.Registration gitHubTokenProviderRegistration; - /** Tracks whether this session instance has been terminated via close(). */ + /** Tracks whether this session instance has been closed or force-stopped. */ private volatile boolean isTerminated = false; + private volatile boolean callbacksRetired = false; + + private static final class EventSubscription { + private final AtomicReference> handler; + + EventSubscription(Consumer handler) { + this.handler = new AtomicReference<>(handler); + } + } private static final class PendingExternalTool { private static final int WAITING = 0; @@ -660,19 +672,33 @@ public CompletableFuture sendAndWait(MessageOptions optio } }; - Closeable subscription = on(handler); + var result = new CompletableFuture(); + Closeable subscription; + synchronized (this) { + subscription = on(handler); + plainWaits.put(result, future); + } + result.whenComplete((value, error) -> plainWaits.remove(result)); - send(options).exceptionally(ex -> { + try { + send(options).exceptionally(ex -> { + try { + subscription.close(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error closing subscription", e); + } + future.completeExceptionally(ex); + return null; + }); + } catch (RuntimeException error) { + plainWaits.remove(result); try { subscription.close(); - } catch (Exception e) { - LOG.log(Level.SEVERE, "Error closing subscription", e); + } catch (IOException closeError) { + error.addSuppressed(closeError); } - future.completeExceptionally(ex); - return null; - }); - - var result = new CompletableFuture(); + throw error; + } // Schedule timeout on the shared session-level scheduler. // Per Javadoc, timeoutMs <= 0 means "no timeout". @@ -691,7 +717,10 @@ public CompletableFuture sendAndWait(MessageOptions optio } catch (IOException closeEx) { e.addSuppressed(closeEx); } - result.completeExceptionally(e); + // Retirement owns completion when it has already shut down the scheduler. + if (!isTerminated) { + result.completeExceptionally(e); + } return result; } } @@ -775,7 +804,9 @@ public CompletableFuture sendAndWait(MessageOptions optio * the returned future completes exceptionally. Concurrent replacements on the * same session are applied in call order. Cancelling the returned future while * an earlier replacement is still in flight sends nothing; once the request is - * sent, an accepted replacement still installs its handlers. + * sent, an accepted replacement still installs its handlers unless the session + * closes first. Closing the session rejects queued replacements and prevents + * late acknowledgements from restoring retired handlers. *

    * The agent sees the new tools from its next model request, which can fall * within a turn in progress. A model request already in flight was made with @@ -803,7 +834,13 @@ public CompletableFuture setTools(List tools) { if (result.isCancelled()) { return CompletableFuture.completedFuture(null); } - return getRpc().tools.set(params).thenRun(() -> toolHandlers.set(replacementHandlers)); + ensureNotTerminated(); + return getRpc().tools.set(params).thenRun(() -> { + synchronized (CopilotSession.this) { + ensureNotTerminated(); + toolHandlers.set(replacementHandlers); + } + }); }); setToolsTail = operation.handle((ignored, error) -> null); } @@ -936,14 +973,17 @@ synchronized void event(SessionEvent event) { } } var state = new State(); - Closeable subscription = on(event -> { - if (event instanceof AssistantMessageEvent || event instanceof SessionIdleEvent - || event instanceof SessionErrorEvent - || event instanceof com.github.copilot.generated.UserMessageEvent) { - state.event(event); - } - }); - structuredWaits.add(result); + Closeable subscription; + synchronized (this) { + subscription = on(event -> { + if (event instanceof AssistantMessageEvent || event instanceof SessionIdleEvent + || event instanceof SessionErrorEvent + || event instanceof com.github.copilot.generated.UserMessageEvent) { + state.event(event); + } + }); + structuredWaits.add(result); + } ScheduledFuture timer; try { timer = timeoutMs > 0 @@ -1022,9 +1062,20 @@ synchronized void event(SessionEvent event) { * @see #setEventErrorPolicy(EventErrorPolicy) */ public Closeable on(Consumer handler) { - ensureNotTerminated(); - eventHandlers.add(handler); - return () -> eventHandlers.remove(handler); + java.util.Objects.requireNonNull(handler, "handler"); + EventSubscription subscription; + synchronized (this) { + ensureNotTerminated(); + subscription = eventHandlers.stream().filter(entry -> handler.equals(entry.handler.get())).findFirst() + .orElseGet(() -> new EventSubscription(handler)); + eventHandlers.add(subscription); + } + return () -> { + synchronized (this) { + subscription.handler.set(null); + eventHandlers.remove(subscription); + } + }; } /** @@ -1080,8 +1131,7 @@ public Closeable on(Class eventType, Consumer han handler.accept(eventType.cast(event)); } }; - eventHandlers.add(wrapper); - return () -> eventHandlers.remove(wrapper); + return on(wrapper); } /** @@ -1108,12 +1158,25 @@ public Closeable on(Class eventType, Consumer han * @see #setEventErrorPolicy(EventErrorPolicy) */ void dispatchEvent(SessionEvent event) { + if (callbacksRetired) { + return; + } // Handle broadcast request events (protocol v3) and passive in-memory state // updates (capabilities, open-canvases snapshot) before dispatching to user // handlers. Fire-and-forget: any RPC response is sent asynchronously. handleBroadcastEventAsync(event); - for (Consumer handler : eventHandlers) { + for (EventSubscription subscription : eventHandlers) { + Consumer handler; + synchronized (this) { + if (callbacksRetired) { + return; + } + handler = subscription.handler.get(); + } + if (handler == null) { + continue; + } try { handler.accept(event); } catch (Exception e) { @@ -1774,6 +1837,18 @@ void registerMcpAuthHandler(McpAuthHandler handler) { mcpAuthHandler.set(handler); } + void registerSkillProvider(SkillProvider provider) { + skillProvider.set(provider); + } + + SkillProvider getSkillProvider() { + return skillProvider.get(); + } + + void clearSkillProvider() { + skillProvider.set(null); + } + /** * Handles a permission request from the Copilot CLI. *

    @@ -2802,23 +2877,16 @@ private void ensureNotTerminated() { *

    * This destroys the session on the server, clears all event handlers, and * releases tool and permission handlers. After calling this method, the session - * cannot be used again. Subsequent calls to this method have no effect. + * cannot be used again. Subsequent calls to this method have no effect. Pending + * response waits fail when the session retires; ordinary callbacks remain + * active until the detach acknowledgement. */ @Override public void close() { - synchronized (this) { - if (isTerminated) { - return; // Already terminated - no-op - } - isTerminated = true; + if (!retire(false)) { + return; } - structuredWaits.forEach(wait -> wait - .completeExceptionally(new IllegalStateException("Session closed before structured output completed"))); - cancelPendingExternalTools(); - timeoutScheduler.shutdownNow(); - releaseGitHubTokenProviderRegistration(); - RuntimeException detachFailure = null; try { SessionDetachResponse response = rpc @@ -2833,21 +2901,94 @@ public void close() { Thread.currentThread().interrupt(); } detachFailure = new IllegalStateException("Failed to detach session " + sessionId, e); + } finally { + retireCallbacks(); + } + + if (detachFailure != null) { + throw detachFailure; } + } + + /** + * Retires a tracked session without an RPC or joining admitted user handlers. + */ + void forceStopLocally() { + retire(true); + } + private boolean retire(boolean force) { + List waitCompletions = new ArrayList<>(); + synchronized (this) { + if (isTerminated) { + if (force) { + retireCallbacks(); + } + return false; + } + isTerminated = true; + structuredWaits.forEach(wait -> waitCompletions.add(() -> wait + .completeExceptionally(new IllegalStateException("Session closed before response completed")))); + plainWaits.forEach((result, terminal) -> { + if (terminal.isDone()) { + // Preserve a received terminal outcome even if its scheduler handoff is queued. + waitCompletions.add(() -> terminal.whenComplete((value, error) -> { + if (error != null) { + result.completeExceptionally(error); + } else { + result.complete(value); + } + })); + } else { + waitCompletions.add(() -> result.completeExceptionally( + new IllegalStateException("Session closed before response completed"))); + } + }); + structuredWaits.clear(); + if (force) { + retireCallbacks(); + } + plainWaits.clear(); + } + + cancelPendingExternalTools(); + clearSkillProvider(); + timeoutScheduler.shutdownNow(); + releaseGitHubTokenProviderRegistration(); + if (!waitCompletions.isEmpty()) { + // Completion can run blocking user continuations; don't borrow a shared or user + // executor. + var completions = Executors.newCachedThreadPool(task -> { + var thread = new Thread(task, "copilot-session-retirement"); + thread.setDaemon(true); + return thread; + }); + try { + waitCompletions.forEach(completions::execute); + } finally { + completions.shutdown(); + } + } + return true; + } + + private synchronized void retireCallbacks() { + callbacksRetired = true; + eventHandlers.forEach(subscription -> subscription.handler.set(null)); eventHandlers.clear(); toolHandlers.set(Map.of()); commandHandlers.clear(); + bearerTokenProviders.clear(); permissionHandler.set(null); + mcpAuthHandler.set(null); userInputHandler.set(null); elicitationHandler.set(null); exitPlanModeHandler.set(null); autoModeSwitchHandler.set(null); hooksHandler.set(null); - - if (detachFailure != null) { - throw detachFailure; - } + skillProvider.set(null); + eventErrorHandler = null; + transformCallbacks = null; } // ===== Internal response types for agent API ===== diff --git a/java/sdk/src/main/java/com/github/copilot/InstallationConfirmationAdapter.java b/java/sdk/src/main/java/com/github/copilot/InstallationConfirmationAdapter.java index 57d5ab26fe..99eddbe545 100644 --- a/java/sdk/src/main/java/com/github/copilot/InstallationConfirmationAdapter.java +++ b/java/sdk/src/main/java/com/github/copilot/InstallationConfirmationAdapter.java @@ -47,7 +47,6 @@ final class InstallationConfirmationAdapter { void registerHandlers(JsonRpcClient rpc) { rpc.registerMethodHandler("installations.confirm", (rpcId, params) -> handleConfirm(rpc, rpcId, params)); - rpc.registerMethodHandler("$/cancelRequest", (rpcId, params) -> handleCancel(rpc, params)); } void closePending() { @@ -80,7 +79,10 @@ private void handleConfirm(JsonRpcClient rpc, String rpcId, JsonNode params) { } } - private void handleCancel(JsonRpcClient rpc, JsonNode params) { + /** + * Cancels the confirmation named by a {@code $/cancelRequest} notification. + */ + void handleCancel(JsonRpcClient rpc, JsonNode params) { if (params == null || !params.has("id") || !params.get("id").canConvertToLong() || !params.get("id").isIntegralNumber()) { return; diff --git a/java/sdk/src/main/java/com/github/copilot/JsonRpcClient.java b/java/sdk/src/main/java/com/github/copilot/JsonRpcClient.java index fc10946829..310733aab8 100644 --- a/java/sdk/src/main/java/com/github/copilot/JsonRpcClient.java +++ b/java/sdk/src/main/java/com/github/copilot/JsonRpcClient.java @@ -14,8 +14,10 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -53,6 +55,8 @@ class JsonRpcClient implements AutoCloseable { private final Map> pendingRequests = new ConcurrentHashMap<>(); private final Map> notificationHandlers = new ConcurrentHashMap<>(); private final ExecutorService readerExecutor; + private final CountDownLatch readerExited = new CountDownLatch(1); + private boolean peerEof; private final Object closeHandlerLock = new Object(); private volatile boolean running = true; private boolean closeNotified; @@ -213,12 +217,20 @@ public void sendResponse(Object id, Object result) throws IOException { * Sends a JSON-RPC error response to a server request. */ public void sendErrorResponse(Object id, int code, String message) throws IOException { + sendErrorResponse(id, code, message, null); + } + + /** + * Sends a JSON-RPC error response to a server request with optional data. + */ + public void sendErrorResponse(Object id, int code, String message, Object data) throws IOException { var response = new JsonRpcResponse(); response.setJsonrpc("2.0"); response.setId(id); var error = new JsonRpcError(); error.setCode(code); error.setMessage(message); + error.setData(data); response.setError(error); sendMessage(response); } @@ -284,6 +296,7 @@ private void startReader() { while (inHeaders) { int b = bis.read(); if (b == -1) { + peerEof = true; return; } @@ -319,6 +332,7 @@ private void startReader() { while (read < contentLength) { int result = bis.read(buffer, read, contentLength - read); if (result == -1) { + peerEof = true; return; } read += result; @@ -334,11 +348,19 @@ private void startReader() { LOG.log(Level.SEVERE, "Error in JSON-RPC reader", e); } } finally { - notifyClose(); + try { + notifyClose(); + } finally { + readerExited.countDown(); + } } }); } + boolean awaitPeerEof(long timeout, TimeUnit unit) throws InterruptedException { + return readerExited.await(Math.max(0, timeout), unit) && peerEof; + } + void setCloseHandler(Runnable handler) { boolean runNow; synchronized (closeHandlerLock) { diff --git a/java/sdk/src/main/java/com/github/copilot/RpcHandlerDispatcher.java b/java/sdk/src/main/java/com/github/copilot/RpcHandlerDispatcher.java index 0dd8a5dc98..888ba538da 100644 --- a/java/sdk/src/main/java/com/github/copilot/RpcHandlerDispatcher.java +++ b/java/sdk/src/main/java/com/github/copilot/RpcHandlerDispatcher.java @@ -8,9 +8,14 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Map; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; @@ -48,6 +53,7 @@ *

  • Lifecycle events
  • * */ +@AllowCopilotExperimental final class RpcHandlerDispatcher { private static final Logger LOG = Logger.getLogger(RpcHandlerDispatcher.class.getName()); @@ -57,6 +63,7 @@ final class RpcHandlerDispatcher { private final LifecycleEventDispatcher lifecycleDispatcher; private final Executor executor; private final GitHubTokenProviderRegistry gitHubTokenProviders; + private final Map> skillProviderCancellations = new ConcurrentHashMap<>(); /** * Creates a dispatcher with session registry and lifecycle dispatcher. @@ -101,6 +108,144 @@ void registerHandlers(JsonRpcClient rpc) { (requestId, params) -> handleProviderTokenGetToken(rpc, requestId, params)); rpc.registerMethodHandler("gitHubToken.getToken", (requestId, params) -> handleGitHubTokenGetToken(rpc, requestId, params)); + rpc.registerMethodHandler("skillProvider.list", + (requestId, params) -> handleSkillProviderList(rpc, requestId, params)); + rpc.registerMethodHandler("skillProvider.read", + (requestId, params) -> handleSkillProviderRead(rpc, requestId, params)); + } + + // The generated skillProvider.* records are package-private because the + // methods are internal, so these handlers read params as JSON. + private void handleSkillProviderList(JsonRpcClient rpc, String requestId, JsonNode params) { + handleSkillProviderRequest(rpc, requestId, params, "skillProvider.list", "listSkills", + (provider, request) -> provider.listSkills(), + skills -> Map.of("skills", skills != null ? skills : Collections.emptyList())); + } + + private void handleSkillProviderRead(JsonRpcClient rpc, String requestId, JsonNode params) { + handleSkillProviderRequest(rpc, requestId, params, "skillProvider.read", "readSkill", + (provider, request) -> provider.readSkill(textParam(request, "name")), markdown -> { + // Write an explicit null: the runtime requires the field and reads null as "not + // found". + var result = MAPPER.createObjectNode(); + if (markdown == null) { + result.putNull("markdown"); + } else { + result.put("markdown", markdown); + } + return result; + }); + } + + private static String textParam(JsonNode params, String field) { + JsonNode value = params != null ? params.get(field) : null; + return value != null && !value.isNull() ? value.asText() : null; + } + + private void handleSkillProviderRequest(JsonRpcClient rpc, String requestId, JsonNode params, String method, + String operation, BiFunction> call, + Function toResult) { + final long requestIdLong = parseRequestId(requestId, method); + if (requestIdLong == -1) { + return; + } + // Registered on the reader thread so a later $/cancelRequest always finds it. + var cancellation = new CompletableFuture(); + skillProviderCancellations.put(requestIdLong, cancellation); + runAsync(() -> { + String failure = "Skill provider " + operation + " failed"; + String sessionId = null; + try { + sessionId = textParam(params, "sessionId"); + SkillProvider provider = findSkillProvider(sessionId); + if (provider == null) { + skillProviderCancellations.remove(requestIdLong, cancellation); + sendSkillProviderError(rpc, requestIdLong, "No skill provider for session: " + sessionId, sessionId, + null); + return; + } + CompletableFuture future = cancellation.isDone() + ? CompletableFuture.failedFuture(new CancellationException()) + : call.apply(provider, params); + if (future == null) { + future = CompletableFuture + .failedFuture(new IllegalStateException(operation + " returned a null future")); + } + final CompletableFuture providerFuture = future; + final String callSessionId = sessionId; + cancellation.thenRun(() -> providerFuture.cancel(true)); + providerFuture.whenComplete((value, error) -> { + skillProviderCancellations.remove(requestIdLong, cancellation); + if (cancellation.isDone()) { + sendSkillProviderCancelled(rpc, requestIdLong); + } else if (error != null) { + sendSkillProviderError(rpc, requestIdLong, failure, callSessionId, error); + } else { + sendSkillProviderResult(rpc, requestIdLong, method, failure, callSessionId, + () -> toResult.apply(value)); + } + }); + } catch (Exception error) { + skillProviderCancellations.remove(requestIdLong, cancellation); + sendSkillProviderError(rpc, requestIdLong, failure, sessionId, error); + } + }); + } + + /** + * Cancels the skill provider call named by a {@code $/cancelRequest} + * notification, if it is still in flight. + */ + void handleCancelRequest(JsonNode params) { + JsonNode id = params != null ? params.get("id") : null; + if (id == null || !id.isIntegralNumber() || !id.canConvertToLong()) { + return; + } + CompletableFuture cancellation = skillProviderCancellations.get(id.asLong()); + if (cancellation != null) { + cancellation.complete(null); + } + } + + /** Cancels every in-flight skill provider call when the connection closes. */ + void cancelSkillProviderCalls() { + skillProviderCancellations.values().forEach(cancellation -> cancellation.complete(null)); + } + + private SkillProvider findSkillProvider(String sessionId) { + CopilotSession session = sessionId != null ? sessions.get(sessionId) : null; + return session != null ? session.getSkillProvider() : null; + } + + private void sendSkillProviderResult(JsonRpcClient rpc, long requestId, String method, String failure, + String sessionId, Supplier result) { + try { + rpc.sendResponse(requestId, result.get()); + } catch (IOException error) { + LOG.log(Level.SEVERE, "Error sending " + method + " response", error); + } catch (RuntimeException error) { + sendSkillProviderError(rpc, requestId, failure, sessionId, error); + } + } + + private void sendSkillProviderCancelled(JsonRpcClient rpc, long requestId) { + try { + rpc.sendErrorResponse(requestId, -32800, "Request cancelled"); + } catch (IOException error) { + LOG.log(Level.FINE, "Error sending skill provider cancellation", error); + } + } + + private void sendSkillProviderError(JsonRpcClient rpc, long requestId, String message, String sessionId, + Throwable error) { + if (error != null) { + LOG.log(Level.WARNING, message + " (sessionId=" + sessionId + ")", error); + } + try { + rpc.sendErrorResponse(requestId, -32603, message); + } catch (IOException sendError) { + LOG.log(Level.SEVERE, "Error sending skill provider error", sendError); + } } private void handleGitHubTokenGetToken(JsonRpcClient rpc, String requestId, JsonNode params) { diff --git a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java index 08101c1542..8d77086746 100644 --- a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -215,6 +215,9 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess config.getEnableHostGitOperations().ifPresent(request::setEnableHostGitOperations); config.getEnableSessionStore().ifPresent(request::setEnableSessionStore); config.getEnableSkills().ifPresent(request::setEnableSkills); + if (config.getSkillProvider() != null) { + request.setHasSkillProvider(true); + } if (config.getEmbeddingCacheStorage() != null) { request.setEmbeddingCacheStorage(config.getEmbeddingCacheStorage()); } @@ -245,6 +248,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setFeatureFlags(config.getFeatureFlags()); request.setExpAssignments(config.getExpAssignments()); config.getEnableManagedSettings().ifPresent(request::setEnableManagedSettings); + config.getEnforceManagedModelDefaults().ifPresent(request::setEnforceManagedModelDefaults); request.setManagedSettings(config.getManagedSettings()); return request; @@ -335,6 +339,9 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo config.getEnableHostGitOperations().ifPresent(request::setEnableHostGitOperations); config.getEnableSessionStore().ifPresent(request::setEnableSessionStore); config.getEnableSkills().ifPresent(request::setEnableSkills); + if (config.getSkillProvider() != null) { + request.setHasSkillProvider(true); + } if (config.getEmbeddingCacheStorage() != null) { request.setEmbeddingCacheStorage(config.getEmbeddingCacheStorage()); } @@ -389,6 +396,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setFeatureFlags(config.getFeatureFlags()); request.setExpAssignments(config.getExpAssignments()); config.getEnableManagedSettings().ifPresent(request::setEnableManagedSettings); + config.getEnforceManagedModelDefaults().ifPresent(request::setEnforceManagedModelDefaults); request.setManagedSettings(config.getManagedSettings()); return request; @@ -435,6 +443,9 @@ static void configureSession(CopilotSession session, SessionConfig config) { if (config.getOnUserInputRequest() != null) { session.registerUserInputHandler(config.getOnUserInputRequest()); } + if (config.getSkillProvider() != null) { + session.registerSkillProvider(config.getSkillProvider()); + } if (config.getHooks() != null) { session.registerHooks(config.getHooks()); } @@ -487,6 +498,9 @@ static void configureSession(CopilotSession session, ResumeSessionConfig config) if (config.getOnUserInputRequest() != null) { session.registerUserInputHandler(config.getOnUserInputRequest()); } + if (config.getSkillProvider() != null) { + session.registerSkillProvider(config.getSkillProvider()); + } if (config.getHooks() != null) { session.registerHooks(config.getHooks()); } diff --git a/java/sdk/src/main/java/com/github/copilot/SkillProvider.java b/java/sdk/src/main/java/com/github/copilot/SkillProvider.java new file mode 100644 index 0000000000..d2907113f2 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/SkillProvider.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * Provides session-scoped skills to the Copilot runtime. + *

    + * A skill provider is registered through {@code SessionConfig} or + * {@code ResumeSessionConfig}. The runtime may invoke provider callbacks + * concurrently, so implementations should be thread-safe and avoid relying on + * callback ordering. + *

    + * When the runtime cancels a call, for example after its time limit or when the + * session disconnects, the SDK cancels the returned future with + * {@link CompletableFuture#cancel(boolean) cancel(true)} and the runtime + * ignores any later result. Cancellation does not interrupt running work; + * observe it with {@link CompletableFuture#isCancelled()} or a completion + * callback to stop early. + * + * @apiNote This API is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +public interface SkillProvider { + + /** + * Lists the skills currently available for the session. + * + * @return a future resolving to the provider's skill descriptors + */ + CompletableFuture> listSkills(); + + /** + * Reads the Markdown content for one skill. + * + * @param name + * the skill invocation name + * @return a future resolving to the skill Markdown, or {@code null} when the + * skill is not found + */ + CompletableFuture readSkill(String name); +} diff --git a/java/sdk/src/main/java/com/github/copilot/SkillProviderDescriptor.java b/java/sdk/src/main/java/com/github/copilot/SkillProviderDescriptor.java new file mode 100644 index 0000000000..04ac45845d --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/SkillProviderDescriptor.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Catalog metadata for one skill supplied by a {@link SkillProvider}. + *

    + * The runtime lists these descriptors without fetching skill content, then + * calls {@link SkillProvider#readSkill(String)} when the skill is used. + * + * @param name + * the invocation and display name + * @param description + * the description shown in skill catalogs + * @param userInvocable + * whether users may invoke the skill directly, or {@code null} for + * the default ({@code true}) + * @param disableModelInvocation + * whether model invocation is disabled, or {@code null} for the + * default ({@code false}) + * @param argumentHint + * an optional freeform argument hint for slash-command catalogs + * @apiNote This API is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@JsonInclude(JsonInclude.Include.NON_NULL) +public record SkillProviderDescriptor(@JsonProperty("name") String name, + @JsonProperty("description") String description, @JsonProperty("userInvocable") Boolean userInvocable, + @JsonProperty("disableModelInvocation") Boolean disableModelInvocation, + @JsonProperty("argumentHint") String argumentHint) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java b/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java index 092dc73026..cfeda84458 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java @@ -12,26 +12,26 @@ * * @see CapiSessionOptions#setAutoTier(AutoTier) */ -public enum AutoTier { +public final class AutoTier { /** Prioritize efficiency. */ - EFFICIENCY("efficiency"), + public static final AutoTier EFFICIENCY = new AutoTier("efficiency"); /** Balance efficiency and intelligence. */ - BALANCE("balance"), + public static final AutoTier BALANCE = new AutoTier("balance"); /** Prioritize intelligence. */ - INTELLIGENCE("intelligence"), + public static final AutoTier INTELLIGENCE = new AutoTier("intelligence"); /** * Integrator-only preset that optimizes for latency. Not a first-party GitHub * Copilot product preference. */ - FAST("fast"); + public static final AutoTier FAST = new AutoTier("fast"); private final String value; - AutoTier(String value) { + private AutoTier(String value) { this.value = value; } @@ -52,7 +52,7 @@ public String getValue() { * the JSON string value * @return the matching tier, or {@code null} if value is {@code null} * @throws IllegalArgumentException - * if the value does not match a known routing tier + * if the value is not a routing identifier */ @JsonCreator public static AutoTier fromValue(String value) { @@ -64,6 +64,26 @@ public static AutoTier fromValue(String value) { return tier; } } - throw new IllegalArgumentException("Unknown AutoTier value: " + value); + return new AutoTier(com.github.copilot.generated.rpc.AutoTier.fromValue(value).getValue()); + } + + /** Returns the known convenience values. @return known routing tiers */ + public static AutoTier[] values() { + return new AutoTier[]{EFFICIENCY, BALANCE, INTELLIGENCE, FAST}; + } + + @Override + public boolean equals(Object other) { + return other instanceof AutoTier tier && value.equals(tier.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + return value; } } diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index d8ab62af95..d18de07fb1 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -205,6 +205,10 @@ public final class CreateSessionRequest { @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean enableSkills; + @JsonProperty("hasSkillProvider") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean hasSkillProvider; + @JsonProperty("embeddingCacheStorage") @JsonInclude(JsonInclude.Include.NON_NULL) private String embeddingCacheStorage; @@ -256,6 +260,10 @@ public final class CreateSessionRequest { @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean enableManagedSettings; + @JsonProperty("enforceManagedModelDefaults") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean enforceManagedModelDefaults; + @JsonProperty("managedSettings") @JsonInclude(JsonInclude.Include.NON_NULL) private ManagedSettings managedSettings; @@ -956,6 +964,23 @@ public void clearEnableSkills() { this.enableSkills = null; } + /** Gets skill-provider availability flag. @return the flag */ + public Boolean getHasSkillProvider() { + return hasSkillProvider; + } + + /** Sets skill-provider availability flag. @param hasSkillProvider the flag */ + public void setHasSkillProvider(boolean hasSkillProvider) { + this.hasSkillProvider = hasSkillProvider; + } + + /** + * Clears the hasSkillProvider setting, reverting to the default behavior. + */ + public void clearHasSkillProvider() { + this.hasSkillProvider = null; + } + /** Gets embedding cache storage mode. @return the mode */ public String getEmbeddingCacheStorage() { return embeddingCacheStorage; @@ -1220,6 +1245,21 @@ public void clearEnableManagedSettings() { this.enableManagedSettings = null; } + /** Gets managed model lock enforcement. @return the flag, or {@code null} */ + public Boolean getEnforceManagedModelDefaults() { + return enforceManagedModelDefaults; + } + + /** Sets managed model lock enforcement. @param enforce whether to enforce */ + public void setEnforceManagedModelDefaults(boolean enforce) { + this.enforceManagedModelDefaults = enforce; + } + + /** Clears managed model lock enforcement. */ + public void clearEnforceManagedModelDefaults() { + this.enforceManagedModelDefaults = null; + } + /** @return host-injected managed settings, or {@code null} when unset */ public ManagedSettings getManagedSettings() { return managedSettings; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ManagedSettingsPermissions.java b/java/sdk/src/main/java/com/github/copilot/rpc/ManagedSettingsPermissions.java index 6755d959b3..f79277c1a1 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ManagedSettingsPermissions.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ManagedSettingsPermissions.java @@ -16,6 +16,9 @@ public final class ManagedSettingsPermissions { @JsonProperty("disableBypassPermissionsMode") private String disableBypassPermissionsMode; + @JsonProperty("disableAssistedPermissionsMode") + private Boolean disableAssistedPermissionsMode; + @JsonProperty("deny") private List deny; @@ -25,6 +28,9 @@ public final class ManagedSettingsPermissions { @JsonProperty("allow") private List allow; + @JsonProperty("limitTo") + private List limitTo; + /** @return the bypass-permissions policy, or {@code null} when unset */ public String getDisableBypassPermissionsMode() { return disableBypassPermissionsMode; @@ -44,6 +50,25 @@ public ManagedSettingsPermissions setDisableBypassPermissionsMode(String value) return this; } + /** + * @return whether Assisted Permissions are disabled, or {@code null} when unset + */ + public Boolean getDisableAssistedPermissionsMode() { + return disableAssistedPermissionsMode; + } + + /** + * Prevents Assisted Permissions from being activated when true. + * + * @param value + * whether Assisted Permissions are disabled + * @return this policy + */ + public ManagedSettingsPermissions setDisableAssistedPermissionsMode(Boolean value) { + this.disableAssistedPermissionsMode = value; + return this; + } + /** @return rules that deny matching operations, or {@code null} when unset */ public List getDeny() { return deny; @@ -88,4 +113,25 @@ public ManagedSettingsPermissions setAllow(List rules) { this.allow = rules == null ? null : new ArrayList<>(rules); return this; } + + /** + * @return the closed-world host boundary, or {@code null} when unset + */ + public List getLimitTo() { + return limitTo; + } + + /** + * Every network request must match this list, and multiple managed layers + * intersect their lists. A present empty list denies all hosts. + * + * @param rules + * host-only {@code Domain(hostname)}, {@code Domain(IP)}, or + * {@code Domain(*.example.com)} boundary rules + * @return this policy + */ + public ManagedSettingsPermissions setLimitTo(List rules) { + this.limitTo = rules == null ? null : new ArrayList<>(rules); + return this; + } } diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index 4bc7de4803..927aa9555d 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -15,6 +15,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.github.copilot.CopilotExperimental; +import com.github.copilot.SkillProvider; import com.github.copilot.generated.SessionEvent; import com.github.copilot.generated.rpc.DiagnosticsConfiguration; import com.github.copilot.generated.rpc.SessionLimitsConfig; @@ -83,6 +84,7 @@ public class ResumeSessionConfig { private Boolean enableHostGitOperations; private Boolean enableSessionStore; private Boolean enableSkills; + private SkillProvider skillProvider; private String embeddingCacheStorage; private boolean disableResume; private boolean streaming; @@ -117,6 +119,7 @@ public class ResumeSessionConfig { private String remoteSession; private CopilotExpAssignmentResponse expAssignments; private Boolean enableManagedSettings; + private Boolean enforceManagedModelDefaults; private Map featureFlags; private ManagedSettings managedSettings; @@ -1311,6 +1314,35 @@ public ResumeSessionConfig clearEnableSkills() { return this; } + /** + * Gets the session-scoped skill provider for the resumed session. + * + * @return the skill provider, or {@code null} when not configured + * @apiNote This API is experimental and may change in a future version. + */ + @CopilotExperimental + @JsonIgnore + public SkillProvider getSkillProvider() { + return skillProvider; + } + + /** + * Sets the session-scoped skill provider for the resumed session. + *

    + * Skill provider registrations are not persisted; re-supply the provider when + * resuming. + * + * @param skillProvider + * the skill provider to expose to the runtime + * @return this config for method chaining + * @apiNote This API is experimental and may change in a future version. + */ + @CopilotExperimental + public ResumeSessionConfig setSkillProvider(SkillProvider skillProvider) { + this.skillProvider = skillProvider; + return this; + } + /** * Gets the embedding cache storage mode. * @@ -2171,6 +2203,28 @@ public ResumeSessionConfig setEnableManagedSettings(boolean enableManagedSetting return this; } + /** + * Gets whether locked managed model controls are enforced on resume. + * + * @return the explicit choice, or empty to use the runtime default + */ + @JsonIgnore + public Optional getEnforceManagedModelDefaults() { + return Optional.ofNullable(enforceManagedModelDefaults); + } + + /** + * Sets whether locked managed model controls are enforced after resume. + * + * @param enforceManagedModelDefaults + * whether to enforce locked managed model controls + * @return this config for method chaining + */ + public ResumeSessionConfig setEnforceManagedModelDefaults(boolean enforceManagedModelDefaults) { + this.enforceManagedModelDefaults = enforceManagedModelDefaults; + return this; + } + /** @return host-injected managed settings, or {@code null} when unset */ public ManagedSettings getManagedSettings() { return managedSettings; @@ -2244,6 +2298,7 @@ public ResumeSessionConfig clone() { copy.enableHostGitOperations = this.enableHostGitOperations; copy.enableSessionStore = this.enableSessionStore; copy.enableSkills = this.enableSkills; + copy.skillProvider = this.skillProvider; copy.embeddingCacheStorage = this.embeddingCacheStorage; copy.disableResume = this.disableResume; copy.streaming = this.streaming; @@ -2283,6 +2338,7 @@ public ResumeSessionConfig clone() { copy.featureFlags = this.featureFlags != null ? new java.util.HashMap<>(this.featureFlags) : null; copy.expAssignments = this.expAssignments; copy.enableManagedSettings = this.enableManagedSettings; + copy.enforceManagedModelDefaults = this.enforceManagedModelDefaults; copy.managedSettings = this.managedSettings; return copy; } diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index d25893ff19..2d6e2474b6 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -147,6 +147,10 @@ public final class ResumeSessionRequest { @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean enableSkills; + @JsonProperty("hasSkillProvider") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean hasSkillProvider; + @JsonProperty("embeddingCacheStorage") @JsonInclude(JsonInclude.Include.NON_NULL) private String embeddingCacheStorage; @@ -261,6 +265,10 @@ public final class ResumeSessionRequest { @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean enableManagedSettings; + @JsonProperty("enforceManagedModelDefaults") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean enforceManagedModelDefaults; + @JsonProperty("managedSettings") @JsonInclude(JsonInclude.Include.NON_NULL) private ManagedSettings managedSettings; @@ -764,6 +772,23 @@ public void clearEnableSkills() { this.enableSkills = null; } + /** Gets skill-provider availability flag. @return the flag */ + public Boolean getHasSkillProvider() { + return hasSkillProvider; + } + + /** Sets skill-provider availability flag. @param hasSkillProvider the flag */ + public void setHasSkillProvider(boolean hasSkillProvider) { + this.hasSkillProvider = hasSkillProvider; + } + + /** + * Clears the hasSkillProvider setting, reverting to the default behavior. + */ + public void clearHasSkillProvider() { + this.hasSkillProvider = null; + } + /** Gets embedding cache storage mode. @return the mode */ public String getEmbeddingCacheStorage() { return embeddingCacheStorage; @@ -1250,6 +1275,21 @@ public void clearEnableManagedSettings() { this.enableManagedSettings = null; } + /** Gets managed model lock enforcement. @return the flag, or {@code null} */ + public Boolean getEnforceManagedModelDefaults() { + return enforceManagedModelDefaults; + } + + /** Sets managed model lock enforcement. @param enforce whether to enforce */ + public void setEnforceManagedModelDefaults(boolean enforce) { + this.enforceManagedModelDefaults = enforce; + } + + /** Clears managed model lock enforcement. */ + public void clearEnforceManagedModelDefaults() { + this.enforceManagedModelDefaults = null; + } + /** @return host-injected managed settings, or {@code null} when unset */ public ManagedSettings getManagedSettings() { return managedSettings; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java index 836b972ae2..926706872b 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -15,6 +15,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.github.copilot.CopilotExperimental; +import com.github.copilot.SkillProvider; import com.github.copilot.generated.SessionEvent; import com.github.copilot.generated.rpc.DiagnosticsConfiguration; import com.github.copilot.generated.rpc.SessionLimitsConfig; @@ -101,6 +102,7 @@ public class SessionConfig { private Boolean enableHostGitOperations; private Boolean enableSessionStore; private Boolean enableSkills; + private SkillProvider skillProvider; private String embeddingCacheStorage; private ModelCapabilitiesOverride modelCapabilities; private Consumer onEvent; @@ -117,6 +119,7 @@ public class SessionConfig { private CloudSessionOptions cloud; private CopilotExpAssignmentResponse expAssignments; private Boolean enableManagedSettings; + private Boolean enforceManagedModelDefaults; private Map featureFlags; private ManagedSettings managedSettings; @@ -1759,6 +1762,35 @@ public SessionConfig clearEnableSkills() { return this; } + /** + * Gets the session-scoped skill provider. + * + * @return the skill provider, or {@code null} when not configured + * @apiNote This API is experimental and may change in a future version. + */ + @CopilotExperimental + @JsonIgnore + public SkillProvider getSkillProvider() { + return skillProvider; + } + + /** + * Sets the session-scoped skill provider. + *

    + * Re-supply this provider when resuming a session. Skill providers are not + * supported for cloud sessions. + * + * @param skillProvider + * the skill provider to expose to the runtime + * @return this config instance for method chaining + * @apiNote This API is experimental and may change in a future version. + */ + @CopilotExperimental + public SessionConfig setSkillProvider(SkillProvider skillProvider) { + this.skillProvider = skillProvider; + return this; + } + /** * Gets the embedding cache storage mode. * @@ -2262,6 +2294,29 @@ public SessionConfig setEnableManagedSettings(boolean enableManagedSettings) { return this; } + /** + * Gets whether locked managed model controls are enforced. + * + * @return the explicit choice, or empty to use the runtime default + */ + @JsonIgnore + public Optional getEnforceManagedModelDefaults() { + return Optional.ofNullable(enforceManagedModelDefaults); + } + + /** + * Sets whether locked managed model controls are enforced for this session. + * Conflicting model changes are rejected; overridable defaults remain mutable. + * + * @param enforceManagedModelDefaults + * whether to enforce locked managed model controls + * @return this config instance for method chaining + */ + public SessionConfig setEnforceManagedModelDefaults(boolean enforceManagedModelDefaults) { + this.enforceManagedModelDefaults = enforceManagedModelDefaults; + return this; + } + /** * Gets host-injected managed settings for this session. * @@ -2366,6 +2421,7 @@ public SessionConfig clone() { copy.enableHostGitOperations = this.enableHostGitOperations; copy.enableSessionStore = this.enableSessionStore; copy.enableSkills = this.enableSkills; + copy.skillProvider = this.skillProvider; copy.embeddingCacheStorage = this.embeddingCacheStorage; copy.modelCapabilities = this.modelCapabilities; copy.onEvent = this.onEvent; @@ -2383,6 +2439,7 @@ public SessionConfig clone() { copy.featureFlags = this.featureFlags != null ? new java.util.HashMap<>(this.featureFlags) : null; copy.expAssignments = this.expAssignments; copy.enableManagedSettings = this.enableManagedSettings; + copy.enforceManagedModelDefaults = this.enforceManagedModelDefaults; copy.managedSettings = this.managedSettings; return copy; } diff --git a/java/sdk/src/test/java/com/github/copilot/BuiltinPluginDirectoriesTest.java b/java/sdk/src/test/java/com/github/copilot/BuiltinPluginDirectoriesTest.java index b5f89e5f9f..7a42c2c45e 100644 --- a/java/sdk/src/test/java/com/github/copilot/BuiltinPluginDirectoriesTest.java +++ b/java/sdk/src/test/java/com/github/copilot/BuiltinPluginDirectoriesTest.java @@ -90,15 +90,16 @@ JsonNode awaitBuiltinParams() throws Exception { private void acceptLoop() { try { Socket socket = serverSocket.accept(); - JsonRpcClient server = JsonRpcClient.fromSocket(socket); - server.registerMethodHandler("ping", (id, params) -> respond(server, id, - Map.of("message", "pong", "timestamp", "2026-09-21T00:00:00Z", "protocolVersion", 3))); - server.registerMethodHandler("connect", (id, params) -> respond(server, id, - Map.of("ok", true, "protocolVersion", 3, "version", "test"))); - server.registerMethodHandler("plugins.builtin.set", (id, params) -> { - builtinSetCount.incrementAndGet(); - builtinParams.complete(params); - respond(server, id, Map.of()); + JsonRpcClient server = JsonRpcClient.fromSocket(socket, rpc -> { + rpc.registerMethodHandler("ping", (id, params) -> respond(rpc, id, + Map.of("message", "pong", "timestamp", "2026-09-21T00:00:00Z", "protocolVersion", 3))); + rpc.registerMethodHandler("connect", (id, params) -> respond(rpc, id, + Map.of("ok", true, "protocolVersion", 3, "version", "test"))); + rpc.registerMethodHandler("plugins.builtin.set", (id, params) -> { + builtinSetCount.incrementAndGet(); + builtinParams.complete(params); + respond(rpc, id, Map.of()); + }); }); ready.complete(server); } catch (IOException e) { diff --git a/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java b/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java index aeb4ffe0ad..cc4e2fde3f 100644 --- a/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java @@ -14,7 +14,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import java.util.stream.Stream; import com.fasterxml.jackson.databind.JsonNode; @@ -68,7 +70,7 @@ void omitsUnsetEnableWebSocketResponses() { } @ParameterizedTest - @CsvSource({"EFFICIENCY,efficiency", "BALANCE,balance", "INTELLIGENCE,intelligence", "FAST,fast"}) + @MethodSource("canonicalAutoTiers") void autoTierCanonicalValuesRoundTripAndForward(AutoTier tier, String value) throws Exception { var mapper = JsonRpcClient.getObjectMapper(); var capi = new CapiSessionOptions().setAutoTier(tier); @@ -90,13 +92,25 @@ void autoTierCanonicalValuesRoundTripAndForward(AutoTier tier, String value) thr } @Test - void autoTierRejectsNoncanonicalValues() { + void autoTierPreservesServerIdentifiersAndRejectsMalformedValues() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); for (String value : new String[]{"balanced", "Balance", "unknown"}) { + var tier = AutoTier.fromValue(value); + assertEquals(value, tier.getValue()); + assertEquals(value, mapper.valueToTree(tier).asText()); + assertEquals(tier, mapper.readValue("\"" + value + "\"", AutoTier.class)); + } + for (String value : new String[]{"", "two words", "\n"}) { assertThrows(IllegalArgumentException.class, () -> AutoTier.fromValue(value)); } assertNull(AutoTier.fromValue(null)); } + private static Stream canonicalAutoTiers() { + return Stream.of(Arguments.of(AutoTier.EFFICIENCY, "efficiency"), Arguments.of(AutoTier.BALANCE, "balance"), + Arguments.of(AutoTier.INTELLIGENCE, "intelligence"), Arguments.of(AutoTier.FAST, "fast")); + } + @Test void clearingAutoTierOmitsIt() { var capi = new CapiSessionOptions().setAutoTier(AutoTier.BALANCE).setAutoTier(null); diff --git a/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java b/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java index 8f937fcb99..fba9a6413b 100644 --- a/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java +++ b/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java @@ -323,6 +323,17 @@ void sessionConfigGranularMultitenancyFieldsCopied() { assertFalse(cloned.getEnableSkills().orElse(true)); } + @Test + @AllowCopilotExperimental + void sessionConfigSkillProviderCloned() { + SkillProvider provider = testSkillProvider(); + SessionConfig original = new SessionConfig().setSkillProvider(provider); + + SessionConfig cloned = original.clone(); + + assertSame(provider, cloned.getSkillProvider()); + } + @Test void resumeSessionConfigEnableSessionTelemetryCopied() { ResumeSessionConfig original = new ResumeSessionConfig(); @@ -362,6 +373,17 @@ void resumeSessionConfigGranularMultitenancyFieldsCopied() { assertTrue(cloned.getEnableSkills().orElse(false)); } + @Test + @AllowCopilotExperimental + void resumeSessionConfigSkillProviderCloned() { + SkillProvider provider = testSkillProvider(); + ResumeSessionConfig original = new ResumeSessionConfig().setSkillProvider(provider); + + ResumeSessionConfig cloned = original.clone(); + + assertSame(provider, cloned.getSkillProvider()); + } + @Test void clonePreservesNullFields() { CopilotClientOptions opts = new CopilotClientOptions(); @@ -548,4 +570,19 @@ void resumeSessionConfigCloneCopiesModeSwitchHandlers() { assertSame(original.getOnExitPlanMode(), cloned.getOnExitPlanMode()); assertSame(original.getOnAutoModeSwitch(), cloned.getOnAutoModeSwitch()); } + + @AllowCopilotExperimental + private static SkillProvider testSkillProvider() { + return new SkillProvider() { + @Override + public CompletableFuture> listSkills() { + return CompletableFuture.completedFuture(List.of()); + } + + @Override + public CompletableFuture readSkill(String name) { + return CompletableFuture.completedFuture(null); + } + }; + } } diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java b/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java index 9055cb19f7..722975b194 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java @@ -9,11 +9,17 @@ import org.junit.jupiter.api.io.TempDir; import com.github.copilot.generated.ExternalToolRequestedEvent; +import com.github.copilot.rpc.CloudSessionOptions; import com.github.copilot.rpc.CopilotClientOptions; +import com.github.copilot.rpc.CopilotClientMode; +import com.github.copilot.rpc.CreateSessionRequest; +import com.github.copilot.rpc.CreateSessionResponse; import com.github.copilot.rpc.DeleteSessionResponse; import com.github.copilot.rpc.GitHubTokenProviderResult; import com.github.copilot.rpc.PermissionHandler; import com.github.copilot.rpc.PingResponse; +import com.github.copilot.rpc.ResumeSessionConfig; +import com.github.copilot.rpc.ResumeSessionResponse; import com.github.copilot.rpc.SessionConfig; import com.github.copilot.rpc.SessionLifecycleEvent; import com.github.copilot.rpc.SessionLifecycleEventTypes; @@ -22,6 +28,7 @@ import java.io.OutputStream; import java.lang.reflect.Field; import java.io.BufferedReader; +import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; @@ -176,6 +183,137 @@ void testDeleteSessionReleasesGitHubTokenProvider() throws Exception { } } + @Test + @AllowCopilotExperimental + void cloudSessionWithSkillProviderIsRejectedBeforeConnectingOrCallingProvider() throws Exception { + var providerCalls = new AtomicInteger(); + SkillProvider provider = new SkillProvider() { + @Override + public CompletableFuture> listSkills() { + providerCalls.incrementAndGet(); + return CompletableFuture.completedFuture(List.of()); + } + + @Override + public CompletableFuture readSkill(String name) { + providerCalls.incrementAndGet(); + return CompletableFuture.completedFuture(null); + } + }; + + try (var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false))) { + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setCloud(new CloudSessionOptions()).setSkillProvider(provider); + + ExecutionException error = assertThrows(ExecutionException.class, () -> client.createSession(config).get()); + assertInstanceOf(IllegalArgumentException.class, error.getCause()); + assertEquals("Skill providers are not supported for cloud sessions.", error.getCause().getMessage()); + assertEquals(0, providerCalls.get()); + } + } + + @Test + @AllowCopilotExperimental + void emptyModeCreateWithSkillProviderSendsFlagsAndRegistersBeforeRpc() throws Exception { + var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false).setMode(CopilotClientMode.EMPTY) + .setCopilotHome(System.getProperty("java.io.tmpdir"))); + var rpc = mock(JsonRpcClient.class); + setConnectionFuture(client, rpc, null); + SkillProvider provider = testSkillProvider(); + var createRequest = new AtomicReference(); + + when(rpc.invoke(eq("session.create"), any(), eq(CreateSessionResponse.class))).thenAnswer(invocation -> { + CreateSessionRequest request = invocation.getArgument(1); + createRequest.set(request); + assertTrue(request.getHasSkillProvider()); + assertFalse(request.getEnableSkills()); + assertSame(provider, registeredSkillProvider(client, "skill-provider-session")); + return CompletableFuture + .completedFuture(new CreateSessionResponse("skill-provider-session", null, null, null)); + }); + when(rpc.invoke(eq("session.options.update"), any(), eq(Void.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + + try { + var config = new SessionConfig().setSessionId("skill-provider-session") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setAvailableTools(List.of()) + .setSkillProvider(provider); + + CopilotSession session = client.createSession(config).get(); + + assertSame(provider, session.getSkillProvider()); + assertNotNull(createRequest.get()); + assertTrue(createRequest.get().getHasSkillProvider()); + assertFalse(createRequest.get().getEnableSkills()); + } finally { + client.forceStop().get(); + } + } + + @Test + @AllowCopilotExperimental + void emptyModeResumeValidationFailureKeepsResidentSkillProvider() throws Exception { + var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false).setMode(CopilotClientMode.EMPTY) + .setCopilotHome(System.getProperty("java.io.tmpdir"))); + var rpc = mock(JsonRpcClient.class); + setConnectionFuture(client, rpc, null); + SkillProvider resident = testSkillProvider(); + when(rpc.invoke(eq("session.create"), any(), eq(CreateSessionResponse.class))).thenReturn( + CompletableFuture.completedFuture(new CreateSessionResponse("resident-session", null, null, null))); + when(rpc.invoke(eq("session.options.update"), any(), eq(Void.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + + try { + client.createSession(new SessionConfig().setSessionId("resident-session") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setAvailableTools(List.of()) + .setSkillProvider(resident)).get(); + + var resumeConfig = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSkillProvider(testSkillProvider()); + var error = assertThrows(ExecutionException.class, + () -> client.resumeSession("resident-session", resumeConfig).get()); + + assertInstanceOf(IllegalArgumentException.class, error.getCause()); + assertSame(resident, registeredSkillProvider(client, "resident-session")); + verify(rpc, never()).invoke(eq("session.resume"), any(), any()); + } finally { + client.forceStop().get(); + } + } + + @Test + @AllowCopilotExperimental + void failedResumeDoesNotEvictAConcurrentResume() throws Exception { + var client = new CopilotClient( + new CopilotClientOptions().setAutoStart(false).setCopilotHome(System.getProperty("java.io.tmpdir"))); + var rpc = mock(JsonRpcClient.class); + setConnectionFuture(client, rpc, null); + when(rpc.invoke(eq("session.create"), any(), eq(CreateSessionResponse.class))).thenReturn( + CompletableFuture.completedFuture(new CreateSessionResponse("shared-session", null, null, null))); + when(rpc.invoke(eq("session.options.update"), any(), eq(Void.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + var firstResume = new CompletableFuture(); + when(rpc.invoke(eq("session.resume"), any(), eq(ResumeSessionResponse.class))).thenReturn(firstResume, + CompletableFuture.completedFuture(new ResumeSessionResponse("shared-session", null, null, null))); + + try { + client.createSession(new SessionConfig().setSessionId("shared-session") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(testSkillProvider())).get(); + var failing = client.resumeSession("shared-session", new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(testSkillProvider())); + SkillProvider winner = testSkillProvider(); + client.resumeSession("shared-session", new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(winner)).get(); + + firstResume.completeExceptionally(new RuntimeException("resume failed")); + + assertThrows(ExecutionException.class, failing::get); + assertSame(winner, registeredSkillProvider(client, "shared-session")); + } finally { + client.forceStop().get(); + } + } + @Test void testClientConstruction() { var client = new CopilotClient(); @@ -274,26 +412,64 @@ void testStartupWriteFailureDrainsStderrBeforeKillingChild(@TempDir Path tempDir process.exit(1); } }); - fs.closeSync(0); process.stdout.write('started\\n'); """); var options = new CopilotClientOptions().setCliPath(script.toString()).setUseStdio(true); var manager = spy(new CliServerManager(options)); var child = new AtomicReference(); + var failedOutput = new AtomicReference(); + var writeFailure = new IOException("Controlled startup write failure"); + doAnswer(invocation -> { + CliServerManager.ProcessInfo info = (CliServerManager.ProcessInfo) invocation.callRealMethod(); + child.set(info.process()); + // Closing fd 0 does not reliably fail the first parent write on Windows. + var output = new FilterOutputStream(info.process().getOutputStream()) { + @Override + public void write(int value) throws IOException { + throw writeFailure; + } + + @Override + public void write(byte[] bytes, int offset, int length) throws IOException { + throw writeFailure; + } + + @Override + public void close() throws IOException { + // No write succeeds, so there is nothing to flush on this failed transport. + out.close(); + } + }; + failedOutput.set(output); + var transportProcess = mock(Process.class, org.mockito.AdditionalAnswers.delegatesTo(info.process())); + doReturn(output).when(transportProcess).getOutputStream(); + return new CliServerManager.ProcessInfo(transportProcess, info.port()); + }).when(manager).startCliServer(); doAnswer(invocation -> { Process process = invocation.getArgument(0); - child.set(process); var reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); assertEquals("started", reader.readLine()); return invocation.callRealMethod(); }).when(manager).connectToServer(any(Process.class), isNull(), isNull()); doAnswer(invocation -> { // Release stderr only when startup drains it, while the child is still alive. + assertTrue(child.get().isAlive(), "Startup must drain stderr before killing the controlled child."); Files.createFile(tempDir.resolve("release-stderr")); return invocation.callRealMethod(); }).when(manager).awaitStderrReader(); - try (var client = new CopilotClient(options)) { + try (AutoCloseable childCleanup = () -> { + var process = child.get(); + if (process != null && process.isAlive()) { + process.destroyForcibly(); + assertTrue(process.waitFor(10, TimeUnit.SECONDS), "Could not reap the controlled child."); + } + }; var client = new CopilotClient(options); AutoCloseable outputCleanup = () -> { + var output = failedOutput.get(); + if (output != null) { + output.close(); + } + }) { Field managerField = CopilotClient.class.getDeclaredField("serverManager"); managerField.setAccessible(true); managerField.set(client, manager); @@ -307,14 +483,9 @@ void testStartupWriteFailureDrainsStderrBeforeKillingChild(@TempDir Path tempDir while (original.getCause() != null) { original = original.getCause(); } - assertInstanceOf(IOException.class, original); + assertSame(writeFailure, original, "Startup must preserve the actual failed transport write."); assertNotSame(cause, original, "The startup error must retain its original transport failure."); assertFalse(child.get().isAlive(), "Failed startup retained its child process."); - } finally { - if (child.get() != null && child.get().isAlive()) { - child.get().destroyForcibly(); - assertTrue(child.get().waitFor(10, TimeUnit.SECONDS), "Could not reap the controlled child."); - } } } @@ -797,4 +968,29 @@ private static void setConnectionFuture(CopilotClient client, JsonRpcClient rpc, field.setAccessible(true); field.set(client, CompletableFuture.completedFuture(connection)); } + + @AllowCopilotExperimental + private static SkillProvider testSkillProvider() { + return new SkillProvider() { + @Override + public CompletableFuture> listSkills() { + return CompletableFuture.completedFuture(List.of()); + } + + @Override + public CompletableFuture readSkill(String name) { + return CompletableFuture.completedFuture(null); + } + }; + } + + @AllowCopilotExperimental + @SuppressWarnings("unchecked") + private static SkillProvider registeredSkillProvider(CopilotClient client, String sessionId) throws Exception { + Field sessionsField = CopilotClient.class.getDeclaredField("sessions"); + sessionsField.setAccessible(true); + Map sessions = (Map) sessionsField.get(client); + CopilotSession session = sessions.get(sessionId); + return session != null ? session.getSkillProvider() : null; + } } diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java b/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java index 94ccda7243..4b876f4bee 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java @@ -17,7 +17,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -178,53 +177,6 @@ void testShouldReceiveSessionEvents() throws Exception { } } - /** - * Verifies that send() returns immediately while events stream in background. - * - * @see Snapshot: - * session/send_returns_immediately_while_events_stream_in_background - */ - @Test - void testSendReturnsImmediatelyWhileEventsStreamInBackground() throws Exception { - ctx.configureForTest("session", "send_returns_immediately_while_events_stream_in_background"); - - try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client - .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); - - var events = new ArrayList(); - var lastMessage = new AtomicReference(); - var done = new CompletableFuture(); - - session.on(evt -> { - events.add(evt.getType()); - if (evt instanceof AssistantMessageEvent msg) { - lastMessage.set(msg); - } else if (evt instanceof SessionIdleEvent) { - done.complete(null); - } - }); - - // Use a slow command so we can verify send() returns before completion - // Use String convenience overload (covers send(String) path) - session.send("Run 'sleep 2 && echo done'").get(); - - // At this point, we might not have received session.idle yet - // The event handling happens asynchronously - - // Wait for completion - done.get(60, TimeUnit.SECONDS); - - assertTrue(events.contains("session.idle")); - assertTrue(events.contains("assistant.message")); - assertNotNull(lastMessage.get()); - assertTrue(lastMessage.get().getData().content().contains("done"), - "Response should contain done: " + lastMessage.get().getData().content()); - - session.close(); - } - } - /** * Verifies that sendAndWait blocks until session is idle and returns the final * assistant message. diff --git a/java/sdk/src/test/java/com/github/copilot/ForceStopSessionTest.java b/java/sdk/src/test/java/com/github/copilot/ForceStopSessionTest.java new file mode 100644 index 0000000000..1b51dce326 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/ForceStopSessionTest.java @@ -0,0 +1,482 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.generated.SessionEvent; +import com.github.copilot.generated.SessionIdleEvent; +import com.github.copilot.rpc.CopilotClientOptions; +import com.github.copilot.rpc.MessageOptions; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; +import com.github.copilot.rpc.SessionHooks; + +class ForceStopSessionTest { + + @CopilotResponse + public record Answer(int value) { + } + + @Test + void gracefulClosePreservesEventsAndSessionEndHooksUntilDetachCompletes() throws Exception { + var events = new AtomicInteger(); + var hooks = new AtomicInteger(); + try (var server = new Server(); var client = server.createClient()) { + server.emitDetachCallbacks = true; + var session = client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setHooks(new SessionHooks().setOnSessionEnd((input, invocation) -> { + assertEquals("user_exit", input.reason()); + hooks.incrementAndGet(); + return CompletableFuture.completedFuture(null); + }))).get(5, TimeUnit.SECONDS); + try (var subscription = session.on(event -> events.incrementAndGet())) { + session.close(); + assertEquals(1, server.detaches.get()); + assertEquals(1, hooks.get(), "Ordinary detach must still invoke the session-end hook"); + assertEquals(1, events.get(), "Ordinary detach must deliver events before callback retirement"); + server.emitIdle(session.getSessionId()); + client.ping("retired detach callbacks barrier").get(5, TimeUnit.SECONDS); + assertEquals(1, events.get(), "Detach completion must retire subsequent callbacks"); + assertThrows(IllegalStateException.class, () -> session.on(event -> fail("Retired handler"))); + session.close(); + assertEquals(1, server.detaches.get(), "Repeated close must not detach again"); + } + } + } + + @Test + void forceStopTerminatesRetainedSessionsWithoutDetachRpc() throws Exception { + try (var server = new Server(); var client = server.createClient()) { + var session = createSession(client); + client.forceStop().get(5, TimeUnit.SECONDS); + + assertThrows(IllegalStateException.class, () -> session.on(event -> fail("Retired handler"))); + assertThrows(IllegalStateException.class, + () -> session.on(SessionIdleEvent.class, event -> fail("Retired typed handler"))); + assertThrows(IllegalStateException.class, () -> session.send("after force stop")); + session.close(); + assertEquals(0, server.detaches.get(), "Local retirement must not detach over RPC"); + } + } + + @Test + void forceStopDropsRemainingSnapshotHandlersWithoutJoiningAnAdmittedHandler() throws Exception { + var entered = new CountDownLatch(1); + var release = new CountDownLatch(1); + var dispatchThread = new AtomicReference(); + var laterCalls = new AtomicInteger(); + try (var server = new Server(); var client = server.createClient()) { + var session = createSession(client); + try (var first = session.on(event -> { + dispatchThread.set(Thread.currentThread()); + entered.countDown(); + awaitRelease(release); + }); var second = session.on(event -> laterCalls.incrementAndGet())) { + try { + server.emitIdle(session.getSessionId()); + assertTrue(entered.await(5, TimeUnit.SECONDS), "The first handler must be admitted"); + client.forceStop().get(5, TimeUnit.SECONDS); + assertEquals(1, release.getCount(), "Force stop must not require releasing the user handler"); + } finally { + release.countDown(); + Thread thread = dispatchThread.get(); + if (thread != null) { + thread.join(5000); + assertFalse(thread.isAlive(), "The stopped event reader must finish after its handler"); + } + } + assertEquals(0, laterCalls.get(), "Retirement must discard the retained dispatch snapshot"); + } + } + } + + @Test + void forceStopRetiresErrorCallbacksWhileGracefulCloseAwaitsDetach() throws Exception { + var entered = new CountDownLatch(1); + var release = new CountDownLatch(1); + var dispatchThread = new AtomicReference(); + var errorCalls = new AtomicInteger(); + try (var server = new Server(); var client = server.createClient()) { + server.holdDetachResponse = true; + var session = createSession(client); + session.setEventErrorHandler((event, error) -> errorCalls.incrementAndGet()); + try (var first = session.on(event -> { + dispatchThread.set(Thread.currentThread()); + entered.countDown(); + awaitRelease(release); + throw new IllegalStateException("Admitted handler failed after forced retirement"); + })) { + try { + server.emitIdle(session.getSessionId()); + assertTrue(entered.await(5, TimeUnit.SECONDS), "The first snapshot handler must be admitted"); + var closing = CompletableFuture.runAsync(session::close); + assertTrue(server.detachEntered.await(5, TimeUnit.SECONDS), "Graceful close must await detach"); + // Prevent graceful close's final retirement from masking force escalation. + synchronized (session) { + client.forceStop().get(5, TimeUnit.SECONDS); + assertFalse(closing.isDone(), "Graceful close must still await its final retirement"); + release.countDown(); + Thread thread = dispatchThread.get(); + thread.join(5000); + assertFalse(thread.isAlive(), "The stopped reader must finish its admitted handler"); + assertEquals(0, errorCalls.get(), "Force stop must discard the unadmitted error callback"); + } + var detachError = assertThrows(ExecutionException.class, () -> closing.get(5, TimeUnit.SECONDS)); + assertInstanceOf(IllegalStateException.class, detachError.getCause()); + } finally { + release.countDown(); + Thread thread = dispatchThread.get(); + if (thread != null) { + thread.join(5000); + assertFalse(thread.isAlive(), "The event reader must finish after forced cleanup"); + } + } + } + } + } + + @ParameterizedTest + @CsvSource({"false, false", "false, true", "true, false", "true, true"}) + void retirementPreservesTerminalOutcomeWhileSchedulerHandoffIsBlocked(boolean terminalError, boolean force) + throws Exception { + var entered = new CountDownLatch(1); + var release = new CountDownLatch(1); + try (var server = new Server(); var client = server.createClient()) { + var session = createSession(client); + var occupying = session.sendAndWait(new MessageOptions().setPrompt("occupy scheduler"), 0); + var continuation = occupying.whenComplete((value, error) -> { + entered.countDown(); + awaitRelease(release); + }); + try (var gate = (AutoCloseable) release::countDown) { + client.ping("first send acknowledgment").get(5, TimeUnit.SECONDS); + server.emitIdle(session.getSessionId()); + assertTrue(entered.await(5, TimeUnit.SECONDS), "A continuation must occupy the session scheduler"); + + var completed = session.sendAndWait(new MessageOptions().setPrompt("completed terminal"), 0); + client.ping("second send acknowledgment").get(5, TimeUnit.SECONDS); + if (terminalError) { + server.emitEvent(session.getSessionId(), "session.error", + Map.of("errorType", "query", "message", "original terminal failure")); + } else { + server.emitEvent(session.getSessionId(), "assistant.message", + Map.of("messageId", "terminal-message", "content", "completed before retirement")); + server.emitIdle(session.getSessionId()); + } + client.ping("terminal dispatch acknowledgment").get(5, TimeUnit.SECONDS); + assertFalse(completed.isDone(), "The received terminal outcome must still await scheduler handoff"); + + if (force) { + client.forceStop().get(5, TimeUnit.SECONDS); + } else { + CompletableFuture.runAsync(session::close).get(5, TimeUnit.SECONDS); + } + if (terminalError) { + var error = assertThrows(ExecutionException.class, () -> completed.get(5, TimeUnit.SECONDS)); + assertEquals("Session error: original terminal failure", error.getCause().getMessage()); + } else { + assertEquals("completed before retirement", completed.get(5, TimeUnit.SECONDS).getData().content()); + } + assertEquals(1, release.getCount(), "Retirement must not join the admitted continuation"); + } + continuation.get(5, TimeUnit.SECONDS); + } + } + + @Test + void forceStopFailsAcknowledgedPlainAndStructuredWaitsWithNoTimeout() throws Exception { + var entered = new CountDownLatch(2); + var release = new CountDownLatch(1); + try (var server = new Server(); var client = server.createClient()) { + var session = createSession(client); + var plain = session.sendAndWait(new MessageOptions().setPrompt("plain"), 0); + var structured = session.sendAndWait(new MessageOptions().setPrompt("structured"), Answer.class, 0); + client.ping("send acknowledgment barrier").get(5, TimeUnit.SECONDS); + assertFalse(plain.isDone()); + assertFalse(structured.isDone()); + + var plainContinuation = plain.whenComplete((value, error) -> { + entered.countDown(); + awaitRelease(release); + }); + var structuredContinuation = structured.whenComplete((value, error) -> { + entered.countDown(); + awaitRelease(release); + }); + try (var gate = (AutoCloseable) release::countDown) { + client.forceStop().get(5, TimeUnit.SECONDS); + assertTrue(entered.await(5, TimeUnit.SECONDS), + "Both waits must retire even while their application continuations are blocked"); + var plainError = assertThrows(ExecutionException.class, () -> plain.get(5, TimeUnit.SECONDS)); + var structuredError = assertThrows(ExecutionException.class, () -> structured.get(5, TimeUnit.SECONDS)); + assertInstanceOf(IllegalStateException.class, plainError.getCause()); + assertInstanceOf(IllegalStateException.class, structuredError.getCause()); + assertTrue(plainError.getCause().getMessage().contains("closed")); + assertTrue(structuredError.getCause().getMessage().contains("closed")); + } + assertThrows(ExecutionException.class, () -> plainContinuation.get(5, TimeUnit.SECONDS)); + assertThrows(ExecutionException.class, () -> structuredContinuation.get(5, TimeUnit.SECONDS)); + } + } + + @Test + void gracefulCloseFailsAcknowledgedWaitsWithoutJoiningTheirContinuations() throws Exception { + var entered = new CountDownLatch(2); + var release = new CountDownLatch(1); + try (var server = new Server(); var client = server.createClient()) { + var session = createSession(client); + var plain = session.sendAndWait(new MessageOptions().setPrompt("plain"), 0); + var structured = session.sendAndWait(new MessageOptions().setPrompt("structured"), Answer.class, 0); + client.ping("send acknowledgment barrier").get(5, TimeUnit.SECONDS); + assertFalse(plain.isDone()); + assertFalse(structured.isDone()); + + var plainContinuation = plain.whenComplete((value, error) -> { + entered.countDown(); + awaitRelease(release); + }); + var structuredContinuation = structured.whenComplete((value, error) -> { + entered.countDown(); + awaitRelease(release); + }); + try (var gate = (AutoCloseable) release::countDown) { + CompletableFuture.runAsync(session::close).get(5, TimeUnit.SECONDS); + assertTrue(entered.await(5, TimeUnit.SECONDS), "Retirement must admit both blocked continuations"); + var plainError = assertThrows(ExecutionException.class, () -> plain.get(5, TimeUnit.SECONDS)); + var structuredError = assertThrows(ExecutionException.class, () -> structured.get(5, TimeUnit.SECONDS)); + assertInstanceOf(IllegalStateException.class, plainError.getCause()); + assertInstanceOf(IllegalStateException.class, structuredError.getCause()); + assertEquals("Session closed before response completed", plainError.getCause().getMessage()); + assertEquals("Session closed before response completed", structuredError.getCause().getMessage()); + assertEquals(1, server.detaches.get(), "Graceful close must still acknowledge one detach"); + session.close(); + assertEquals(1, server.detaches.get(), "Repeated close must not detach again"); + } + assertThrows(ExecutionException.class, () -> plainContinuation.get(5, TimeUnit.SECONDS)); + assertThrows(ExecutionException.class, () -> structuredContinuation.get(5, TimeUnit.SECONDS)); + } + } + + @Test + void duplicateUntypedSubscriptionsStillDispatchOnceAndShareUnsubscription() throws Exception { + var calls = new AtomicInteger(); + try (var server = new Server(); var client = server.createClient()) { + var session = createSession(client); + Consumer handler = event -> calls.incrementAndGet(); + try (var first = session.on(handler); var second = session.on(handler)) { + server.emitIdle(session.getSessionId()); + client.ping("first dispatch barrier").get(5, TimeUnit.SECONDS); + assertEquals(1, calls.get()); + + first.close(); + server.emitIdle(session.getSessionId()); + client.ping("unsubscription barrier").get(5, TimeUnit.SECONDS); + assertEquals(1, calls.get()); + client.forceStop().get(5, TimeUnit.SECONDS); + } + } + } + + private static CopilotSession createSession(CopilotClient client) throws Exception { + return client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(5, + TimeUnit.SECONDS); + } + + private static void awaitRelease(CountDownLatch release) { + boolean interrupted = false; + try { + while (true) { + try { + release.await(); + return; + } catch (InterruptedException error) { + interrupted = true; + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + + private static final class Server implements AutoCloseable { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private final ServerSocket listener; + private final Thread thread; + private final AtomicInteger detaches = new AtomicInteger(); + private final CountDownLatch detachEntered = new CountDownLatch(1); + private volatile Socket socket; + private volatile boolean closed; + private volatile boolean emitDetachCallbacks; + private volatile boolean holdDetachResponse; + private volatile Exception failure; + + Server() throws IOException { + listener = new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1")); + thread = new Thread(this::serve, "force-stop-session-server"); + thread.setDaemon(true); + thread.start(); + } + + CopilotClient createClient() { + return new CopilotClient(new CopilotClientOptions().setCliUrl("127.0.0.1:" + listener.getLocalPort())); + } + + void emitIdle(String sessionId) throws IOException { + emitEvent(sessionId, "session.idle", Map.of()); + } + + void emitEvent(String sessionId, String type, Map data) throws IOException { + assertNotNull(socket, "Session creation must establish the connection before events"); + writeMessage(Map.of("jsonrpc", "2.0", "method", "session.event", "params", + Map.of("sessionId", sessionId, "event", + Map.of("type", type, "id", "00000000-0000-0000-0000-000000000001", "timestamp", + "2026-10-05T00:00:00Z", "data", data)))); + } + + private void serve() { + try (Socket accepted = listener.accept()) { + socket = accepted; + JsonNode pendingDetachId = null; + while (!closed) { + JsonNode request = readMessage(accepted.getInputStream()); + if (request == null) { + return; + } + JsonNode params = request.path("params"); + if (holdDetachResponse && request.path("method").asText().equals("session.detach")) { + detaches.incrementAndGet(); + detachEntered.countDown(); + continue; + } + if (!request.has("method") && request.path("id").asLong() == 9_000_000L) { + if (pendingDetachId == null || request.has("error")) { + throw new IOException("Unexpected detach hook response: " + request); + } + var response = MAPPER.createObjectNode().put("jsonrpc", "2.0"); + response.set("id", pendingDetachId); + response.set("result", MAPPER.valueToTree(Map.of("success", true))); + pendingDetachId = null; + writeMessage(response); + continue; + } + if (emitDetachCallbacks && request.path("method").asText().equals("session.detach")) { + detaches.incrementAndGet(); + pendingDetachId = request.get("id"); + String sessionId = params.path("sessionId").asText(); + emitIdle(sessionId); + var hookInput = Map.of("sessionId", sessionId, "timestamp", 0, "cwd", ".", "reason", + "user_exit"); + writeMessage(Map.of("jsonrpc", "2.0", "id", 9_000_000L, "method", "hooks.invoke", "params", + Map.of("sessionId", sessionId, "hookType", "sessionEnd", "input", hookInput))); + continue; + } + Object result = switch (request.path("method").asText()) { + case "connect" -> Map.of("protocolVersion", SdkProtocolVersion.get()); + case "session.create" -> Map.of("sessionId", params.path("sessionId").asText()); + case "session.send" -> Map.of("messageId", params.path("prompt").asText()); + case "session.detach" -> { + detaches.incrementAndGet(); + yield Map.of("success", true); + } + case "ping" -> Map.of("message", params.path("message").asText(), "protocolVersion", + SdkProtocolVersion.get()); + default -> throw new IOException("Unexpected RPC: " + request.path("method")); + }; + var response = MAPPER.createObjectNode().put("jsonrpc", "2.0"); + response.set("id", request.get("id")); + response.set("result", MAPPER.valueToTree(result)); + writeMessage(response); + } + } catch (Exception error) { + if (!closed) { + failure = error; + } + } + } + + private static JsonNode readMessage(InputStream input) throws IOException { + var header = new StringBuilder(); + while (!header.toString().endsWith("\r\n\r\n")) { + int value = input.read(); + if (value < 0) { + if (header.isEmpty()) { + return null; + } + throw new IOException("Truncated RPC header"); + } + header.append((char) value); + if (header.length() > 8192) { + throw new IOException("Oversized RPC header"); + } + } + int length; + try { + length = Integer.parseInt(header.substring(header.indexOf(":") + 1).trim()); + } catch (NumberFormatException error) { + throw new IOException("Invalid Content-Length", error); + } + if (length < 0 || length > 1_048_576) { + throw new IOException("Invalid Content-Length: " + length); + } + byte[] body = input.readNBytes(length); + if (body.length != length) { + throw new IOException("Truncated RPC body"); + } + return MAPPER.readTree(body); + } + + private synchronized void writeMessage(Object message) throws IOException { + byte[] body = MAPPER.writeValueAsBytes(message); + var output = socket.getOutputStream(); + output.write(("Content-Length: " + body.length + "\r\n\r\n").getBytes(StandardCharsets.UTF_8)); + output.write(body); + output.flush(); + } + + @Override + public void close() throws Exception { + closed = true; + listener.close(); + if (socket != null) { + socket.close(); + } + thread.join(5000); + assertFalse(thread.isAlive(), "The RPC fixture must stop"); + assertNull(failure, "RPC fixture failure: " + failure); + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/ManagedSettingsTest.java b/java/sdk/src/test/java/com/github/copilot/ManagedSettingsTest.java index d6341b26c5..7b1910753f 100644 --- a/java/sdk/src/test/java/com/github/copilot/ManagedSettingsTest.java +++ b/java/sdk/src/test/java/com/github/copilot/ManagedSettingsTest.java @@ -23,22 +23,29 @@ class ManagedSettingsTest { @Test void forwardsManagedSettingsOnCreateAndResume() throws Exception { var permissions = new ManagedSettingsPermissions() - .setDisableBypassPermissionsMode(DisableBypassPermissionsModes.DISABLE).setDeny(List.of("Shell(rm *)")) - .setAsk(List.of("Domain(publish.example)")).setAllow(List.of("Read(**)")); + .setDisableBypassPermissionsMode(DisableBypassPermissionsModes.DISABLE) + .setDisableAssistedPermissionsMode(true).setDeny(List.of("Shell(rm *)")) + .setAsk(List.of("Domain(publish.example)")).setAllow(List.of("Read(**)")) + .setLimitTo(List.of("Domain(github.com)")); var managedSettings = new ManagedSettings().setPermissions(permissions); - var create = SessionRequestBuilder.buildCreateRequest( - new SessionConfig().setEnableManagedSettings(true).setManagedSettings(managedSettings), - "managed-create"); + var create = SessionRequestBuilder.buildCreateRequest(new SessionConfig().setEnableManagedSettings(true) + .setEnforceManagedModelDefaults(true).setManagedSettings(managedSettings), "managed-create"); var resume = SessionRequestBuilder.buildResumeRequest("managed-resume", - new ResumeSessionConfig().setEnableManagedSettings(true).setManagedSettings(managedSettings)); + new ResumeSessionConfig().setEnableManagedSettings(true).setEnforceManagedModelDefaults(true) + .setManagedSettings(managedSettings)); assertEquals(managedSettings, create.getManagedSettings()); assertEquals(managedSettings, resume.getManagedSettings()); + assertTrue(create.getEnforceManagedModelDefaults()); + assertTrue(resume.getEnforceManagedModelDefaults()); var json = new ObjectMapper().writeValueAsString(create); assertTrue(json.contains("\"enableManagedSettings\":true")); + assertTrue(json.contains("\"enforceManagedModelDefaults\":true")); assertTrue(json.contains("\"managedSettings\":{\"permissions\"")); assertTrue(json.contains("\"disableBypassPermissionsMode\":\"disable\"")); + assertTrue(json.contains("\"limitTo\":[\"Domain(github.com)\"]")); + assertTrue(json.contains("\"disableAssistedPermissionsMode\":true")); } @Test @@ -55,7 +62,8 @@ void preservesExplicitEmptyPermissionArrays() throws Exception { // absent (null) list imposes no such restriction. Jackson NON_NULL must // emit an explicit empty array as `[]` and omit null fields, so the two // remain distinguishable on the wire. - var permissions = new ManagedSettingsPermissions().setDeny(List.of()).setAsk(List.of()).setAllow(List.of()); + var permissions = new ManagedSettingsPermissions().setDeny(List.of()).setAsk(List.of()).setAllow(List.of()) + .setLimitTo(List.of()); var managedSettings = new ManagedSettings().setPermissions(permissions); var create = SessionRequestBuilder.buildCreateRequest(new SessionConfig().setManagedSettings(managedSettings), "managed-empty"); @@ -64,6 +72,7 @@ void preservesExplicitEmptyPermissionArrays() throws Exception { assertTrue(json.contains("\"deny\":[]"), json); assertTrue(json.contains("\"ask\":[]"), json); assertTrue(json.contains("\"allow\":[]"), json); + assertTrue(json.contains("\"limitTo\":[]"), json); } @Test diff --git a/java/sdk/src/test/java/com/github/copilot/PackagedConsumerIT.java b/java/sdk/src/test/java/com/github/copilot/PackagedConsumerIT.java new file mode 100644 index 0000000000..29cb03f487 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/PackagedConsumerIT.java @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import javax.tools.ToolProvider; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class PackagedConsumerIT { + + @Test + void runsStandaloneConsumerWithManifestClasspath(@TempDir Path directory) throws Exception { + Path build = Path.of(System.getProperty("project.build.directory")); + Path library = Files.createDirectory(directory.resolve("lib")); + var dependencies = new ArrayList(); + dependencies.add(build.resolve(System.getProperty("project.build.finalName") + ".jar")); + try (var jars = Files.list(build.resolve("consumer-dependencies"))) { + dependencies.addAll(jars.filter(path -> path.toString().endsWith(".jar")).sorted().toList()); + } + for (Path dependency : dependencies) { + Files.copy(dependency, library.resolve(dependency.getFileName())); + } + + Path source = directory.resolve("Consumer.java"); + Files.writeString(source, """ + import com.github.copilot.CopilotClient; + import com.github.copilot.ConnectionState; + import com.github.copilot.rpc.MessageOptions; + import com.fasterxml.jackson.databind.ObjectMapper; + import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + + public class Consumer { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + var mapper = new ObjectMapper().registerModule(new JavaTimeModule()); + var json = mapper.writeValueAsString(new MessageOptions().setPrompt("consumer")); + if (!mapper.readTree(json).get("prompt").asText().equals("consumer") + || client.getState() != ConnectionState.DISCONNECTED) { + throw new AssertionError("Packaged SDK consumer failed"); + } + } + System.out.println("consumer-ok"); + } + } + """); + + String classpath = String.join(File.pathSeparator, + dependencies.stream().map(path -> library.resolve(path.getFileName()).toString()).toList()); + var diagnostics = new ByteArrayOutputStream(); + int compilation = ToolProvider.getSystemJavaCompiler().run(null, diagnostics, diagnostics, "--release", "17", + "-proc:none", "-classpath", classpath, source.toString()); + assertEquals(0, compilation, diagnostics.toString(StandardCharsets.UTF_8)); + + var manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, "Consumer"); + manifest.getMainAttributes().put(Attributes.Name.CLASS_PATH, + String.join(" ", dependencies.stream().map(path -> "lib/" + path.getFileName()).toList())); + Path consumerJar = directory.resolve("consumer.jar"); + try (var jar = new JarOutputStream(Files.newOutputStream(consumerJar), manifest)) { + jar.putNextEntry(new JarEntry("Consumer.class")); + Files.copy(directory.resolve("Consumer.class"), jar); + jar.closeEntry(); + } + Files.delete(directory.resolve("Consumer.class")); + + Path java = Path.of(System.getProperty("java.home"), "bin", + System.getProperty("os.name").startsWith("Windows") ? "java.exe" : "java"); + Path output = directory.resolve("output.log"); + ProcessBuilder builder = new ProcessBuilder(java.toString(), "-jar", consumerJar.toString()) + .directory(directory.toFile()).redirectErrorStream(true).redirectOutput(output.toFile()); + builder.environment().remove("COPILOT_SDK_DEFAULT_CONNECTION"); + Process process = builder.start(); + try { + boolean exited = process.waitFor(30, TimeUnit.SECONDS); + String text = Files.readString(output); + assertTrue(exited, "Consumer JVM did not exit. Output:\n" + text); + assertEquals(0, process.exitValue(), text); + assertTrue(text.contains("consumer-ok"), text); + } finally { + if (process.isAlive()) { + process.destroyForcibly().waitFor(30, TimeUnit.SECONDS); + } + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/RpcHandlerDispatcherTest.java b/java/sdk/src/test/java/com/github/copilot/RpcHandlerDispatcherTest.java index 44bff36444..a92a4b561d 100644 --- a/java/sdk/src/test/java/com/github/copilot/RpcHandlerDispatcherTest.java +++ b/java/sdk/src/test/java/com/github/copilot/RpcHandlerDispatcherTest.java @@ -16,8 +16,13 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -43,6 +48,7 @@ * identified by JaCoCo: unknown sessions, missing fields, error paths, and edge * cases for each handler method. */ +@AllowCopilotExperimental class RpcHandlerDispatcherTest { private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); @@ -509,6 +515,213 @@ void userInputRequestHandlerFails() throws Exception { assertEquals(-32603, response.get("error").get("code").asInt()); } + // ===== skillProvider.* tests ===== + + @Test + void skillProviderListAndReadSuccess() throws Exception { + CopilotSession session = createSession("s1"); + session.registerSkillProvider(skillProvider( + () -> CompletableFuture.completedFuture( + List.of(new SkillProviderDescriptor("docs", "Documentation", null, null, "topic"))), + name -> CompletableFuture.completedFuture("# " + name))); + + ObjectNode listParams = MAPPER.createObjectNode(); + listParams.put("sessionId", "s1"); + invokeHandler("skillProvider.list", "60", listParams); + + JsonNode listResponse = readResponse(); + JsonNode descriptor = listResponse.at("/result/skills/0"); + assertEquals("docs", descriptor.path("name").asText()); + assertEquals("Documentation", descriptor.path("description").asText()); + assertEquals("topic", descriptor.path("argumentHint").asText()); + assertFalse(descriptor.has("userInvocable")); + assertFalse(descriptor.has("disableModelInvocation")); + + ObjectNode readParams = MAPPER.createObjectNode(); + readParams.put("sessionId", "s1"); + readParams.put("name", "docs"); + invokeHandler("skillProvider.read", "61", readParams); + + JsonNode readResponse = readResponse(); + assertEquals("# docs", readResponse.at("/result/markdown").asText()); + } + + @Test + void skillProviderNullListReturnsEmptyList() throws Exception { + CopilotSession session = createSession("s1"); + session.registerSkillProvider(skillProvider(() -> CompletableFuture.completedFuture(null), + name -> CompletableFuture.completedFuture("# " + name))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + invokeHandler("skillProvider.list", "62", params); + + JsonNode response = readResponse(); + assertTrue(response.at("/result/skills").isArray()); + assertEquals(0, response.at("/result/skills").size()); + } + + @Test + void skillProviderReadNullReturnsNullMarkdown() throws Exception { + CopilotSession session = createSession("s1"); + session.registerSkillProvider(skillProvider(() -> CompletableFuture.completedFuture(List.of()), + name -> CompletableFuture.completedFuture(null))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("name", "missing"); + invokeHandler("skillProvider.read", "63", params); + + JsonNode response = readResponse(); + assertNull(response.get("error")); + assertTrue(response.at("/result").has("markdown")); + assertTrue(response.at("/result/markdown").isNull()); + } + + @Test + void skillProviderFailuresUseStableMessagesWithoutData() throws Exception { + CopilotSession session = createSession("s1"); + session.registerSkillProvider(skillProvider( + () -> CompletableFuture.failedFuture(new IllegalStateException("secret list failure")), name -> { + throw new IllegalStateException("secret read failure"); + })); + + ObjectNode listParams = MAPPER.createObjectNode(); + listParams.put("sessionId", "s1"); + invokeHandler("skillProvider.list", "64", listParams); + JsonNode listResponse = readResponse(); + assertEquals(-32603, listResponse.at("/error/code").asInt()); + assertEquals("Skill provider listSkills failed", listResponse.at("/error/message").asText()); + assertFalse(listResponse.path("error").has("data")); + + ObjectNode readParams = MAPPER.createObjectNode(); + readParams.put("sessionId", "s1"); + readParams.put("name", "docs"); + invokeHandler("skillProvider.read", "65", readParams); + JsonNode readResponse = readResponse(); + assertEquals(-32603, readResponse.at("/error/code").asInt()); + assertEquals("Skill provider readSkill failed", readResponse.at("/error/message").asText()); + assertFalse(readResponse.path("error").has("data")); + } + + @Test + void skillProviderErrorsForUnknownMissingOrClearedProviderHaveNoData() throws Exception { + ObjectNode unknownParams = MAPPER.createObjectNode(); + unknownParams.put("sessionId", "missing-session"); + invokeHandler("skillProvider.list", "66", unknownParams); + JsonNode unknownResponse = readResponse(); + assertEquals(-32603, unknownResponse.at("/error/code").asInt()); + assertEquals("No skill provider for session: missing-session", unknownResponse.at("/error/message").asText()); + assertFalse(unknownResponse.path("error").has("data")); + + createSession("s1"); + ObjectNode noProviderParams = MAPPER.createObjectNode(); + noProviderParams.put("sessionId", "s1"); + noProviderParams.put("name", "docs"); + invokeHandler("skillProvider.read", "67", noProviderParams); + JsonNode noProviderResponse = readResponse(); + assertEquals(-32603, noProviderResponse.at("/error/code").asInt()); + assertEquals("No skill provider for session: s1", noProviderResponse.at("/error/message").asText()); + assertFalse(noProviderResponse.path("error").has("data")); + + CopilotSession session = sessions.get("s1"); + session.registerSkillProvider(skillProvider(() -> CompletableFuture.completedFuture(List.of()), + name -> CompletableFuture.completedFuture("# " + name))); + session.clearSkillProvider(); + invokeHandler("skillProvider.list", "68", noProviderParams); + JsonNode clearedResponse = readResponse(); + assertEquals(-32603, clearedResponse.at("/error/code").asInt()); + assertEquals("No skill provider for session: s1", clearedResponse.at("/error/message").asText()); + assertFalse(clearedResponse.path("error").has("data")); + } + + @Test + void skillProviderCallIsCancelledByCancelRequest() throws Exception { + CopilotSession session = createSession("s1"); + var entered = new CountDownLatch(1); + var pending = new CompletableFuture>(); + session.registerSkillProvider(skillProvider(() -> { + entered.countDown(); + return pending; + }, name -> CompletableFuture.completedFuture(null))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + invokeHandler("skillProvider.list", "69", params); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + dispatcher.handleCancelRequest(MAPPER.createObjectNode().put("id", 69)); + + JsonNode response = readResponse(); + assertEquals(69, response.path("id").asInt()); + assertEquals(-32800, response.at("/error/code").asInt()); + assertTrue(pending.isCancelled()); + } + + @Test + void skillProviderCallCancelledBeforeDispatchIsNotStarted() throws Exception { + CopilotSession session = createSession("s1"); + var calls = new AtomicInteger(); + session.registerSkillProvider(skillProvider(() -> { + calls.incrementAndGet(); + return CompletableFuture.completedFuture(List.of()); + }, name -> CompletableFuture.completedFuture(null))); + var blockedExecutor = new CompletableFuture(); + var cancellingDispatcher = new RpcHandlerDispatcher(sessions, lifecycleEvents::add, + task -> blockedExecutor.thenRun(task), gitHubTokenProviders); + cancellingDispatcher.registerHandlers(rpc); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + invokeHandler("skillProvider.list", "70", params); + cancellingDispatcher.handleCancelRequest(MAPPER.createObjectNode().put("id", 70)); + blockedExecutor.complete(null); + + assertEquals(-32800, readResponse().at("/error/code").asInt()); + assertEquals(0, calls.get()); + } + + @Test + void skillProviderCallsAreCancelledWhenTheConnectionCloses() throws Exception { + CopilotSession session = createSession("s1"); + var entered = new CountDownLatch(1); + var pending = new CompletableFuture(); + session.registerSkillProvider(skillProvider(() -> CompletableFuture.completedFuture(List.of()), name -> { + entered.countDown(); + return pending; + })); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("name", "docs"); + invokeHandler("skillProvider.read", "71", params); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + dispatcher.cancelSkillProviderCalls(); + + assertEquals(-32800, readResponse().at("/error/code").asInt()); + assertTrue(pending.isCancelled()); + } + + @Test + void cancelRequestForAnotherRequestLeavesSkillProviderCallRunning() throws Exception { + CopilotSession session = createSession("s1"); + var entered = new CountDownLatch(1); + var pending = new CompletableFuture(); + session.registerSkillProvider(skillProvider(() -> CompletableFuture.completedFuture(List.of()), name -> { + entered.countDown(); + return pending; + })); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("name", "docs"); + invokeHandler("skillProvider.read", "72", params); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + dispatcher.handleCancelRequest(MAPPER.createObjectNode().put("id", 73)); + pending.complete("# docs"); + + assertEquals("# docs", readResponse().at("/result/markdown").asText()); + } + // ===== hooks.invoke tests ===== @Test @@ -649,4 +862,19 @@ void systemMessageTransformWithKnownSessionNoCallbacks() throws Exception { assertNotNull(resultSections); assertEquals("Original content", resultSections.get("identity").get("content").asText()); } + + private static SkillProvider skillProvider(Supplier>> listSkills, + Function> readSkill) { + return new SkillProvider() { + @Override + public CompletableFuture> listSkills() { + return listSkills.get(); + } + + @Override + public CompletableFuture readSkill(String name) { + return readSkill.apply(name); + } + }; + } } diff --git a/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java b/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java index 774ae10164..b81685fea0 100644 --- a/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java +++ b/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java @@ -50,6 +50,7 @@ import com.github.copilot.generated.rpc.SessionConnectorsRefreshParams; import com.github.copilot.generated.rpc.SessionConnectorsRefreshResult; import com.github.copilot.generated.rpc.SessionModelSwitchToParams; +import com.github.copilot.generated.rpc.SessionModelSwitchToRequest; import com.github.copilot.generated.rpc.SessionRpc; /** @@ -445,8 +446,9 @@ void sessionRpc_model_switchTo_merges_sessionId_with_extra_params() { var switchParams = new SessionModelSwitchToParams(null, "gpt-5", null, null, null, null, null, null, null, null, null, null, null, null, null, null); session.model.switchTo(switchParams); + session.model.switchTo(new SessionModelSwitchToRequest("gpt-5").setProviderId("catalog-provider")); - assertEquals(1, stub.calls.size()); + assertEquals(2, stub.calls.size()); assertEquals("session.model.switchTo", stub.calls.get(0).method()); // Params should be a JsonNode containing both sessionId and modelId @@ -454,7 +456,12 @@ void sessionRpc_model_switchTo_merges_sessionId_with_extra_params() { assertInstanceOf(com.fasterxml.jackson.databind.node.ObjectNode.class, params); var node = (com.fasterxml.jackson.databind.node.ObjectNode) params; assertEquals("sess-xyz", node.get("sessionId").asText()); + assertFalse(node.has("providerId")); assertEquals("gpt-5", node.get("modelId").asText()); + var providerRequest = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(1).params(); + assertEquals("sess-xyz", providerRequest.get("sessionId").asText()); + assertEquals("catalog-provider", providerRequest.get("providerId").asText()); + assertEquals("gpt-5", providerRequest.get("modelId").asText()); } @Test diff --git a/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java index 4378c105cd..ef7034c455 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java @@ -30,11 +30,11 @@ class SessionAutoTierEventTest { private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); @ParameterizedTest - @CsvSource({"session.start,EFFICIENCY,efficiency", "session.start,BALANCE,balance", - "session.start,INTELLIGENCE,intelligence", "session.start,FAST,fast", - "session.resume,EFFICIENCY,efficiency", "session.resume,BALANCE,balance", - "session.resume,INTELLIGENCE,intelligence", "session.resume,FAST,fast"}) - void canonicalAutoTierRoundTrips(String type, AutoTier tier, String value) throws Exception { + @CsvSource({"session.start,efficiency", "session.start,balance", "session.start,intelligence", "session.start,fast", + "session.start,premium-v2", "session.resume,efficiency", "session.resume,balance", + "session.resume,intelligence", "session.resume,fast", "session.resume,premium-v2"}) + void canonicalAutoTierRoundTrips(String type, String value) throws Exception { + AutoTier tier = AutoTier.fromValue(value); String json = """ {"type":"%s","data":{"selectedModel":"auto","autoTier":"%s"}} """.formatted(type, value); diff --git a/java/sdk/src/test/java/com/github/copilot/SessionAutoTierSwitchTest.java b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierSwitchTest.java index a1c5dd2f44..96b9bf0319 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionAutoTierSwitchTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierSwitchTest.java @@ -33,6 +33,45 @@ @AllowCopilotExperimental class SessionAutoTierSwitchTest { + @Test + void customTierRoundTripsAcrossConvenienceAndGeneratedApis() throws Exception { + ObjectMapper mapper = JsonRpcClient.getObjectMapper(); + AutoTier tier = AutoTier.fromValue("premium-v2"); + assertEquals("\"premium-v2\"", mapper.writeValueAsString(tier)); + assertEquals(tier, mapper.readValue("\"premium-v2\"", AutoTier.class)); + var generated = mapper.readValue("\"premium-v2\"", com.github.copilot.generated.rpc.AutoTier.class); + assertEquals("premium-v2", generated.getValue()); + assertEquals("\"premium-v2\"", mapper.writeValueAsString(generated)); + assertEquals(AutoTier.BALANCE, AutoTier.fromValue("balance")); + } + + @Test + void legacy_model_switch_preserves_reasoning_and_omits_provider() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-legacy", sockets.client()); + session.setModel("gpt-5", "high"); + var params = sockets.stubServer().readOneMessage().get("params"); + assertEquals("gpt-5", params.get("modelId").asText()); + assertEquals("high", params.get("reasoningEffort").asText()); + assertFalse(params.has("providerId")); + assertFalse(params.has("autoTier")); + } + } + + @Test + void model_switch_with_summary_preserves_its_wire_fields() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-summary", sockets.client()); + session.setModel("gpt-5", "high", "concise", null); + var params = sockets.stubServer().readOneMessage().get("params"); + assertEquals("gpt-5", params.get("modelId").asText()); + assertEquals("high", params.get("reasoningEffort").asText()); + assertEquals("concise", params.get("reasoningSummary").asText()); + assertFalse(params.has("providerId")); + assertFalse(params.has("autoTier")); + } + } + @Test void setModel_omits_autoTier_when_no_preference_is_requested() throws Exception { try (var sockets = new SocketPair()) { diff --git a/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java b/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java index 46b71d7d3f..8f2edf52e0 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java @@ -35,6 +35,7 @@ import com.github.copilot.generated.SessionIdleEvent; import com.github.copilot.generated.SessionErrorEvent; import com.github.copilot.generated.SessionMode; +import com.github.copilot.generated.SessionResumeEvent; import com.github.copilot.generated.SessionStartEvent; import com.github.copilot.generated.rpc.SessionToolsGetCurrentMetadataResult; import com.github.copilot.rpc.MessageOptions; @@ -230,6 +231,24 @@ void testTypedEventHandler() { assertEquals("Second message", receivedMessages.get(1).getData().content()); } + @Test + void testSendReturnsAfterRpcAcceptanceWithoutWaitingForIdle() throws Exception { + var rpc = mock(JsonRpcClient.class); + when(rpc.invoke(eq("session.send"), any(), eq(SendMessageResponse.class))) + .thenReturn(CompletableFuture.completedFuture(new SendMessageResponse("message-1"))); + session = createTestSession(rpc); + var events = new ArrayList(); + session.on(events::add); + + session.send("hello").get(1, TimeUnit.SECONDS); + + verify(rpc).invoke(eq("session.send"), any(), eq(SendMessageResponse.class)); + assertTrue(events.isEmpty()); + dispatchEvent(createAssistantMessageEvent("done")); + dispatchEvent(createSessionIdleEvent()); + assertEquals(List.of("assistant.message", "session.idle"), events.stream().map(SessionEvent::getType).toList()); + } + @Test void testSendAndWaitSkipsAutopilotContinuationIdle() throws Exception { var rpc = mock(JsonRpcClient.class); @@ -385,9 +404,7 @@ void testHandlerReceivesCorrectEventData() { capturedSessionId.set(start.getData().sessionId()); }); - 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, null, null, null)); + SessionStartEvent startEvent = createSessionStartEvent("my-session-123"); dispatchEvent(startEvent); AssistantMessageEvent msgEvent = createAssistantMessageEvent("Test content"); @@ -397,6 +414,17 @@ void testHandlerReceivesCorrectEventData() { assertEquals("Test content", capturedContent.get()); } + @Test + void testLegacyLifecycleEventConstructorsRemainAvailable() { + var start = new SessionStartEvent.SessionStartEventData("session", null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null); + var resume = new SessionResumeEvent.SessionResumeEventData(null, 0L, null, null, null, null, null, null, null, + null, null, null, null, null, null, null); + + assertEquals("session", start.sessionId()); + assertEquals(0L, resume.eventCount()); + } + @Test void testHandlerExceptionDoesNotBreakOtherHandlers() { var handler2Events = new ArrayList(); @@ -1071,9 +1099,9 @@ private SessionStartEvent createSessionStartEvent(String sessionId) { private AssistantMessageEvent createAssistantMessageEvent(String content) { var event = new AssistantMessageEvent(); - var data = new AssistantMessageEvent.AssistantMessageEventData(null, null, null, content, null, null, null, + var data = new AssistantMessageEvent.AssistantMessageEventData(null, null, null, null, content, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null); + null, null); event.setData(data); return event; } diff --git a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index c7cb1be6bc..978346e486 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -111,6 +111,7 @@ void testBuildCreateRequestNullConfig() { assertNull(request.getModel()); assertTrue(request.getRequestPermission(), "requestPermission should be true even for null config"); assertEquals("direct", request.getEnvValueMode(), "envValueMode should be 'direct' even for null config"); + assertNull(request.getHasSkillProvider()); } @Test @@ -162,6 +163,36 @@ void askUserVariantDefaultsToOmittedLegacyBehavior() throws Exception { assertFalse(mapper.readTree(mapper.writeValueAsBytes(resumeRequest)).has("askUserVariant")); } + @Test + @AllowCopilotExperimental + void skillProviderFlagIsForwardedAndSerializedForCreateAndResume() throws Exception { + SkillProvider provider = testSkillProvider(); + var mapper = JsonRpcClient.getObjectMapper(); + + var createRequest = SessionRequestBuilder.buildCreateRequest(new SessionConfig().setSkillProvider(provider), + "skill-provider-create"); + var resumeRequest = SessionRequestBuilder.buildResumeRequest("skill-provider-resume", + new ResumeSessionConfig().setSkillProvider(provider)); + + assertTrue(createRequest.getHasSkillProvider()); + assertTrue(resumeRequest.getHasSkillProvider()); + assertTrue(mapper.readTree(mapper.writeValueAsBytes(createRequest)).path("hasSkillProvider").asBoolean()); + assertTrue(mapper.readTree(mapper.writeValueAsBytes(resumeRequest)).path("hasSkillProvider").asBoolean()); + } + + @Test + void skillProviderFlagIsOmittedWhenUnset() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var createRequest = SessionRequestBuilder.buildCreateRequest(new SessionConfig(), "skill-provider-create"); + var resumeRequest = SessionRequestBuilder.buildResumeRequest("skill-provider-resume", + new ResumeSessionConfig()); + + assertNull(createRequest.getHasSkillProvider()); + assertNull(resumeRequest.getHasSkillProvider()); + assertFalse(mapper.readTree(mapper.writeValueAsBytes(createRequest)).has("hasSkillProvider")); + assertFalse(mapper.readTree(mapper.writeValueAsBytes(resumeRequest)).has("hasSkillProvider")); + } + @Test void diagnosticsAreForwardedAndOmittedForCreateAndColdResume() throws Exception { var mapper = JsonRpcClient.getObjectMapper(); @@ -399,6 +430,7 @@ void testBuildResumeRequestNullConfig() { assertNull(request.getContinuePendingWork()); assertTrue(request.getRequestPermission(), "requestPermission should be true even for null config"); assertEquals("direct", request.getEnvValueMode(), "envValueMode should be 'direct' even for null config"); + assertNull(request.getHasSkillProvider()); } @Test @@ -850,6 +882,17 @@ void configureSessionWithNullConfig_returnsEarly() { assertDoesNotThrow(() -> SessionRequestBuilder.configureSession(session, (SessionConfig) null)); } + @Test + @AllowCopilotExperimental + void configureSessionWithSkillProvider_registersProvider() { + CopilotSession session = new CopilotSession("session-1", null); + SkillProvider provider = testSkillProvider(); + + SessionRequestBuilder.configureSession(session, new SessionConfig().setSkillProvider(provider)); + + assertSame(provider, session.getSkillProvider()); + } + @Test void configureSessionWithCommands_registersCommands() { CopilotSession session = new CopilotSession("session-1", null); @@ -922,6 +965,17 @@ void configureResumedSessionWithOnEvent_registersEventHandler() { SessionRequestBuilder.configureSession(session, config); } + @Test + @AllowCopilotExperimental + void configureResumedSessionWithSkillProvider_registersProvider() { + CopilotSession session = new CopilotSession("session-1", null); + SkillProvider provider = testSkillProvider(); + + SessionRequestBuilder.configureSession(session, new ResumeSessionConfig().setSkillProvider(provider)); + + assertSame(provider, session.getSkillProvider()); + } + @Test void testBuildCreateRequestWithDefaultAgent() { var defaultAgent = new DefaultAgentConfig().setExcludedTools(List.of("secret_tool")); @@ -1300,4 +1354,19 @@ void githubMcpToolConfigIsMappedAndSerializedForCreateAndResume() throws Excepti mapper.writeValueAsString(SessionRequestBuilder.buildCreateRequest(new SessionConfig(), "session-2")) .contains("\"githubMcpToolConfig\"")); } + + @AllowCopilotExperimental + private static SkillProvider testSkillProvider() { + return new SkillProvider() { + @Override + public CompletableFuture> listSkills() { + return CompletableFuture.completedFuture(List.of()); + } + + @Override + public CompletableFuture readSkill(String name) { + return CompletableFuture.completedFuture(null); + } + }; + } } diff --git a/java/sdk/src/test/java/com/github/copilot/SetToolsTest.java b/java/sdk/src/test/java/com/github/copilot/SetToolsTest.java index 6f4723ce58..267d1375e4 100644 --- a/java/sdk/src/test/java/com/github/copilot/SetToolsTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SetToolsTest.java @@ -173,6 +173,32 @@ void cancellingWhileQueuedSendsNothing() throws Exception { assertNull(session.getTool("queued")); } + @Test + void closeRejectsLateAcknowledgementsAndQueuedReplacements() throws Exception { + var accepted = new CompletableFuture(); + var requests = captureSetToolsRequests(accepted); + when(rpc.invoke(eq("session.detach"), any(), eq(CopilotSession.SessionDetachResponse.class))) + .thenReturn(CompletableFuture.completedFuture(new CopilotSession.SessionDetachResponse(true, null))); + + var pending = session.setTools(List.of(tool("pending", "Pending", "first", new CopyOnWriteArrayList<>()))); + var queued = session.setTools(List.of(tool("queued", "Queued", "second", new CopyOnWriteArrayList<>()))); + assertEquals(1, requests.size(), "Only the admitted replacement may reach the RPC boundary"); + assertFalse(pending.isDone()); + assertFalse(queued.isDone()); + + session.close(); + assertTrue(accepted.complete(null), "Acknowledge the admitted replacement only after retirement"); + + var pendingError = assertThrows(ExecutionException.class, () -> pending.get(1, TimeUnit.SECONDS)); + var queuedError = assertThrows(ExecutionException.class, () -> queued.get(1, TimeUnit.SECONDS)); + assertInstanceOf(IllegalStateException.class, pendingError.getCause()); + assertInstanceOf(IllegalStateException.class, queuedError.getCause()); + assertEquals("Session is closed", pendingError.getCause().getMessage()); + assertEquals("Session is closed", queuedError.getCause().getMessage()); + assertEquals(1, requests.size(), "A replacement queued before retirement must not start afterwards"); + assertThrows(IllegalStateException.class, () -> session.setTools(List.of())); + } + private List captureSetToolsRequests(CompletableFuture result) { var requests = new ArrayList(); when(rpc.invoke(eq("session.tools.set"), any(), eq(Void.class))).thenAnswer(invocation -> { diff --git a/java/sdk/src/test/java/com/github/copilot/SkillProviderTest.java b/java/sdk/src/test/java/com/github/copilot/SkillProviderTest.java new file mode 100644 index 0000000000..7a4e6f3329 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/SkillProviderTest.java @@ -0,0 +1,421 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.generated.AssistantMessageEvent; +import com.github.copilot.generated.SessionEvent; +import com.github.copilot.generated.ToolExecutionCompleteEvent; +import com.github.copilot.generated.rpc.Skill; +import com.github.copilot.generated.rpc.SkillSource; +import com.github.copilot.rpc.CloudSessionOptions; +import com.github.copilot.rpc.MessageOptions; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.ResumeSessionConfig; +import com.github.copilot.rpc.SessionConfig; + +@AllowCopilotExperimental +class SkillProviderTest { + + 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 should_load_provider_skill_lazily_through_skill_tool() throws Exception { + ctx.configureForTest("skill_provider", "should_load_provider_skill_lazily_through_skill_tool"); + + var provider = new TestSkillProvider( + List.of(skill("provider-lookup", "Reports the provider lookup verification word.", + "# Provider lookup\n\nThe verification word is TANGERINE_QUARTZ_19. Reply with it.\n"))); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(provider)) + .get(30, TimeUnit.SECONDS); + + try { + Skill listed = session.getRpc().skills.list().get(30, TimeUnit.SECONDS).skills().stream() + .filter(s -> "provider-lookup".equals(s.name())).findFirst().orElse(null); + assertNotNull(listed, "Expected provider-lookup to be listed"); + assertEquals(SkillSource.SDK, listed.source()); + assertTrue(listed.enabled()); + assertEquals("", listed.path() == null ? "" : listed.path()); + assertEquals(List.of(), provider.reads()); + + AssistantMessageEvent message = session.sendAndWait(new MessageOptions().setPrompt( + "Use the skill tool to load the provider-lookup skill, then reply with its verification word."), + 60_000).get(90, TimeUnit.SECONDS); + + assertEquals(List.of("provider-lookup"), provider.reads()); + // Validate the final assistant response arrived (guards against truncated + // captures) + assertTrue(assistantText(message).contains("TANGERINE_QUARTZ_19")); + } finally { + session.close(); + } + } + } + + @Test + void should_load_provider_and_file_based_skills_together() throws Exception { + ctx.configureForTest("skill_provider", "should_load_provider_and_file_based_skills_together"); + + Path skillsDir = ctx.getWorkDir().resolve("file-skills"); + Files.createDirectories(skillsDir.resolve("file-notes")); + Files.writeString(skillsDir.resolve("file-notes").resolve("SKILL.md"), + "---\nname: file-notes\ndescription: Reports the file notes verification word.\n---\n\nThe file notes verification word is MAPLE_FALCON_27.\n"); + + var provider = new TestSkillProvider(List.of(skill("provider-audit", + "Reports the provider audit verification word.", + "---\nname: provider-audit\nallowed-tools: view\n---\n\nThe provider audit verification word is COBALT_HERON_58.\n"))); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSkillDirectories(List.of(skillsDir.toString())).setSkillProvider(provider)) + .get(30, TimeUnit.SECONDS); + + try { + List skills = session.getRpc().skills.list().get(30, TimeUnit.SECONDS).skills(); + Skill fileSkill = findSkill(skills, "file-notes"); + Skill providerSkill = findSkill(skills, "provider-audit"); + assertNotNull(fileSkill, "Expected file-notes to be listed"); + assertNotEquals(SkillSource.SDK, fileSkill.source()); + assertNotNull(fileSkill.path()); + assertNotNull(providerSkill, "Expected provider-audit to be listed"); + assertEquals(SkillSource.SDK, providerSkill.source()); + + AssistantMessageEvent message = session.sendAndWait(new MessageOptions().setPrompt( + "Use the skill tool to load the file-notes skill and the provider-audit skill, then reply with both verification words."), + 60_000).get(90, TimeUnit.SECONDS); + + assertEquals(List.of("provider-audit"), provider.reads()); + assertTrue(assistantText(message).contains("MAPLE_FALCON_27")); + // Validate the final assistant response arrived (guards against truncated + // captures) + assertTrue(assistantText(message).contains("COBALT_HERON_58")); + } finally { + session.close(); + } + } + } + + @Test + void should_rebind_skill_provider_on_resume() throws Exception { + ctx.configureForTest("skill_provider", "should_rebind_skill_provider_on_resume"); + + var original = new TestSkillProvider(List.of(skill("rebind-check", "Reports the rebind verification word.", + "The rebind verification word is AMBER_ALPHA_11.\n"))); + var replacement = new TestSkillProvider(List.of(skill("rebind-check", "Reports the rebind verification word.", + "The rebind verification word is BRONZE_BETA_22.\n"))); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession first = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(original)) + .get(30, TimeUnit.SECONDS); + String sessionId = first.getSessionId(); + AssistantMessageEvent ready = first + .sendAndWait(new MessageOptions() + .setPrompt("Without using any tools or skills, reply with exactly REBIND_READY."), 60_000) + .get(90, TimeUnit.SECONDS); + assertTrue(assistantText(ready).contains("REBIND_READY")); + first.close(); + assertEquals(List.of(), original.reads()); + int originalCallsBeforeResume = original.callCount(); + + CopilotSession session = client + .resumeSession(sessionId, new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(replacement)) + .get(30, TimeUnit.SECONDS); + + try { + AssistantMessageEvent message = session.sendAndWait(new MessageOptions().setPrompt( + "Use the skill tool to load the rebind-check skill, then reply with its verification word."), + 60_000).get(90, TimeUnit.SECONDS); + + assertEquals(List.of("rebind-check"), replacement.reads()); + assertEquals(originalCallsBeforeResume, original.callCount()); + // Validate the final assistant response arrived (guards against truncated + // captures) + assertTrue(assistantText(message).contains("BRONZE_BETA_22")); + assertFalse(assistantText(message).contains("AMBER_ALPHA_11")); + } finally { + session.close(); + } + } + } + + @Test + void should_report_provider_read_failure_without_leaking_details() throws Exception { + ctx.configureForTest("skill_provider", "should_report_provider_read_failure_without_leaking_details"); + + String secret = "PROVIDER_SECRET_7F3A9C"; + var provider = new TestSkillProvider(List.of(new ProvidedSkill(new SkillProviderDescriptor("broken-lookup", + "Reports the broken lookup verification word.", null, null, null), () -> { + throw new IllegalStateException("database unavailable: " + secret); + }))); + List events = new CopyOnWriteArrayList<>(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(provider)) + .get(30, TimeUnit.SECONDS); + session.on(events::add); + + try { + AssistantMessageEvent message = session.sendAndWait(new MessageOptions().setPrompt( + "Use the skill tool to load the broken-lookup skill. If loading fails, reply with exactly LOAD_FAILED."), + 60_000).get(90, TimeUnit.SECONDS); + + assertTrue(provider.reads().contains("broken-lookup")); + List failures = failedToolExecutions(events); + assertEquals(1, failures.size()); + assertFalse(JsonRpcClient.getObjectMapper().writeValueAsString(events).contains(secret)); + // Validate the final assistant response arrived (guards against truncated + // captures) + assertTrue(assistantText(message).contains("LOAD_FAILED")); + } finally { + session.close(); + } + } + } + + @Test + void should_report_missing_provider_skill_as_not_found() throws Exception { + ctx.configureForTest("skill_provider", "should_report_missing_provider_skill_as_not_found"); + + var provider = new TestSkillProvider(List.of(new ProvidedSkill(new SkillProviderDescriptor("vanished-lookup", + "Reports the vanished lookup verification word.", null, null, null), () -> null))); + List events = new CopyOnWriteArrayList<>(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(provider)) + .get(30, TimeUnit.SECONDS); + session.on(events::add); + + try { + AssistantMessageEvent message = session.sendAndWait(new MessageOptions().setPrompt( + "Use the skill tool to load the vanished-lookup skill. If loading fails, reply with exactly LOAD_FAILED."), + 60_000).get(90, TimeUnit.SECONDS); + + assertTrue(provider.reads().contains("vanished-lookup")); + List failures = failedToolExecutions(events); + assertEquals(1, failures.size()); + String failureJson = JsonRpcClient.getObjectMapper().writeValueAsString(failures.get(0)) + .toLowerCase(Locale.ROOT); + assertTrue(failureJson.contains("not found"), "Expected not found failure, got: " + failureJson); + // Validate the final assistant response arrived (guards against truncated + // captures) + assertTrue(assistantText(message).contains("LOAD_FAILED")); + } finally { + session.close(); + } + } + } + + @Test + void should_keep_provider_dormant_when_skills_disabled() throws Exception { + ctx.initializeProxy(); + + var provider = new TestSkillProvider(List.of(skill("dormant-lookup", "Never listed.", "Never read.\n"))); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setEnableSkills(false).setSkillProvider(provider)) + .get(30, TimeUnit.SECONDS); + + try { + session.getRpc().skills.ensureLoaded().get(30, TimeUnit.SECONDS); + List skills = session.getRpc().skills.list().get(30, TimeUnit.SECONDS).skills(); + + assertEquals(List.of(), sdkSkills(skills)); + assertEquals(List.of(), provider.calls()); + } finally { + session.close(); + } + } + } + + @Test + void should_unbind_provider_when_resumed_without_one() throws Exception { + ctx.initializeProxy(); + + var provider = new TestSkillProvider( + List.of(skill("unbound-lookup", "Reports the unbound lookup word.", "Unbound.\n"))); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession first = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(provider)) + .get(30, TimeUnit.SECONDS); + List before = first.getRpc().skills.list().get(30, TimeUnit.SECONDS).skills(); + assertTrue(before.stream().anyMatch(s -> "unbound-lookup".equals(s.name()))); + int callsBeforeResume = provider.callCount(); + + CopilotSession session = client + .resumeSession(first.getSessionId(), + new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); + + try { + session.getRpc().skills.reload().get(30, TimeUnit.SECONDS); + List skills = session.getRpc().skills.list().get(30, TimeUnit.SECONDS).skills(); + + assertEquals(List.of(), sdkSkills(skills)); + assertEquals(callsBeforeResume, provider.callCount()); + } finally { + session.close(); + } + } + } + + @Test + void should_cancel_a_blocked_provider_call_when_the_session_disconnects() throws Exception { + ctx.initializeProxy(); + + var entered = new CountDownLatch(1); + var pending = new CompletableFuture>(); + var provider = new SkillProvider() { + @Override + public CompletableFuture> listSkills() { + entered.countDown(); + return pending; + } + + @Override + public CompletableFuture readSkill(String name) { + return CompletableFuture.completedFuture(null); + } + }; + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSkillProvider(provider)) + .get(30, TimeUnit.SECONDS); + + // The list RPC fails once the binding is removed; only the provider's + // cancellation matters here. + session.getRpc().skills.list(); + assertTrue(entered.await(30, TimeUnit.SECONDS), "provider listSkills was not called"); + + session.close(); + assertThrows(CancellationException.class, () -> pending.get(10, TimeUnit.SECONDS)); + } + } + + @Test + void should_reject_skill_provider_for_cloud_sessions() throws Exception { + ctx.initializeProxy(); + + var provider = new TestSkillProvider(List.of(skill("cloud-lookup", "Never listed.", "Never read.\n"))); + + try (CopilotClient client = ctx.createClient()) { + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setCloud(new CloudSessionOptions()).setSkillProvider(provider); + + ExecutionException error = assertThrows(ExecutionException.class, + () -> client.createSession(config).get(30, TimeUnit.SECONDS)); + assertInstanceOf(IllegalArgumentException.class, error.getCause()); + assertEquals("Skill providers are not supported for cloud sessions.", error.getCause().getMessage()); + assertEquals(List.of(), provider.calls()); + } + } + + private static ProvidedSkill skill(String name, String description, String markdown) { + return new ProvidedSkill(new SkillProviderDescriptor(name, description, null, null, null), () -> markdown); + } + + private static Skill findSkill(List skills, String name) { + return skills.stream().filter(s -> name.equals(s.name())).findFirst().orElse(null); + } + + private static List sdkSkills(List skills) { + return skills.stream().filter(s -> s.source() == SkillSource.SDK).toList(); + } + + private static List failedToolExecutions(List events) { + return events.stream().filter(ToolExecutionCompleteEvent.class::isInstance) + .map(ToolExecutionCompleteEvent.class::cast) + .filter(event -> !Boolean.TRUE.equals(event.getData().success())).toList(); + } + + private static String assistantText(AssistantMessageEvent response) { + assertNotNull(response, "Expected a response from the assistant"); + assertNotNull(response.getData(), "Expected assistant response data"); + assertNotNull(response.getData().content(), "Expected assistant response content"); + return response.getData().content(); + } + + private record ProvidedSkill(SkillProviderDescriptor descriptor, Supplier reader) { + String read() { + return reader.get(); + } + } + + private static final class TestSkillProvider implements SkillProvider { + private final List calls = new CopyOnWriteArrayList<>(); + private final List skills; + + private TestSkillProvider(List skills) { + this.skills = skills; + } + + List calls() { + return List.copyOf(calls); + } + + int callCount() { + return calls.size(); + } + + List reads() { + return calls.stream().filter(call -> call.startsWith("read:")).map(call -> call.substring("read:".length())) + .toList(); + } + + @Override + public CompletableFuture> listSkills() { + calls.add("list"); + return CompletableFuture.completedFuture(skills.stream().map(ProvidedSkill::descriptor).toList()); + } + + @Override + public CompletableFuture readSkill(String name) { + calls.add("read:" + name); + return CompletableFuture + .completedFuture(skills.stream().filter(skill -> skill.descriptor().name().equals(name)).findFirst() + .map(ProvidedSkill::read).orElse(null)); + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/StdioShutdownIT.java b/java/sdk/src/test/java/com/github/copilot/StdioShutdownIT.java index ed3200e671..9bfb98c277 100644 --- a/java/sdk/src/test/java/com/github/copilot/StdioShutdownIT.java +++ b/java/sdk/src/test/java/com/github/copilot/StdioShutdownIT.java @@ -7,8 +7,15 @@ import com.github.copilot.rpc.CopilotClientOptions; import java.nio.file.Files; +import java.nio.file.FileSystems; import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Comparator; +import java.util.List; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -42,6 +49,72 @@ void closeWaitsForCleanupAfterStdinEof() throws Exception { } } + @Test + void failedShutdownStillWaitsForCleanupAfterStdinEof() throws Exception { + try (var fixture = new Fixture("shutdown-error"); var watcher = FileSystems.getDefault().newWatchService()) { + fixture.client.start().get(30, TimeUnit.SECONDS); + fixture.directory.register(watcher, StandardWatchEventKinds.ENTRY_CREATE); + Path eof = fixture.marker.resolveSibling(fixture.marker.getFileName() + ".eof"); + Path release = fixture.marker.resolveSibling(fixture.marker.getFileName() + ".release"); + var stopping = fixture.client.stop(); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + while (!Files.isRegularFile(eof)) { + var key = watcher.poll(Math.max(0, deadline - System.nanoTime()), TimeUnit.NANOSECONDS); + assertNotNull(key, "Failed shutdown must still deliver stdin EOF before terminating the runtime"); + key.pollEvents(); + assertTrue(key.reset(), "The owned cleanup directory must remain available"); + } + assertFalse(stopping.isDone(), "Shutdown must not return before EOF-driven cleanup completes"); + Files.writeString(release, ""); + stopping.get(60, TimeUnit.SECONDS); + fixture.assertCleanExit(); + } + } + + @Test + void stopPreservesDetachedServiceAfterLauncherAndRuntimeExit() throws Exception { + try (var fixture = new Fixture("stop", true, true)) { + fixture.client.start().get(30, TimeUnit.SECONDS); + fixture.assertServiceResponsive(); + fixture.client.stop().get(60, TimeUnit.SECONDS); + fixture.assertCleanExit(); + fixture.assertServiceResponsive(); + } + } + + @Test + void closePreservesDetachedServiceAfterLauncherAndRuntimeExit() throws Exception { + try (var fixture = new Fixture("dispose", true, true)) { + fixture.client.start().get(30, TimeUnit.SECONDS); + fixture.assertServiceResponsive(); + fixture.client.close(); + fixture.assertCleanExit(); + fixture.assertServiceResponsive(); + } + } + + @Test + void stopPreservesDetachedTcpServiceWithoutPeerEof() throws Exception { + try (var fixture = new Fixture("stop", true, true, false)) { + fixture.client.start().get(30, TimeUnit.SECONDS); + fixture.assertServiceResponsive(); + fixture.client.stop().get(60, TimeUnit.SECONDS); + fixture.assertCleanExit(); + fixture.assertServiceResponsive(); + } + } + + @Test + void closePreservesDetachedTcpServiceWithoutPeerEof() throws Exception { + try (var fixture = new Fixture("dispose", true, true, false)) { + fixture.client.start().get(30, TimeUnit.SECONDS); + fixture.assertServiceResponsive(); + fixture.client.close(); + fixture.assertCleanExit(); + fixture.assertServiceResponsive(); + } + } + @Test void forceStopDoesNotWaitForGracefulCleanup() throws Exception { try (var fixture = new Fixture("force")) { @@ -76,6 +149,15 @@ void closeTerminatesChildThatDoesNotExitAfterEof() throws Exception { } } + @Test + void stopTerminatesLauncherAndItsChildAfterEof() throws Exception { + try (var fixture = new Fixture("fallback", true)) { + fixture.client.start().get(30, TimeUnit.SECONDS); + fixture.client.stop().get(60, TimeUnit.SECONDS); + fixture.assertCleanExit(); + } + } + @Test void failedStartupTerminatesChild() throws Exception { try (var fixture = new Fixture("start-failure")) { @@ -89,16 +171,96 @@ private static final class Fixture implements AutoCloseable { private final Path directory; private final Path marker; private final Path pid; + private final Path launcherPid; + private final Path servicePid; + private final Path servicePort; private final CopilotClient client; private Fixture(String mode) throws Exception { + this(mode, false); + } + + private Fixture(String mode, boolean useLauncher) throws Exception { + this(mode, useLauncher, false); + } + + private Fixture(String mode, boolean useLauncher, boolean detachedService) throws Exception { + this(mode, useLauncher, detachedService, true); + } + + private Fixture(String mode, boolean useLauncher, boolean detachedService, boolean useStdio) throws Exception { directory = Files.createDirectories(Path.of("target", "shutdown-" + UUID.randomUUID()).toAbsolutePath()); marker = directory.resolve("cleanup.jsonl"); pid = directory.resolve("pid"); + launcherPid = useLauncher ? directory.resolve("launcher-pid") : null; + servicePid = detachedService ? directory.resolve("service-pid") : null; + servicePort = detachedService ? directory.resolve("service-port") : null; Path script = Path.of("..", "..", "test", "harness", "stdio-shutdown-runtime.cjs").toAbsolutePath(); assertTrue(Files.isRegularFile(script), "Shared shutdown fixture must exist"); - client = new CopilotClient(new CopilotClientOptions().setAutoStart(false).setCliPath("node") - .setCliArgs(new String[]{script.toString(), marker.toString(), mode, pid.toString()})); + String nodePath = TestUtil.findExecutableInPath("node"); + assertNotNull(nodePath, "Node.js was not found in PATH"); + var args = new ArrayList(); + if (useLauncher) { + Path launcher = directory.resolve("launcher.cjs"); + Files.writeString(launcher, """ + const { spawn } = require("node:child_process"); + require("node:fs").writeFileSync(process.argv[2], String(process.pid)); + const args = process.argv.slice(3); + const fail = error => { console.error(error); process.exit(1); }; + const launch = () => { + if (args[0] === "--in-process-runtime") { + args.shift(); + const script = args.shift(); + process.argv = [process.execPath, script, ...args]; + require(script); + return; + } + const child = spawn(process.execPath, args, { stdio: "inherit" }); + child.on("error", fail); + child.on("exit", code => process.exit(code ?? 1)); + }; + if (args[0] === "--detached-service") { + args.shift(); + const service = spawn(process.execPath, ["-e", ` + const fs = require("node:fs"); + const server = require("node:net").createServer(socket => { + socket.write(process.argv[3] + ":"); + socket.pipe(socket); + }); + server.listen(0, "127.0.0.1", () => { + fs.writeFileSync(process.argv[1], String(process.pid)); + fs.writeFileSync(process.argv[2], String(server.address().port)); + process.send("ready"); + process.disconnect(); + }); + `, args.shift(), args.shift(), args.shift()], + { detached: true, stdio: ["ignore", "ignore", "ignore", "ipc"] }); + service.on("error", fail); + service.once("message", message => { + if (message !== "ready") return fail(new Error("Unexpected service handshake")); + service.unref(); + launch(); + }); + } else { + launch(); + } + """); + args.add(launcher.toString()); + args.add(launcherPid.toString()); + if (detachedService) { + args.addAll(List.of("--detached-service", servicePid.toString(), servicePort.toString(), + directory.getFileName().toString())); + } + } + if (useLauncher && !useStdio) { + args.add("--in-process-runtime"); + } + args.addAll(List.of(script.toString(), marker.toString(), mode, pid.toString())); + if (!useStdio) { + args.add("--fixture-tcp"); + } + client = new CopilotClient(new CopilotClientOptions().setAutoStart(false).setCliPath(nodePath) + .setCliArgs(args.toArray(String[]::new)).setUseStdio(useStdio)); } private void assertCleanExit() throws Exception { @@ -106,7 +268,29 @@ private void assertCleanExit() throws Exception { assertExited(); } + private void assertServiceResponsive() throws Exception { + assertTrue(Files.isRegularFile(servicePort), "The detached service must acknowledge readiness"); + try (var socket = new Socket()) { + socket.connect(new InetSocketAddress("127.0.0.1", Integer.parseInt(Files.readString(servicePort))), + 5000); + socket.setSoTimeout(5000); + String nonce = UUID.randomUUID().toString(); + byte[] expected = (directory.getFileName() + ":" + nonce).getBytes(StandardCharsets.UTF_8); + socket.getOutputStream().write(nonce.getBytes(StandardCharsets.UTF_8)); + socket.getOutputStream().flush(); + assertArrayEquals(expected, socket.getInputStream().readNBytes(expected.length), + "The detached service must still serve requests, not merely retain a PID"); + } + } + private void assertExited() throws Exception { + assertExited(pid); + if (launcherPid != null) { + assertExited(launcherPid); + } + } + + private static void assertExited(Path pid) throws Exception { assertTrue(Files.isRegularFile(pid), "Child must have started"); assertFalse( ProcessHandle.of(Long.parseLong(Files.readString(pid))).map(ProcessHandle::isAlive).orElse(false), @@ -115,11 +299,21 @@ private void assertExited() throws Exception { @Override public void close() throws Exception { - if (Files.isRegularFile(pid)) { - var process = ProcessHandle.of(Long.parseLong(Files.readString(pid))); - if (process.isPresent() && process.get().isAlive()) { - process.get().destroyForcibly(); - process.get().onExit().get(10, TimeUnit.SECONDS); + var ownedPids = new ArrayList(); + if (servicePid != null) { + ownedPids.add(servicePid); + } + ownedPids.add(pid); + if (launcherPid != null) { + ownedPids.add(launcherPid); + } + for (var ownedPid : ownedPids) { + if (Files.isRegularFile(ownedPid)) { + var process = ProcessHandle.of(Long.parseLong(Files.readString(ownedPid))); + if (process.isPresent() && process.get().isAlive()) { + process.get().destroyForcibly(); + process.get().onExit().get(10, TimeUnit.SECONDS); + } } } client.close(); diff --git a/java/sdk/src/test/java/com/github/copilot/SubagentHooksE2ETest.java b/java/sdk/src/test/java/com/github/copilot/SubagentHooksE2ETest.java index 536a33cb95..bc83502f87 100644 --- a/java/sdk/src/test/java/com/github/copilot/SubagentHooksE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/SubagentHooksE2ETest.java @@ -157,8 +157,7 @@ void shouldApplySubagentLifecycleHookOutputs() throws Exception { SubagentStartHookInput start = subagentStarts.element(); assertEquals(session.getSessionId(), start.sessionId()); assertTrue(start.timestamp() > 0); - assertEquals(ctx.getWorkDir().toAbsolutePath().normalize(), - Path.of(start.cwd()).toAbsolutePath().normalize()); + assertEquals(ctx.getWorkDir().toRealPath(), Path.of(start.cwd()).toRealPath()); assertNotNull(start.transcriptPath()); assertEquals("explore", start.agentName()); assertNull(start.agentDisplayName()); diff --git a/java/sdk/src/test/java/com/github/copilot/TestUtil.java b/java/sdk/src/test/java/com/github/copilot/TestUtil.java index 8e90c2f48f..b8ac48748e 100644 --- a/java/sdk/src/test/java/com/github/copilot/TestUtil.java +++ b/java/sdk/src/test/java/com/github/copilot/TestUtil.java @@ -95,7 +95,7 @@ static String preparePinnedCli(Path repoRoot) throws Exception { return output; } - private static String findExecutableInPath(String name) { + static String findExecutableInPath(String name) { var pathValue = System.getenv("PATH"); if (pathValue == null || pathValue.isEmpty()) { return null; diff --git a/java/sdk/src/test/java/com/github/copilot/TimeoutEdgeCaseTest.java b/java/sdk/src/test/java/com/github/copilot/TimeoutEdgeCaseTest.java index 01530bc25f..4eee8f5f97 100644 --- a/java/sdk/src/test/java/com/github/copilot/TimeoutEdgeCaseTest.java +++ b/java/sdk/src/test/java/com/github/copilot/TimeoutEdgeCaseTest.java @@ -4,7 +4,10 @@ package com.github.copilot; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; @@ -16,6 +19,8 @@ import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; @@ -84,10 +89,7 @@ public synchronized void flush() throws IOException { * After {@code close()}, the future returned by {@code sendAndWait} must NOT be * completed by a stale timeout. *

    - * Contract: {@code close()} shuts down the timeout scheduler before the - * blocking {@code session.detach} RPC call, so any pending timeout task is - * cancelled and the future remains incomplete (not exceptionally completed with - * {@code TimeoutException}). + * Closing fails the pending response with {@code IllegalStateException}. */ @Test void testTimeoutDoesNotFireAfterSessionClose() throws Exception { @@ -100,12 +102,12 @@ void testTimeoutDoesNotFireAfterSessionClose() throws Exception { assertFalse(result.isDone(), "Future should be pending before timeout fires"); - // close() blocks up to 5s on session.detach RPC. The 2s timeout - // fires during that window with the current per-call scheduler. session.close(); - assertFalse(result.isDone(), "Future should not be completed by a timeout after session is closed. " - + "The per-call ScheduledExecutorService leaked a TimeoutException."); + ExecutionException error = assertThrows(ExecutionException.class, + () -> result.get(5, TimeUnit.SECONDS)); + IllegalStateException failure = assertInstanceOf(IllegalStateException.class, error.getCause()); + assertEquals("Session closed before response completed", failure.getMessage()); } } finally { rpc.close(); diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java index 3eb27a9178..a7ede09723 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java @@ -63,164 +63,184 @@ void newRpcEndpoints_dispatch_expected_method_params_and_result_type(String expe } static Stream newRpcDispatches() { - return Stream.of( - dispatch("catalog.select", CatalogSelectionResult.class, """ - {"contract":{"protocolVersion":3,"requiredCapabilities":["catalog-selection"]}, - "sessionId":"catalog-session","selectionRef":"selection-1","outcome":"selected"} - """, - (server, session) -> server.catalog.select( - new CatalogSelectParams(new CatalogClientContract(3L, List.of("catalog-selection")), - "catalog-session", "selection-1", CatalogSelectionDecision.SELECTED))), - dispatch("session.managedSettings.get", SessionManagedSettingsGetResult.class, """ + return Stream + .of(dispatch("session.mcp.list", SessionMcpListResult.class, """ {"sessionId":"bound-session"} - """, (server, session) -> session.managedSettings.get()), - dispatch("session.instructions.reload", SessionInstructionsGetSourcesResult.class, """ - {"sessionId":"bound-session"} - """, (server, session) -> session.instructions.reload()), - dispatch("session.customizations.reload", CustomizationsReloadResult.class, """ - {"sessionId":"bound-session"} - """, (server, session) -> session.customizations.reload()), - dispatch("session.plugins.install", SessionPluginsInstallResult.class, """ - {"sessionId":"bound-session","source":"plugin@market"} - """, - (server, session) -> session.plugins - .install(new SessionPluginsInstallParams("foreign-session", "plugin@market"))), - dispatch("session.plugins.uninstall", Void.class, """ - {"sessionId":"bound-session","name":"plugin","directSourceId":"source-1"} - """, - (server, session) -> session.plugins - .uninstall(new SessionPluginsUninstallParams("foreign-session", "plugin", "source-1"))), - dispatch("session.plugins.update", SessionPluginsUpdateResult.class, """ - {"sessionId":"bound-session","name":"plugin@market"} - """, - (server, session) -> session.plugins - .update(new SessionPluginsUpdateParams("foreign-session", "plugin@market"))), - dispatch("session.plugins.enable", Void.class, """ - {"sessionId":"bound-session","names":["first@market","second@market"]} - """, - (server, session) -> session.plugins.enable(new SessionPluginsEnableParams("foreign-session", - List.of("first@market", "second@market")))), - dispatch("session.plugins.disable", Void.class, """ - {"sessionId":"bound-session","names":["plugin@market"]} - """, - (server, session) -> session.plugins - .disable(new SessionPluginsDisableParams("foreign-session", List.of("plugin@market")))), - dispatch("session.plugins.marketplaces.list", SessionPluginsMarketplacesListResult.class, """ - {"sessionId":"bound-session"} - """, (server, session) -> session.plugins.marketplaces.list()), - dispatch("session.plugins.marketplaces.add", SessionPluginsMarketplacesAddResult.class, """ - {"sessionId":"bound-session","source":"./market","workingDirectory":"/workspace"} - """, - (server, session) -> session.plugins.marketplaces.add( + """, (server, session) -> session.mcp.list()), + dispatch("session.mcp.listConfigured", SessionMcpListConfiguredResult.class, """ + {"sessionId":"bound-session"} + """, (server, session) -> session.mcp.listConfigured()), + dispatch("catalog.select", CatalogSelectionResult.class, """ + {"contract":{"protocolVersion":3,"requiredCapabilities":["catalog-selection"]}, + "sessionId":"catalog-session","selectionRef":"selection-1","outcome":"selected"} + """, + (server, session) -> server.catalog.select(new CatalogSelectParams( + new CatalogClientContract(3L, List.of("catalog-selection")), "catalog-session", + "selection-1", CatalogSelectionDecision.SELECTED))), + dispatch("session.managedSettings.get", SessionManagedSettingsGetResult.class, """ + {"sessionId":"bound-session"} + """, (server, session) -> session.managedSettings.get()), + dispatch("session.instructions.reload", SessionInstructionsGetSourcesResult.class, """ + {"sessionId":"bound-session"} + """, (server, session) -> session.instructions.reload()), + dispatch("session.customizations.reload", CustomizationsReloadResult.class, """ + {"sessionId":"bound-session"} + """, (server, session) -> session.customizations.reload()), + dispatch("session.plugins.install", SessionPluginsInstallResult.class, """ + {"sessionId":"bound-session","source":"plugin@market"} + """, + (server, session) -> session.plugins + .install(new SessionPluginsInstallParams("foreign-session", "plugin@market"))), + dispatch("session.plugins.uninstall", Void.class, """ + {"sessionId":"bound-session","name":"plugin","directSourceId":"source-1"} + """, + (server, session) -> session.plugins.uninstall( + new SessionPluginsUninstallParams("foreign-session", "plugin", "source-1"))), + dispatch("session.plugins.update", SessionPluginsUpdateResult.class, """ + {"sessionId":"bound-session","name":"plugin@market"} + """, + (server, session) -> session.plugins + .update(new SessionPluginsUpdateParams("foreign-session", "plugin@market"))), + dispatch("session.plugins.enable", Void.class, """ + {"sessionId":"bound-session","names":["first@market","second@market"]} + """, + (server, session) -> session.plugins.enable(new SessionPluginsEnableParams( + "foreign-session", List.of("first@market", "second@market")))), + dispatch("session.plugins.disable", Void.class, """ + {"sessionId":"bound-session","names":["plugin@market"]} + """, + (server, session) -> session.plugins.disable( + new SessionPluginsDisableParams("foreign-session", List.of("plugin@market")))), + dispatch("session.plugins.marketplaces.list", SessionPluginsMarketplacesListResult.class, """ + {"sessionId":"bound-session"} + """, (server, session) -> session.plugins.marketplaces.list()), + dispatch("session.plugins.marketplaces.add", SessionPluginsMarketplacesAddResult.class, """ + {"sessionId":"bound-session","source":"./market","workingDirectory":"/workspace"} + """, (server, session) -> session.plugins.marketplaces.add( new SessionPluginsMarketplacesAddParams("foreign-session", "./market", "/workspace"))), - dispatch("session.plugins.marketplaces.remove", SessionPluginsMarketplacesRemoveResult.class, """ - {"sessionId":"bound-session","name":"market","force":true} - """, - (server, session) -> session.plugins.marketplaces - .remove(new SessionPluginsMarketplacesRemoveParams("foreign-session", "market", true))), - dispatch("session.plugins.marketplaces.browse", SessionPluginsMarketplacesBrowseResult.class, """ - {"sessionId":"bound-session","name":"market"} - """, - (server, session) -> session.plugins.marketplaces - .browse(new SessionPluginsMarketplacesBrowseParams("foreign-session", "market"))), - dispatch("session.plugins.marketplaces.refresh", SessionPluginsMarketplacesRefreshResult.class, """ - {"sessionId":"bound-session","name":"market"} - """, - (server, session) -> session.plugins.marketplaces - .refresh(new SessionPluginsMarketplacesRefreshParams("foreign-session", "market"))), - dispatch("session.plugins.marketplaces.refresh", SessionPluginsMarketplacesRefreshResult.class, """ - {"sessionId":"bound-session"} - """, (server, session) -> session.plugins.marketplaces.refresh(null)), - dispatch("session.queue.withdrawMessage", SessionQueueWithdrawMessageResult.class, """ - {"sessionId":"bound-session","messageId":"message-1","expectedPrompt":"original"} - """, - (server, session) -> session.queue.withdrawMessage( - new SessionQueueWithdrawMessageParams("foreign-session", "message-1", "original"))), - dispatch("session.queue.appendSteering", SessionQueueAppendSteeringResult.class, """ - {"sessionId":"bound-session","messageId":"message-1","expectedPrompt":"original", - "agentMode":"interactive","prompt":"more","displayPrompt":"preview","attachments":[]} - """, (server, - session) -> session.queue.appendSteering(new SessionQueueAppendSteeringParams("foreign-session", - "message-1", "original", SendAgentMode.INTERACTIVE, "more", "preview", List.of()))), - dispatch("session.workflow.run", SessionWorkflowRunResult.class, """ - {"sessionId":"bound-session","name":"workflow-1","args":{"topic":"input"}} - """, - (server, session) -> session.workflow.run(new SessionWorkflowRunParams("foreign-session", - "workflow-1", Map.of("topic", "input"), null))), - dispatch("session.workflow.resume", SessionWorkflowResumeResult.class, """ - {"sessionId":"bound-session","runId":"run-1","notifyOnComplete":true,"logPhaseNames":false} - """, - (server, session) -> session.workflow.resume( - new SessionWorkflowResumeParams("foreign-session", "run-1", null, true, false))), - dispatch("session.workflow.runFromTool", WorkflowRunResult.class, """ - {"sessionId":"bound-session","name":"workflow-1","args":{"topic":"input"},"toolCallId":"tool-1"} - """, - (server, session) -> session.workflow.runFromTool(new SessionWorkflowRunFromToolParams( - "foreign-session", "workflow-1", Map.of("topic", "input"), null, "tool-1"))), - dispatch("session.workflow.resumeFromTool", WorkflowResumeResult.class, """ - {"sessionId":"bound-session","runId":"run-1","toolCallId":"tool-1"} - """, - (server, session) -> session.workflow.resumeFromTool( + dispatch("session.plugins.marketplaces.remove", SessionPluginsMarketplacesRemoveResult.class, + """ + {"sessionId":"bound-session","name":"market","force":true} + """, + (server, session) -> session.plugins.marketplaces.remove( + new SessionPluginsMarketplacesRemoveParams("foreign-session", "market", true))), + dispatch("session.plugins.marketplaces.browse", SessionPluginsMarketplacesBrowseResult.class, + """ + {"sessionId":"bound-session","name":"market"} + """, + (server, session) -> session.plugins.marketplaces.browse( + new SessionPluginsMarketplacesBrowseParams("foreign-session", "market"))), + dispatch("session.plugins.marketplaces.refresh", SessionPluginsMarketplacesRefreshResult.class, + """ + {"sessionId":"bound-session","name":"market"} + """, + (server, session) -> session.plugins.marketplaces.refresh( + new SessionPluginsMarketplacesRefreshParams("foreign-session", "market"))), + dispatch( + "session.plugins.marketplaces.refresh", SessionPluginsMarketplacesRefreshResult.class, + """ + {"sessionId":"bound-session"} + """, (server, session) -> session.plugins.marketplaces.refresh(null)), + dispatch("session.queue.withdrawMessage", SessionQueueWithdrawMessageResult.class, """ + {"sessionId":"bound-session","messageId":"message-1","expectedPrompt":"original"} + """, (server, + session) -> session.queue.withdrawMessage(new SessionQueueWithdrawMessageParams( + "foreign-session", "message-1", "original"))), + dispatch("session.queue.appendSteering", SessionQueueAppendSteeringResult.class, """ + {"sessionId":"bound-session","messageId":"message-1","expectedPrompt":"original", + "agentMode":"interactive","prompt":"more","displayPrompt":"preview","attachments":[]} + """, + (server, session) -> session.queue.appendSteering( + new SessionQueueAppendSteeringParams("foreign-session", "message-1", "original", + SendAgentMode.INTERACTIVE, "more", "preview", List.of()))), + dispatch("session.workflow.run", SessionWorkflowRunResult.class, """ + {"sessionId":"bound-session","name":"workflow-1","args":{"topic":"input"}} + """, + (server, session) -> session.workflow.run(new SessionWorkflowRunParams( + "foreign-session", "workflow-1", Map.of("topic", "input"), null))), + dispatch( + "session.workflow.resume", SessionWorkflowResumeResult.class, + """ + {"sessionId":"bound-session","runId":"run-1","notifyOnComplete":true,"logPhaseNames":false} + """, + (server, session) -> session.workflow.resume(new SessionWorkflowResumeParams( + "foreign-session", "run-1", null, true, false))), + dispatch( + "session.workflow.runFromTool", WorkflowRunResult.class, + """ + {"sessionId":"bound-session","name":"workflow-1","args":{"topic":"input"},"toolCallId":"tool-1"} + """, + (server, session) -> session.workflow.runFromTool(new SessionWorkflowRunFromToolParams( + "foreign-session", "workflow-1", Map.of("topic", "input"), null, "tool-1"))), + dispatch("session.workflow.resumeFromTool", WorkflowResumeResult.class, """ + {"sessionId":"bound-session","runId":"run-1","toolCallId":"tool-1"} + """, (server, session) -> session.workflow.resumeFromTool( new SessionWorkflowResumeFromToolParams("foreign-session", "run-1", null, "tool-1"))), - dispatch("session.workflow.getRun", SessionWorkflowGetRunResult.class, """ - {"sessionId":"bound-session","runId":"run-1"} - """, - (server, session) -> session.workflow - .getRun(new SessionWorkflowGetRunParams("foreign-session", "run-1"))), - dispatch("session.workflow.listRuns", SessionWorkflowListRunsResult.class, """ - {"sessionId":"bound-session","afterSeq":2,"limit":5} - """, - (server, session) -> session.workflow - .listRuns(new SessionWorkflowListRunsParams("foreign-session", 2L, null, 5L))), - dispatch("session.workflow.getRunDetail", SessionWorkflowGetRunDetailResult.class, """ - {"sessionId":"bound-session","runId":"run-1"} - """, - (server, session) -> session.workflow - .getRunDetail(new SessionWorkflowGetRunDetailParams("foreign-session", "run-1"))), - dispatch("session.workflow.getRunProgress", SessionWorkflowGetRunProgressResult.class, """ - {"sessionId":"bound-session","runId":"run-1","phaseId":"phase-1","afterSeq":2,"limit":5} - """, - (server, session) -> session.workflow.getRunProgress(new SessionWorkflowGetRunProgressParams( - "foreign-session", "run-1", "phase-1", 2L, null, 5L))), - dispatch("session.workflow.cancel", SessionWorkflowCancelResult.class, """ - {"sessionId":"bound-session","runId":"run-1"} - """, - (server, session) -> session.workflow - .cancel(new SessionWorkflowCancelParams("foreign-session", "run-1"))), - dispatch("session.workflow.pause", SessionWorkflowPauseResult.class, """ - {"sessionId":"bound-session","runId":"run-1"} - """, - (server, session) -> session.workflow - .pause(new SessionWorkflowPauseParams("foreign-session", "run-1"))), - dispatch("session.workflow.pauseAtCheckpoint", WorkflowPauseCheckpointResult.class, - """ - {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"checkpoint-1"} + dispatch("session.workflow.getRun", SessionWorkflowGetRunResult.class, """ + {"sessionId":"bound-session","runId":"run-1"} + """, + (server, session) -> session.workflow + .getRun(new SessionWorkflowGetRunParams("foreign-session", "run-1"))), + dispatch("session.workflow.listRuns", SessionWorkflowListRunsResult.class, """ + {"sessionId":"bound-session","afterSeq":2,"limit":5} + """, + (server, session) -> session.workflow + .listRuns(new SessionWorkflowListRunsParams("foreign-session", 2L, null, 5L))), + dispatch("session.workflow.getRunDetail", SessionWorkflowGetRunDetailResult.class, """ + {"sessionId":"bound-session","runId":"run-1"} """, - (server, session) -> session.workflow - .pauseAtCheckpoint(new SessionWorkflowPauseAtCheckpointParams("foreign-session", - "run-1", "execution-1", "checkpoint-1"))), - dispatch("session.workflow.log", Void.class, """ - {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1", - "lines":[{"seq":1,"kind":"log","text":"progress"}]} - """, (server, - session) -> session.workflow.log(new SessionWorkflowLogParams("foreign-session", "run-1", - "execution-1", List.of(new WorkflowLogLine(1L, WorkflowLogLineKind.LOG, "progress"))))), - dispatch("session.workflow.agent", SessionWorkflowAgentResult.class, - """ - {"sessionId":"bound-session","workflowRunId":"run-1","executionToken":"execution-1","prompt":"inspect"} + (server, session) -> session.workflow.getRunDetail( + new SessionWorkflowGetRunDetailParams("foreign-session", "run-1"))), + dispatch("session.workflow.getRunProgress", SessionWorkflowGetRunProgressResult.class, """ + {"sessionId":"bound-session","runId":"run-1","phaseId":"phase-1","afterSeq":2,"limit":5} + """, (server, + session) -> session.workflow.getRunProgress(new SessionWorkflowGetRunProgressParams( + "foreign-session", "run-1", "phase-1", 2L, null, 5L))), + dispatch("session.workflow.cancel", SessionWorkflowCancelResult.class, """ + {"sessionId":"bound-session","runId":"run-1"} """, - (server, session) -> session.workflow.agent(new SessionWorkflowAgentParams("foreign-session", - "run-1", "execution-1", "inspect", null))), - dispatch("session.workflow.journal.get", SessionWorkflowJournalGetResult.class, """ - {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"journal-1"} - """, - (server, session) -> session.workflow.journal.get(new SessionWorkflowJournalGetParams( - "foreign-session", "run-1", "execution-1", "journal-1"))), - dispatch("session.workflow.journal.put", Void.class, """ - {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"journal-1", - "resultJson":{"answer":42}} - """, (server, session) -> session.workflow.journal.put(new SessionWorkflowJournalPutParams( - "foreign-session", "run-1", "execution-1", "journal-1", Map.of("answer", 42))))); + (server, session) -> session.workflow + .cancel(new SessionWorkflowCancelParams("foreign-session", "run-1"))), + dispatch("session.workflow.pause", SessionWorkflowPauseResult.class, """ + {"sessionId":"bound-session","runId":"run-1"} + """, + (server, session) -> session.workflow + .pause(new SessionWorkflowPauseParams("foreign-session", "run-1"))), + dispatch( + "session.workflow.pauseAtCheckpoint", WorkflowPauseCheckpointResult.class, + """ + {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"checkpoint-1"} + """, + (server, session) -> session.workflow + .pauseAtCheckpoint(new SessionWorkflowPauseAtCheckpointParams("foreign-session", + "run-1", "execution-1", "checkpoint-1"))), + dispatch("session.workflow.log", Void.class, """ + {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1", + "lines":[{"seq":1,"kind":"log","text":"progress"}]} + """, + (server, session) -> session.workflow.log(new SessionWorkflowLogParams( + "foreign-session", "run-1", "execution-1", + List.of(new WorkflowLogLine(1L, WorkflowLogLineKind.LOG, "progress"))))), + dispatch("session.workflow.agent", SessionWorkflowAgentResult.class, + """ + {"sessionId":"bound-session","workflowRunId":"run-1","executionToken":"execution-1","prompt":"inspect"} + """, + (server, session) -> session.workflow.agent(new SessionWorkflowAgentParams( + "foreign-session", "run-1", "execution-1", "inspect", null))), + dispatch("session.workflow.journal.get", SessionWorkflowJournalGetResult.class, + """ + {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"journal-1"} + """, + (server, session) -> session.workflow.journal.get(new SessionWorkflowJournalGetParams( + "foreign-session", "run-1", "execution-1", "journal-1"))), + dispatch("session.workflow.journal.put", Void.class, + """ + {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"journal-1", + "resultJson":{"answer":42}} + """, + (server, session) -> session.workflow.journal + .put(new SessionWorkflowJournalPutParams("foreign-session", "run-1", + "execution-1", "journal-1", Map.of("answer", 42))))); } private static Arguments dispatch(String method, Class resultType, String expectedParams, @@ -371,7 +391,7 @@ void sessionRpc_mode_set_merges_sessionId() { var session = new SessionRpc(stub, "sess-mode-set"); var modeParams = new SessionModeSetParams(null, null, null, null, null, null, null, null, null, null, null, - null, null); + null, null, null); session.mode.set(modeParams); assertEquals(1, stub.calls.size()); @@ -551,8 +571,9 @@ void sessionRpc_mcp_list_injects_sessionId() { assertEquals(1, stub.calls.size()); assertEquals("session.mcp.list", stub.calls.get(0).method()); - var params = (Map) stub.calls.get(0).params(); - assertEquals("sess-mcp", params.get("sessionId")); + var mapper = new ObjectMapper(); + assertEquals(mapper.valueToTree(Map.of("sessionId", "sess-mcp")), + mapper.valueToTree(stub.calls.get(0).params())); } @Test diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java index dace015cff..348a2d9f1a 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -334,12 +334,6 @@ void sessionMcpEnableParams_record() { assertEquals("mcp-server-2", params.serverName()); } - @Test - void sessionMcpListParams_record() { - var params = new SessionMcpListParams("sess-27"); - assertEquals("sess-27", params.sessionId()); - } - @Test void sessionMcpReloadParams_record() { var params = new SessionMcpReloadParams("sess-28"); @@ -355,7 +349,7 @@ void sessionModeGetParams_record() { @Test void sessionModeSetParams_record() { var params = new SessionModeSetParams("sess-30", SessionMode.PLAN, null, null, null, null, null, null, null, - null, null, null, null); + null, null, null, null, null); assertEquals("sess-30", params.sessionId()); assertEquals(SessionMode.PLAN, params.mode()); } @@ -475,6 +469,7 @@ void sessionModelSwitchParams_distinguishRequiredNullFromOmittedOptionalValue() var switchTo = mapper.valueToTree(new SessionModelSwitchToParams("sess-32", "auto", null, null, null, null, null, null, null, null, null, null, null, null, null, null)); assertFalse(switchTo.has("autoTier")); + assertFalse(switchTo.has("providerId")); } @Test @@ -897,8 +892,8 @@ void sessionMcpListResult_status_enum_all_values() { @Test void sessionModelGetCurrentResult_record() { - var result = new SessionModelGetCurrentResult("claude-sonnet-5", "claude-sonnet-4.5", null, null, null, null, - null); + var result = new SessionModelGetCurrentResult("claude-sonnet-5", null, "claude-sonnet-4.5", null, null, null, + null, null, null); assertEquals("claude-sonnet-5", result.modelId()); assertEquals("claude-sonnet-4.5", result.planBaseModelId()); } @@ -1009,8 +1004,8 @@ void sessionUiHandlePendingElicitationResult_record() { @Test void sessionUsageGetMetricsResult_nested() { var changes = new UsageMetricsCodeChanges(100L, 50L, 5L, null); - var result = new SessionUsageGetMetricsResult(0.5, 10L, null, null, 2000L, null, changes, null, null, "gpt-5", - 1000L, 500L); + var result = new SessionUsageGetMetricsResult(null, 0.5, 10L, null, null, 2000L, null, changes, null, null, + null, "gpt-5", 1000L, 500L); assertEquals(0.5, result.totalPremiumRequestCost()); assertEquals(10L, result.totalUserRequests()); assertNotNull(result.codeChanges()); @@ -1070,8 +1065,8 @@ void modelsListResult_nested() { var policy = new ModelPolicy(ModelPolicyState.ENABLED, null); var promo = new ModelBillingPromo("summer-2026", 25.0, "2026-08-01T00:00:00Z", "Summer discount", true); var billing = new ModelBilling(1.0, null, null, promo); - var modelItem = new Model("gpt-5", "GPT-5", capabilities, null, policy, billing, null, null, null, null, null, - null, null, null, null); + var modelItem = new Model("gpt-5", "GPT-5", capabilities, null, null, policy, billing, null, null, null, null, + null, null, null, null, null); var result = new ModelsListResult(List.of(modelItem)); assertEquals(1, result.models().size()); diff --git a/java/sdk/src/test/prompts/PROMPT-smoke-test.md b/java/sdk/src/test/prompts/PROMPT-smoke-test.md deleted file mode 100644 index 4013002aca..0000000000 --- a/java/sdk/src/test/prompts/PROMPT-smoke-test.md +++ /dev/null @@ -1,135 +0,0 @@ -# Prompt: Generate and Run the copilot-sdk-java Smoke Test - -## Objective - -Create a Maven project that acts as a smoke test for `copilot-sdk-java`. The project must compile, build, and run to completion with exit code 0 as the definition of success. - -## Step 1 — Read the source README - -Read the file `README.md` at the top level of this repository. You will need two sections from it: - -- **"Snapshot Builds"** — provides the Maven GAV (groupId, artifactId, version) and the Maven Central Snapshots repository configuration to use for the dependency under test. -- **"Quick Start"** — provides the exact Java source code for the smoke test program. Use this code verbatim. Do not modify it, fix it, or improve it. If it does not compile or run correctly against the artifact under test, that is itself a smoke test failure and must be reported as such rather than silently corrected. - -## Step 2 — Create the Maven project - -Create the following file layout in a subdirectory named `smoke-test/` at the top level of this repository: - -``` -smoke-test/ - pom.xml - src/main/java/(Class name taken from the code in the "Quick Start" section in the README).java ← verbatim from README "Quick Start" -``` - -### `pom.xml` requirements - -- **groupId**: `com.github` (or any reasonable value) -- **artifactId**: `copilot-sdk-smoketest` -- **version**: `1.0-SNAPSHOT` -- **packaging**: `jar` -- **Java source/target**: (taken from the "Requirements" section in the README) (via `maven.compiler.source` and `maven.compiler.target` properties) -- **`mainClass` property**: (taken from the "Quick Start" section in the README) (the class is in the default package) - -#### Snapshot repository - -Configure the Maven Central Snapshots repository exactly as specified in the "Snapshot Builds" section of `README.md`, and add `always` inside the `` block so that every build fetches the latest snapshot without requiring `-U`: - -```xml - - central-snapshots - https://central.sonatype.com/repository/maven-snapshots/ - - true - always - - -``` - -#### Dependency - -Use the GAV from the "Snapshot Builds" section of `README.md` verbatim — do not substitute the release version from the "Maven" section. - -#### Plugins — REQUIRED configuration - -**Do not use `maven-shade-plugin`.** Use the `Class-Path` manifest approach instead: - -1. **`maven-jar-plugin`** (version **3.4.1** — pin explicitly to suppress Maven version warnings): - - ```xml - - org.apache.maven.plugins - maven-jar-plugin - 3.4.1 - - - - ${mainClass} - true - lib/ - false - - - - - ``` - - **Critical**: `false` is mandatory. Without it, the manifest `Class-Path:` entry uses the timestamped SNAPSHOT filename (e.g. `copilot-sdk-java-0.1.33-20260312.125508-3.jar`) while `copy-dependencies` writes the base SNAPSHOT filename (`copilot-sdk-java-0.1.33-SNAPSHOT.jar`), causing `NoClassDefFoundError` at runtime. - -2. **`maven-dependency-plugin`** (version **3.6.1**): - - ```xml - - org.apache.maven.plugins - maven-dependency-plugin - 3.6.1 - - - copy-dependencies - package - copy-dependencies - - ${project.build.directory}/lib - - - - - ``` - - This copies all runtime dependency JARs into `target/lib/`, which is where the manifest `Class-Path:` points. - -## Step 3 — Build - -```bash -mvn -U clean package -``` - -The `-U` flag forces a fresh snapshot metadata check regardless of local cache. The `always` already handles this for normal invocations, but `-U` is the safest choice for CI. - -Build must succeed with `BUILD SUCCESS` before proceeding. - -## Step 4 — Run - -```bash -java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar -``` - -The JAR must be run from the `smoke-test/` directory so that the relative `lib/` path in the manifest resolves correctly. Do not use `-cp` or `-classpath` — the test specifically validates that `java -jar` works with the manifest `Class-Path:` approach. - -## Step 5 — Verify success - -The smoke test passes if and only if the process exits with code **0**. - -The "Quick Start" code in `README.md` already contains the exit-code logic: it captures the last assistant message and calls `System.exit(0)` if it contains `"4"` (the expected answer to "What is 2+2?"), or `System.exit(-1)` otherwise. - -Check the exit code: -```bash -echo "Exit code: $?" -``` - -Expected: `Exit code: 0` - -## Important API notes (do not apply these as fixes — they are here for diagnostic context only) - -If the build fails with compilation errors such as `cannot find symbol` on methods like `getContent()`, `getCurrentTokens()`, `getTokenLimit()`, or `getMessagesLength()`, this indicates a mismatch between the Quick Start code and the SDK implementation. **Do not silently fix the code.** Report the failure. The purpose of this smoke test is precisely to catch such regressions. - -For reference: the data classes in `copilot-sdk-java` are Java **records**. Record accessor methods have no `get` prefix — they are named `content()`, `currentTokens()`, `tokenLimit()`, and `messagesLength()`. If the README Quick Start uses `getContent()` etc., that is a bug in the README that must be surfaced, not silently corrected. diff --git a/nodejs/README.md b/nodejs/README.md index 1135d7ea08..6aab91b3c9 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -345,6 +345,11 @@ This graceful-exit timeout is separate from the shutdown RPC and post-terminatio ##### `forceStop(): Promise` Force stop the CLI server without graceful cleanup. Use when `stop()` takes too long. +Retained sessions are disconnected locally, and pending `sendAndWait()` calls reject +without waiting for `session.idle` or their response timeout. +Retirement releases session-owned tool, hook, auth/input, provider, workflow, canvas, +and transform registrations without changing caller-owned configuration. Late +registration and callback dispatch are rejected; callbacks already admitted may finish. ##### `createSession(config?: SessionConfig): Promise` @@ -369,6 +374,7 @@ Create a new conversation session. - `askUserVariant?: "legacy" | "elicitation"` - Selects the model-facing `ask_user` tool shape when creating or cold-resuming a session. Defaults to `"legacy"`; use `"elicitation"` with `onElicitationRequest`. - `onElicitationRequest?: ElicitationHandler` - Handler for elicitation requests dispatched by the server. Enables this client to present form-based UI dialogs on behalf of the agent or other session participants. See [Elicitation Requests](#elicitation-requests) section. - `hooks?: SessionHooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. +- `skillProvider?: SkillProvider` - **Experimental.** Serves skills from host storage instead of skill directories. Not persisted; pass it again on resume. See [Skill providers](#skill-providers-experimental). ```typescript const session = await client.createSession({ @@ -1120,6 +1126,46 @@ const session = await client.createSession({ }); ``` +### Skill providers (experimental) + +A skill provider serves skills from your own storage, such as a database, instead of `SKILL.md` +files on disk. Provider skills join the session's skill catalog and load on demand through the +`skill` tool, just like file-based skills. + +```typescript +import type { SkillProvider } from "@github/copilot-sdk"; + +const skillProvider: SkillProvider = { + listSkills: async () => [ + { name: "release-notes", description: "Writes release notes in the team's format." }, + ], + readSkill: async (name, { signal }) => (await db.findSkill(name, { signal }))?.markdown ?? null, +}; + +const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider, +}); +``` + +- `listSkills()` returns the catalog metadata. `readSkill(name)` returns the skill's markdown, or + `null`/`undefined` if the skill no longer exists. The markdown may omit YAML frontmatter; when + frontmatter is present, its fields must agree with the listed metadata, and `allowed-tools` is + read only from frontmatter. +- Each call receives `{ signal }`, an `AbortSignal` that fires when the runtime cancels the call, + for example after its 30-second limit or when the session disconnects. It doesn't fire when the + connection closes or the client is force-stopped; a running call then continues until it returns. +- The provider is never persisted. Pass it again to `resumeSession`; resuming without it unbinds + the provider. +- A provider enables skills unless you set `enableSkills: false`, which keeps it bound but unused. + In `mode: "empty"`, skills stay disabled until you set `enableSkills: true`. +- Errors thrown by the provider are reported to the model as a generic load failure; their + messages are not forwarded. +- The runtime may call the provider concurrently, so both methods must be safe for concurrent use. +- Skill providers are not supported for cloud sessions. + +See [Custom skills](../docs/features/skills.md#skill-providers-experimental) for limits and details. + ### Multiple Sessions ```typescript diff --git a/nodejs/docs/workflows.md b/nodejs/docs/workflows.md index 872cbcb0fe..7f2985f614 100644 --- a/nodejs/docs/workflows.md +++ b/nodejs/docs/workflows.md @@ -186,7 +186,7 @@ session.workflow.resume( Set `notifyOnComplete` to `true` for workflows that are likely to be invoked by an agent, so the originating session is notified when the workflow completes. Set it to `false` for workflows intended to be invoked programmatically, where the caller awaits the result directly. Set `logPhaseNames` to emit workflow phase names to the session transcript. Both options apply to new and resumed runs. -Both resolve with the run envelope (`WorkflowRunResult`) for **every** outcome—`completed`, `error`, `halted`, `paused`, and `cancelled` alike. Inspect `status` and read `result` only when the run completed; a limit breach carries a typed `failure`. A `paused` envelope means that the current attempt settled, not that the durable run is permanently finished. Resume the same run ID to start another attempt with its journal and accounting intact. SDK-initiated `run` and `resume` do not request permission, so they have no declined outcome. An SDK-initiated run is refused only when the session already has its maximum number of active top-level runs. Pre-execution resume failures throw `WorkflowResumeError`, whose `code` is one of `not_found`, `non_resumable`, `workflow_run_not_resumable`, `already_active`, `workflow_already_running`, `workflow_limits_invalid`, `workflow_session_disposed`, `workflow_storage_unavailable`, or `workflow_storage_corrupt`. +Both resolve with the run envelope (`WorkflowRunResult`) for **every** outcome—`completed`, `error`, `halted`, `paused`, and `cancelled` alike. Inspect `status` and read `result` only when the run completed; a limit breach carries a typed `failure`. A `paused` envelope means that the current attempt settled, not that the durable run is permanently finished. Resume the same run ID to start another attempt with its journal and accounting intact. SDK-initiated `run` and `resume` do not request permission, so they have no declined outcome. They still enforce execution eligibility, input validation, and active-run limits. Recognized pre-execution resume failures throw `WorkflowResumeError`, whose `code` is one of `not_found`, `non_resumable`, `workflow_run_not_resumable`, `already_active`, `workflow_already_running`, `workflow_limits_invalid`, `workflow_session_disposed`, `workflow_storage_unavailable`, or `workflow_storage_corrupt`. Pause a running attempt from outside its workflow body: @@ -206,7 +206,13 @@ The first attempt pauses at `"review-ready"` and ends through cooperative cancel ## Observe a run -The calling session can inspect its own workflow runs: +The calling session can inspect its own workflow runs independently of its current execution eligibility. An empty registry enumerates successfully; losing eligibility does not hide already-admitted runs or terminal history. Observation still requires a valid, non-disposed session, and validation, missing-run, and storage errors still propagate. + +Workflow storage requires SQLite support from a custom session filesystem. A provider without that capability reports `workflow_storage_unavailable`, not an empty registry or a local-storage fallback. + +Compaction waits at most five seconds for its optional active-workflow summary. If storage fails or times out, compaction logs a warning and continues without that summary; aborting compaction cancels the wait. This does not change the results or error handling of explicit workflow observation calls. + +Observation does not restore execution eligibility. Starting, resuming, pausing, or cancelling runs, and workflow-owned agent, log, and journal/checkpoint operations still require eligible credentials (token-based billing or trusted HMAC authentication). Ineligible execution requests fail with JSON-RPC `-32601` and `data.code: "dynamic_workflows_unavailable"`. ```ts const runs = await session.workflow.listRuns(); diff --git a/nodejs/package.json b/nodejs/package.json index e6b1e9992c..6fd3860f14 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/github/copilot-sdk.git" }, "version": "0.0.0-dev", - "copilotCliVersion": "1.0.92-4", + "copilotCliVersion": "1.0.94-4", "description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC", "main": "./dist/cjs/index.js", "types": "./dist/index.d.ts", diff --git a/nodejs/scripts/package-release.ts b/nodejs/scripts/package-release.ts index 90ad30615c..7cd973a833 100644 --- a/nodejs/scripts/package-release.ts +++ b/nodejs/scripts/package-release.ts @@ -22,6 +22,7 @@ import { export const JAVA_CLASSIFIERS = { "linux-arm64": "linux.arm64", "linuxmusl-x64": "linuxmusl.x64", + "linuxmusl-arm64": "linuxmusl.arm64", "win32-x64": "win32", "win32-arm64": "win32.arm64", "darwin-arm64": "darwin", diff --git a/nodejs/scripts/publish-release.ts b/nodejs/scripts/publish-release.ts index 292926d409..4a164d7e92 100644 --- a/nodejs/scripts/publish-release.ts +++ b/nodejs/scripts/publish-release.ts @@ -24,6 +24,13 @@ type NativeCommand = ( env?: NodeJS.ProcessEnv ) => string; +type GithubRelease = { + tag_name: string; + draft: boolean; + prerelease: boolean; + published_at: string | null; +}; + const runNativeCommand: NativeCommand = (command, args, cwd, env = process.env) => execFileSync(command, args, { cwd, @@ -232,12 +239,7 @@ export async function publishJava( } export function previousSdkReleaseTag( - releases: { - tag_name: string; - draft: boolean; - prerelease: boolean; - published_at: string | null; - }[], + releases: GithubRelease[], identity: ReleaseIdentity ): string | undefined { return releases @@ -321,9 +323,24 @@ export async function publishSourceRelease( assert.equal(release.draft, false, "Existing SDK release is a draft"); return; } - const releases = JSON.parse( - gh(["api", "--paginate", "--slurp", `repos/${repository}/releases?per_page=100`]) - ).flat(); + let releases: GithubRelease[]; + if (identity.channel === "latest") { + try { + releases = [JSON.parse(gh(["api", `repos/${repository}/releases/latest`]))]; + } catch (error) { + if (!/\(HTTP 404\)/.test(String((error as { stderr?: string }).stderr))) throw error; + releases = []; + } + } else { + const output = gh([ + "api", + "--paginate", + "--jq", + ".[] | {tag_name, draft, prerelease, published_at}", + `repos/${repository}/releases?per_page=100`, + ]); + releases = output ? output.split("\n").map((line) => JSON.parse(line)) : []; + } const previous = previousSdkReleaseTag(releases, identity); gh([ "release", diff --git a/nodejs/scripts/unified-release.ts b/nodejs/scripts/unified-release.ts index 56b1b2ddae..5e1477ceb5 100644 --- a/nodejs/scripts/unified-release.ts +++ b/nodejs/scripts/unified-release.ts @@ -91,13 +91,12 @@ export function publicationSdks( destination !== "public" || visibility === "public", "Internal releases cannot publish publicly" ); - // Python publishes in the caller so PyPI authentication and attestations share one workflow identity. - return packagedSdks(channel, visibility).filter( - (language) => - language !== "python" && - (destination === "public" || - language === "nodejs" || - (channel !== "unstable" && language === "dotnet")) + // Public Python publishes in the caller so PyPI authentication and attestations share one workflow identity. + return packagedSdks(channel, visibility).filter((language) => + destination === "public" + ? language !== "python" + : language === "nodejs" || + (channel !== "unstable" && (language === "dotnet" || language === "python")) ); } diff --git a/nodejs/src/cliVersion.ts b/nodejs/src/cliVersion.ts index 93029c8524..c239e98f25 100644 --- a/nodejs/src/cliVersion.ts +++ b/nodejs/src/cliVersion.ts @@ -1,3 +1,3 @@ -export const COPILOT_CLI_VERSION = "1.0.92-4"; +export const COPILOT_CLI_VERSION = "1.0.94-4"; export const COPILOT_CLI_USE_NPM_PACKAGE = false; diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 24f7f6ff2e..936d5feeec 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1662,6 +1662,9 @@ export class CopilotClient { if (config.gitHubToken !== undefined && config.gitHubTokenProvider !== undefined) { throw new Error("gitHubToken and gitHubTokenProvider are mutually exclusive"); } + if (config.cloud != null && config.skillProvider) { + throw new Error("Skill providers are not supported for cloud sessions."); + } if (!this.connection) { await this.start(); } @@ -1745,12 +1748,17 @@ export class CopilotClient { if (config.hooks) { s.registerHooks(config.hooks); } + if (config.skillProvider) { + s.registerSkillProvider(config.skillProvider); + } if (transformCallbacks) { s.registerTransformCallbacks(transformCallbacks); } if (config.onEvent) { s.on(config.onEvent); } + // Session-FS setup can throw, so finish it before the session + // becomes routable; a failed open must not leave handlers behind. this.setupSessionFs(s, config); this.sessions.set(sessionId, s); return s; @@ -1862,6 +1870,7 @@ export class CopilotClient { enableSessionStore: config.enableSessionStore, enableSkills: config.enableSkills, skillDirectories: config.skillDirectories, + ...(config.skillProvider ? { hasSkillProvider: true } : {}), pluginDirectories: config.pluginDirectories, instructionDirectories: config.instructionDirectories, disabledSkills: config.disabledSkills, @@ -1875,6 +1884,7 @@ export class CopilotClient { featureFlags: config.featureFlags, expAssignments: config.expAssignments, enableManagedSettings: config.enableManagedSettings, + enforceManagedModelDefaults: config.enforceManagedModelDefaults, managedSettings: config.managedSettings, }); @@ -2041,6 +2051,9 @@ export class CopilotClient { if (config.hooks) { session.registerHooks(config.hooks); } + if (config.skillProvider) { + session.registerSkillProvider(config.skillProvider); + } const modeDefaults = this.configDefaultsForMode(); config = { ...modeDefaults, ...config }; @@ -2057,10 +2070,13 @@ export class CopilotClient { if (config.onEvent) { session.on(config.onEvent); } + // Validation and session-FS setup can throw, so finish them before the + // replacement session becomes routable over any resident one. + const toolFilterOptions = this.resolveToolFilterOptions(config); this.setupSessionFs(session, config); + const replacedSession = this.sessions.get(sessionId); this.sessions.set(sessionId, session); - const toolFilterOptions = this.resolveToolFilterOptions(config); const gitHubTokenProviderRegistrationId = this.registerGitHubTokenProvider( config.gitHubTokenProvider, sessionId @@ -2159,6 +2175,7 @@ export class CopilotClient { defaultAgent: config.defaultAgent, agent: config.agent, skillDirectories: config.skillDirectories, + ...(config.skillProvider ? { hasSkillProvider: true } : {}), pluginDirectories: config.pluginDirectories, instructionDirectories: config.instructionDirectories, disabledSkills: config.disabledSkills, @@ -2174,6 +2191,7 @@ export class CopilotClient { featureFlags: config.featureFlags, expAssignments: config.expAssignments, enableManagedSettings: config.enableManagedSettings, + enforceManagedModelDefaults: config.enforceManagedModelDefaults, managedSettings: config.managedSettings, ...(extensionOptions?.requestedEnvironmentVariables ? { @@ -2222,7 +2240,15 @@ export class CopilotClient { this.commitGitHubTokenProvider(sessionId, gitHubTokenProviderRegistrationId); } catch (e) { session._markDisconnected(); - this.sessions.delete(sessionId); + // The runtime keeps the resident session's bindings when a resume + // fails, so its callbacks must keep reaching the replaced session. + if (this.sessions.get(sessionId) === session) { + if (replacedSession) { + this.sessions.set(sessionId, replacedSession); + } else { + this.sessions.delete(sessionId); + } + } if (gitHubTokenProviderRegistrationId !== undefined) { this.githubTokenProviders.delete(gitHubTokenProviderRegistrationId); } @@ -3513,6 +3539,23 @@ export class CopilotClient { } ); + // Internal experimental callbacks backing `SessionConfigBase.skillProvider`. + this.connection.onRequest( + "skillProvider.list", + async (params: { sessionId: string }, token: CancellationToken) => + await this.resolveSkillProviderSession(params)._handleSkillProviderList(token) + ); + this.connection.onRequest( + "skillProvider.read", + async (params: { sessionId: string; name: string }, token: CancellationToken) => { + const session = this.resolveSkillProviderSession(params); + if (typeof params.name !== "string") { + throw new Error("Invalid skillProvider.read payload"); + } + return await session._handleSkillProviderRead(params.name, token); + } + ); + const connection = this.connection; const messageWriter = this.messageWriter; const cliProcess = this.isExternalServer ? null : this.cliProcess; @@ -3736,6 +3779,17 @@ export class CopilotClient { return { response }; } + private resolveSkillProviderSession(params: { sessionId: string }): CopilotSession { + if (!params || typeof params.sessionId !== "string") { + throw new Error("Invalid skillProvider payload"); + } + const session = this.sessions.get(params.sessionId); + if (!session) { + throw new Error(`Session not found: ${params.sessionId}`); + } + return session; + } + private async handleHooksInvoke(params: { sessionId: string; hookType: string; diff --git a/nodejs/src/extension.ts b/nodejs/src/extension.ts index e7bb21e3b2..99a1c0a3be 100644 --- a/nodejs/src/extension.ts +++ b/nodejs/src/extension.ts @@ -24,7 +24,7 @@ export { export type JoinSessionConfig = Omit< ResumeSessionConfig, - "onPermissionRequest" | "extensionSdkPath" + "onPermissionRequest" | "extensionSdkPath" | "skillProvider" > & { onPermissionRequest?: PermissionHandler; /** @@ -124,16 +124,21 @@ export async function joinSession(config: JoinSessionConfig = {}): Promise

    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountAuthInfo { + /// Host coordinate owned by the account's model provider. + pub host: String, + /// Login identifying the provider-owned account. + pub login: String, + /// Provider-owned account authentication. + pub r#type: AccountAuthInfoType, +} + /// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. /// ///
    @@ -2337,6 +2417,45 @@ pub struct AgentReloadResult { pub agents: Vec, } +/// The models a custom agent asks for, and the models actually available. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsCustomAgentInitialModelDecisionParams { + /// The agent's declared `model:` entry, serialized. A single name or an ordered list of acceptable names. + pub agent_models_json: String, + /// The models available to this session, serialized in the shape the model list carries. + pub available_models_json: String, +} + +/// The model to switch to, and the warning to show when the agent's preference could not be met. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsCustomAgentInitialModelDecisionResult { + /// The reasoning effort attached to the selected model preference. Absent when that preference does not specify an effort. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, + /// The first available model that matches the agent's preferences. Absent when none of the requested models is available. + #[serde(skip_serializing_if = "Option::is_none")] + pub target_model: Option, + /// What to tell the user about an unmet preference. Absent when the preference was met. A warning with no `targetModel` means the agent's models are all unavailable. + #[serde(skip_serializing_if = "Option::is_none")] + pub warning: Option, +} + /// Optional project paths to include in agent discovery. /// ///
    @@ -2403,6 +2522,140 @@ pub struct AgentSetPromptRequest { pub prompt: String, } +/// The feature flags to evaluate shipped agents against. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetAvailableBuiltinsRequest { + /// The surface asking, which gates agents that only apply to one client. Omit or pass null to apply no client filter. + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, + /// Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or pass null for no flags. + #[serde(skip_serializing_if = "Option::is_none")] + pub feature_flags: Option>, + /// Flag overrides keyed by name. A null entry uses the corresponding base flag; false explicitly disables it. Omit or pass null for no overrides. + #[serde(skip_serializing_if = "Option::is_none")] + pub overrides: Option>, +} + +/// A shipped agent, named and described. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct BuiltinAgentSummary { + /// One-line description of what the agent does. + pub description: String, + /// The agent name, as it appears in `getBuiltins`. + pub name: String, +} + +/// The shipped agents available under the requested flags. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetAvailableBuiltinsResult { + /// Available shipped agents, in the runtime's own order. + #[doc(hidden)] + pub(crate) agents: Vec, +} + +/// The shipped agent whose definition to load. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetBuiltinDefinitionRequest { + /// The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside that list is special-cased in code and has no definition, and is reported as an error rather than as an empty definition. + pub name: String, +} + +/// One shipped agent's definition. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetBuiltinDefinitionResult { + /// The agent's definition, serialized as JSON. It carries the authored keys plus the runtime's projected `__nativeCustomAgent` view of the same agent. It is a string rather than an object because the runtime parses it with the agent schema's tolerant shape, which accepts keys this contract does not name. + pub definition_json: String, +} + +/// The shipped agent whose listing entry to load. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetBuiltinListingDefinitionRequest { + /// The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here too, from its in-code definition. + pub name: String, +} + +/// One shipped agent, projected for a listing. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetBuiltinListingDefinitionResult { + /// The agent projected as a custom agent, serialized as JSON. It is a string rather than an object for the same reason as `getBuiltinDefinition`: the runtime parses the underlying definition with the agent schema's tolerant shape, which accepts keys this contract does not name. + pub definition_json: String, +} + +/// The agents this runtime ships, named so a consumer can tell them apart from authored ones. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetBuiltinsResult { + /// The subset of `names` a user is allowed to turn off. A shipped agent outside this list is always active and a client should not offer a toggle for it. + pub disableable_names: Vec, + /// Every agent name this runtime ships. + pub names: Vec, + /// The subset of `names` defined by a shipped YAML definition. The remainder are special-cased in code and have no definition to load. + pub yaml_based_names: Vec, +} + /// Optional project paths to include when enumerating agent discovery directories. /// ///
    @@ -3008,6 +3261,27 @@ pub struct AuthIdentityMetadata { pub r#type: AuthInfoType, } +/// A credential-free account choice after sign-in. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthLoginAccount { + /// Host coordinate owned by the selected account's provider. + pub host: String, + /// Provider kind that owns this account choice. + pub kind: AccountKind, + /// Human-readable login for the account choice. + pub login: String, + /// Opaque identifier supplied to the next login step to select this account. + pub selection_id: String, +} + /// Advance an in-flight login flow, optionally fulfilling an input-required step. /// ///
    @@ -3101,7 +3375,7 @@ pub struct AuthLoginStepNeedsInteraction { pub kind: AuthLoginStepNeedsInteractionKind, } -/// Terminal result of an interactive login flow. +/// Result of an interactive login flow. Pending consent or account selection is not terminal. /// ///
    /// @@ -3112,13 +3386,16 @@ pub struct AuthLoginStepNeedsInteraction { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AuthLoginResultDto { + /// Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 first. + #[serde(skip_serializing_if = "Option::is_none")] + pub accounts: Option>, /// Host that was signed in, when completed. #[serde(skip_serializing_if = "Option::is_none")] pub host: Option, /// Login that was signed in, when completed. #[serde(skip_serializing_if = "Option::is_none")] pub login: Option, - /// Terminal disposition of the login. + /// Current disposition of the login, including pending user decisions. pub status: AuthLoginResultStatus, } @@ -3134,7 +3411,7 @@ pub struct AuthLoginResultDto { pub struct AuthLoginStepCompleted { /// Login flow step variant discriminator. pub kind: AuthLoginStepCompletedKind, - /// The terminal login result. + /// Login result. When status is needs-plaintext-consent or needs-account-selection, advance with the user's decision to continue. pub result: AuthLoginResultDto, } @@ -3199,6 +3476,9 @@ pub struct AuthReadValueActiveAccount { /// The active account, or absent when not logged in. #[serde(skip_serializing_if = "Option::is_none")] pub account: Option, + /// Credential-free identity metadata for the active account, including resolved Copilot user information when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_info: Option, /// Account read-datum variant discriminator. pub kind: AuthReadValueActiveAccountKind, } @@ -3364,6 +3644,70 @@ pub struct AutopilotObjectiveGetStateResult { pub state: Option, } +/// Availability of a server-advertised routing preference. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutoTierStatus { + /// Whether the provider permits selecting this preference. + pub enabled: bool, + /// Human-readable explanation of availability. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + /// Extensible machine-readable unavailability reason. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +/// A server-advertised routing preference. Identifiers and execution types are extensible. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutoTierDescriptor { + /// Description displayed beside the preference. + pub description: String, + /// Human-readable label, not a routing identifier. + pub display_name: String, + /// Opaque routing identifier transmitted unchanged to the provider. + pub id: String, + /// Current account-specific availability. + pub status: AutoTierStatus, + /// Execution kind; this client supports `auto` preferences on the Auto model. + pub r#type: String, +} + +/// Account-bound discovery metadata for the virtual `auto` model. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutoTierMetadata { + /// Provider-default preference, used only when no explicit preference exists. + pub default_tier: String, + /// Provider that supplied this metadata, when the catalog is provider-attributed. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_id: Option, + /// Routing preferences in the server's presentation order. + pub tiers: Vec, +} + /// A well-known model in the runtime's built-in catalog. /// ///
    @@ -5899,6 +6243,147 @@ pub struct ConnectorDisconnectResult { pub status: ConnectorStatus, } +/// Account metadata. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryAuthInfo { + /// Host. + pub host: String, + /// Login. + pub login: String, + /// Authentication type. + pub r#type: AuthInfoType, +} + +/// Eligible account. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryAccount { + /// Opaque account ID. + pub account_id: String, + /// Account metadata. + pub auth_info: ConnectorDiscoveryAuthInfo, +} + +/// Eligible accounts. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryAccountList { + /// Eligible accounts. + pub accounts: Vec, + /// Availability. + pub availability: ConnectorDiscoveryAvailability, +} + +/// Selected account. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryAccountRequest { + /// Opaque account ID. + pub account_id: String, +} + +/// Feature availability. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryCapabilities { + /// API version. + pub api_version: i64, + /// Availability. + pub availability: ConnectorDiscoveryAvailability, + /// Whether results are cached. + pub conditional_cache: bool, + /// Whether accounts are selected by opaque ID. + pub opaque_account_selection: bool, +} + +/// Entry. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryCatalogEntry { + /// Description. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Display name. + pub display_name: String, + /// Logo. + #[serde(skip_serializing_if = "Option::is_none")] + pub logo: Option, + /// Name. + pub name: String, + /// Release tag. + #[serde(skip_serializing_if = "Option::is_none")] + pub release_tag: Option, + /// Status. + pub status: ConnectorCatalogStatus, + /// Tier. + #[serde(skip_serializing_if = "Option::is_none")] + pub tier: Option, +} + +/// Entries for the selected account. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryCatalogResult { + /// Opaque account ID. + pub account_id: String, + /// Entries. + pub connectors: Vec, + /// Refresh time in Unix epoch milliseconds. + pub refreshed_at_ms: i64, + /// Revision. + pub revision: i64, +} + /// Requests authoritative Connector-to-MCP reconciliation for the pinned account. /// ///
    @@ -6141,6 +6626,12 @@ pub struct CurrentModel { /// Captured base model to restore when leaving plan mode. Omitted outside plan mode or when no plan override has captured a base model. Persistent agent model requirements apply to this model rather than the temporary plan model. #[serde(skip_serializing_if = "Option::is_none")] pub plan_base_model_id: Option, + /// Provider of planBaseModelId, when that saved selection is provider-qualified. + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_base_provider_id: Option, + /// Provider selected for this model. Together with modelId, identifies a catalog entry even when providers serve the same model. Omitted for unattributed legacy selections. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_id: Option, /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, @@ -7793,6 +8284,191 @@ pub struct FolderTrustCheckResult { pub trusted: bool, } +/// The remote the checked-out branch tracks. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitCurrentBranchRemoteResult { + /// Name of the tracked remote. Reports `origin` whenever the working tree has no tracking configuration to read, including on a detached HEAD, so this is never null and never empty. + pub remote: String, +} + +/// Working-tree path a git query applies to. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitCwdRequest { + /// Absolute path to a directory inside the git working tree to query. + pub cwd: String, +} + +/// A GitHub login the authenticated user may act as: their own account, or an organization they belong to. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubOwnerOption { + /// The owner's GitHub login. + pub login: String, + /// Which kind of owner this is. The authenticated user's own account is always reported as `user`. + pub r#type: String, +} + +/// The owner listing to abandon. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubOwnersCancelRequest { + /// Request id the listing was started with. + pub request_id: i64, +} + +/// Whether the id named a running owner listing. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubOwnersCancelResult { + /// True when a listing with the id was running and the cancel stopped it. False when the id was never registered, was registered but unused, was released after being abandoned, or its listing had ended. An unused id is released and cannot start a later listing. + pub canceled: bool, +} + +/// Credential to list owners under, and the request id that makes the listing cancellable. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubOwnersListRequest { + /// The credential the listing runs under, carried opaquely because its shape is the host's own and the runtime only resolves a token and a GitHub host from it. No credential travels: this selects one the runtime already holds. + pub auth_info: serde_json::Value, + /// Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled before use, released after being abandoned, or already used is refused rather than silently running uncancellable. + pub request_id: i64, +} + +/// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubOwnersListResult { + /// Why no owners could be listed, phrased for a user. Present when the listing failed in a way the caller should render rather than raise. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + /// The owners, on success: the authenticated user first, then the organizations they belong to. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) owners: Option>, + /// A malformed request or an unreadable credential, which the caller raises instead of rendering. Kept a field rather than a dispatch error so it stays distinct from `message`, which the caller renders. + #[serde(skip_serializing_if = "Option::is_none")] + pub throw_error: Option, + /// A line the caller should log. Present only alongside `message`, and only for failures worth recording. + #[serde(skip_serializing_if = "Option::is_none")] + pub warning: Option, +} + +/// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubOwnersRequestIdResult { + /// Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`. + pub request_id: i64, +} + +/// Working-tree path whose owning GitHub repository should be resolved. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubRepositoryAtPathRequest { + /// Absolute path to a directory inside the git working tree to resolve. + pub path: String, +} + +/// Owner, name, and host of a GitHub repository, as resolved from a git remote URL. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitHubRepositoryIdentity { + /// Host the remote points at, for example `github.com` or a GitHub Enterprise hostname. + pub host: String, + /// Repository name, without the owner prefix or the `.git` suffix. + pub name: String, + /// Repository owner login (user or organization). + pub owner: String, +} + +/// The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubRepositoryAtPathResult { + /// Resolved repository identity, or null when the selected remote resolves to no GitHub host. + #[serde(skip_serializing_if = "Option::is_none")] + pub repository: Option, +} + /// Client environment metadata describing the process that produced a telemetry event. /// ///
    @@ -7951,6 +8627,230 @@ pub struct GitHubTokenAcquireResultCancelled { pub kind: GitHubTokenAcquireResultCancelledKind, } +/// A GitHub repository one of a working tree's remotes points at. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitRemoteRepository { + /// GitHub host serving the repository, which is not `github.com` for a GitHub Enterprise remote. + pub host: String, + /// Repository name, without the owner. + pub name: String, + /// Account or organization owning the repository. + pub owner: String, + /// Name of the first remote that produced this distinct repository entry, such as `origin` or `upstream`. + pub remote_name: String, +} + +/// Git working tree whose GitHub remotes should be listed. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitReposFromRemotesRequest { + /// Absolute path to the root of the git working tree. + pub git_root: String, +} + +/// The GitHub repositories a working tree's remotes point at. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitReposFromRemotesResult { + /// One entry per distinct GitHub repository, in the order git reports the first remote for each repository. Empty when no remote points at a GitHub host, which a caller should read as `not connected to GitHub`. Failing to read the remotes is an error, not an empty list. + #[doc(hidden)] + pub(crate) repositories: Vec, +} + +/// Selects the configuration directory whose machine-wide state to read. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GlobalStateLoadForConfigDirRequest { + /// Copilot configuration directory to read the state document from, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to read the directory the server resolved for itself. + #[serde(skip_serializing_if = "Option::is_none")] + pub config_dir: Option, +} + +/// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InstalledPlugin { + /// Path where the plugin is cached locally + #[serde(rename = "cache_path", skip_serializing_if = "Option::is_none")] + pub cache_path: Option, + /// Whether the plugin is currently enabled + pub enabled: bool, + /// Installation timestamp + #[serde(rename = "installed_at")] + pub installed_at: String, + /// Absolute path of the marketplace directory a live plugin was resolved from. Present only on live, never-persisted records — those synthesized at session start for a directory/local marketplace, whose cache_path points at the real plugin directory on disk rather than a copy under the installed-plugins cache. Its presence is what marks a record as live, and no record carrying it is ever written to the persisted installedPlugins key. + #[serde(rename = "installed_from", skip_serializing_if = "Option::is_none")] + pub installed_from: Option, + /// Marketplace the plugin came from (empty string for direct repo installs) + pub marketplace: String, + /// Plugin name + pub name: String, + /// Source for direct repo installs (when marketplace is empty) + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + /// Per-plugin source fingerprint (a SHA-256 hash of the plugin's catalog source spec plus its resolved source subtree — NOT a Git commit SHA) captured at marketplace install/update time. Auto-update compares it against the freshly recomputed fingerprint to detect a content change that does not bump the version. Absent for pre-existing installs and for direct (non-marketplace) installs. + #[serde(rename = "source_sha", skip_serializing_if = "Option::is_none")] + pub source_sha: Option, + /// Version installed (if available) + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +/// An account the host has signed in to, identified by the server it lives on and the login it uses there. The same person can appear more than once when they use both github.com and an Enterprise server. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct LoggedInUser { + /// Source account this account was derived from, when one was recorded. + #[serde(skip_serializing_if = "Option::is_none")] + pub derived_from: Option, + /// Host the account belongs to, such as `github.com` or an Enterprise server. + pub host: String, + /// Account kind, when the host recorded one. Consumers must tolerate new strings. + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + /// Account login on that host. + pub login: String, +} + +/// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GlobalStateLoadResult { + /// Whether the user has answered the prompt suggesting they install the desktop app. + #[serde(skip_serializing_if = "Option::is_none")] + pub app_install_nudge_responded: Option, + /// Whether the app tip has been shown. + #[serde(skip_serializing_if = "Option::is_none")] + pub app_tip_shown: Option, + /// Terminals the user has already been asked to set up, so the host does not ask twice. + #[serde(skip_serializing_if = "Option::is_none")] + pub asked_setup_terminals: Option>, + /// When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the once-per-day cap for non-staff users across restarts. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_feedback_last_prompted_at: Option, + /// When the host first ran on this machine. + #[serde(skip_serializing_if = "Option::is_none")] + pub first_launch_at: Option, + /// Plugins installed on this machine. + #[serde(skip_serializing_if = "Option::is_none")] + pub installed_plugins: Option>, + /// Account used for the most recent sign-in. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) last_logged_in_user: Option, + /// Every account the host has signed in to on this machine. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) logged_in_users: Option>, + /// Whether the one-off cleanup of stored reasoning summaries has run. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_summaries_cleanup_done: Option, + /// Models the user selected recently, most recent first. + #[serde(skip_serializing_if = "Option::is_none")] + pub recent_model_ids: Option>, + /// Whether the user declined to trust the sandbox credential proxy CA. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_credential_proxy_ca_declined: Option, + /// Whether the sandbox onboarding has been shown. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_onboarding_shown: Option, + /// Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only behavior. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff: Option, + /// Whether the user was recognized as GitHub staff. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_github: Option, + /// When the staff-only log level migration last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_log_level_migration_at: Option, + /// Whether the user was recognized as Microsoft staff. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_microsoft: Option, + /// When the staff-only model reset last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_model_reset_at: Option, + /// When the staff-only update channel migration last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_update_channel_migration_at: Option, + /// Folders where the user declined the init prompt, so it stays hidden there. + #[serde(skip_serializing_if = "Option::is_none")] + pub suppress_init_folders: Option>, + /// Folders the user has marked as trusted. + #[serde(skip_serializing_if = "Option::is_none")] + pub trusted_folders: Option>, +} + +/// A single top-level key to record in the host's machine-wide state. The write replaces only that key and leaves the rest of the document untouched, so two writers recording different one-off flags do not overwrite each other. The stored credential keys cannot be written through this method. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GlobalStateWriteKeyRequest { + /// Copilot configuration directory to write the state document in, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to write the directory the server resolved for itself. Mirrors `globalState.loadForConfigDir`, so a caller can read and write the same directory. + #[serde(skip_serializing_if = "Option::is_none")] + pub config_dir: Option, + /// Top-level key to write, named as it appears in the result of `globalState.load`. It must be one of the writable keys that `globalState.writeKey` lists. + pub key: String, + /// Value to store for the key. Omit it, or pass null, to remove the key instead. + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, +} + /// Pending external tool call request ID, with the tool result or an error describing why it failed. /// ///
    @@ -9418,43 +10318,6 @@ pub struct InstallationConfirmationResponse { pub review_fingerprint: String, } -/// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct InstalledPlugin { - /// Path where the plugin is cached locally - #[serde(rename = "cache_path", skip_serializing_if = "Option::is_none")] - pub cache_path: Option, - /// Whether the plugin is currently enabled - pub enabled: bool, - /// Installation timestamp - #[serde(rename = "installed_at")] - pub installed_at: String, - /// Absolute path of the marketplace directory a live plugin was resolved from. Present only on live, never-persisted records — those synthesized at session start for a directory/local marketplace, whose cache_path points at the real plugin directory on disk rather than a copy under the installed-plugins cache. Its presence is what marks a record as live, and no record carrying it is ever written to the persisted installedPlugins key. - #[serde(rename = "installed_from", skip_serializing_if = "Option::is_none")] - pub installed_from: Option, - /// Marketplace the plugin came from (empty string for direct repo installs) - pub marketplace: String, - /// Plugin name - pub name: String, - /// Source for direct repo installs (when marketplace is empty) - #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, - /// Per-plugin source fingerprint (a SHA-256 hash of the plugin's catalog source spec plus its resolved source subtree — NOT a Git commit SHA) captured at marketplace install/update time. Auto-update compares it against the freshly recomputed fingerprint to detect a content change that does not bump the version. Absent for pre-existing installs and for direct (non-marketplace) installs. - #[serde(rename = "source_sha", skip_serializing_if = "Option::is_none")] - pub source_sha: Option, - /// Version installed (if available) - #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, -} - /// Information about an installed plugin tracked in global state. /// ///
    @@ -10097,6 +10960,9 @@ pub struct ManagedMcpServerConfig { pub struct ManagedSettingMeta { /// Whether users and repositories may choose a different value. `false` means policy locks the value. pub overridable: bool, + /// Original managed value when the runtime adjusted it to a supported effective value. Omitted when no adjustment was needed. + #[serde(skip_serializing_if = "Option::is_none")] + pub requested: Option, /// Channel that supplied this scalar value, matching a `layers[].source`: `device`, `server`, or `policyHelper`. These scalar defaults select one winning channel, not a mixed source. Treat unknown values as additional channels; more may be added. pub source: String, } @@ -10185,6 +11051,12 @@ pub struct ManagedSettingsMeta { /// Lock state and provenance of `values.autoTier`. #[serde(skip_serializing_if = "Option::is_none")] pub auto_tier: Option, + /// Lock state and provenance of `values.contextTier`. + #[serde(skip_serializing_if = "Option::is_none")] + pub context_tier: Option, + /// Lock state and provenance of `values.effortLevel`. + #[serde(skip_serializing_if = "Option::is_none")] + pub effort_level: Option, /// Lock state and provenance of `values.model`. #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, @@ -10244,7 +11116,13 @@ pub struct ManagedSettingsValues { /// Managed Auto routing preference, used when the selected model is `auto`. #[serde(skip_serializing_if = "Option::is_none")] pub auto_tier: Option, - /// Managed default model identifier, as configured. New sessions start with it; it can name a model the account cannot use, so hosts match it against the listed models. + /// Managed context-tier default for the managed concrete model. + #[serde(skip_serializing_if = "Option::is_none")] + pub context_tier: Option, + /// Managed reasoning-effort default for the managed concrete model. The runtime clamps it to an entitled effort when model availability is known. + #[serde(skip_serializing_if = "Option::is_none")] + pub effort_level: Option, + /// Managed default model identifier. When model availability was resolved, aliases and family names are projected to a concrete available model ID; otherwise the configured value is returned. #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, } @@ -10313,6 +11191,9 @@ pub struct ManagedSettingsResolveRequest { /// Opaque account identifier returned by `account.getAllUsers`. When omitted, the current account is used, or device policy only when no account is signed in. #[serde(skip_serializing_if = "Option::is_none")] pub selection_id: Option, + /// Working directory used to run an organization policy helper. When omitted, sessionless resolution does not run the helper. + #[serde(skip_serializing_if = "Option::is_none")] + pub working_directory: Option, } /// Effective enterprise managed settings for an account, resolved without a session. @@ -11006,6 +11887,71 @@ pub struct McpConfigUpdateRequest { pub name: String, } +/// Observational state for a matching already materialized MCP server. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpConfiguredServerState { + /// Observed connection error, when the materialized server failed. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Observed connection status. This is not a configuration or readiness guarantee. + pub status: McpServerStatus, +} + +/// Effective MCP configuration entry. Configuration enablement is distinct from the optional live observation. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpConfiguredServer { + /// Human-readable display name supplied by configuration. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// Whether this configured server is enabled after session configuration and policy filtering. + pub enabled: bool, + /// Observed state from an already materialized matching server. Omitted when no live graph has this configured server; it never determines configuration enablement. + #[serde(skip_serializing_if = "Option::is_none")] + pub live: Option, + /// Server name (config key) + pub name: String, + /// Configuration provenance: user, workspace, plugin, builtin, or managed. + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + /// Plugin name that provided this server, when source is plugin. + #[serde(skip_serializing_if = "Option::is_none")] + pub source_plugin: Option, + /// Plugin version that provided this server, when source is plugin. + #[serde(skip_serializing_if = "Option::is_none")] + pub source_plugin_version: Option, +} + +/// Effective MCP configuration with optional live observations from matching already materialized servers. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpConfiguredServerList { + /// Effective configured MCP servers. + pub servers: Vec, +} + /// Credential-free authentication identity used to configure GitHub MCP. /// ///
    @@ -13304,6 +14250,9 @@ pub struct McpServer { pub source_plugin_version: Option, /// Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured pub status: McpServerStatus, + /// Configured URL for an HTTP/SSE server, regardless of configuration source. Omitted for local and in-memory servers. + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, } /// Authentication settings with optional redirect port configuration. @@ -13824,7 +14773,7 @@ pub struct MemoryConfiguration { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MetadataContextAttributionResultContextAttributionCategories { - /// Output reserve plus post-blocking-threshold buffer. + /// Overlapping output reservation plus post-blocking-threshold buffer. pub buffer: i64, /// Custom-instructions tokens (0 when none are configured). pub custom_instructions: i64, @@ -13871,7 +14820,7 @@ pub struct MetadataContextAttributionResultContextAttributionEntriesItem { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MetadataContextAttributionResultContextAttribution { - /// Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. + /// Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. pub buffer_tokens: i64, /// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. pub categories: MetadataContextAttributionResultContextAttributionCategories, @@ -13881,13 +14830,13 @@ pub struct MetadataContextAttributionResultContextAttribution { pub compaction_threshold: i64, /// Flat list of per-source attribution entries. Group by `kind` and render unrecognized kinds generically. Nesting and rollups are expressed via `parentId`. pub entries: Vec, - /// Prompt limit plus the model's output reserve: the full context window `categories.freeSpace` and `categories.buffer` are measured against. Mirrors `SessionContextInfo.limit`. + /// Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`. pub limit: i64, /// The concrete model id the entire breakdown was tokenized against (feeds the per-model token multiplier). Under `Auto` (Free/Student) this is the resolved model, not the literal `auto` sentinel, so totals are not undercounted. A single-model approximation of a potentially multi-model Auto session. pub model_id: String, /// How `modelId` was chosen. Not a closed set — tolerate unknown values. Known values today: `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected model), `default` (a fallback before any model is known). pub model_source: String, - /// Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. + /// Effective input budget after reserving requested output against the combined context ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. pub prompt_token_limit: i64, /// Total token count of the current context window the entries are measured against (system message + conversation messages + tool definitions — the same total reported by /context). Divide an entry's `tokens` by this to derive its share. pub total_tokens: i64, @@ -13952,10 +14901,13 @@ pub struct MetadataContextHeaviestMessagesResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MetadataContextInfoRequest { - /// Maximum output tokens allowed by the target model. Pass 0 if unknown. + /// Requested output allowance to reserve against the combined context ceiling. Pass 0 to resolve the session's request cap, falling back to the model's advertised output limit. pub output_token_limit: i64, - /// Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. + /// Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from the session. pub prompt_token_limit: i64, + /// Provider owning selectedModel. Omit to use the current provider for the current model, or deterministic legacy lookup for a different model. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_id: Option, /// Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. #[serde(skip_serializing_if = "Option::is_none")] pub selected_model: Option, @@ -13965,20 +14917,26 @@ pub struct MetadataContextInfoRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MetadataContextInfoResultContextInfo { - /// Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) + /// Output reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%). pub buffer_tokens: i64, /// Token count at which background compaction starts (configurable percentage of promptTokenLimit) pub compaction_threshold: i64, /// Tokens consumed by user/assistant/tool messages pub conversation_tokens: i64, - /// Prompt token limit plus the model's full output token limit. + /// Provider-owned model display label, or the Auto routing label. Presentation only; modelName remains the tokenization identity. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_model_name: Option, + /// Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays. pub limit: i64, /// Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools) pub mcp_tools_tokens: i64, /// The model used for token counting pub model_name: String, - /// Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) + /// Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified. pub prompt_token_limit: i64, + /// Provider whose catalog supplies this context window. Omitted when provider identity is unavailable. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider: Option, /// Tokens consumed by the system prompt pub system_tokens: i64, /// Tokens consumed by tool definitions sent to the model (excludes deferred tools) @@ -14385,25 +15343,6 @@ pub struct ModelPolicy { pub terms: Option, } -/// A neutral reference to the model provider that produced a model: an opaque id, a human-readable label, and the provider kind. Carried on each enumerated Model so consumers can group by provider without reaching into a provider-shaped internal type. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ModelProviderRef { - /// Opaque, stable id of the provider that produced this model. Matches the enumerated `ModelProviderDescriptor.id`. - pub id: String, - /// The provider kind. - pub kind: ModelProviderKind, - /// Human-readable provider label, owned by the runtime so every consumer renders identical text. - pub label: String, -} - /// Service-published warning text that hosts should display when presenting a model. /// ///
    @@ -14467,6 +15406,9 @@ pub struct Model { /// Supported reasoning effort levels (only present if model supports reasoning effort) #[serde(skip_serializing_if = "Option::is_none")] pub supported_reasoning_efforts: Option>, + /// Model vendor as the Copilot API reports it, for example "Anthropic" or "Azure OpenAI". Open vocabulary, passed through unchanged. It can name the vendor that serves the model instead of the one that built it, or a label that is not a vendor, such as "Experimental". Absent when the Copilot API reports no vendor. + #[serde(skip_serializing_if = "Option::is_none")] + pub vendor: Option, /// Warnings the service published for this model, such as a deprecated client version. Present only when the service published at least one warning. The model remains usable; hosts should surface these as advisory rather than blocking. #[serde(skip_serializing_if = "Option::is_none")] pub warning_messages: Option>, @@ -14516,6 +15458,9 @@ pub struct ModelApplyStartupOverlayRequest { /// Model selected by repository settings, when configured. #[serde(skip_serializing_if = "Option::is_none")] pub repo_model: Option, + /// Provider owning the repository-selected model. Omitted repository provider identity retains legacy bare-model behavior. + #[serde(skip_serializing_if = "Option::is_none")] + pub repo_model_provider_id: Option, /// Reasoning effort selected by repository settings, when configured. #[serde(skip_serializing_if = "Option::is_none")] pub repo_reasoning_effort: Option, @@ -14624,6 +15569,35 @@ pub struct ModelCapabilitiesOverride { pub supports: Option, } +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ModelClearStartupSeedRequest { + pub expected_model: String, + /// Provider captured with the startup seed; omission matches only an unqualified seed. + #[serde(skip_serializing_if = "Option::is_none")] + pub expected_provider_id: Option, +} + +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ModelClearStartupSeedResult { + pub cleared: bool, +} + /// List of Copilot models available to the resolved user, including capabilities and billing metadata. /// ///
    @@ -15283,7 +16257,7 @@ pub struct ModelSwitchToRequest { /// Settings scope used when persisting the selected model. #[serde(skip_serializing_if = "Option::is_none")] pub model_change_scope: Option, - /// Model selection id to switch to, as returned by `list`. A bare id (e.g. `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. + /// Model id to switch to, as returned by `list`. Include providerId to select an exact catalog entry when providers share the id. Without providerId, a bare id retains incumbent selection behavior; a registry-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a BYOK model. pub model_id: String, /// Optional settings context and explicit-override flags used to persist a picker selection. #[serde(skip_serializing_if = "Option::is_none")] @@ -15311,6 +16285,142 @@ pub struct ModelSwitchToRequest { pub verbosity: Option, } +/// Extensible [`ModelSwitchToRequest`], including inputs added after it was published. +/// +/// Required inputs are [`ModelSwitchToOptions::new`] arguments; optional inputs have fluent setters. +/// Input-only: it serialises to the flat wire request and is not deserialisable. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelSwitchToOptions { + #[serde(flatten)] + legacy: ModelSwitchToRequest, + #[serde(skip_serializing_if = "Option::is_none")] + provider_id: Option, +} + +impl ModelSwitchToOptions { + /// Creates options with the required inputs. + pub fn new(model_id: impl Into) -> Self { + Self { + legacy: ModelSwitchToRequest { + model_id: model_id.into(), + auto_tier: None, + reasoning_effort: None, + reasoning_summary: None, + verbosity: None, + model_capabilities: None, + context_tier: None, + source: None, + defer_if_model_change_queued: None, + compaction_decision: None, + run_compaction_preflight: None, + repo_scope: None, + model_change_scope: None, + require_available: None, + picker_persistence: None, + }, + provider_id: None, + } + } + + /// Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. + pub fn auto_tier(mut self, value: AutoTier) -> Self { + self.legacy.auto_tier = Some(value); + self + } + + /// Reasoning effort level to use for the model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. "none" disables reasoning. Pass null to clear any session effort override and fall back to the model's default. When omitted, the session's current effort is kept. + pub fn reasoning_effort(mut self, value: impl Into) -> Self { + self.legacy.reasoning_effort = Some(value.into()); + self + } + + /// Reasoning summary mode to request for supported model clients + pub fn reasoning_summary(mut self, value: ReasoningSummary) -> Self { + self.legacy.reasoning_summary = Some(value); + self + } + + /// Output verbosity level to request for supported models + pub fn verbosity(mut self, value: Verbosity) -> Self { + self.legacy.verbosity = Some(value); + self + } + + /// Override individual model capabilities resolved by the runtime + pub fn model_capabilities(mut self, value: ModelCapabilitiesOverride) -> Self { + self.legacy.model_capabilities = Some(value); + self + } + + /// Explicit context tier for the selected model. `"default"` / `"long_context"` apply the requested tier; omit this field to use normal model behavior with no explicit tier. + pub fn context_tier(mut self, value: ContextTier) -> Self { + self.legacy.context_tier = Some(value); + self + } + + /// Origin to record on the effective `session.model_change` event for trusted in-process calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. + pub fn source(mut self, value: ModelChangeSource) -> Self { + self.legacy.source = Some(value); + self + } + + /// When true, defer this switch (enqueue it) if another model change is already queued, even when no turn is active — so it drains last (FIFO) and wins over the already-queued change. Intended for genuine user-initiated model selections; internal restore/reapply switches omit it and apply immediately when no turn is active. When no other model change is queued this has no effect (a switch still applies immediately unless a turn is active). + pub fn defer_if_model_change_queued(mut self, value: bool) -> Self { + self.legacy.defer_if_model_change_queued = Some(value); + self + } + + /// Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. + pub fn compaction_decision(mut self, value: impl Into) -> Self { + self.legacy.compaction_decision = Some(value.into()); + self + } + + /// When true, evaluate context-window compaction policy before applying the switch. + pub fn run_compaction_preflight(mut self, value: bool) -> Self { + self.legacy.run_compaction_preflight = Some(value); + self + } + + /// Optional repository settings scope to persist after the switch commits. + pub fn repo_scope(mut self, value: impl Into) -> Self { + self.legacy.repo_scope = Some(value.into()); + self + } + + /// Settings scope used when persisting the selected model. + pub fn model_change_scope(mut self, value: impl Into) -> Self { + self.legacy.model_change_scope = Some(value.into()); + self + } + + /// Require the target to be currently available and enabled before applying the switch. + pub fn require_available(mut self, value: bool) -> Self { + self.legacy.require_available = Some(value); + self + } + + /// Optional settings context and explicit-override flags used to persist a picker selection. + pub fn picker_persistence(mut self, value: ModelPickerPersistenceRequest) -> Self { + self.legacy.picker_persistence = Some(value); + self + } + + /// Provider id from the selected list entry's provider reference. Selects this exact provider/model pair; an unavailable pair fails rather than using another provider. Omit for deterministic legacy bare-model selection. + pub fn provider_id(mut self, value: impl Into) -> Self { + self.provider_id = Some(value.into()); + self + } +} + /// The model identifier active on the session after the switch. /// ///
    @@ -15394,6 +16504,9 @@ pub struct ModeSetRequest { /// Whether a dedicated plan model is configured. #[serde(skip_serializing_if = "Option::is_none")] pub plan_model_configured: Option, + /// Provider owning planModel. Omit for legacy bare-model selection. + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_model_provider_id: Option, /// Reasoning effort to use with the dedicated plan model. #[serde(skip_serializing_if = "Option::is_none")] pub plan_reasoning_effort: Option, @@ -15575,6 +16688,49 @@ pub struct OptionsUpdateAdditionalContentExclusionPolicy { pub scope: OptionsUpdateAdditionalContentExclusionPolicyScope, } +/// External tool call of the session or one of its sub-agents that is still waiting for session.tools.handlePendingToolCall. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PendingExternalToolRequest { + /// Sub-agent instance identifier (the envelope agentId of its events) of the agent that issued the call; absent for calls issued by the root agent + #[serde(skip_serializing_if = "Option::is_none")] + pub agent_id: Option, + /// Arguments to pass to the external tool + #[serde(skip_serializing_if = "Option::is_none")] + pub arguments: Option, + /// Stable identity of the provider that offered the tool, for hosts that route extension-owned tools by provider + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_id: Option, + /// Request ID to pass to session.tools.handlePendingToolCall + pub request_id: RequestId, + /// Tool call ID assigned to this external tool invocation + pub tool_call_id: String, + /// Name of the external tool to invoke + pub tool_name: String, +} + +/// External tool calls still waiting for a result. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PendingExternalToolRequestList { + /// External tool calls of the session and its sub-agents that session.tools.handlePendingToolCall would still accept, in the order they were requested. Includes calls that were pending when the session was last suspended and resumed with pending work continued, so a host that did not observe the original external_tool.requested event can answer them. + pub items: Vec, +} + /// Pending permission prompt reconstructed from event history, with request ID and user-facing prompt details. /// ///
    @@ -19137,6 +20293,26 @@ pub struct QueueWithdrawMessageResult { pub removed: bool, } +/// A pending session quota warning, consumed once by takeWarnings. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct QuotaWarningProjection { + /// User-facing warning text. + pub message: String, + /// Related account or upgrade link. + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + /// Existing warning category. + pub warning_type: String, +} + /// Event type to register consumer interest for, used by runtime gating logic. /// ///
    @@ -19888,7 +21064,7 @@ pub struct SandboxGrantPathForRequestResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SandboxHostCapability { - /// The policy feature, as an extensible string: ignore names you do not recognize. Known values: `network` (sandboxed commands can reach the network; on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns), `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`), `denied_paths` (native enforcement of `filesystem.deniedPaths`), `shell` (shell commands inside the sandbox), and `filesystem_enumeration` (enumerate-only filesystem grants; on Windows this needs Process Security Environment 1.1 filesystem enumeration support, and without it sandboxed PowerShell still runs but cannot resolve its current location; other platforms always report it). + /// The policy feature, as an extensible string: ignore names you do not recognize. Known values: `network` (sandboxed commands can reach the network; on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns), `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support or MXC's PSEC 1.0-only proxy-loopback compatibility capability, and a policy that uses it must also set `network.allowLocalNetwork`; compatibility applies only to an explicit identity-less runtime proxy, not general host-loopback access, and other policy restrictions still apply), `denied_paths` (native enforcement of `filesystem.deniedPaths`), `shell` (shell commands inside the sandbox), and `filesystem_enumeration` (enumerate-only filesystem grants; on Windows this needs Process Security Environment 1.1 filesystem enumeration support, and without it sandboxed PowerShell still runs but cannot resolve its current location; other platforms always report it). pub name: String, /// Human-readable reason and remedy when the feature is unsupported, such as a package to install or an OS update. Present only when `supported` is false. #[serde(skip_serializing_if = "Option::is_none")] @@ -20637,11 +21813,28 @@ pub struct SessionBulkDeleteResult { pub freed_bytes: HashMap, } +/// The IDE a host is connected to, as reported to the session. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConnectedIdeInfo { + /// Display name of the connected IDE, for example `VS Code`. + pub ide_name: String, + /// Absolute path of the workspace folder the IDE has open. + pub workspace_folder: String, +} + /// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionContextAttributionCategories { - /// Output reserve plus post-blocking-threshold buffer. + /// Overlapping output reservation plus post-blocking-threshold buffer. pub buffer: i64, /// Custom-instructions tokens (0 when none are configured). pub custom_instructions: i64, @@ -20695,7 +21888,7 @@ pub struct SessionContextAttributionEntriesItem { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionContextAttribution { - /// Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. + /// Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. pub buffer_tokens: i64, /// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. pub categories: SessionContextAttributionCategories, @@ -20705,13 +21898,13 @@ pub struct SessionContextAttribution { pub compaction_threshold: i64, /// Flat list of per-source attribution entries. Group by `kind` and render unrecognized kinds generically. Nesting and rollups are expressed via `parentId`. pub entries: Vec, - /// Prompt limit plus the model's output reserve: the full context window `categories.freeSpace` and `categories.buffer` are measured against. Mirrors `SessionContextInfo.limit`. + /// Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`. pub limit: i64, /// The concrete model id the entire breakdown was tokenized against (feeds the per-model token multiplier). Under `Auto` (Free/Student) this is the resolved model, not the literal `auto` sentinel, so totals are not undercounted. A single-model approximation of a potentially multi-model Auto session. pub model_id: String, /// How `modelId` was chosen. Not a closed set — tolerate unknown values. Known values today: `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected model), `default` (a fallback before any model is known). pub model_source: String, - /// Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. + /// Effective input budget after reserving requested output against the combined context ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. pub prompt_token_limit: i64, /// Total token count of the current context window the entries are measured against (system message + conversation messages + tool definitions — the same total reported by /context). Divide an entry's `tokens` by this to derive its share. pub total_tokens: i64, @@ -20728,20 +21921,26 @@ pub struct SessionContextAttribution { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionContextInfo { - /// Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) + /// Output reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%). pub buffer_tokens: i64, /// Token count at which background compaction starts (configurable percentage of promptTokenLimit) pub compaction_threshold: i64, /// Tokens consumed by user/assistant/tool messages pub conversation_tokens: i64, - /// Prompt token limit plus the model's full output token limit. + /// Provider-owned model display label, or the Auto routing label. Presentation only; modelName remains the tokenization identity. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_model_name: Option, + /// Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays. pub limit: i64, /// Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools) pub mcp_tools_tokens: i64, /// The model used for token counting pub model_name: String, - /// Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) + /// Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified. pub prompt_token_limit: i64, + /// Provider whose catalog supplies this context window. Omitted when provider identity is unavailable. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider: Option, /// Tokens consumed by the system prompt pub system_tokens: i64, /// Tokens consumed by tool definitions sent to the model (excludes deferred tools) @@ -21625,9 +22824,15 @@ pub struct SessionManagedPermissions { /// Permission rules that block matching operations. Deny has highest precedence. #[serde(skip_serializing_if = "Option::is_none")] pub deny: Option>, + /// When true, prevents Assisted Permissions from being activated. An actively Assisted session falls back to Manual Approval while the policy is in force. Omit the key or set it to false to impose no restriction. + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_assisted_permissions_mode: Option, /// When set to `disable`, prevents bypass/allow-all permission modes. Advisory auto-approval remains available because normal prompt paths stay active. Any other value is accepted rather than failing the session, but is enforced as `disable`: the key is only present to restrict something, so a mode this runtime cannot interpret fails closed to the most restrictive one it knows. Omit the key entirely to impose no restriction. #[serde(skip_serializing_if = "Option::is_none")] pub disable_bypass_permissions_mode: Option, + /// Closed-world host boundary expressed as `Domain(hostname)`, `Domain(IP)`, or `Domain(*.example.com)` rules. Schemes, ports, paths, queries, and fragments are rejected because every network request must be enforceable at host-level egress. Multiple managed sources intersect their lists; an empty list denies all hosts. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit_to: Option>, } /// Managed settings an SDK host may inject at session startup. Only permissions are accepted in this initial contract. @@ -21646,6 +22851,22 @@ pub struct SessionManagedSettings { pub permissions: Option, } +/// Records which IDE the host is connected to, or clears it. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpSetConnectedIdeInfoParams { + /// The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports that it is disconnected. + #[serde(skip_serializing_if = "Option::is_none")] + pub ide: Option, +} + /// Public-facing projection of workspace metadata for SDK / TUI consumers #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -21760,6 +22981,9 @@ pub struct SessionModelPriceCategory { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionModelList { + /// Ordered Auto routing preferences discovered for this session's account. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto: Option, /// Available models, ordered with the most preferred default first. Includes both Copilot (CAPI) models and any registry BYOK models; a BYOK model appears under its provider-qualified selection id (`provider/id`). pub list: Vec, /// Cost categories for the full CAPI catalog, including picker-disabled models that Auto may select. Metadata only; entries absent from `list` are not manually selectable. @@ -21972,6 +23196,10 @@ pub struct SessionOpenOptions { /// Initial authentication info for the session. #[serde(skip_serializing_if = "Option::is_none")] pub auth_info: Option, + /// Whether a CLI host explicitly requested the initial Auto preference. False preserves a settings-derived preference without validating availability during creation; execution still validates it. Defaults to true and is ignored for non-CLI callers. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) auto_tier_is_explicit: Option, /// Allowlist of available tool names. #[serde(skip_serializing_if = "Option::is_none")] pub available_tools: Option>, @@ -22042,6 +23270,9 @@ pub struct SessionOpenOptions { /// Whether model responses stream as delta events. #[serde(skip_serializing_if = "Option::is_none")] pub enable_streaming: Option, + /// Opt in to enforcing non-overridable managed model controls on session model, Auto-tier, reasoning-effort, and context-tier changes. Managed defaults still apply when omitted; this option only turns conflicting changes into errors. + #[serde(skip_serializing_if = "Option::is_none")] + pub enforce_managed_model_defaults: Option, /// How MCP server environment values are interpreted. #[serde(skip_serializing_if = "Option::is_none")] pub env_value_mode: Option, @@ -22516,6 +23747,107 @@ pub struct SessionPruneResult { pub skipped: Vec, } +/// Existing delegation guidance for an account without delegation access. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionQuotaDelegateWarning { + /// User-facing guidance. + pub text: String, + /// Link to upgrade the account. + pub url: String, +} + +/// Session quota snapshot, preserving the incumbent numeric units and reset metadata. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionQuotaSnapshot { + /// Entitled quantity in this quota's units. + pub entitlement_requests: f64, + /// Whether the provider reports available quota. + #[serde(skip_serializing_if = "Option::is_none")] + pub has_quota: Option, + /// Whether the entitlement is unlimited. + pub is_unlimited_entitlement: bool, + /// Additional usage in this quota's units. + pub overage: f64, + /// Whether additional usage is allowed after quota exhaustion. + pub overage_allowed_with_exhausted_quota: bool, + /// Additional-usage budget cap, when provided. + #[serde(skip_serializing_if = "Option::is_none")] + pub overage_entitlement: Option, + /// Percentage of the entitlement remaining. + pub remaining_percentage: f64, + /// Quota reset time in milliseconds since the Unix epoch, when known. + #[serde(skip_serializing_if = "Option::is_none")] + pub reset_date_epoch_ms: Option, + /// Whether the reset time is estimated. + #[serde(skip_serializing_if = "Option::is_none")] + pub reset_date_estimated: Option, + /// Whether this quota uses token-based billing. + #[serde(skip_serializing_if = "Option::is_none")] + pub token_based_billing: Option, + /// Whether usage is allowed after quota exhaustion. + pub usage_allowed_with_exhausted_quota: bool, + /// Consumed quantity in this quota's units. + pub used_requests: f64, +} + +/// The incumbent session-owned quota and account projection. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionQuotaProjection { + /// Whether the account can sign up for Copilot Free. + pub can_signup_for_copilot_free: bool, + /// Whether the account can delegate tasks to GitHub. + pub delegate_available: bool, + /// Existing delegation warning for a free account. + #[serde(skip_serializing_if = "Option::is_none")] + pub delegate_warning: Option, + /// Whether dynamic workflows are enabled for the active account. + pub dynamic_workflows_enabled: bool, + /// Whether dynamic workflows are visible under the session's feature flags. + pub dynamic_workflows_ui_visible: bool, + /// Whether the account uses the free limited Copilot plan. + pub is_free_user: bool, + /// Whether the account uses token-based billing. + pub is_tbb_user: bool, + /// Whether model cost columns should be shown. + pub model_cost_column_visible: bool, + /// Resolved Copilot plan tier. + pub plan_tier: SessionQuotaPlanTier, + /// Whether premium-request costs are billable. + pub premium_requests_billable: bool, + /// Provider-owned account quota state, including snapshots without numeric balances. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_quotas: Option>, + /// Known quota snapshots, keyed by the provider's quota type. + pub snapshots: HashMap, + /// Upgrade link for a free account. + #[serde(skip_serializing_if = "Option::is_none")] + pub upgrade_url: Option, +} + /// Session IDs to close, deactivate, and delete from disk. /// ///
    @@ -22588,6 +23920,85 @@ pub struct SessionsCloseRequest { #[serde(rename_all = "camelCase")] pub struct SessionsCloseResult {} +/// A working-directory context together with the client that produced it. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionWorkingDirectoryContextWithClient { + /// Merge-base commit SHA + #[serde(skip_serializing_if = "Option::is_none")] + pub base_commit: Option, + /// Current git branch name + #[serde(skip_serializing_if = "Option::is_none")] + pub branch: Option, + /// Name of the client that created the session + #[serde(skip_serializing_if = "Option::is_none")] + pub client_name: Option, + /// Current working directory path + pub cwd: String, + /// Root directory of the git repository + #[serde(skip_serializing_if = "Option::is_none")] + pub git_root: Option, + /// Head commit of the current git branch + #[serde(skip_serializing_if = "Option::is_none")] + pub head_commit: Option, + /// Hosting platform type of the repository + #[serde(skip_serializing_if = "Option::is_none")] + pub host_type: Option, + /// Repository identifier derived from the git remote URL + #[serde(skip_serializing_if = "Option::is_none")] + pub repository: Option, + /// Raw host string from the git remote URL + #[serde(skip_serializing_if = "Option::is_none")] + pub repository_host: Option, +} + +/// Identity, state location and starting context for a workspace record. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsCreateWorkspaceRequest { + /// Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, + /// `windows` (any letter case) selects Windows path rules. Any other value selects POSIX path rules. + pub convention: String, + /// User-supplied display name for the workspace + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Session ID the workspace record belongs to + pub session_id: SessionId, + /// Directory the session's state is written under when no session filesystem provider is configured. Ignored when a provider is configured; the provider's session state path is used instead. + pub session_state_path: String, +} + +/// The workspace record that was written. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsCreateWorkspaceResult { + /// The created workspace record, as JSON + pub workspace_json: String, +} + /// Session ID to delete from disk. /// ///
    @@ -23282,6 +24693,39 @@ pub struct SessionsLoadDeferredRepoHooksRequest { pub session_id: SessionId, } +/// Where the session's state lives, as a root directory and the session ID under it. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsLoadWorkspaceRequest { + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + pub session_id: SessionId, + /// Root directory every session's state directory sits under + pub sessions_home: String, +} + +/// The workspace record on disk, omitted when the session has none. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsLoadWorkspaceResult { + /// The workspace record, as JSON. Omitted when the record does not exist. + #[serde(skip_serializing_if = "Option::is_none")] + pub workspace_json: Option, +} + /// Age threshold and optional flags controlling which old sessions are pruned (or simulated when dryRun is true). /// ///
    @@ -23510,6 +24954,37 @@ pub struct SessionsTransferRemoteControlRequest { pub to_session_id: String, } +/// Where the session's state lives, plus workspace-schema fields to merge into its workspace record. Stored keys outside the schema are not preserved, and a stored `fork_count` is never replaced. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsUpdateWorkspaceFieldsRequest { + /// Workspace-schema fields to merge into the record, as a JSON object. Fields the object omits keep their stored values, except stored keys outside the schema are not preserved and a stored `fork_count` is never replaced. + pub fields_json: String, + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + pub session_id: SessionId, + /// Root directory every session's state directory sits under + pub sessions_home: String, +} + +/// The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsUpdateWorkspaceFieldsResult {} + /// Telemetry engagement ID for the session, when available. /// ///
    @@ -24378,7 +25853,7 @@ pub struct SkillPlanUninstallRequest { pub policy_session_id: String, } -/// Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily. +/// Authoritative catalog metadata for one SDK-provided skill. The skill's SKILL.md text is fetched separately and lazily. /// ///
    /// @@ -24436,7 +25911,7 @@ pub(crate) struct SkillProviderReadRequest { pub name: String, } -/// Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported. +/// Text-only SKILL.md content returned by an SDK session's skill provider. YAML frontmatter is optional: fields it omits come from the catalog descriptor, fields it declares must match the descriptor, and `allowed-tools` is read only from frontmatter. Related files and assets are not supported. /// ///
    /// @@ -24447,8 +25922,8 @@ pub(crate) struct SkillProviderReadRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct SkillProviderReadResult { - /// Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. - pub markdown: String, + /// SKILL.md text, with or without YAML frontmatter, or null when the provider has no skill with the requested name. The runtime enforces a 1 MiB UTF-8 byte limit. + pub markdown: Option, } /// Skill names to mark as disabled in global configuration, replacing any previous list. @@ -26046,6 +27521,18 @@ pub struct ToolsGetCurrentMetadataResult { #[serde(rename_all = "camelCase")] pub struct ToolsInitializeAndValidateResult {} +/// No parameters; returns the external tool calls of the session and its sub-agents that are still waiting for a result. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolsListPendingRequestsRequest {} + /// Optional model identifier whose tool overrides should be applied to the listing. /// ///
    @@ -26783,150 +28270,7 @@ pub struct UpdateSubagentSettingsRequest { pub subagents: Option, } -/// Request count and cost metrics for this model -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UsageMetricsModelMetricRequests { - /// User-initiated premium request cost (with multiplier applied) - pub cost: f64, - /// Number of API requests made with this model - pub count: i64, -} - -/// Per-model token-detail entry containing the accumulated token count for one token type. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UsageMetricsModelMetricTokenDetail { - /// Accumulated token count for this token type - pub token_count: i64, -} - -/// Token usage metrics for this model -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UsageMetricsModelMetricUsage { - /// Total tokens read from prompt cache - pub cache_read_tokens: i64, - /// Total tokens written to prompt cache - pub cache_write_tokens: i64, - /// Total input tokens consumed - pub input_tokens: i64, - /// Total output tokens produced - pub output_tokens: i64, - /// Total output tokens used for reasoning - #[serde(skip_serializing_if = "Option::is_none")] - pub reasoning_tokens: Option, -} - -/// Per-model usage metrics, including request counts/costs, token usage, nano-AI units, and per-token-type details. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UsageMetricsModelMetric { - /// Latest known prompt-cache expiration for this model. A timestamp in the past indicates that the observed cache has expired. - #[serde(skip_serializing_if = "Option::is_none")] - pub cache_expires_at: Option, - /// Request count and cost metrics for this model - pub requests: UsageMetricsModelMetricRequests, - /// Token count details per type - #[serde(skip_serializing_if = "Option::is_none")] - pub token_details: Option>, - /// Accumulated nano-AI units cost for this model - #[serde(skip_serializing_if = "Option::is_none")] - pub total_nano_aiu: Option, - /// Token usage metrics for this model - pub usage: UsageMetricsModelMetricUsage, -} - -/// Usage attributed to one agent instance, including its identity, API duration, AI units, and per-model breakdown. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UsageMetricsAgentMetric { - /// Human-readable label for this subagent invocation, copied from the originating `subagent.started` event. For task-tool subagents this is the invocation's task description rather than the agent's configured display name, so group by `agentName` for stable per-agent labels. - #[serde(skip_serializing_if = "Option::is_none")] - pub agent_display_name: Option, - /// Configured agent name, when this is a subagent - #[serde(skip_serializing_if = "Option::is_none")] - pub agent_name: Option, - /// Per-model usage for this agent, keyed by model identifier - pub model_metrics: HashMap, - /// Time spent in model API calls by this agent, in milliseconds - pub total_api_duration_ms: i64, - /// Accumulated nano-AI units cost for this agent - pub total_nano_aiu: f64, -} - -/// Aggregated code change metrics -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UsageMetricsCodeChanges { - /// Distinct file paths modified during the session - pub files_modified: Vec, - /// Number of distinct files modified - pub files_modified_count: i64, - /// Total lines of code added - pub lines_added: i64, - /// Total lines of code removed - pub lines_removed: i64, -} - -/// Session-wide token-detail entry containing the accumulated token count for one token type. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UsageMetricsTokenDetail { - /// Accumulated token count for this token type - pub token_count: i64, -} - -/// Accumulated session usage metrics, including premium request cost, token counts, model breakdown, and code-change totals. +/// Internal absolute code-change totals reported by the owning host. /// ///
    /// @@ -26936,35 +28280,14 @@ pub struct UsageMetricsTokenDetail { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct UsageGetMetricsResult { - /// Per-agent usage metrics, keyed by agent instance identifier. The main conversation uses the stable key `main`. - #[serde(skip_serializing_if = "Option::is_none")] - pub agent_metrics: Option>, - /// Aggregated code change metrics - pub code_changes: UsageMetricsCodeChanges, - /// Currently active model identifier +pub(crate) struct UsageSetCodeChangesRequest { + /// Absolute changed-file count; omission preserves the previous count. #[serde(skip_serializing_if = "Option::is_none")] - pub current_model: Option, - /// Input tokens from the most recent main-agent API call - pub last_call_input_tokens: i64, - /// Output tokens from the most recent main-agent API call - pub last_call_output_tokens: i64, - /// Per-model token and request metrics, keyed by model identifier - pub model_metrics: HashMap, - /// ISO 8601 timestamp when the session started - pub session_start_time: String, - /// Session-wide per-token-type accumulated token counts - #[serde(skip_serializing_if = "Option::is_none")] - pub token_details: Option>, - /// Total time spent in model API calls (milliseconds) - pub total_api_duration_ms: i64, - /// Session-wide accumulated nano-AI units cost - #[serde(skip_serializing_if = "Option::is_none")] - pub total_nano_aiu: Option, - /// Total user-initiated premium request cost across all models (may be fractional due to multipliers) - pub total_premium_request_cost: f64, - /// Raw count of user-initiated API requests - pub total_user_requests: i64, + pub files_count: Option, + /// Absolute added-line total, replacing the previous reading. + pub lines_added: f64, + /// Absolute removed-line total, replacing the previous reading. + pub lines_removed: f64, } /// Result of a user-requested shell command. @@ -27011,7 +28334,7 @@ pub struct UserSettingMetadata { pub value: serde_json::Value, } -/// Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. +/// Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. /// ///
    /// @@ -27041,21 +28364,6 @@ pub struct UserSettingsSetRequest { pub settings: serde_json::Value, } -/// Outcome of writing user settings. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UserSettingsSetResult { - /// Top-level keys whose write landed in settings.json but is shadowed by a value still present in the legacy config.json (config.json wins on read). The write does not take effect until the legacy value is removed. - pub shadowed_keys: Vec, -} - /// Current sharing status and shareable GitHub URL for a session. /// ///
    @@ -28053,6 +29361,25 @@ pub(crate) struct WorkflowToolRunRequest { pub tool_call_id: Option, } +/// Complete text used to generate one session diff. These are display contents, with the same text decoding as the patch, not a file-restore contract. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceDiffContents { + /// Complete current text read when computing the diff. Omitted for a deleted file; an empty string represents an existing empty file. + #[serde(skip_serializing_if = "Option::is_none")] + pub after: Option, + /// Complete text before the session first changed the file. Omitted when the file did not exist; an empty string represents an existing empty file. + #[serde(skip_serializing_if = "Option::is_none")] + pub before: Option, +} + /// A single changed file and its unified diff. /// ///
    @@ -28066,6 +29393,9 @@ pub(crate) struct WorkflowToolRunRequest { pub struct WorkspaceDiffFileChange { /// Type of change represented by this file diff. pub change_type: WorkspaceDiffFileChangeType, + /// Full text used for this patch, only when includeContents was requested for session mode. Omitted for binary, oversized or unavailable contents, and for fallback results. Read isFallback and isTruncated before treating an absent value as a missing file. + #[serde(skip_serializing_if = "Option::is_none")] + pub contents: Option, /// Unified diff content for the file. Empty when the diff was truncated. pub diff: String, /// Whether the diff content was omitted because it exceeded the per-file size limit. @@ -28243,6 +29573,51 @@ pub struct WorkspacesDiffRequest { pub mode: WorkspaceDiffMode, } +/// Extensible [`WorkspacesDiffRequest`], including inputs added after it was published. +/// +/// Required inputs are [`WorkspacesDiffOptions::new`] arguments; optional inputs have fluent setters. +/// Input-only: it serialises to the flat wire request and is not deserialisable. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspacesDiffOptions { + #[serde(flatten)] + legacy: WorkspacesDiffRequest, + #[serde(skip_serializing_if = "Option::is_none")] + include_contents: Option, +} + +impl WorkspacesDiffOptions { + /// Creates options with the required inputs. + pub fn new(mode: WorkspaceDiffMode) -> Self { + Self { + legacy: WorkspacesDiffRequest { + mode, + ignore_whitespace: None, + }, + include_contents: None, + } + } + + /// When true, ignore whitespace-only changes (git `--ignore-all-space`). Defaults to false. + pub fn ignore_whitespace(mut self, value: bool) -> Self { + self.legacy.ignore_whitespace = Some(value); + self + } + + /// Include the full before/after text used to compute each session diff. Defaults to false; true is accepted only for session mode. Existing capture/read limits still apply, and binary or unavailable contents are not returned. This can substantially increase response size. + pub fn include_contents(mut self, value: bool) -> Self { + self.include_contents = Some(value); + self + } +} + /// Optional session context used when creating a local workspace. /// ///
    @@ -29183,6 +30558,81 @@ pub struct InstructionsGetDiscoveryPathsResult { pub paths: Vec, } +/// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GlobalStateLoadForConfigDirResult { + /// Whether the user has answered the prompt suggesting they install the desktop app. + #[serde(skip_serializing_if = "Option::is_none")] + pub app_install_nudge_responded: Option, + /// Whether the app tip has been shown. + #[serde(skip_serializing_if = "Option::is_none")] + pub app_tip_shown: Option, + /// Terminals the user has already been asked to set up, so the host does not ask twice. + #[serde(skip_serializing_if = "Option::is_none")] + pub asked_setup_terminals: Option>, + /// When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the once-per-day cap for non-staff users across restarts. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_feedback_last_prompted_at: Option, + /// When the host first ran on this machine. + #[serde(skip_serializing_if = "Option::is_none")] + pub first_launch_at: Option, + /// Plugins installed on this machine. + #[serde(skip_serializing_if = "Option::is_none")] + pub installed_plugins: Option>, + /// Account used for the most recent sign-in. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) last_logged_in_user: Option, + /// Every account the host has signed in to on this machine. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) logged_in_users: Option>, + /// Whether the one-off cleanup of stored reasoning summaries has run. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_summaries_cleanup_done: Option, + /// Models the user selected recently, most recent first. + #[serde(skip_serializing_if = "Option::is_none")] + pub recent_model_ids: Option>, + /// Whether the user declined to trust the sandbox credential proxy CA. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_credential_proxy_ca_declined: Option, + /// Whether the sandbox onboarding has been shown. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_onboarding_shown: Option, + /// Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only behavior. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff: Option, + /// Whether the user was recognized as GitHub staff. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_github: Option, + /// When the staff-only log level migration last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_log_level_migration_at: Option, + /// Whether the user was recognized as Microsoft staff. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_microsoft: Option, + /// When the staff-only model reset last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_model_reset_at: Option, + /// When the staff-only update channel migration last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_update_channel_migration_at: Option, + /// Folders where the user declined the init prompt, so it stays hidden there. + #[serde(skip_serializing_if = "Option::is_none")] + pub suppress_init_folders: Option>, + /// Folders the user has marked as trusted. + #[serde(skip_serializing_if = "Option::is_none")] + pub trusted_folders: Option>, +} + /// Slash commands available in the session, after applying any include/exclude filters. /// ///
    @@ -29198,6 +30648,57 @@ pub struct CommandsListResult { pub commands: Vec, } +/// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubOwnersNextRequestIdResult { + /// Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`. + pub request_id: i64, +} + +/// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitWorkingDirectoryContextResult { + /// Merge-base commit SHA (fork point from the remote default branch) + #[serde(skip_serializing_if = "Option::is_none")] + pub base_commit: Option, + /// Current git branch name + #[serde(skip_serializing_if = "Option::is_none")] + pub branch: Option, + /// Current working directory path + pub cwd: String, + /// Root directory of the git repository, resolved via git rev-parse + #[serde(skip_serializing_if = "Option::is_none")] + pub git_root: Option, + /// Head commit of the current git branch + #[serde(skip_serializing_if = "Option::is_none")] + pub head_commit: Option, + /// Hosting platform type of the repository + #[serde(skip_serializing_if = "Option::is_none")] + pub host_type: Option, + /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps) + #[serde(skip_serializing_if = "Option::is_none")] + pub repository: Option, + /// Raw host string from the git remote URL (e.g. "github.com", "dev.azure.com") + #[serde(skip_serializing_if = "Option::is_none")] + pub repository_host: Option, +} + /// Result of opening a session. /// ///
    @@ -29465,6 +30966,86 @@ pub struct SessionsGetRemoteControlStatusResult { pub status: serde_json::Value, } +/// Feature availability. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorsGetCapabilitiesResult { + /// API version. + pub api_version: i64, + /// Availability. + pub availability: ConnectorDiscoveryAvailability, + /// Whether results are cached. + pub conditional_cache: bool, + /// Whether accounts are selected by opaque ID. + pub opaque_account_selection: bool, +} + +/// Eligible accounts. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorsGetAccountsResult { + /// Eligible accounts. + pub accounts: Vec, + /// Availability. + pub availability: ConnectorDiscoveryAvailability, +} + +/// Entries for the selected account. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorsListResult { + /// Opaque account ID. + pub account_id: String, + /// Entries. + pub connectors: Vec, + /// Refresh time in Unix epoch milliseconds. + pub refreshed_at_ms: i64, + /// Revision. + pub revision: i64, +} + +/// Entries for the selected account. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorsRefreshResult { + /// Opaque account ID. + pub account_id: String, + /// Entries. + pub connectors: Vec, + /// Refresh time in Unix epoch milliseconds. + pub refreshed_at_ms: i64, + /// Revision. + pub revision: i64, +} + /// Identifies the target session. /// ///
    @@ -30495,6 +32076,12 @@ pub struct SessionModelGetCurrentResult { /// Captured base model to restore when leaving plan mode. Omitted outside plan mode or when no plan override has captured a base model. Persistent agent model requirements apply to this model rather than the temporary plan model. #[serde(skip_serializing_if = "Option::is_none")] pub plan_base_model_id: Option, + /// Provider of planBaseModelId, when that saved selection is provider-qualified. + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_base_provider_id: Option, + /// Provider selected for this model. Together with modelId, identifies a catalog entry even when providers serve the same model. Omitted for unattributed legacy selections. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_id: Option, /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, @@ -30543,7 +32130,76 @@ pub struct SessionModelSwitchToResult { pub warning: Option, } -/// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. +/// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionModelSwitchAutoTierResult { + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + #[serde(skip_serializing_if = "Option::is_none")] + pub activating_auto_tier: Option, + /// Auto preference currently committed for the session. + #[serde(skip_serializing_if = "Option::is_none")] + pub effective_auto_tier: Option, + /// Latest unclaimed Auto preference waiting for a future user turn. + #[serde(skip_serializing_if = "Option::is_none")] + pub pending_auto_tier: Option, + /// Immediate request status. `pending` means accepted but not committed. + pub status: ModelSwitchAutoTierStatus, + /// Earlier unclaimed preference replaced by this request. This can be present with either status, including when selecting the effective preference cancels pending work. + #[serde(skip_serializing_if = "Option::is_none")] + pub superseded_auto_tier: Option, +} + +/// The model identifier active on the session after the switch. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionModelApplyStartupOverlayResult { + /// Compaction confirmation projection when status is confirmation_required + #[serde(skip_serializing_if = "Option::is_none")] + pub confirmation: Option, + /// True when the switch was deferred (enqueued as a cancellable `/model` command) because a turn was active or another model change was already queued, rather than applied immediately. When true, the session's live model is unchanged until the queued change drains. + #[serde(skip_serializing_if = "Option::is_none")] + pub deferred: Option, + /// Deprecation warnings associated with the selected model or options. + #[serde(skip_serializing_if = "Option::is_none")] + pub deprecation_warnings: Option>, + /// User-facing outcome message for the model switch. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + /// Currently active model identifier after the switch + #[serde(skip_serializing_if = "Option::is_none")] + pub model_id: Option, + /// Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_state: Option, + /// Persistence failure encountered after applying the model switch. + #[serde(skip_serializing_if = "Option::is_none")] + pub persistence_error: Option, + /// Stable queue item identifier when this request was enqueued. Remains present if the item drains before the response is returned. + #[serde(skip_serializing_if = "Option::is_none")] + pub queue_id: Option, + /// Lifecycle result for the requested switch + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, + /// User-facing warning produced while applying the model switch. + #[serde(skip_serializing_if = "Option::is_none")] + pub warning: Option, +} + /// ///
    /// @@ -30553,24 +32209,13 @@ pub struct SessionModelSwitchToResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionModelSwitchAutoTierResult { - /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. - #[serde(skip_serializing_if = "Option::is_none")] - pub activating_auto_tier: Option, - /// Auto preference currently committed for the session. - #[serde(skip_serializing_if = "Option::is_none")] - pub effective_auto_tier: Option, - /// Latest unclaimed Auto preference waiting for a future user turn. +pub(crate) struct SessionModelClearStartupSeedParams { + pub expected_model: String, + /// Provider captured with the startup seed; omission matches only an unqualified seed. #[serde(skip_serializing_if = "Option::is_none")] - pub pending_auto_tier: Option, - /// Immediate request status. `pending` means accepted but not committed. - pub status: ModelSwitchAutoTierStatus, - /// Earlier unclaimed preference replaced by this request. This can be present with either status, including when selecting the effective preference cancels pending work. - #[serde(skip_serializing_if = "Option::is_none")] - pub superseded_auto_tier: Option, + pub expected_provider_id: Option, } -/// The model identifier active on the session after the switch. /// ///
    /// @@ -30580,37 +32225,8 @@ pub struct SessionModelSwitchAutoTierResult { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionModelApplyStartupOverlayResult { - /// Compaction confirmation projection when status is confirmation_required - #[serde(skip_serializing_if = "Option::is_none")] - pub confirmation: Option, - /// True when the switch was deferred (enqueued as a cancellable `/model` command) because a turn was active or another model change was already queued, rather than applied immediately. When true, the session's live model is unchanged until the queued change drains. - #[serde(skip_serializing_if = "Option::is_none")] - pub deferred: Option, - /// Deprecation warnings associated with the selected model or options. - #[serde(skip_serializing_if = "Option::is_none")] - pub deprecation_warnings: Option>, - /// User-facing outcome message for the model switch. - #[serde(skip_serializing_if = "Option::is_none")] - pub message: Option, - /// Currently active model identifier after the switch - #[serde(skip_serializing_if = "Option::is_none")] - pub model_id: Option, - /// Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. - #[serde(skip_serializing_if = "Option::is_none")] - pub model_state: Option, - /// Persistence failure encountered after applying the model switch. - #[serde(skip_serializing_if = "Option::is_none")] - pub persistence_error: Option, - /// Stable queue item identifier when this request was enqueued. Remains present if the item drains before the response is returned. - #[serde(skip_serializing_if = "Option::is_none")] - pub queue_id: Option, - /// Lifecycle result for the requested switch - #[serde(skip_serializing_if = "Option::is_none")] - pub status: Option, - /// User-facing warning produced while applying the model switch. - #[serde(skip_serializing_if = "Option::is_none")] - pub warning: Option, +pub(crate) struct SessionModelClearStartupSeedResult { + pub cleared: bool, } /// The applied host allowlist and effective session model policy after intersection. @@ -30664,6 +32280,9 @@ pub struct SessionModelSetReasoningEffortResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionModelListResult { + /// Ordered Auto routing preferences discovered for this session's account. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto: Option, /// Available models, ordered with the most preferred default first. Includes both Copilot (CAPI) models and any registry BYOK models; a BYOK model appears under its provider-qualified selection id (`provider/id`). pub list: Vec, /// Cost categories for the full CAPI catalog, including picker-disabled models that Auto may select. Metadata only; entries absent from `list` are not manually selectable. @@ -32167,6 +33786,36 @@ pub struct SessionMcpListResult { pub servers: Vec, } +/// Identifies the target session. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpListConfiguredParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Effective MCP configuration with optional live observations from matching already materialized servers. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpListConfiguredResult { + /// Effective configured MCP servers. + pub servers: Vec, +} + /// Tools exposed by the connected MCP server. Throws when the server is not connected. /// ///
    @@ -33385,6 +35034,36 @@ pub struct SessionExtensionsReloadParams { pub session_id: SessionId, } +/// Identifies the target session. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionExtensionsReconcileParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Extensions discovered for the session, with their current status. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionExtensionsReconcileResult { + /// Discovered extensions and their current status + pub extensions: Vec, +} + /// Rust-owned built-in tool descriptors for the session. /// ///
    @@ -33446,6 +35125,21 @@ pub struct SessionToolsHandlePendingToolCallResult { pub success: bool, } +/// External tool calls still waiting for a result. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionToolsListPendingRequestsResult { + /// External tool calls of the session and its sub-agents that session.tools.handlePendingToolCall would still accept, in the order they were requested. Includes calls that were pending when the session was last suspended and resumed with pending work continued, so a host that did not observe the original external_tool.requested event can answer them. + pub items: Vec, +} + /// Identifies the target session. /// ///
    @@ -34384,20 +36078,26 @@ pub struct SessionMetadataActivityResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionMetadataContextInfoResultContextInfo { - /// Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) + /// Output reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%). pub buffer_tokens: i64, /// Token count at which background compaction starts (configurable percentage of promptTokenLimit) pub compaction_threshold: i64, /// Tokens consumed by user/assistant/tool messages pub conversation_tokens: i64, - /// Prompt token limit plus the model's full output token limit. + /// Provider-owned model display label, or the Auto routing label. Presentation only; modelName remains the tokenization identity. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_model_name: Option, + /// Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays. pub limit: i64, /// Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools) pub mcp_tools_tokens: i64, /// The model used for token counting pub model_name: String, - /// Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) + /// Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified. pub prompt_token_limit: i64, + /// Provider whose catalog supplies this context window. Omitted when provider identity is unavailable. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider: Option, /// Tokens consumed by the system prompt pub system_tokens: i64, /// Tokens consumed by tool definitions sent to the model (excludes deferred tools) @@ -34440,7 +36140,7 @@ pub struct SessionMetadataGetContextAttributionParams { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionMetadataGetContextAttributionResultContextAttributionCategories { - /// Output reserve plus post-blocking-threshold buffer. + /// Overlapping output reservation plus post-blocking-threshold buffer. pub buffer: i64, /// Custom-instructions tokens (0 when none are configured). pub custom_instructions: i64, @@ -34487,7 +36187,7 @@ pub struct SessionMetadataGetContextAttributionResultContextAttributionEntriesIt #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionMetadataGetContextAttributionResultContextAttribution { - /// Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. + /// Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. pub buffer_tokens: i64, /// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. pub categories: SessionMetadataGetContextAttributionResultContextAttributionCategories, @@ -34497,13 +36197,13 @@ pub struct SessionMetadataGetContextAttributionResultContextAttribution { pub compaction_threshold: i64, /// Flat list of per-source attribution entries. Group by `kind` and render unrecognized kinds generically. Nesting and rollups are expressed via `parentId`. pub entries: Vec, - /// Prompt limit plus the model's output reserve: the full context window `categories.freeSpace` and `categories.buffer` are measured against. Mirrors `SessionContextInfo.limit`. + /// Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`. pub limit: i64, /// The concrete model id the entire breakdown was tokenized against (feeds the per-model token multiplier). Under `Auto` (Free/Student) this is the resolved model, not the literal `auto` sentinel, so totals are not undercounted. A single-model approximation of a potentially multi-model Auto session. pub model_id: String, /// How `modelId` was chosen. Not a closed set — tolerate unknown values. Known values today: `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected model), `default` (a fallback before any model is known). pub model_source: String, - /// Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. + /// Effective input budget after reserving requested output against the combined context ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. pub prompt_token_limit: i64, /// Total token count of the current context window the entries are measured against (system message + conversation messages + tool definitions — the same total reported by /context). Divide an entry's `tokens` by this to derive its share. pub total_tokens: i64, @@ -35421,7 +37121,7 @@ pub struct SessionUsageGetMetricsParams { pub session_id: SessionId, } -/// Accumulated session usage metrics, including premium request cost, token counts, model breakdown, and code-change totals. +/// Internal absolute code-change totals reported by the owning host. /// ///
    /// @@ -35431,35 +37131,143 @@ pub struct SessionUsageGetMetricsParams { ///
    #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionUsageGetMetricsResult { - /// Per-agent usage metrics, keyed by agent instance identifier. The main conversation uses the stable key `main`. +pub(crate) struct SessionUsageSetCodeChangesParams { + /// Absolute added-line total, replacing the previous reading. + pub lines_added: f64, + /// Absolute removed-line total, replacing the previous reading. + pub lines_removed: f64, + /// Absolute changed-file count; omission preserves the previous count. #[serde(skip_serializing_if = "Option::is_none")] - pub agent_metrics: Option>, - /// Aggregated code change metrics - pub code_changes: UsageMetricsCodeChanges, - /// Currently active model identifier + pub files_count: Option, +} + +/// Identifies the target session. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionQuotaGetParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// The incumbent session-owned quota and account projection. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionQuotaGetResult { + /// Whether the account can sign up for Copilot Free. + pub can_signup_for_copilot_free: bool, + /// Whether the account can delegate tasks to GitHub. + pub delegate_available: bool, + /// Existing delegation warning for a free account. + #[serde(skip_serializing_if = "Option::is_none")] + pub delegate_warning: Option, + /// Whether dynamic workflows are enabled for the active account. + pub dynamic_workflows_enabled: bool, + /// Whether dynamic workflows are visible under the session's feature flags. + pub dynamic_workflows_ui_visible: bool, + /// Whether the account uses the free limited Copilot plan. + pub is_free_user: bool, + /// Whether the account uses token-based billing. + pub is_tbb_user: bool, + /// Whether model cost columns should be shown. + pub model_cost_column_visible: bool, + /// Resolved Copilot plan tier. + pub plan_tier: SessionQuotaPlanTier, + /// Whether premium-request costs are billable. + pub premium_requests_billable: bool, + /// Provider-owned account quota state, including snapshots without numeric balances. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_quotas: Option>, + /// Known quota snapshots, keyed by the provider's quota type. + pub snapshots: HashMap, + /// Upgrade link for a free account. + #[serde(skip_serializing_if = "Option::is_none")] + pub upgrade_url: Option, +} + +/// Identifies the target session. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionQuotaRefreshParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// The incumbent session-owned quota and account projection. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionQuotaRefreshResult { + /// Whether the account can sign up for Copilot Free. + pub can_signup_for_copilot_free: bool, + /// Whether the account can delegate tasks to GitHub. + pub delegate_available: bool, + /// Existing delegation warning for a free account. #[serde(skip_serializing_if = "Option::is_none")] - pub current_model: Option, - /// Input tokens from the most recent main-agent API call - pub last_call_input_tokens: i64, - /// Output tokens from the most recent main-agent API call - pub last_call_output_tokens: i64, - /// Per-model token and request metrics, keyed by model identifier - pub model_metrics: HashMap, - /// ISO 8601 timestamp when the session started - pub session_start_time: String, - /// Session-wide per-token-type accumulated token counts - #[serde(skip_serializing_if = "Option::is_none")] - pub token_details: Option>, - /// Total time spent in model API calls (milliseconds) - pub total_api_duration_ms: i64, - /// Session-wide accumulated nano-AI units cost - #[serde(skip_serializing_if = "Option::is_none")] - pub total_nano_aiu: Option, - /// Total user-initiated premium request cost across all models (may be fractional due to multipliers) - pub total_premium_request_cost: f64, - /// Raw count of user-initiated API requests - pub total_user_requests: i64, + pub delegate_warning: Option, + /// Whether dynamic workflows are enabled for the active account. + pub dynamic_workflows_enabled: bool, + /// Whether dynamic workflows are visible under the session's feature flags. + pub dynamic_workflows_ui_visible: bool, + /// Whether the account uses the free limited Copilot plan. + pub is_free_user: bool, + /// Whether the account uses token-based billing. + pub is_tbb_user: bool, + /// Whether model cost columns should be shown. + pub model_cost_column_visible: bool, + /// Resolved Copilot plan tier. + pub plan_tier: SessionQuotaPlanTier, + /// Whether premium-request costs are billable. + pub premium_requests_billable: bool, + /// Provider-owned account quota state, including snapshots without numeric balances. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_quotas: Option>, + /// Known quota snapshots, keyed by the provider's quota type. + pub snapshots: HashMap, + /// Upgrade link for a free account. + #[serde(skip_serializing_if = "Option::is_none")] + pub upgrade_url: Option, +} + +/// Identifies the target session. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionQuotaTakeWarningsParams { + /// Target session identifier + pub session_id: SessionId, } /// GitHub URL for the session and a flag indicating whether remote steering is enabled. @@ -35929,7 +37737,17 @@ pub type McpExecuteSamplingResult = HashMap; ///
    pub type McpPlanSecretReference = String; -/// Extensible identifier of a sandbox policy feature whose availability varies between hosts. A plain string, so an older client decodes a name added by a newer runtime; ignore names you do not recognize. Known values: `network` — sandboxed commands can reach the network (`network.allowOutbound`, on by default); on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns. `network_filtering` — host rules and the sandbox proxy (`network.allowedHosts`, `network.blockedHosts`, `network.proxy`); on Linux this needs the same tooling as `network`; on Windows it needs a version with Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`, because Windows reaches the local proxy only together with private-network access. `denied_paths` — native enforcement of `filesystem.deniedPaths`; on Windows this needs a version whose sandbox contract reports denied-path support. `shell` — shell commands inside the sandbox: bash on macOS and Linux, PowerShell on Windows. `filesystem_enumeration` — enumerate-only filesystem grants, which PowerShell's drive roots use on Windows; this needs a version with Process Security Environment 1.1 filesystem enumeration support. Without it, sandboxed PowerShell still runs, but `Get-Location` may report the drive root, `Set-Location` may fail, and relative paths may resolve against the drive root; the session also receives a `session.warning` with `warningType` `sandbox`. Other platforms always report it. +/// Pending warnings returned and cleared by session.quota.takeWarnings. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +pub type QuotaTakeWarningsResult = Vec; + +/// Extensible identifier of a sandbox policy feature whose availability varies between hosts. A plain string, so an older client decodes a name added by a newer runtime; ignore names you do not recognize. Known values: `network` — sandboxed commands can reach the network (`network.allowOutbound`, on by default); on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns. `network_filtering` — host rules and the sandbox proxy (`network.allowedHosts`, `network.blockedHosts`, `network.proxy`); on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support or MXC's PSEC 1.0-only proxy-loopback compatibility capability, and a policy that uses it must also set `network.allowLocalNetwork`, because Windows reaches the local proxy only together with private-network access. Compatibility applies only to an explicit identity-less runtime proxy, not general host-loopback access, and other policy restrictions still apply. `denied_paths` — native enforcement of `filesystem.deniedPaths`; on Windows this needs a version whose sandbox contract reports denied-path support. `shell` — shell commands inside the sandbox: bash on macOS and Linux, PowerShell on Windows. `filesystem_enumeration` — enumerate-only filesystem grants, which PowerShell's drive roots use on Windows; this needs a version with Process Security Environment 1.1 filesystem enumeration support. Without it, sandboxed PowerShell still runs, but `Get-Location` may report the drive root, `Set-Location` may fail, and relative paths may resolve against the drive root; the session also receives a `session.warning` with `warningType` `sandbox`. Other platforms always report it. /// ///
    /// @@ -36059,6 +37877,16 @@ pub type SessionMetadataGetClientMetadataResult = HashMap; ///
    pub type SessionMetadataUpdateClientMetadataResult = HashMap; +/// Pending warnings returned and cleared by session.quota.takeWarnings. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +pub type SessionQuotaTakeWarningsResult = Vec; + /// Indicates whether the command was accepted into the local execution queue. /// ///
    @@ -36138,6 +37966,14 @@ pub enum UserAuthInfoType { User, } +/// Provider-owned account authentication. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AccountAuthInfoType { + #[serde(rename = "account")] + #[default] + Account, +} + /// Authentication via the `gh` CLI's saved credentials. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum GhCliAuthInfoType { @@ -36171,6 +38007,7 @@ pub enum AuthInfo { TokenProvider(TokenProviderAuthInfo), CopilotApiToken(CopilotApiTokenAuthInfo), User(UserAuthInfo), + Account(AccountAuthInfo), GhCli(GhCliAuthInfo), ApiKey(ApiKeyAuthInfo), } @@ -36877,6 +38714,9 @@ pub enum AuthInfoType { /// Authentication from an interactive user sign-in. #[serde(rename = "user")] User, + /// Authentication from a selected provider-owned account, without a GitHub credential. + #[serde(rename = "account")] + Account, /// Authentication delegated to the GitHub CLI. #[serde(rename = "gh-cli")] GhCli, @@ -36938,7 +38778,7 @@ pub enum AuthLoginStepCompletedKind { Completed, } -/// Terminal disposition of a login persistence attempt. +/// Disposition of a login attempt, including pending user decisions. /// ///
    /// @@ -36948,12 +38788,15 @@ pub enum AuthLoginStepCompletedKind { ///
    #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum AuthLoginResultStatus { - /// The credential was persisted and the account is signed in. + /// The credential was persisted and the selected account is signed in. #[serde(rename = "completed")] Completed, /// Persistence needs explicit consent to store the token in plaintext. #[serde(rename = "needs-plaintext-consent")] NeedsPlaintextConsent, + /// Credentials are saved; select an account using a returned selectionId as advance input to complete sign-in. + #[serde(rename = "needs-account-selection")] + NeedsAccountSelection, /// The user declined plaintext persistence. #[serde(rename = "declined")] Declined, @@ -38652,6 +40495,31 @@ pub enum ConnectorConnectResult { Pending(ConnectorConnectResultPending), } +/// Availability. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ConnectorDiscoveryAvailability { + /// Enabled. + #[serde(rename = "enabled")] + Enabled, + /// Disabled. + #[serde(rename = "disabled")] + Disabled, + /// Unavailable. + #[serde(rename = "unavailable")] + Unavailable, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Closed set of public task kinds a connection can negotiate. /// ///
    @@ -41827,28 +43695,6 @@ pub enum ModelPolicyState { Unknown, } -/// The neutral kind of a model provider — the model analog of `AccountKind`. A model provider is the live, entitled source a model came from; central code never branches on this beyond a single dispatch. -/// -///
    -/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
    -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum ModelProviderKind { - /// GitHub Copilot / CAPI models, spawned by a github-resolving account that holds a Copilot seat. - #[serde(rename = "copilot")] - Copilot, - /// Microsoft 365 Copilot (Loki) inference models, spawned by a resolvable Entra-derived Loki account. - #[serde(rename = "loki")] - Loki, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// When the runtime may run an adapter without an explicit user action. /// ///
    @@ -44122,6 +45968,46 @@ pub enum SessionsOpenStatus { Unknown, } +/// Copilot plan tier used by the session quota projection. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SessionQuotaPlanTier { + /// Copilot Free. + #[serde(rename = "free")] + Free, + /// Copilot education entitlement. + #[serde(rename = "edu")] + Edu, + /// Copilot Pro. + #[serde(rename = "pro")] + Pro, + /// Copilot Pro+. + #[serde(rename = "pro_plus")] + ProPlus, + /// Copilot Business. + #[serde(rename = "business")] + Business, + /// Copilot Enterprise. + #[serde(rename = "enterprise")] + Enterprise, + /// Copilot Max. + #[serde(rename = "max")] + Max, + /// The account's plan tier is not known. + #[serde(rename = "unknown")] + UnknownValue, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// SDK-side token authentication; the host configured the token directly via the SDK. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum SettableTokenAuthInfoType { @@ -44146,6 +46032,7 @@ pub enum SettableAuthInfo { Token(SettableTokenAuthInfo), CopilotApiToken(CopilotApiTokenAuthInfo), User(UserAuthInfo), + Account(AccountAuthInfo), GhCli(GhCliAuthInfo), ApiKey(ApiKeyAuthInfo), } diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index cf5da5b4a5..8e57a670bd 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -11,7 +11,7 @@ #![allow(dead_code)] use super::api_types::{rpc_methods, *}; -use super::session_events::SessionMode; +use super::session_events::{SessionMode, UsageGetMetricsResult}; use crate::session::Session; use crate::{Client, Error}; @@ -57,6 +57,13 @@ impl<'a> ClientRpc<'a> { } } + /// `connectors.*` sub-namespace. + pub fn connectors(&self) -> ClientRpcConnectors<'a> { + ClientRpcConnectors { + client: self.client, + } + } + /// `environments.*` sub-namespace. pub fn environments(&self) -> ClientRpcEnvironments<'a> { ClientRpcEnvironments { @@ -71,6 +78,34 @@ impl<'a> ClientRpc<'a> { } } + /// `git.*` sub-namespace. + pub fn git(&self) -> ClientRpcGit<'a> { + ClientRpcGit { + client: self.client, + } + } + + /// `gitHubOwners.*` sub-namespace. + pub fn git_hub_owners(&self) -> ClientRpcGitHubOwners<'a> { + ClientRpcGitHubOwners { + client: self.client, + } + } + + /// `gitHubRepository.*` sub-namespace. + pub fn git_hub_repository(&self) -> ClientRpcGitHubRepository<'a> { + ClientRpcGitHubRepository { + client: self.client, + } + } + + /// `globalState.*` sub-namespace. + pub fn global_state(&self) -> ClientRpcGlobalState<'a> { + ClientRpcGlobalState { + client: self.client, + } + } + /// `hooks.*` sub-namespace. pub fn hooks(&self) -> ClientRpcHooks<'a> { ClientRpcHooks { @@ -475,18 +510,610 @@ pub struct ClientRpcAgents<'a> { pub(crate) client: &'a Client, } -impl<'a> ClientRpcAgents<'a> { - /// Discovers custom agents across user, project, plugin, and remote sources. +impl<'a> ClientRpcAgents<'a> { + /// Discovers custom agents across user, project, plugin, and remote sources. + /// + /// Wire method: `agents.discover`. + /// + /// # Parameters + /// + /// * `params` - Optional project paths to include in agent discovery. + /// + /// # Returns + /// + /// Agents discovered across user, project, plugin, and remote sources. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn discover(&self, params: AgentsDiscoverRequest) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::AGENTS_DISCOVER, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Returns the canonical directories where a client may create custom agents that the runtime will recognize, including ones that do not exist yet. Project directories become active once created. + /// + /// Wire method: `agents.getDiscoveryPaths`. + /// + /// # Parameters + /// + /// * `params` - Optional project paths to include when enumerating agent discovery directories. + /// + /// # Returns + /// + /// Canonical locations where custom agents can be created so the runtime will recognize them. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn get_discovery_paths( + &self, + params: AgentsGetDiscoveryPathsRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::AGENTS_GETDISCOVERYPATHS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Lists the agents this runtime ships, by name. A consumer separating shipped agents from ones the user or a plugin authored should compare against these names rather than against `AgentInfo.source`: an authored agent may carry the `builtin` source while not being one of these, and the runtime treats the two as separate questions. `disableableNames` is the subset a user may turn off, which a client needs to decide whether to offer a toggle. `yamlBasedNames` is the subset backed by a shipped YAML definition, which a client needs before asking the runtime to load one. + /// + /// Wire method: `agents.getBuiltins`. + /// + /// # Returns + /// + /// The agents this runtime ships, named so a consumer can tell them apart from authored ones. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub(crate) async fn get_builtins(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::AGENTS_GETBUILTINS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Lists the shipped agents a client should offer right now, filtered by the feature flags it passes. `getBuiltins` names every agent the runtime knows about; some of those are gated, so a client rendering a picker wants this narrower list together with the description to show beside each name. + /// + /// Wire method: `agents.getAvailableBuiltins`. + /// + /// # Parameters + /// + /// * `params` - The feature flags to evaluate shipped agents against. + /// + /// # Returns + /// + /// The shipped agents available under the requested flags. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub(crate) async fn get_available_builtins( + &self, + params: AgentsGetAvailableBuiltinsRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::AGENTS_GETAVAILABLEBUILTINS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Loads one shipped agent's YAML definition, for a client that needs what the agent declares rather than only its name. `getBuiltins` reports which names have a definition to load: a name outside its `yamlBasedNames` is special-cased in code and has none. The definition crosses as its own JSON rather than as contract-typed fields, because the runtime parses it with the agent schema's tolerant shape and re-typing it here would drop the keys that shape accepts and this one does not. The projected `__nativeCustomAgent` view the runtime derives is included, so a caller reading the declared model and a caller rendering the agent see the same definition. + /// + /// Wire method: `agents.getBuiltinDefinition`. + /// + /// # Parameters + /// + /// * `params` - The shipped agent whose definition to load. + /// + /// # Returns + /// + /// One shipped agent's definition. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub(crate) async fn get_builtin_definition( + &self, + params: AgentsGetBuiltinDefinitionRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::AGENTS_GETBUILTINDEFINITION, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Projects one shipped agent the way a picker lists it, reading only the metadata at the head of the definition file and stopping before the prompt body. `getBuiltinDefinition` answers the whole definition instead, so a client listing every shipped agent should prefer this one: the cost of a listing grows with the number of agents, and the prompt body is the part a listing never shows. The two also differ in shape. This returns the projected custom agent on its own, whereas `getBuiltinDefinition` returns the authored definition with that projection nested under `__nativeCustomAgent`. + /// + /// Wire method: `agents.getBuiltinListingDefinition`. + /// + /// # Parameters + /// + /// * `params` - The shipped agent whose listing entry to load. + /// + /// # Returns + /// + /// One shipped agent, projected for a listing. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub(crate) async fn get_builtin_listing_definition( + &self, + params: AgentsGetBuiltinListingDefinitionRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call( + rpc_methods::AGENTS_GETBUILTINLISTINGDEFINITION, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Resolves the model a custom agent asks for against the models actually available, and answers both the model to switch to and the warning a user should see when the agent's preference cannot be met. A custom agent may name several acceptable models in preference order, so the decision is a match rather than a lookup, and an agent whose preference is unavailable is a normal outcome that produces a warning rather than an error. A host must call this rather than pick the first available name itself, because the preference order and the wording of the warning are what keep one installation's agent selection the same as another's. + /// + /// Wire method: `agents.customAgentInitialModelDecision`. + /// + /// # Parameters + /// + /// * `params` - The models a custom agent asks for, and the models actually available. + /// + /// # Returns + /// + /// The model to switch to, and the warning to show when the agent's preference could not be met. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub(crate) async fn custom_agent_initial_model_decision( + &self, + params: AgentsCustomAgentInitialModelDecisionParams, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call( + rpc_methods::AGENTS_CUSTOMAGENTINITIALMODELDECISION, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `catalog.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcCatalog<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcCatalog<'a> { + /// Requests a bounded catalog search. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with search available returns inert candidate summaries, each with an opaque single-use handle scoped to this runtime instance; a runtime without it returns the typed search-unavailable result. Public authorities may be searched anonymously, while an authority that requires credentials yields the typed authentication-required result. All returned text, URLs, and package metadata are untrusted external data and can never trigger instructions, tools, or installation. Read-only: nothing is installed, configured, or persisted. + /// + /// Wire method: `catalog.search`. + /// + /// # Parameters + /// + /// * `params` - A bounded catalog search. Both the query length and the result count are capped by the schema so a caller cannot request an unbounded scan. + /// + /// # Returns + /// + /// Outcome of a catalog.search call: either bounded inert candidates, or one typed refusal. Never a partial success. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn search(&self, params: CatalogSearchRequest) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::CATALOG_SEARCH, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Requests a bounded catalog search. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with search available returns inert candidate summaries, each with an opaque single-use handle scoped to this runtime instance; a runtime without it returns the typed search-unavailable result. Public authorities may be searched anonymously, while an authority that requires credentials yields the typed authentication-required result. All returned text, URLs, and package metadata are untrusted external data and can never trigger instructions, tools, or installation. Read-only: nothing is installed, configured, or persisted. + /// + /// Wire method: `catalog.search`. + /// + /// # Parameters + /// + /// * `params` - A bounded catalog search. Both the query length and the result count are capped by the schema so a caller cannot request an unbounded scan. + /// + /// # Returns + /// + /// Outcome of a catalog.search call: either bounded inert candidates, or one typed refusal. Never a partial success. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + /// + /// Accepts [`CatalogSearchOptions`], including inputs added after [`CatalogSearchRequest`]. + pub async fn search_with_options( + &self, + params: CatalogSearchOptions, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::CATALOG_SEARCH, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Terminates one retained catalog selection group. A selected outcome returns the native host a fresh single-use candidate handle plus the original searchId for a later explicit mcp.planInstall call; non-selected outcomes release the group without producing a planning input. Candidate state, cards, URLs, credentials and private identifiers remain inside the runtime. The model-facing catalog_select tool projects the result separately and never exposes the candidate handle or searchId. + /// + /// Wire method: `catalog.select`. + /// + /// # Parameters + /// + /// * `params` - Terminates one retained catalog selection group through an opaque reference previously returned by the model-safe search projection. + /// + /// # Returns + /// + /// Typed outcome of catalog.select. Only the selected host result carries a fresh candidate handle; the model-facing projection removes both that handle and searchId. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn select( + &self, + params: CatalogSelectionRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::CATALOG_SELECT, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `commands.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcCommands<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcCommands<'a> { + /// Lists the well-known built-in slash commands that work as the first message in a new session (e.g. /plan, /env), without requiring an active session. Commands that depend on session state, authentication, or a synced session are omitted. + /// + /// Wire method: `commands.list`. + /// + /// # Returns + /// + /// Slash commands available in the session, after applying any include/exclude filters. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn list(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::COMMANDS_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `connectors.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcConnectors<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcConnectors<'a> { + /// Returns feature availability. + /// + /// Wire method: `connectors.getCapabilities`. + /// + /// # Returns + /// + /// Feature availability. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn get_capabilities(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::CONNECTORS_GETCAPABILITIES, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Returns eligible accounts. + /// + /// Wire method: `connectors.getAccounts`. + /// + /// # Returns + /// + /// Eligible accounts. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn get_accounts(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::CONNECTORS_GETACCOUNTS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Lists entries for the selected account. + /// + /// Wire method: `connectors.list`. + /// + /// # Parameters + /// + /// * `params` - Selected account. + /// + /// # Returns + /// + /// Entries for the selected account. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn list( + &self, + params: ConnectorDiscoveryAccountRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::CONNECTORS_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Refreshes entries for the selected account. + /// + /// Wire method: `connectors.refresh`. + /// + /// # Parameters + /// + /// * `params` - Selected account. + /// + /// # Returns + /// + /// Entries for the selected account. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn refresh( + &self, + params: ConnectorDiscoveryAccountRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::CONNECTORS_REFRESH, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `environments.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcEnvironments<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcEnvironments<'a> { + /// Lists GitHub Mission Control environments visible to the authenticated identity. Does not require a running host and excludes host relay credentials. + /// + /// Wire method: `environments.list`. + /// + /// # Parameters + /// + /// * `params` - Optional discovery filters supported by GitHub Mission Control. + /// + /// # Returns + /// + /// Environments visible to the authenticated caller and matching the supplied filters. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn list( + &self, + params: EnvironmentsListRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::ENVIRONMENTS_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Gets safe discovery information for a GitHub Mission Control environment without requiring a running host. + /// + /// Wire method: `environments.get`. + /// + /// # Parameters + /// + /// * `params` - Identify a Mission Control environment to retrieve. + /// + /// # Returns + /// + /// Safe discovery information for the requested environment. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn get( + &self, + params: EnvironmentsGetRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::ENVIRONMENTS_GET, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Deletes a user-managed GitHub Mission Control environment. GitHub-managed environments cannot be deleted. Does not stop a running host, which may register again. + /// + /// Wire method: `environments.delete`. + /// + /// # Parameters + /// + /// * `params` - Identify a user-managed Mission Control environment to delete. + /// + /// # Returns + /// + /// Acknowledgement that the requested environment was deleted. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn delete( + &self, + params: EnvironmentsDeleteRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::ENVIRONMENTS_DELETE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `extensions.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcExtensions<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcExtensions<'a> { + /// Discovers user and enabled installed-plugin extensions from persisted Copilot home state, including enablement preferences. Launch-scoped additional plugins are not included. + /// + /// Wire method: `extensions.discover`. + /// + /// # Returns + /// + /// Extensions discovered from persisted Copilot home state and their effective loading mode. Launch-scoped additional plugins are not included. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn discover(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::EXTENSIONS_DISCOVER, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Persistently enables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.enable to update them. /// - /// Wire method: `agents.discover`. + /// Wire method: `extensions.enable`. /// /// # Parameters /// - /// * `params` - Optional project paths to include in agent discovery. - /// - /// # Returns - /// - /// Agents discovered across user, project, plugin, and remote sources. + /// * `params` - Source-qualified extension identifiers to persistently enable for future sessions. /// ///
    /// @@ -495,26 +1122,22 @@ impl<'a> ClientRpcAgents<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn discover(&self, params: AgentsDiscoverRequest) -> Result { + pub async fn enable(&self, params: DiscoveredExtensionsEnableRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::AGENTS_DISCOVER, Some(wire_params)) + .call(rpc_methods::EXTENSIONS_ENABLE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Returns the canonical directories where a client may create custom agents that the runtime will recognize, including ones that do not exist yet. Project directories become active once created. + /// Persistently disables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.disable to update them. /// - /// Wire method: `agents.getDiscoveryPaths`. + /// Wire method: `extensions.disable`. /// /// # Parameters /// - /// * `params` - Optional project paths to include when enumerating agent discovery directories. - /// - /// # Returns - /// - /// Canonical locations where custom agents can be created so the runtime will recognize them. + /// * `params` - Source-qualified extension identifiers to persistently disable for future sessions. /// ///
    /// @@ -523,37 +1146,34 @@ impl<'a> ClientRpcAgents<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get_discovery_paths( - &self, - params: AgentsGetDiscoveryPathsRequest, - ) -> Result { + pub async fn disable(&self, params: DiscoveredExtensionsDisableRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::AGENTS_GETDISCOVERYPATHS, Some(wire_params)) + .call(rpc_methods::EXTENSIONS_DISABLE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } } -/// `catalog.*` RPCs. +/// `git.*` RPCs. #[derive(Clone, Copy)] -pub struct ClientRpcCatalog<'a> { +pub struct ClientRpcGit<'a> { pub(crate) client: &'a Client, } -impl<'a> ClientRpcCatalog<'a> { - /// Requests a bounded catalog search. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with search available returns inert candidate summaries, each with an opaque single-use handle scoped to this runtime instance; a runtime without it returns the typed search-unavailable result. Public authorities may be searched anonymously, while an authority that requires credentials yields the typed authentication-required result. All returned text, URLs, and package metadata are untrusted external data and can never trigger instructions, tools, or installation. Read-only: nothing is installed, configured, or persisted. +impl<'a> ClientRpcGit<'a> { + /// Reads the remote that the branch checked out in a working tree tracks, as `branch..remote` configures it. Reports `origin` rather than failing whenever there is no tracking configuration to read — on a detached HEAD, on a branch with no upstream, or when git itself fails — because a caller asking which remote to talk to needs an answer it can act on, not an error. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. /// - /// Wire method: `catalog.search`. + /// Wire method: `git.currentBranchRemote`. /// /// # Parameters /// - /// * `params` - A bounded catalog search. Both the query length and the result count are capped by the schema so a caller cannot request an unbounded scan. + /// * `params` - Working-tree path a git query applies to. /// /// # Returns /// - /// Outcome of a catalog.search call: either bounded inert candidates, or one typed refusal. Never a partial success. + /// The remote the checked-out branch tracks. /// ///
    /// @@ -562,26 +1182,29 @@ impl<'a> ClientRpcCatalog<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn search(&self, params: CatalogSearchRequest) -> Result { + pub(crate) async fn current_branch_remote( + &self, + params: GitCwdRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::CATALOG_SEARCH, Some(wire_params)) + .call(rpc_methods::GIT_CURRENTBRANCHREMOTE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Requests a bounded catalog search. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with search available returns inert candidate summaries, each with an opaque single-use handle scoped to this runtime instance; a runtime without it returns the typed search-unavailable result. Public authorities may be searched anonymously, while an authority that requires credentials yields the typed authentication-required result. All returned text, URLs, and package metadata are untrusted external data and can never trigger instructions, tools, or installation. Read-only: nothing is installed, configured, or persisted. + /// Collects the repository context of a working directory in one call: working tree root, repository identifier and host, current branch, and the HEAD and base commits. Every repository field is omitted when the path is not inside a git working tree, and the requested path is echoed back as `cwd`. The answer is the same `SessionWorkingDirectoryContext` that `session.metadata.recordContextChange` accepts, so a caller polling for a context change can forward the result unchanged. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. It can become public once an SDK consumer needs to derive session context from a directory itself. /// - /// Wire method: `catalog.search`. + /// Wire method: `git.workingDirectoryContext`. /// /// # Parameters /// - /// * `params` - A bounded catalog search. Both the query length and the result count are capped by the schema so a caller cannot request an unbounded scan. + /// * `params` - Working-tree path a git query applies to. /// /// # Returns /// - /// Outcome of a catalog.search call: either bounded inert candidates, or one typed refusal. Never a partial success. + /// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. /// ///
    /// @@ -590,31 +1213,29 @@ impl<'a> ClientRpcCatalog<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - /// - /// Accepts [`CatalogSearchOptions`], including inputs added after [`CatalogSearchRequest`]. - pub async fn search_with_options( + pub(crate) async fn working_directory_context( &self, - params: CatalogSearchOptions, - ) -> Result { + params: GitCwdRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::CATALOG_SEARCH, Some(wire_params)) + .call(rpc_methods::GIT_WORKINGDIRECTORYCONTEXT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Terminates one retained catalog selection group. A selected outcome returns the native host a fresh single-use candidate handle plus the original searchId for a later explicit mcp.planInstall call; non-selected outcomes release the group without producing a planning input. Candidate state, cards, URLs, credentials and private identifiers remain inside the runtime. The model-facing catalog_select tool projects the result separately and never exposes the candidate handle or searchId. + /// Lists the GitHub repositories a working tree's remotes point at, one entry per distinct repository, so a caller can resolve a base and head repository without parsing remote URLs itself. When several remotes name the same repository, only the first is listed, and the entry keeps that remote name. Remotes pointing at no GitHub host are left out, so an empty list means the tree reaches GitHub through no remote. Failing to read the remotes is reported as an error rather than as an empty list, because the two mean different things to a caller. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. /// - /// Wire method: `catalog.select`. + /// Wire method: `git.reposFromRemotes`. /// /// # Parameters /// - /// * `params` - Terminates one retained catalog selection group through an opaque reference previously returned by the model-safe search projection. + /// * `params` - Git working tree whose GitHub remotes should be listed. /// /// # Returns /// - /// Typed outcome of catalog.select. Only the selected host result carries a fresh candidate handle; the model-facing projection removes both that handle and searchId. + /// The GitHub repositories a working tree's remotes point at. /// ///
    /// @@ -623,33 +1244,33 @@ impl<'a> ClientRpcCatalog<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn select( + pub(crate) async fn repos_from_remotes( &self, - params: CatalogSelectionRequest, - ) -> Result { + params: GitReposFromRemotesRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::CATALOG_SELECT, Some(wire_params)) + .call(rpc_methods::GIT_REPOSFROMREMOTES, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } } -/// `commands.*` RPCs. +/// `gitHubOwners.*` RPCs. #[derive(Clone, Copy)] -pub struct ClientRpcCommands<'a> { +pub struct ClientRpcGitHubOwners<'a> { pub(crate) client: &'a Client, } -impl<'a> ClientRpcCommands<'a> { - /// Lists the well-known built-in slash commands that work as the first message in a new session (e.g. /plan, /env), without requiring an active session. Commands that depend on session state, authentication, or a synced session are omitted. +impl<'a> ClientRpcGitHubOwners<'a> { + /// Registers a cancellable owner listing and returns its request id. Separate from `gitHubOwners.list` so the id exists before the listing starts: a caller that abandons the listing the moment it begins would otherwise have nothing to name in `gitHubOwners.cancel`. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. /// - /// Wire method: `commands.list`. + /// Wire method: `gitHubOwners.nextRequestId`. /// /// # Returns /// - /// Slash commands available in the session, after applying any include/exclude filters. + /// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. /// ///
    /// @@ -658,34 +1279,26 @@ impl<'a> ClientRpcCommands<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list(&self) -> Result { + pub(crate) async fn next_request_id(&self) -> Result { let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::COMMANDS_LIST, Some(wire_params)) + .call(rpc_methods::GITHUBOWNERS_NEXTREQUESTID, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `environments.*` RPCs. -#[derive(Clone, Copy)] -pub struct ClientRpcEnvironments<'a> { - pub(crate) client: &'a Client, -} -impl<'a> ClientRpcEnvironments<'a> { - /// Lists GitHub Mission Control environments visible to the authenticated identity. Does not require a running host and excludes host relay credentials. + /// Lists the logins the authenticated user may act as — their own account first, then the organizations they belong to — by asking the GitHub API under the supplied credential. No credential travels in the request: `authInfo` selects one the runtime already holds, and the runtime resolves the token and the GitHub host from it. A failure the caller should render arrives as `message`; one it should raise arrives as `throwError`. /// - /// Wire method: `environments.list`. + /// Wire method: `gitHubOwners.list`. /// /// # Parameters /// - /// * `params` - Optional discovery filters supported by GitHub Mission Control. + /// * `params` - Credential to list owners under, and the request id that makes the listing cancellable. /// /// # Returns /// - /// Environments visible to the authenticated caller and matching the supplied filters. + /// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. /// ///
    /// @@ -694,29 +1307,29 @@ impl<'a> ClientRpcEnvironments<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn list( + pub(crate) async fn list( &self, - params: EnvironmentsListRequest, - ) -> Result { + params: GitHubOwnersListRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::ENVIRONMENTS_LIST, Some(wire_params)) + .call(rpc_methods::GITHUBOWNERS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Gets safe discovery information for a GitHub Mission Control environment without requiring a running host. + /// Abandons an owner listing started with the given request id. Answers `canceled: true` while a listing with that id is running. Answers `canceled: false` when the id was never registered, was registered but not used, was released after being abandoned, or its listing has ended. Canceling an unused id releases it, and a later `list` with that id is refused. The cancel acts only on owner listings and never reaches another request of the host. /// - /// Wire method: `environments.get`. + /// Wire method: `gitHubOwners.cancel`. /// /// # Parameters /// - /// * `params` - Identify a Mission Control environment to retrieve. + /// * `params` - The owner listing to abandon. /// /// # Returns /// - /// Safe discovery information for the requested environment. + /// Whether the id named a running owner listing. /// ///
    /// @@ -725,29 +1338,37 @@ impl<'a> ClientRpcEnvironments<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn get( + pub(crate) async fn cancel( &self, - params: EnvironmentsGetRequest, - ) -> Result { + params: GitHubOwnersCancelRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::ENVIRONMENTS_GET, Some(wire_params)) + .call(rpc_methods::GITHUBOWNERS_CANCEL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Deletes a user-managed GitHub Mission Control environment. GitHub-managed environments cannot be deleted. Does not stop a running host, which may register again. +/// `gitHubRepository.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcGitHubRepository<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcGitHubRepository<'a> { + /// Resolves the GitHub repository that owns a working-tree path by reading the selected git remote configured for it, preferring `origin`. Returns a null `repository` when the path is inside a git working tree but that selected remote does not resolve to a GitHub host. Fails when the path is not inside a git working tree at all, so a caller can tell 'not a repository' apart from 'a repository with no GitHub remote'. /// - /// Wire method: `environments.delete`. + /// Wire method: `gitHubRepository.atPath`. /// /// # Parameters /// - /// * `params` - Identify a user-managed Mission Control environment to delete. + /// * `params` - Working-tree path whose owning GitHub repository should be resolved. /// /// # Returns /// - /// Acknowledgement that the requested environment was deleted. + /// The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. /// ///
    /// @@ -756,33 +1377,33 @@ impl<'a> ClientRpcEnvironments<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn delete( + pub(crate) async fn at_path( &self, - params: EnvironmentsDeleteRequest, - ) -> Result { + params: GitHubRepositoryAtPathRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::ENVIRONMENTS_DELETE, Some(wire_params)) + .call(rpc_methods::GITHUBREPOSITORY_ATPATH, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } } -/// `extensions.*` RPCs. +/// `globalState.*` RPCs. #[derive(Clone, Copy)] -pub struct ClientRpcExtensions<'a> { +pub struct ClientRpcGlobalState<'a> { pub(crate) client: &'a Client, } -impl<'a> ClientRpcExtensions<'a> { - /// Discovers user and enabled installed-plugin extensions from persisted Copilot home state, including enablement preferences. Launch-scoped additional plugins are not included. +impl<'a> ClientRpcGlobalState<'a> { + /// Reads the host's machine-wide state: which plugins are installed and the one-off flags and timestamps that record what the user has already been shown or migrated. This is the state that outlives a single session and a single workspace, so a host reads it to decide whether to run a first-launch step, offer an onboarding prompt, or skip one it has already completed. The stored credentials are deliberately not part of this result; a caller that needs an authenticated identity asks the account methods for it instead. Reading is non-destructive and every field is optional, because a fresh install has recorded nothing yet. /// - /// Wire method: `extensions.discover`. + /// Wire method: `globalState.load`. /// /// # Returns /// - /// Extensions discovered from persisted Copilot home state and their effective loading mode. Launch-scoped additional plugins are not included. + /// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. /// ///
    /// @@ -791,22 +1412,26 @@ impl<'a> ClientRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn discover(&self) -> Result { + pub(crate) async fn load(&self) -> Result { let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::EXTENSIONS_DISCOVER, Some(wire_params)) + .call(rpc_methods::GLOBALSTATE_LOAD, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Persistently enables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.enable to update them. + /// Reads the host's machine-wide state exactly as `globalState.load` does, but from a caller-supplied configuration directory instead of the one the server resolved for itself. Use this when a consumer scopes a session to its own Copilot home — the SDK's per-session `configDir` override — so the state read matches the directory that session actually uses. An absent or empty `configDir` resolves the server's own home, making this identical to `globalState.load`. The stored credentials are omitted here for the same reason they are omitted from `globalState.load`: a caller that needs an authenticated identity asks the account methods instead, so pointing this at another directory cannot be used to read the credentials kept in it. /// - /// Wire method: `extensions.enable`. + /// Wire method: `globalState.loadForConfigDir`. /// /// # Parameters /// - /// * `params` - Source-qualified extension identifiers to persistently enable for future sessions. + /// * `params` - Selects the configuration directory whose machine-wide state to read. + /// + /// # Returns + /// + /// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. /// ///
    /// @@ -815,22 +1440,25 @@ impl<'a> ClientRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn enable(&self, params: DiscoveredExtensionsEnableRequest) -> Result<(), Error> { + pub(crate) async fn load_for_config_dir( + &self, + params: GlobalStateLoadForConfigDirRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::EXTENSIONS_ENABLE, Some(wire_params)) + .call(rpc_methods::GLOBALSTATE_LOADFORCONFIGDIR, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Persistently disables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.disable to update them. + /// Records one top-level key in the host's machine-wide state, the counterpart to `globalState.load`. A host calls this to remember that it has shown an onboarding step, asked a one-off question, or completed a migration, so the next run can skip it. Only the named key is replaced and the rest of the document is preserved, which lets two writers record different flags without overwriting each other; passing no value removes the key instead. Only the keys a host records itself are writable: `appInstallNudgeResponded`, `appTipShown`, `askedSetupTerminals`, `autoFeedbackLastPromptedAt`, `firstLaunchAt`, `recentModelIds`, `sandboxCredentialProxyCaDeclined` and `sandboxOnboardingShown`. Every other key is refused, including `installedPlugins`, the stored credentials, `trustedFolders`, the staff flags and the signed-in accounts. Plugin enablement must use the plugin APIs, which apply repository and managed-policy checks. /// - /// Wire method: `extensions.disable`. + /// Wire method: `globalState.writeKey`. /// /// # Parameters /// - /// * `params` - Source-qualified extension identifiers to persistently disable for future sessions. + /// * `params` - A single top-level key to record in the host's machine-wide state. The write replaces only that key and leaves the rest of the document untouched, so two writers recording different one-off flags do not overwrite each other. The stored credential keys cannot be written through this method. /// ///
    /// @@ -839,11 +1467,11 @@ impl<'a> ClientRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn disable(&self, params: DiscoveredExtensionsDisableRequest) -> Result<(), Error> { + pub(crate) async fn write_key(&self, params: GlobalStateWriteKeyRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::EXTENSIONS_DISABLE, Some(wire_params)) + .call(rpc_methods::GLOBALSTATE_WRITEKEY, Some(wire_params)) .await?; Ok(()) } @@ -3521,6 +4149,102 @@ impl<'a> ClientRpcSessions<'a> { Ok(serde_json::from_value(_value)?) } + /// Creates the workspace record for a session that has not been opened yet. A host that hands a session off to another application — writing the record and then launching that application against the session ID — needs the record on disk before any session exists to carry it, which the session-scoped workspace methods cannot do. Replaces any existing record and resets the checkpoint index. When writing to the local filesystem, a stored `fork_count` survives on disk. Returns the record it built, so a surviving stored `fork_count` can differ from the answer. + /// + /// Wire method: `sessions.createWorkspace`. + /// + /// # Parameters + /// + /// * `params` - Identity, state location and starting context for a workspace record. + /// + /// # Returns + /// + /// The workspace record that was written. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub(crate) async fn create_workspace( + &self, + params: SessionsCreateWorkspaceRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_CREATEWORKSPACE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Reads a session's workspace record straight from disk, without opening the session. Resuming by session ID has to know where the session lives before it can connect, so the lookup cannot come from the session-scoped workspace methods, which resolve their location from a live session's context. Returns no record when the file is absent. + /// + /// Wire method: `sessions.loadWorkspace`. + /// + /// # Parameters + /// + /// * `params` - Where the session's state lives, as a root directory and the session ID under it. + /// + /// # Returns + /// + /// The workspace record on disk, omitted when the session has none. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub(crate) async fn load_workspace( + &self, + params: SessionsLoadWorkspaceRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_LOADWORKSPACE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Merges fields into a session's workspace record on disk, creating the record when it is absent. The counterpart to `sessions.loadWorkspace`, for the same before-the-session-exists case. It preserves stored workspace-schema fields the request does not supply, does not preserve stored keys outside the workspace schema, and never replaces a stored `fork_count`. + /// + /// Wire method: `sessions.updateWorkspaceFields`. + /// + /// # Parameters + /// + /// * `params` - Where the session's state lives, plus workspace-schema fields to merge into its workspace record. Stored keys outside the schema are not preserved, and a stored `fork_count` is never replaced. + /// + /// # Returns + /// + /// The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub(crate) async fn update_workspace_fields( + &self, + params: SessionsUpdateWorkspaceFieldsRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call( + rpc_methods::SESSIONS_UPDATEWORKSPACEFIELDS, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Reloads user, plugin, and (optionally) repo hooks on the active session. /// /// Wire method: `sessions.reloadPluginHooks`. @@ -4360,33 +5084,13 @@ pub struct ClientRpcUserSettings<'a> { } impl<'a> ClientRpcUserSettings<'a> { - /// Drops this runtime process's in-memory user settings cache so the next settings read observes disk. - /// - /// Wire method: `user.settings.reload`. - /// - ///
    - /// - /// **Experimental.** This API is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. Pin both the - /// SDK and CLI versions if your code depends on it. - /// - ///
    - pub async fn reload(&self) -> Result<(), Error> { - let wire_params = serde_json::json!({}); - let _value = self - .client - .call(rpc_methods::USER_SETTINGS_RELOAD, Some(wire_params)) - .await?; - Ok(()) - } - - /// Lists every known user setting (settings.json overlaid with the legacy config.json, config.json wins), each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. + /// Lists every known user setting from settings.json, each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. /// /// Wire method: `user.settings.get`. /// /// # Returns /// - /// Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. + /// Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. /// ///
    /// @@ -4404,7 +5108,7 @@ impl<'a> ClientRpcUserSettings<'a> { Ok(serde_json::from_value(_value)?) } - /// Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. Returns the keys whose new value is shadowed by a legacy config.json entry (config.json wins on read), which the runtime leaves in place — such writes do not take effect until the legacy value is removed. + /// Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. /// /// Wire method: `user.settings.set`. /// @@ -4412,10 +5116,6 @@ impl<'a> ClientRpcUserSettings<'a> { /// /// * `params` - Partial user settings to write to settings.json. Each top-level key is written individually, replacing the existing value; a key whose value is null is removed. /// - /// # Returns - /// - /// Outcome of writing user settings. - /// ///
    /// /// **Experimental.** This API is part of an experimental wire-protocol surface @@ -4423,16 +5123,13 @@ impl<'a> ClientRpcUserSettings<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn set( - &self, - params: UserSettingsSetRequest, - ) -> Result { + pub async fn set(&self, params: UserSettingsSetRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client .call(rpc_methods::USER_SETTINGS_SET, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } } @@ -4667,6 +5364,13 @@ impl<'a> SessionRpc<'a> { } } + /// `session.quota.*` sub-namespace. + pub fn quota(&self) -> SessionRpcQuota<'a> { + SessionRpcQuota { + session: self.session, + } + } + /// `session.remote.*` sub-namespace. pub fn remote(&self) -> SessionRpcRemote<'a> { SessionRpcRemote { @@ -6840,7 +7544,7 @@ impl<'a> SessionRpcExtensions<'a> { Ok(serde_json::from_value(_value)?) } - /// Enables an extension for the session. + /// Enables an extension for the session and persists the preference when the session has a settings store. Hosts synchronizing effective membership should use extensions.reconcile instead. /// /// Wire method: `session.extensions.enable`. /// @@ -6866,7 +7570,7 @@ impl<'a> SessionRpcExtensions<'a> { Ok(()) } - /// Disables an extension for the session. + /// Disables an extension for the session and persists the preference when the session has a settings store. Hosts synchronizing effective membership should use extensions.reconcile instead. /// /// Wire method: `session.extensions.disable`. /// @@ -6887,14 +7591,39 @@ impl<'a> SessionRpcExtensions<'a> { let _value = self .session .client() - .call(rpc_methods::SESSION_EXTENSIONS_DISABLE, Some(wire_params)) + .call(rpc_methods::SESSION_EXTENSIONS_DISABLE, Some(wire_params)) + .await?; + Ok(()) + } + + /// Reloads extension definitions and processes for the session. + /// + /// Wire method: `session.extensions.reload`. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn reload(&self) -> Result<(), Error> { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_EXTENSIONS_RELOAD, Some(wire_params)) .await?; Ok(()) } - /// Reloads extension definitions and processes for the session. + /// Host-only reconciliation of authoritative session-effective extension membership and enablement. Refreshes runtime-owned discovery and preferences without persisting settings, installing plugins, or restarting unchanged activations. Returns ExtensionList only after required starts and process/contribution cleanup settle. Takes no caller inventory or overrides. Missing controllers, unready/incomplete discovery, unavailable workspaces, superseded inputs, and lifecycle failures are errors, not empty membership. Independently proven revocations may be applied before an error; retry converges without restarting healthy activations. Error data contains lifecycleChangesApplied and code: extension_reconciliation_host_required, extension_reconciliation_unavailable, extension_reconciliation_not_ready, extension_reconciliation_discovery_failed, extension_reconciliation_workspace_unavailable, extension_reconciliation_superseded, or extension_reconciliation_lifecycle_failed. Mark host reconciliation state applied only on success. On older runtimes, method-not-found must not fall back to global discovery and persistent extension disables. /// - /// Wire method: `session.extensions.reload`. + /// Wire method: `session.extensions.reconcile`. + /// + /// # Returns + /// + /// Extensions discovered for the session, with their current status. /// ///
    /// @@ -6903,14 +7632,14 @@ impl<'a> SessionRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
    - pub async fn reload(&self) -> Result<(), Error> { + pub async fn reconcile(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_EXTENSIONS_RELOAD, Some(wire_params)) + .call(rpc_methods::SESSION_EXTENSIONS_RECONCILE, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } /// Push attachments into the next user-message turn from an extension. The host should surface them as composer pills and forward them via the next session.send call. Callable only by extension-owned connections. @@ -7846,7 +8575,39 @@ impl<'a> SessionRpcMcp<'a> { } } - /// Lists MCP servers configured for the session, their connection status, and host-level state. The host-level state (disabled/filtered servers, failed/needs-auth/pending connections, mcp3p policy, full config) is empty/zero when no MCP host has been initialized for the session. + /// Records the IDE the host is connected to, so the agent's system prompt can name it and its workspace folder. Null or an omitted `ide` clears the recorded value, which is how a host reports that it is disconnected; there is no separate clear method. Both `ideName` and `workspaceFolder` are required together, because half a state cannot be attributed to a project. + /// + /// Wire method: `session.mcp.setConnectedIdeInfo`. + /// + /// # Parameters + /// + /// * `params` - Records which IDE the host is connected to, or clears it. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub(crate) async fn set_connected_ide_info( + &self, + params: SessionMcpSetConnectedIdeInfoParams, + ) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_MCP_SETCONNECTEDIDEINFO, + Some(wire_params), + ) + .await?; + Ok(()) + } + + /// Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers. /// /// Wire method: `session.mcp.list`. /// @@ -7871,6 +8632,31 @@ impl<'a> SessionRpcMcp<'a> { Ok(serde_json::from_value(_value)?) } + /// Lists effective MCP configuration without starting, restarting, authenticating, or waiting for servers. An optional live observation is from an already materialized matching server; this is not a readiness guarantee. + /// + /// Wire method: `session.mcp.listConfigured`. + /// + /// # Returns + /// + /// Effective MCP configuration with optional live observations from matching already materialized servers. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn list_configured(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MCP_LISTCONFIGURED, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. /// /// Wire method: `session.mcp.listTools`. @@ -9770,6 +10556,41 @@ impl<'a> SessionRpcModel<'a> { Ok(serde_json::from_value(_value)?) } + /// Switches the session to a model and optional reasoning configuration. + /// + /// Wire method: `session.model.switchTo`. + /// + /// # Parameters + /// + /// * `params` - Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. + /// + /// # Returns + /// + /// The model identifier active on the session after the switch. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + /// + /// Accepts [`ModelSwitchToOptions`], including inputs added after [`ModelSwitchToRequest`]. + pub async fn switch_to_with_options( + &self, + params: ModelSwitchToOptions, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MODEL_SWITCHTO, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`. /// /// Wire method: `session.model.switchAutoTier`. @@ -9839,6 +10660,34 @@ impl<'a> SessionRpcModel<'a> { Ok(serde_json::from_value(_value)?) } + /// Clears an unchanged startup model/provider seed before default-model resolution. Never clears a resumed or user-selected model. + /// + /// Wire method: `session.model.clearStartupSeed`. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub(crate) async fn clear_startup_seed( + &self, + params: SessionModelClearStartupSeedParams, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_MODEL_CLEARSTARTUPSEED, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Replaces or clears the host-supplied model allowlist for a running session. /// /// Wire method: `session.model.setAllowedModels`. @@ -12423,6 +13272,89 @@ impl<'a> SessionRpcQueue<'a> { } } +/// `session.quota.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcQuota<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcQuota<'a> { + /// Gets the session's current quota and account projection without making a network request. + /// + /// Wire method: `session.quota.get`. + /// + /// # Returns + /// + /// The incumbent session-owned quota and account projection. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn get(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_QUOTA_GET, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Refreshes the session's provider model catalog bypassing its cache, folds quota snapshots into session state, and returns the updated projection. Failures leave the last known quota intact. + /// + /// Wire method: `session.quota.refresh`. + /// + /// # Returns + /// + /// The incumbent session-owned quota and account projection. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn refresh(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_QUOTA_REFRESH, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Returns and clears the session's pending quota warnings. Reading or refreshing quota does not drain warnings. + /// + /// Wire method: `session.quota.takeWarnings`. + /// + /// # Returns + /// + /// Pending warnings returned and cleared by session.quota.takeWarnings. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn take_warnings(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_QUOTA_TAKEWARNINGS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `session.remote.*` RPCs. #[derive(Clone, Copy)] pub struct SessionRpcRemote<'a> { @@ -13883,6 +14815,34 @@ impl<'a> SessionRpcTools<'a> { Ok(serde_json::from_value(_value)?) } + /// Lists the external tool calls of the session and its sub-agents that are still waiting for session.tools.handlePendingToolCall. + /// + /// Wire method: `session.tools.listPendingRequests`. + /// + /// # Returns + /// + /// External tool calls still waiting for a result. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub async fn list_pending_requests(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_TOOLS_LISTPENDINGREQUESTS, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Resolves, builds, and validates the runtime tool list for the session. /// /// Wire method: `session.tools.initializeAndValidate`. @@ -14501,6 +15461,35 @@ impl<'a> SessionRpcUsage<'a> { .await?; Ok(serde_json::from_value(_value)?) } + + /// Internal host-only replacement of absolute code-change totals reported by a relay host. + /// + /// Wire method: `session.usage.setCodeChanges`. + /// + /// # Parameters + /// + /// * `params` - Internal absolute code-change totals reported by the owning host. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + pub(crate) async fn set_code_changes( + &self, + params: SessionUsageSetCodeChangesParams, + ) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_USAGE_SETCODECHANGES, Some(wire_params)) + .await?; + Ok(()) + } } /// `session.visibility.*` RPCs. @@ -15712,4 +16701,39 @@ impl<'a> SessionRpcWorkspaces<'a> { .await?; Ok(serde_json::from_value(_value)?) } + + /// Computes a diff for the session workspace. Never rejects for a busy session: a `session`-mode diff that cannot read the session's file-change captures falls back to an unstaged git diff with `isFallback: true` and reports why in `unavailableReason`. + /// + /// Wire method: `session.workspaces.diff`. + /// + /// # Parameters + /// + /// * `params` - Parameters for computing a workspace diff. + /// + /// # Returns + /// + /// Workspace diff result for the requested mode. + /// + ///
    + /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
    + /// + /// Accepts [`WorkspacesDiffOptions`], including inputs added after [`WorkspacesDiffRequest`]. + pub async fn diff_with_options( + &self, + params: WorkspacesDiffOptions, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_WORKSPACES_DIFF, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } } diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 569a16a92c..63ad37ae6f 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -83,6 +83,15 @@ pub enum SessionEventType { SessionShutdown, #[serde(rename = "session.usage_checkpoint")] SessionUsageCheckpoint, + /// + ///
    + /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
    + #[serde(rename = "session.quota_observation")] + SessionQuotaObservation, #[serde(rename = "session.context_changed")] SessionContextChanged, #[serde(rename = "session.usage_info")] @@ -639,6 +648,15 @@ pub enum SessionEventData { SessionShutdown(SessionShutdownData), #[serde(rename = "session.usage_checkpoint")] SessionUsageCheckpoint(SessionUsageCheckpointData), + /// + ///
    + /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
    + #[serde(rename = "session.quota_observation")] + SessionQuotaObservation(SessionQuotaObservationData), #[serde(rename = "session.context_changed")] SessionContextChanged(SessionContextChangedData), #[serde(rename = "session.usage_info")] @@ -1181,12 +1199,18 @@ pub struct SessionStartData { /// Auto routing preference selected at session creation time #[serde(skip_serializing_if = "Option::is_none")] pub auto_tier: Option, + /// True when autoTier is a managed-policy default. Omitted for user-authored and legacy values. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier_managed: Option, /// Working directory and git context at session start #[serde(skip_serializing_if = "Option::is_none")] pub context: Option, /// Context tier selected at session creation time for models with tiered context pricing; null when no tier is selected (e.g., non-tiered model) #[serde(skip_serializing_if = "Option::is_none")] pub context_tier: Option, + /// True when contextTier is a managed-policy default. Omitted for user-authored and legacy values. + #[serde(skip_serializing_if = "Option::is_none")] + pub context_tier_managed: Option, /// Version string of the Copilot application pub copilot_version: String, /// 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. @@ -1197,9 +1221,15 @@ pub struct SessionStartData { pub github_mcp_tool_config: Option, /// Identifier of the software producing the events (e.g., "copilot-agent") pub producer: String, + /// Provider of selectedModel at creation time, when explicitly selected. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_id: Option, /// Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, + /// True when the reasoning effort is a managed-policy default bound to reasoningEffortModel. Omitted for agent-authored, user-authored, independent, and legacy effort. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort_managed: Option, /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort_model: Option, @@ -1236,12 +1266,18 @@ pub struct SessionResumeData { /// Auto routing preference active at resume time #[serde(skip_serializing_if = "Option::is_none")] pub auto_tier: Option, + /// True when autoTier is a managed-policy default. Omitted for user-authored and legacy values. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier_managed: Option, /// Updated working directory and git context at resume time #[serde(skip_serializing_if = "Option::is_none")] pub context: Option, /// Context tier currently selected at resume time; null when no tier is active #[serde(skip_serializing_if = "Option::is_none")] pub context_tier: Option, + /// True when contextTier is a managed-policy default. Omitted for user-authored and legacy values. + #[serde(skip_serializing_if = "Option::is_none")] + pub context_tier_managed: Option, /// 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 or omitted, pending work is normally marked as interrupted unless the resume passively joined live work owned by another client; sessionWasActive distinguishes that case. #[serde(skip_serializing_if = "Option::is_none")] pub continue_pending_work: Option, @@ -1250,9 +1286,15 @@ pub struct SessionResumeData { /// On-disk byte size of the session's persisted events.jsonl file at resume time; omitted when the file does not exist or cannot be stat'd #[serde(skip_serializing_if = "Option::is_none")] pub events_file_size_bytes: Option, + /// Provider of selectedModel at resume time, when explicitly selected. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_id: Option, /// Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, + /// True when the reasoning effort is a managed-policy default bound to reasoningEffortModel. Omitted for agent-authored, user-authored, independent, and legacy effort. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort_managed: Option, /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort_model: Option, @@ -1526,12 +1568,18 @@ pub struct SessionModelChangeData { /// Committed Auto preference after the model configuration change, when applicable. #[serde(skip_serializing_if = "Option::is_none")] pub auto_tier: Option, + /// True when autoTier is a managed-policy default. Omitted for user-authored and legacy values. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier_managed: Option, /// Reason the change happened, when not user-initiated. `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path, or `"refusal_fallback"` when the active model declined a request (content refusal) and the runtime switched to the configured refusal-fallback model. UI clients can use this to render contextual copy. #[serde(skip_serializing_if = "Option::is_none")] pub cause: Option, /// Context tier after the model change; null explicitly clears a previously selected tier #[serde(skip_serializing_if = "Option::is_none")] pub context_tier: Option, + /// True when contextTier is a managed-policy default. Omitted for user-authored and legacy values. + #[serde(skip_serializing_if = "Option::is_none")] + pub context_tier_managed: Option, /// Newly selected model identifier pub new_model: String, /// Previously committed Auto preference, when one was explicitly selected. @@ -1540,6 +1588,9 @@ pub struct SessionModelChangeData { /// Model that was previously selected, if any #[serde(skip_serializing_if = "Option::is_none")] pub previous_model: Option, + /// Provider of previousModel, when known. A provider-only change is a model selection change even when the model identifiers are equal. + #[serde(skip_serializing_if = "Option::is_none")] + pub previous_provider_id: Option, /// Reasoning effort level before the model change, if applicable #[serde(skip_serializing_if = "Option::is_none")] pub previous_reasoning_effort: Option, @@ -1549,9 +1600,15 @@ pub struct SessionModelChangeData { /// Output verbosity level before the model change, if applicable #[serde(skip_serializing_if = "Option::is_none")] pub previous_verbosity: Option, + /// Provider selected for newModel. Omitted for legacy or unattributed selections; never inferred from a later selection. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_id: Option, /// Reasoning effort level after the model change, if applicable #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, + /// True when the reasoning effort is a managed-policy default bound to reasoningEffortModel. Omitted for agent-authored, user-authored, independent, and legacy effort. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort_managed: Option, /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort_model: Option, @@ -1772,6 +1829,178 @@ pub struct SessionSnapshotRewindData { pub up_to_event_id: String, } +/// Request count and cost metrics for this model +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UsageMetricsModelMetricRequests { + /// User-initiated premium request cost (with multiplier applied) + pub cost: f64, + /// Number of API requests made with this model + pub count: i64, +} + +/// Per-model token-detail entry containing the accumulated token count for one token type. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UsageMetricsModelMetricTokenDetail { + /// Accumulated token count for this token type + pub token_count: i64, +} + +/// Token usage metrics for this model +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UsageMetricsModelMetricUsage { + /// Total tokens read from prompt cache + pub cache_read_tokens: i64, + /// Total tokens written to prompt cache + pub cache_write_tokens: i64, + /// Total input tokens consumed + pub input_tokens: i64, + /// Total output tokens produced + pub output_tokens: i64, + /// Total output tokens used for reasoning + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_tokens: Option, +} + +/// Per-model usage metrics, including request counts/costs, token usage, nano-AI units, and per-token-type details. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UsageMetricsModelMetric { + /// Completeness of this model's reported AI-credit subtotal. Explicitly reported zero is complete, not unavailable. + #[serde(skip_serializing_if = "Option::is_none")] + pub ai_credits_status: Option, + /// Latest known prompt-cache expiration for this model. A timestamp in the past indicates that the observed cache has expired. + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_expires_at: Option, + /// Request count and cost metrics for this model + pub requests: UsageMetricsModelMetricRequests, + /// Token count details per type + #[serde(skip_serializing_if = "Option::is_none")] + pub token_details: Option>, + /// Accumulated nano-AI units cost for this model + #[serde(skip_serializing_if = "Option::is_none")] + pub total_nano_aiu: Option, + /// Token usage metrics for this model + pub usage: UsageMetricsModelMetricUsage, +} + +/// Usage attributed to one agent instance, including its identity, API duration, AI units, and per-model breakdown. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UsageMetricsAgentMetric { + /// Human-readable label for this subagent invocation, copied from the originating `subagent.started` event. For task-tool subagents this is the invocation's task description rather than the agent's configured display name, so group by `agentName` for stable per-agent labels. + #[serde(skip_serializing_if = "Option::is_none")] + pub agent_display_name: Option, + /// Configured agent name, when this is a subagent + #[serde(skip_serializing_if = "Option::is_none")] + pub agent_name: Option, + /// Per-model usage for this agent, keyed by model identifier + pub model_metrics: HashMap, + /// Time spent in model API calls by this agent, in milliseconds + pub total_api_duration_ms: i64, + /// Accumulated nano-AI units cost for this agent + pub total_nano_aiu: f64, +} + +/// Aggregated code change metrics +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UsageMetricsCodeChanges { + /// Distinct file paths modified during the session + pub files_modified: Vec, + /// Number of distinct files modified + pub files_modified_count: i64, + /// Total lines of code added + pub lines_added: i64, + /// Total lines of code removed + pub lines_removed: i64, +} + +/// A neutral reference to the model provider that produced a model: an opaque id, a human-readable label, and the provider kind. Carried on each enumerated Model so consumers can group by provider without reaching into a provider-shaped internal type. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderRef { + /// Opaque, stable id of the provider that produced this model. Matches the enumerated `ModelProviderDescriptor.id`. + pub id: String, + /// The provider kind. + pub kind: ModelProviderKind, + /// Human-readable provider label, owned by the runtime so every consumer renders identical text. + pub label: String, +} + +/// Usage for one recorded provider and model, without merging identical model IDs across providers. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UsageMetricsProviderModelMetric { + /// Request, token, and cost totals for this provider/model. + pub metrics: UsageMetricsModelMetric, + /// Model display name captured at call time, when known. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_display_name: Option, + /// Model identity, or null for legacy aggregate-only usage. + pub model_id: Option, + /// Provider identity and product label captured when the call was dispatched; null when unknown. + pub provider: Option, +} + +/// Session-wide token-detail entry containing the accumulated token count for one token type. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UsageMetricsTokenDetail { + /// Accumulated token count for this token type + pub token_count: i64, +} + +/// Accumulated session usage metrics, including premium request cost, token counts, model breakdown, and code-change totals. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UsageGetMetricsResult { + /// Per-agent usage metrics, keyed by agent instance identifier. The main conversation uses the stable key `main`. + #[serde(skip_serializing_if = "Option::is_none")] + pub agent_metrics: Option>, + /// Completeness of totalNanoAiu. Numeric totals remain the reported subtotal; zero is not a claim of free usage when unavailable. + #[serde(skip_serializing_if = "Option::is_none")] + pub ai_credits_status: Option, + /// Aggregated code change metrics + pub code_changes: UsageMetricsCodeChanges, + /// Currently active model identifier + #[serde(skip_serializing_if = "Option::is_none")] + pub current_model: Option, + /// Input tokens from the most recent main-agent API call + pub last_call_input_tokens: i64, + /// Output tokens from the most recent main-agent API call + pub last_call_output_tokens: i64, + /// Per-model token and request metrics, keyed by model identifier + pub model_metrics: HashMap, + /// Provider-attributed model metrics, ordered by first observed provider and model. + /// Null attribution preserves historical usage whose provider or model was not recorded. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_model_metrics: Option>, + /// ISO 8601 timestamp when the session started + pub session_start_time: String, + /// Session-wide per-token-type accumulated token counts + #[serde(skip_serializing_if = "Option::is_none")] + pub token_details: Option>, + /// Total time spent in model API calls (milliseconds) + pub total_api_duration_ms: i64, + /// Session-wide accumulated nano-AI units cost + #[serde(skip_serializing_if = "Option::is_none")] + pub total_nano_aiu: Option, + /// Total user-initiated premium request cost across all models (may be fractional due to multipliers) + pub total_premium_request_cost: f64, + /// Raw count of user-initiated API requests + pub total_user_requests: i64, +} + /// Request count and cost metrics #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1888,6 +2117,10 @@ pub struct ShutdownTokenDetail { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionShutdownData { + /// Complete accounting state captured atomically with receipt watermarks. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) accounting_snapshot: Option, /// Per-agent usage breakdown, keyed by agent instance identifier. The main conversation uses the stable key `main`. #[serde(skip_serializing_if = "Option::is_none")] pub agent_metrics: Option>, @@ -1910,6 +2143,9 @@ pub struct SessionShutdownData { pub events_file_size_bytes: Option, /// Per-model usage breakdown, keyed by model identifier pub model_metrics: HashMap, + /// Provider/model accounting preserved for replay without consulting current provider selection. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_model_metrics: Option>, /// Unix timestamp (milliseconds) when the session started pub session_start_time: i64, /// Whether the session ended normally ("routine") or due to a crash/fatal error ("error") @@ -1939,6 +2175,9 @@ pub struct SessionShutdownData { #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) total_premium_requests: Option, + /// Per-source accounting sequence watermarks subsuming preceding usage receipts. + #[serde(skip_serializing_if = "Option::is_none")] + pub usage_accounting_watermarks: Option>, } /// Internal prompt-cache expiration state for one model @@ -1958,6 +2197,10 @@ pub(crate) struct UsageCheckpointModelCacheState { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionUsageCheckpointData { + /// Complete accounting state captured atomically with the receipt watermarks. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) accounting_snapshot: Option, /// Internal per-model prompt-cache state used to restore expiration tracking on resume #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] @@ -1966,12 +2209,130 @@ pub struct SessionUsageCheckpointData { #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) prompt_cache_break_state: Option>, + /// Provider/model accounting snapshot that subsumes preceding usage receipts. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_model_metrics: Option>, /// Session-wide accumulated nano-AI units cost at checkpoint time pub total_nano_aiu: f64, /// Total number of premium API requests used at checkpoint time #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) total_premium_requests: Option, + /// Highest accounted sequence per source session; no per-call deduplication history is retained. + #[serde(skip_serializing_if = "Option::is_none")] + pub usage_accounting_watermarks: Option>, +} + +/// Authoritative budget measurements and policy metadata, independent of provider. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderQuotaBudgetMetadata { + /// Exact measured consumption in the budget's unit. + pub consumed: f64, + /// Exact budget entitlement, retaining incumbent fractional-unit compatibility. + pub entitlement: f64, + /// Usage beyond entitlement, in the budget's unit. + pub overage: f64, + /// Whether additional usage is allowed when the budget is exhausted. + pub overage_allowed_when_exhausted: bool, + /// Optional additional-usage budget cap. + #[serde(skip_serializing_if = "Option::is_none")] + pub overage_limit: Option, + /// Service-reported remaining percentage for authoritative budget presentation. + pub remaining_percentage: f64, + /// Reset instant in epoch milliseconds when this budget actually defines a window. + #[serde(skip_serializing_if = "Option::is_none")] + pub reset_at_epoch_ms: Option, + /// Whether the reset instant is an estimate. + #[serde(skip_serializing_if = "Option::is_none")] + pub reset_estimated: Option, + /// Whether this budget uses token-based billing. + #[serde(skip_serializing_if = "Option::is_none")] + pub token_based_billing: Option, + /// Whether the budget has unlimited entitlement. + pub unlimited: bool, + /// Whether service policy allows continued usage after exhaustion. + pub usage_allowed_when_exhausted: bool, +} + +/// An account quota reading. Absence of a quantity is unknown, never zero. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderQuotaState { + /// Whether the service permits access, independently of balance. + pub access_state: ProviderQuotaAccessState, + /// Service error code or client acquisition category, separate from the business-state reason. + #[serde(skip_serializing_if = "Option::is_none")] + pub acquisition_error: Option, + /// Acquisition outcome. Unavailable/failed readings have no service verdict; access/capacity unavailable are compatibility placeholders only. + #[serde(skip_serializing_if = "Option::is_none")] + pub acquisition_status: Option, + /// Independently reported signed 64-bit available quantity. Zero does not override the service access/capacity verdict; omission and null are preserved. + #[serde(skip_serializing_if = "Option::is_none")] + pub available_quantity: Option, + /// Optional metadata for an authoritative budget. Advisory balances and admission-only observations do not populate this. + #[serde(skip_serializing_if = "Option::is_none")] + pub budget_metadata: Option, + /// Service capacity state; missing quantities do not imply exhaustion. + pub capacity_state: ProviderQuotaCapacityState, + /// Key for the backwards-compatible snapshots projection, when the authoritative budget supports that contract. + #[serde(skip_serializing_if = "Option::is_none")] + pub compatibility_key: Option, + /// Independently reported signed 64-bit entitlement. -1 is an unlimited sentinel, not a capacity-state rewrite; omission and null are preserved. + #[serde(skip_serializing_if = "Option::is_none")] + pub entitled_quantity: Option, + /// Explicit service admission flag, when reported. + #[serde(skip_serializing_if = "Option::is_none")] + pub has_quota: Option, + /// HTTP status from acquisition, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub http_status: Option, + /// Whether this is a GET account reading or a pre-response admission observation. Observations are never merged across kinds. + #[serde(skip_serializing_if = "Option::is_none")] + pub observation_kind: Option, + /// When the runtime observed this reading, not a charge timestamp or guarantee that consumption has settled. + #[serde(skip_serializing_if = "Option::is_none")] + pub observed_at: Option, + /// Provider/account whose service reported this state. + pub provider: ModelProviderRef, + /// Whether quantities are authoritative budget measurements, advisory balances, or absent. Only authoritative budgets support percentage presentation. + #[serde(skip_serializing_if = "Option::is_none")] + pub quantity_kind: Option, + /// Service-owned quota identifier within this provider. + pub quota_id: String, + /// Service-reported explanation for the state. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Service name owning this reading. + #[serde(skip_serializing_if = "Option::is_none")] + pub service: Option, + /// Service-reported quota source. + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + /// Unit of entitledQuantity and availableQuantity. + pub unit: ProviderQuotaUnit, +} + +/// Session event "session.quota_observation". A provider-owned quota observation, distinct from per-call usage and charge accounting. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionQuotaObservationData { + /// The admitted provider's state observation. Admission observations never contain quantities or reset/percentage semantics. + pub observation: ProviderQuotaState, } /// Session event "session.context_changed". Updated working directory and git context after the change @@ -2106,6 +2467,9 @@ pub(crate) struct CompactionCompleteCompactionTokensUsedCopilotUsage { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CompactionCompleteCompactionTokensUsed { + /// Whether the compaction's credit amount was reported for all, some, or none of its calls. + #[serde(skip_serializing_if = "Option::is_none")] + pub ai_credits_status: Option, /// Cached input tokens reused in the compaction LLM call #[serde(skip_serializing_if = "Option::is_none")] pub cache_read_tokens: Option, @@ -2125,9 +2489,15 @@ pub struct CompactionCompleteCompactionTokensUsed { /// Model identifier used for the compaction LLM call #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Model display name captured for the compaction call. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_display_name: Option, /// Output tokens produced by the compaction LLM call #[serde(skip_serializing_if = "Option::is_none")] pub output_tokens: Option, + /// Provider captured for the compaction call; absent when attribution is unknown. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider: Option, } /// Original request-level and effective conversation reasoning effort for a provider history boundary; the historical type name is retained for compatibility @@ -3389,6 +3759,9 @@ pub struct AssistantMessageData { /// Generation phase for phased-output models (e.g., thinking vs. response phases) #[serde(skip_serializing_if = "Option::is_none")] pub phase: Option, + /// Opaque origin provider identity captured at dispatch. Only an exact match with the target provider permits replay of reasoningOpaque, encryptedContent, reasoningBlocks, serverTools, and provider response/item identifiers. Matching model ids or wire protocols is insufficient. Omitted for legacy or unattributed messages; the runtime excludes unproven opaque state when projecting to a known provider. Text content and client tool requests/results remain portable semantic history. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_id: Option, /// Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping. `reasoningText` and `reasoningOpaque` are a lossy derived view of these blocks, retained for display. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_blocks: Option, @@ -3470,6 +3843,18 @@ pub struct AssistantIdleData { pub aborted: Option, } +/// Ordered accounting identity assigned under the source session's emission lock. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UsageAccountingIdentity { + /// Monotonically increasing sequence within the source session. + pub sequence: i64, + /// Session that assigned this accounting sequence. + pub source_session_id: String, + /// Existing API call identifier, or a runtime-generated identity when none was supplied. + pub usage_id: String, +} + /// Token usage detail for a single billing category #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -3552,6 +3937,12 @@ pub struct AssistantUsageData { /// Number of accepted speculative prediction tokens #[serde(skip_serializing_if = "Option::is_none")] pub accepted_prediction_tokens: Option, + /// Runtime-owned identity shared with the durable accounting receipt. + #[serde(skip_serializing_if = "Option::is_none")] + pub accounting: Option, + /// Availability of this call's reported AI-credit amount. Missing billing is unavailable, not a zero charge. + #[serde(skip_serializing_if = "Option::is_none")] + pub ai_credits_status: Option, /// Completion ID from the model provider (e.g., chatcmpl-abc123) #[serde(skip_serializing_if = "Option::is_none")] pub api_call_id: Option, @@ -3644,6 +4035,9 @@ pub struct AssistantUsageData { pub max_prompt_tokens: Option, /// Model identifier used for this API call pub model: String, + /// Model display name captured for this call, not derived from the current selection. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_display_name: Option, /// Fixed-set provider family serving the bring-your-own-key model (for example "openai", "anthropic", "azure_openai", "ollama", "llama_cpp", or "other"). Never the caller-supplied provider name. Absent for Copilot-served models. #[serde(skip_serializing_if = "Option::is_none")] pub model_provider: Option, @@ -3662,6 +4056,9 @@ pub struct AssistantUsageData { #[deprecated] #[serde(skip_serializing_if = "Option::is_none")] pub parent_tool_call_id: Option, + /// Provider identity and product label captured by the actual call's dispatch path. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider: Option, /// GitHub request tracing ID (x-github-request-id header) for server-side log correlation #[serde(skip_serializing_if = "Option::is_none")] pub provider_call_id: Option, @@ -3681,6 +4078,9 @@ pub struct AssistantUsageData { /// Number of rejected speculative prediction tokens #[serde(skip_serializing_if = "Option::is_none")] pub rejected_prediction_tokens: Option, + /// Serialized (uncompressed) byte length of the request body. A content-free size signal. + #[serde(skip_serializing_if = "Option::is_none")] + pub request_body_bytes: Option, /// Per-request treatment/eligibility signal returned by the Copilot API in the `X-GitHub-Copilot-Request-TE` response header for the associated model call; `false` when the header was absent or unparseable. #[serde(skip_serializing_if = "Option::is_none")] pub rte: Option, @@ -3709,6 +4109,12 @@ pub struct AssistantUsageData { /// Transport used for this model call (http or websocket) #[serde(skip_serializing_if = "Option::is_none")] pub transport: Option, + /// Milliseconds spent on the WebSocket attempt before falling back to HTTP + #[serde(skip_serializing_if = "Option::is_none")] + pub websocket_fallback_after_ms: Option, + /// Why the call was carried by the HTTP fallback of a WebSocket-capable dispatcher; absent when no fallback occurred + #[serde(skip_serializing_if = "Option::is_none")] + pub websocket_fallback_reason: Option, } /// Session event "prompt_cache_break". A detected loss of a previously cached prompt prefix @@ -3805,6 +4211,12 @@ pub struct PromptCacheBreakData { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ModelCallFailureRequestFingerprint { + /// Summed byte length of opaque or encrypted reasoning payloads + #[serde(skip_serializing_if = "Option::is_none")] + pub encrypted_content_bytes: Option, + /// Summed byte length of inline image payloads (data URLs and base64 sources) + #[serde(skip_serializing_if = "Option::is_none")] + pub image_bytes: Option, /// Total number of image content parts pub image_part_count: i64, /// Image parts whose media type cannot be determined (rejected by strict providers) @@ -3816,6 +4228,9 @@ pub struct ModelCallFailureRequestFingerprint { pub message_count: i64, /// Tool calls whose name is missing or empty (rejected by strict providers) pub nameless_tool_call_count: i64, + /// Number of messages carrying opaque or encrypted reasoning + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_item_count: Option, /// Total number of tool calls across assistant messages pub tool_call_count: i64, /// Number of "tool" result messages in the request @@ -3900,9 +4315,15 @@ pub struct ModelCallFailureData { /// Reasoning effort level used for the failed model call, if applicable #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, + /// Serialized (uncompressed) byte length of the failed request body. A content-free size signal. + #[serde(skip_serializing_if = "Option::is_none")] + pub request_body_bytes: Option, /// Content-free structural summary of the failing request. Contains only counts and shape flags (no prompt content), so it is safe for unrestricted telemetry. Populated only for client-error (4xx) failures. #[serde(skip_serializing_if = "Option::is_none")] pub request_fingerprint: Option, + /// Zero-based orchestrator retry index of the failed attempt + #[serde(skip_serializing_if = "Option::is_none")] + pub retry_attempt: Option, /// Per-request treatment/eligibility signal returned by the Copilot API in the `X-GitHub-Copilot-Request-TE` response header for the associated model call; `false` when the header was absent or unparseable. #[serde(skip_serializing_if = "Option::is_none")] pub rte: Option, @@ -3917,6 +4338,12 @@ pub struct ModelCallFailureData { /// Transport used for the failed model call (http or websocket) #[serde(skip_serializing_if = "Option::is_none")] pub transport: Option, + /// Milliseconds spent on the WebSocket attempt before falling back to HTTP + #[serde(skip_serializing_if = "Option::is_none")] + pub websocket_fallback_after_ms: Option, + /// Why the failed call was carried by the HTTP fallback of a WebSocket-capable dispatcher; absent when no fallback occurred + #[serde(skip_serializing_if = "Option::is_none")] + pub websocket_fallback_reason: Option, } /// Session event "model.call_final_result". Internal telemetry result for one logical model operation after all orchestrator-owned retries settle @@ -5582,6 +6009,28 @@ pub struct PermissionRequestShell { pub warning: Option, } +/// A named, complete UTF-8 text snapshot for a pending file operation. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PermissionWriteFileContent { + /// Complete file contents, including an empty string for an empty file. + pub content: String, + /// Absolute path in the session filesystem namespace. + pub path: String, +} + +/// Complete text snapshots for one proposed file change. At least one side is present. Different before and after paths describe a move. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PermissionWriteFileEdit { + /// Complete file content the operation plans to write. Absent only when the file is being deleted. + #[serde(skip_serializing_if = "Option::is_none")] + pub after: Option, + /// File content read while planning the operation. Absent only when the file is being created. + #[serde(skip_serializing_if = "Option::is_none")] + pub before: Option, +} + /// File write permission request #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -5590,6 +6039,9 @@ pub struct PermissionRequestWrite { pub can_offer_session_approval: bool, /// Unified diff showing the proposed changes pub diff: String, + /// Complete before/after text previews captured while planning this request. Omitted when a complete preview is unavailable, including pre-read sandbox requests and non-UTF-8 files. Missing sides mean creation or deletion, not unavailable content. + #[serde(skip_serializing_if = "Option::is_none")] + pub file_edits: Option>, /// Path of the file being written to pub file_name: String, /// Human-readable description of the intended file change @@ -6062,6 +6514,9 @@ pub struct PermissionPromptRequestWrite { pub can_offer_session_approval: bool, /// Unified diff showing the proposed changes pub diff: String, + /// Complete before/after text previews captured while planning this request. Omitted when a complete preview is unavailable, including pre-read sandbox requests and non-UTF-8 files. Missing sides mean creation or deletion, not unavailable content. + #[serde(skip_serializing_if = "Option::is_none")] + pub file_edits: Option>, /// Path of the file being written to pub file_name: String, /// Human-readable description of the intended file change @@ -7552,7 +8007,7 @@ pub struct SessionManagedSettingsResolvedData { pub source: ManagedSettingsResolvedSource, } -/// Session event "session.managed_settings_enforced". Runtime enforcement of enterprise managed settings: fires when the session blocks or caps a runtime action because enterprise policy governs it, so SDK clients can explain *why* an action was governed. Unlike `session.managed_settings_resolved` (which reports *what* is managed), this reports a concrete governed action — e.g. a user or host tried to turn on a bypass-permissions escalation while policy disables it. Emitted live (not persisted to the session event log) on user/host-initiated attempts only, never for silent policy application. Marked experimental while the managed-settings surface stabilizes. +/// Session event "session.managed_settings_enforced". Runtime enforcement of enterprise managed settings: fires when the session blocks or caps a runtime action because enterprise policy governs it, so SDK clients can explain *why* an action was governed. Unlike `session.managed_settings_resolved` (which reports *what* is managed), this reports a concrete governed action — e.g. a user or host tried to turn on bypass permissions or Assisted Permissions while the corresponding policy disables it. Emitted live (not persisted to the session event log) on user/host-initiated attempts only, never for silent policy application. Marked experimental while the managed-settings surface stabilizes. /// ///
    /// @@ -7565,14 +8020,14 @@ pub struct SessionManagedSettingsResolvedData { pub struct SessionManagedSettingsEnforcedData { /// The category of runtime action that managed policy governed. pub action: ManagedSettingsEnforcedAction, - /// For a `bypass_permissions_blocked` action, which permission-escalation primitive was refused. Absent for actions without a specific escalation primitive. + /// For a `bypass_permissions_blocked` action, which permission-mode or escalation primitive was refused. Absent for actions without a specific primitive. #[serde(skip_serializing_if = "Option::is_none")] pub escalation: Option, /// Whether the enforcement was forced by fail-closed handling (managed policy could not be determined) rather than an explicit managed setting. When true, `setting` still names the restriction that was applied. pub fail_closed: bool, /// A human-readable explanation of why the action was governed, suitable for surfacing to the user. pub message: String, - /// The managed setting key responsible for the enforcement (e.g. `permissions.disableBypassPermissionsMode`). + /// The managed setting key responsible for the enforcement (for example `permissions.disableBypassPermissionsMode` or `permissions.disableAssistedPermissionsMode`). pub setting: String, } @@ -8265,25 +8720,52 @@ pub struct McpAppToolCallCompleteData { /// Session event "session.indexed_search". Transient indexed-search status and diagnostics from the live runtime service. Never persisted or used to infer activation from session history. pub type SessionIndexedSearchData = IndexedSearchData; -/// Routing preference used when the session model is `auto`. `fast` is an integrator-only latency preset and is not a first-party GitHub Copilot product preference. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +/// Extensible routing preference for the virtual `auto` model. New identifiers must be advertised and enabled by the provider. `fast` is an integrator-only latency preset. +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub enum AutoTier { /// Optimize for efficiency. - #[serde(rename = "efficiency")] Efficiency, /// Balance efficiency and intelligence. - #[serde(rename = "balance")] Balance, /// Optimize for intelligence. - #[serde(rename = "intelligence")] Intelligence, /// Integrator-only preset that optimizes for latency. - #[serde(rename = "fast")] Fast, /// Unknown variant for forward compatibility. #[default] - #[serde(other)] Unknown, + /// Provider-advertised identifier, retained verbatim. + Custom(String), +} +impl AutoTier { + /// Returns the routing identifier without losing unknown values. + pub fn as_str(&self) -> &str { + match self { + Self::Efficiency => "efficiency", + Self::Balance => "balance", + Self::Intelligence => "intelligence", + Self::Fast => "fast", + Self::Unknown => "Unknown", + Self::Custom(value) => value, + } + } +} +impl Serialize for AutoTier { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(self.as_str()) + } +} +impl<'de> Deserialize<'de> for AutoTier { + fn deserialize>(deserializer: D) -> Result { + let value = String::deserialize(deserializer)?; + Ok(match value.as_str() { + "efficiency" => Self::Efficiency, + "balance" => Self::Balance, + "intelligence" => Self::Intelligence, + "fast" => Self::Fast, + _ => Self::Custom(value), + }) + } } /// Hosting platform type of the repository (github or ado) @@ -8670,22 +9152,48 @@ pub enum ModelDeselectedReason { Unknown, } -/// Auto preferences that Copilot API can recommend. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +/// Enabled Auto preferences that Copilot API can recommend. +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub enum RecommendedAutoTier { /// Optimize for efficiency. - #[serde(rename = "efficiency")] Efficiency, /// Balance efficiency and intelligence. - #[serde(rename = "balance")] Balance, /// Optimize for intelligence. - #[serde(rename = "intelligence")] Intelligence, /// Unknown variant for forward compatibility. #[default] - #[serde(other)] Unknown, + /// Provider-advertised identifier, retained verbatim. + Custom(String), +} +impl RecommendedAutoTier { + /// Returns the routing identifier without losing unknown values. + pub fn as_str(&self) -> &str { + match self { + Self::Efficiency => "efficiency", + Self::Balance => "balance", + Self::Intelligence => "intelligence", + Self::Unknown => "Unknown", + Self::Custom(value) => value, + } + } +} +impl Serialize for RecommendedAutoTier { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(self.as_str()) + } +} +impl<'de> Deserialize<'de> for RecommendedAutoTier { + fn deserialize>(deserializer: D) -> Result { + let value = String::deserialize(deserializer)?; + Ok(match value.as_str() { + "efficiency" => Self::Efficiency, + "balance" => Self::Balance, + "intelligence" => Self::Intelligence, + _ => Self::Custom(value), + }) + } } /// Terminal reason an Auto preference activation failed. @@ -8782,6 +9290,46 @@ pub enum HandoffSourceType { Unknown, } +/// Whether the accumulated numeric AI-credit subtotal covers the observed calls. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AiCreditsStatus { + /// Every observed call supplied an AI-credit amount, including an explicit zero. + #[serde(rename = "complete")] + Complete, + /// Some calls supplied amounts and some did not; the numeric value is only a reported subtotal. + #[serde(rename = "partial")] + Partial, + /// No AI-credit amount was reported for the observed calls. Numeric zero is not a zero-cost claim. + #[serde(rename = "unavailable")] + Unavailable, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// The neutral kind of a model provider — the model analog of `AccountKind`. A model provider is the live, entitled source a model came from; central code never branches on this beyond a single dispatch. +/// +///
    +/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
    +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ModelProviderKind { + /// GitHub Copilot / CAPI models, spawned by a github-resolving account that holds a Copilot seat. + #[serde(rename = "copilot")] + Copilot, + /// Microsoft 365 Copilot (Loki) inference models, spawned by a resolvable Entra-derived Loki account. + #[serde(rename = "loki")] + Loki, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Whether the session ended normally ("routine") or due to a crash/fatal error ("error") #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ShutdownType { @@ -8797,6 +9345,132 @@ pub enum ShutdownType { Unknown, } +/// Access policy reported by the quota service. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderQuotaAccessState { + /// Access is allowed. + #[serde(rename = "allowed")] + Allowed, + /// Access is denied. + #[serde(rename = "denied")] + Denied, + /// Quota is not required for this access. + #[serde(rename = "not_required")] + NotRequired, + /// The service did not supply a recognized access state. + #[serde(rename = "unknown")] + UnknownValue, + /// Client-only compatibility placeholder when acquisitionStatus is unavailable or failed; not an observed service verdict. + #[serde(rename = "unavailable")] + Unavailable, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Acquisition state, independent of the service's business access/capacity verdict. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderQuotaAcquisitionStatus { + /// A service quota observation was received. + #[serde(rename = "succeeded")] + Succeeded, + /// No service snapshot is available for this caller or surface. + #[serde(rename = "unavailable")] + Unavailable, + /// Acquisition failed; inspect httpStatus/acquisitionError without inferring a business verdict. + #[serde(rename = "failed")] + Failed, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Capacity is independent of whether a numeric balance was supplied. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderQuotaCapacityState { + /// Capacity is available. + #[serde(rename = "available")] + Available, + /// The service explicitly reports exhaustion. + #[serde(rename = "exhausted")] + Exhausted, + /// The reported entitlement is unlimited. + #[serde(rename = "unlimited")] + Unlimited, + /// No billable quota is required. + #[serde(rename = "not_required")] + NotRequired, + /// The service reports that capacity is not applicable. + #[serde(rename = "not_applicable")] + NotApplicable, + /// No recognized capacity state was supplied. + #[serde(rename = "unknown")] + UnknownValue, + /// Client-only compatibility placeholder when acquisitionStatus is unavailable or failed; not an observed service verdict. + #[serde(rename = "unavailable")] + Unavailable, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Timing and purpose of a provider quota observation. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderQuotaObservationKind { + /// An explicit account read; consumption may still be subject to service aggregation delay. + #[serde(rename = "account_snapshot")] + AccountSnapshot, + /// A verdict attached before an inference response. May be cached; never a post-charge balance. + #[serde(rename = "admission_state")] + AdmissionState, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Interpretation permitted for independently reported quantities. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderQuotaQuantityKind { + /// Quantities form an authoritative available-of-entitled budget suitable for percentage presentation. + #[serde(rename = "authoritative_budget")] + AuthoritativeBudget, + /// Raw advisory quantities; do not infer permission, usage percentage, reset cadence, or post-charge balance. + #[serde(rename = "advisory_balance")] + AdvisoryBalance, + /// No quantity measurement, as with admission-only response headers. + #[serde(rename = "none")] + None, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Units explicitly reported by a quota provider. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderQuotaUnit { + /// AI credits, not tokens or currency. + #[serde(rename = "ai_credits")] + AiCredits, + /// Requests charged against an entitlement. + #[serde(rename = "requests")] + Requests, + /// Model tokens. + #[serde(rename = "tokens")] + Tokens, + /// The observation does not establish a quantity unit. + #[serde(rename = "unknown")] + UnknownValue, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// What initiated a conversation compaction #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum CompactionTrigger { @@ -9414,6 +10088,30 @@ pub enum AssistantUsageTransport { Unknown, } +/// Why a WebSocket-capable model call was carried by the HTTP fallback +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ModelCallWebSocketFallbackReason { + /// The WebSocket connection could not be established. + #[serde(rename = "connect_failed")] + ConnectFailed, + /// No usable WebSocket connection was available for the request. + #[serde(rename = "connection_unavailable")] + ConnectionUnavailable, + /// Sending the request over the WebSocket failed. + #[serde(rename = "send_failed")] + SendFailed, + /// The WebSocket returned a retryable API error. + #[serde(rename = "api_error")] + ApiError, + /// The WebSocket transport failed before any output reached the consumer. + #[serde(rename = "transport_failed")] + TransportFailed, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// For HTTP 400 failures only: whether the response carried a structured CAPI error envelope (structured_error, a deterministic validation failure) or no error body (bodyless, the transient gateway/proxy signature). Absent for non-400 failures. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ModelCallFailureBadRequestKind { @@ -9537,7 +10235,7 @@ pub enum AbortReason { Unknown, } -/// Configuration source: user, workspace, plugin, builtin, or managed +/// Configuration source: user, workspace, plugin, builtin, managed, or account #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum McpServerSource { /// Server configured in the user's global MCP configuration. @@ -9555,6 +10253,9 @@ pub enum McpServerSource { /// Server supplied by a trusted host-managed catalog. #[serde(rename = "managed")] Managed, + /// Server contributed by a signed-in account; enablement and organization policy still apply. + #[serde(rename = "account")] + Account, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -10494,6 +11195,9 @@ pub enum PermissionApprovalEvaluationReasonCode { /// The script path was not authorized for inspection. #[serde(rename = "path-not-authorized")] PathNotAuthorized, + /// A code source was excluded from review by content exclusion policy. + #[serde(rename = "content-excluded")] + ContentExcluded, /// The script working directory was invalid. #[serde(rename = "invalid-working-directory")] InvalidWorkingDirectory, @@ -10530,6 +11234,24 @@ pub enum PermissionApprovalEvaluationReasonCode { /// The script argument binding could not be reviewed. #[serde(rename = "argument-binding-unreviewable")] ArgumentBindingUnreviewable, + /// The shell command could not be analyzed for execution evidence. + #[serde(rename = "unsupported-command-shape")] + UnsupportedCommandShape, + /// The shell command used a code source that cannot be bound for review. + #[serde(rename = "unsupported-source")] + UnsupportedSource, + /// The shell command used a code source computed at run time. + #[serde(rename = "dynamic-source")] + DynamicSource, + /// The shell command referenced more code sources than can be reviewed. + #[serde(rename = "too-many-sources")] + TooManySources, + /// A code-bearing executable could not be inspected. + #[serde(rename = "executable-unavailable")] + ExecutableUnavailable, + /// A code-bearing executable exceeded the binding size limit. + #[serde(rename = "executable-too-large")] + ExecutableTooLarge, /// The script review metadata was malformed. #[serde(rename = "malformed-script-action-review")] MalformedScriptActionReview, @@ -11355,7 +12077,7 @@ pub enum ManagedSettingsResolvedSource { /// The category of runtime action that enterprise managed settings governed (blocked or capped) #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ManagedSettingsEnforcedAction { - /// An attempt to turn on a bypass-permissions ("yolo") escalation was refused or capped because policy disables bypass-permissions mode. + /// An attempt to enter a permission mode governed by managed policy was refused or capped. The `setting` and `escalation` fields identify whether this was bypass permissions or Assisted Permissions. #[serde(rename = "bypass_permissions_blocked")] BypassPermissionsBlocked, /// Unknown variant for forward compatibility. @@ -11364,7 +12086,7 @@ pub enum ManagedSettingsEnforcedAction { Unknown, } -/// For a `bypass_permissions_blocked` action, which permission-escalation primitive was refused +/// For a `bypass_permissions_blocked` action, which permission-mode or escalation primitive was refused #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ManagedSettingsEnforcedEscalation { /// Full allow-all permissions — automatically approving tools, paths, and URLs. @@ -11373,7 +12095,7 @@ pub enum ManagedSettingsEnforcedEscalation { /// Automatic approval of all tool permission requests. #[serde(rename = "approve_all")] ApproveAll, - /// Assisted mode — keeps normal prompt paths and adds an LLM recommendation, distinct from allow-all. + /// Assisted Permissions — uses an LLM review to reduce prompts, distinct from allow-all and not a hard security boundary. #[serde(rename = "assisted_approval")] AssistedApproval, /// Unrestricted filesystem access outside the session's allowed directories. @@ -11553,7 +12275,7 @@ pub enum McpServerStatus { /// The server is configured but disabled. #[serde(rename = "disabled")] Disabled, - /// The server was intentionally stopped and can be restarted on demand when policy permits; a server quarantined by restrictive managed policy stays stopped and cannot be restarted until the policy allows it. + /// The server is not running: it may not have started yet, may have been explicitly stopped, or may be quarantined by restrictive managed policy. It can be restarted on demand when policy permits. #[serde(rename = "stopped")] Stopped, /// The server is not configured for this session. diff --git a/rust/src/installation_confirmation.rs b/rust/src/installation_confirmation.rs index 42a52092ba..ccb9b46b01 100644 --- a/rust/src/installation_confirmation.rs +++ b/rust/src/installation_confirmation.rs @@ -2,14 +2,12 @@ //! Connection-global human confirmation for experimental installation operations. -use std::collections::HashMap; -use std::collections::hash_map::Entry; use std::panic::AssertUnwindSafe; use std::sync::{Arc, OnceLock, Weak}; use async_trait::async_trait; use futures_util::FutureExt; -use parking_lot::{Mutex, RwLock}; +use parking_lot::RwLock; use tokio_util::sync::CancellationToken; use tracing::warn; @@ -22,7 +20,6 @@ use crate::{ }; pub(crate) const CONFIRM_METHOD: &str = "installations.confirm"; -const REQUEST_CANCELLED: i32 = -32800; /// The cancellation lifetime of one confirmation. /// @@ -66,63 +63,6 @@ pub trait InstallationConfirmationHandler: Send + Sync + 'static { ) -> Result; } -/// Registered synchronously by the transport before forwarding each confirmation. -/// This preserves request/cancellation ordering across the router's separate queues. -#[derive(Default)] -pub(crate) struct ConfirmationRequests { - pending: Mutex>>, -} - -impl ConfirmationRequests { - pub(crate) fn register(&self, id: u64) -> bool { - let mut pending = self.pending.lock(); - match pending.entry(id) { - Entry::Vacant(entry) => { - entry.insert(Arc::new(CancellationToken::new())); - true - } - Entry::Occupied(_) => false, - } - } - - pub(crate) fn cancel(&self, id: u64) { - if let Some(token) = self.pending.lock().get(&id) { - token.cancel(); - } - } - - pub(crate) fn clear(&self) { - self.pending.lock().clear(); - } - - fn claim(self: &Arc, id: u64) -> Option { - let cancellation = self.pending.lock().get(&id)?.clone(); - Some(PendingConfirmation { - requests: self.clone(), - id, - cancellation, - }) - } -} - -struct PendingConfirmation { - requests: Arc, - id: u64, - cancellation: Arc, -} - -impl Drop for PendingConfirmation { - fn drop(&mut self) { - let mut pending = self.requests.pending.lock(); - if pending - .get(&self.id) - .is_some_and(|token| Arc::ptr_eq(token, &self.cancellation)) - { - pending.remove(&self.id); - } - } -} - pub(crate) struct InstallationConfirmationDispatcher { handler: RwLock>>, client: OnceLock>, @@ -153,11 +93,11 @@ impl InstallationConfirmationDispatcher { let Some(client) = self.client.get().and_then(Weak::upgrade) else { return; }; - let Some(pending) = client.rpc.confirmation_requests.claim(request.id) else { + let Some(pending) = client.rpc.cancellable_requests.claim(request.id) else { warn!("confirmation request retired before dispatch"); return; }; - let request_cancelled = pending.cancellation.as_ref().clone(); + let request_cancelled = pending.cancellation().clone(); let connection_closed = client.rpc.connection_closed_token(); let context = InstallationConfirmationContext { cancellation: connection_closed.child_token(), @@ -170,7 +110,7 @@ impl InstallationConfirmationDispatcher { _ = connection_closed.cancelled() => return, _ = request_cancelled.cancelled() => { context.cancellation.cancel(); - Err((REQUEST_CANCELLED, "Installation confirmation request cancelled")) + Err((error_codes::REQUEST_CANCELLED, "Installation confirmation request cancelled")) } outcome = Self::handle(handler, request.params, context.clone()) => outcome, }; @@ -180,7 +120,7 @@ impl InstallationConfirmationDispatcher { let outcome = if request_cancelled.is_cancelled() { context.cancellation.cancel(); Err(( - REQUEST_CANCELLED, + error_codes::REQUEST_CANCELLED, "Installation confirmation request cancelled", )) } else { diff --git a/rust/src/jsonrpc.rs b/rust/src/jsonrpc.rs index 8da2c72fb5..2f69da5ba9 100644 --- a/rust/src/jsonrpc.rs +++ b/rust/src/jsonrpc.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::collections::hash_map::Entry; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Instant; @@ -98,6 +99,8 @@ pub mod error_codes { /// Internal server error (-32603). #[allow(dead_code, reason = "standard JSON-RPC code, reserved for future use")] pub const INTERNAL_ERROR: i32 = -32603; + /// Request cancelled by the peer's `$/cancelRequest` (-32800). + pub const REQUEST_CANCELLED: i32 = -32800; } /// A JSON-RPC 2.0 notification (no `id`, no response expected). @@ -276,6 +279,85 @@ struct WriteCommand { ack: oneshot::Sender>, } +/// Inbound requests that honor `$/cancelRequest`. +/// +/// The read loop registers each one synchronously before forwarding it. This +/// preserves request/cancellation ordering across the router's separate queues. +#[derive(Default)] +pub(crate) struct CancellableRequests { + pending: Mutex>>, +} + +impl CancellableRequests { + fn honors_cancellation(method: &str) -> bool { + use crate::generated::api_types::rpc_methods; + + matches!( + method, + crate::installation_confirmation::CONFIRM_METHOD + | rpc_methods::SKILLPROVIDER_LIST + | rpc_methods::SKILLPROVIDER_READ + ) + } + + fn register(&self, id: u64) -> bool { + let mut pending = self.pending.lock(); + match pending.entry(id) { + Entry::Vacant(entry) => { + entry.insert(Arc::new(CancellationToken::new())); + true + } + Entry::Occupied(_) => false, + } + } + + fn cancel(&self, id: u64) { + if let Some(token) = self.pending.lock().get(&id) { + token.cancel(); + } + } + + fn clear(&self) { + self.pending.lock().clear(); + } + + /// Take ownership of a registered request's cancellation until the + /// returned guard drops, or `None` if the connection already retired it. + pub(crate) fn claim(self: &Arc, id: u64) -> Option { + let cancellation = self.pending.lock().get(&id)?.clone(); + Some(PendingCancellation { + requests: self.clone(), + id, + cancellation, + }) + } +} + +pub(crate) struct PendingCancellation { + requests: Arc, + id: u64, + cancellation: Arc, +} + +impl PendingCancellation { + /// Cancelled when the runtime sends `$/cancelRequest` for this request. + pub(crate) fn cancellation(&self) -> &CancellationToken { + &self.cancellation + } +} + +impl Drop for PendingCancellation { + fn drop(&mut self) { + let mut pending = self.requests.pending.lock(); + if pending + .get(&self.id) + .is_some_and(|token| Arc::ptr_eq(token, &self.cancellation)) + { + pending.remove(&self.id); + } + } +} + /// Low-level JSON-RPC 2.0 client over Content-Length-framed streams. /// /// # Cancel safety @@ -300,7 +382,7 @@ pub struct JsonRpcClient { request_tx: mpsc::UnboundedSender, request_handlers: RequestHandlers, connection_closed: CancellationToken, - pub(crate) confirmation_requests: Arc, + pub(crate) cancellable_requests: Arc, read_task: Mutex>>, write_task: Mutex>>, } @@ -342,9 +424,7 @@ impl JsonRpcClient { request_tx, request_handlers: Arc::new(RwLock::new(HashMap::new())), connection_closed: CancellationToken::new(), - confirmation_requests: Arc::new( - crate::installation_confirmation::ConfirmationRequests::default(), - ), + cancellable_requests: Arc::new(CancellableRequests::default()), read_task: Mutex::new(None), write_task: Mutex::new(Some(write_task)), }; @@ -353,7 +433,7 @@ impl JsonRpcClient { let notification_tx_clone = client.notification_tx.clone(); let request_tx_clone = client.request_tx.clone(); let connection_closed = client.connection_closed.clone(); - let confirmation_requests = client.confirmation_requests.clone(); + let cancellable_requests = client.cancellable_requests.clone(); let request_handlers = client.request_handlers.clone(); let write_tx = client.write_tx.clone(); let reader_span = tracing::error_span!("jsonrpc_read_loop"); @@ -367,7 +447,7 @@ impl JsonRpcClient { request_tx_clone, request_handlers, write_tx, - (connection_closed, confirmation_requests), + (connection_closed, cancellable_requests), ) .await; } @@ -380,7 +460,7 @@ impl JsonRpcClient { pub(crate) fn force_close(&self) { self.connection_closed.cancel(); - self.confirmation_requests.clear(); + self.cancellable_requests.clear(); let handlers = std::mem::take(&mut *self.request_handlers.write()); drop(handlers); if let Some(task) = self.read_task.lock().take() { @@ -464,13 +544,10 @@ impl JsonRpcClient { request_tx: mpsc::UnboundedSender, request_handlers: RequestHandlers, write_tx: mpsc::UnboundedSender, - connection: ( - CancellationToken, - Arc, - ), + connection: (CancellationToken, Arc), ) { let mut reader = BufReader::new(reader); - let (connection_closed, confirmation_requests) = connection; + let (connection_closed, cancellable_requests) = connection; loop { match Self::read_message(&mut reader).await { @@ -537,7 +614,7 @@ impl JsonRpcClient { .and_then(|params| params.get("id")) .and_then(Value::as_u64) { - confirmation_requests.cancel(id); + cancellable_requests.cancel(id); } else { warn!("invalid numeric request cancellation"); } @@ -552,10 +629,10 @@ impl JsonRpcClient { let _ = notifications.0.send(notification); } JsonRpcMessage::Request(request) => { - if request.method == crate::installation_confirmation::CONFIRM_METHOD - && !confirmation_requests.register(request.id) + if CancellableRequests::honors_cancellation(&request.method) + && !cancellable_requests.register(request.id) { - warn!("duplicate pending installation confirmation request ID"); + warn!(method = %request.method, "duplicate pending cancellable request ID"); break; } let handler = request_handlers.read().get(&request.method).cloned(); @@ -608,7 +685,7 @@ impl JsonRpcClient { } } connection_closed.cancel(); - confirmation_requests.clear(); + cancellable_requests.clear(); // A handler may own the last Client clone, whose drop closes the RPC. // Release the registry lock before dropping those captured values. let handlers = std::mem::take(&mut *request_handlers.write()); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 43040d0944..93790a94b8 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -69,6 +69,8 @@ pub mod session; /// Custom session filesystem provider (virtualizable filesystem layer). pub mod session_fs; mod session_fs_dispatch; +/// Session-scoped skill provider callbacks. +pub mod skill_provider; /// Per-phase timing breakdown for [`Client::start`]. pub mod startup_timings; /// Event subscription handles returned by `subscribe()` methods. @@ -131,6 +133,7 @@ pub(crate) use jsonrpc::{ }; pub use mode::{BUILTIN_TOOLS_ISOLATED, ClientMode, ToolSet}; pub use provider_token::{BearerTokenError, BearerTokenProvider, ProviderTokenArgs}; +pub use skill_provider::{SkillProvider, SkillProviderDescriptor}; /// Re-exported JSON-RPC internals for integration tests (requires `test-support` feature). #[cfg(feature = "test-support")] @@ -2055,6 +2058,22 @@ impl Client { ) } + /// Construct a client from raw streams with a preset mode, for integration + /// tests that need mode-specific session defaults without spawning a CLI. + #[doc(hidden)] + #[cfg(any(test, feature = "test-support"))] + pub fn from_streams_with_mode_for_test( + reader: impl AsyncRead + Unpin + Send + 'static, + writer: impl AsyncWrite + Unpin + Send + 'static, + cwd: PathBuf, + mode: ClientMode, + ) -> Result { + Self::from_transport( + reader, writer, None, None, cwd, None, None, false, false, false, None, None, None, + mode, None, false, + ) + } + #[allow(clippy::too_many_arguments)] fn from_transport( reader: impl AsyncRead + Unpin + Send + 'static, @@ -2596,6 +2615,31 @@ impl Client { self.inner.router.register(session_id) } + /// Like [`register_session`](Self::register_session), but keeps the + /// registration it displaced so a failed resume can restore it with + /// [`restore_session_owned`](Self::restore_session_owned). + pub(crate) fn replace_session_registration( + &self, + session_id: &SessionId, + ) -> ( + crate::router::SessionRegistration, + Option, + ) { + self.inner.router.ensure_started(&self.inner); + self.inner.router.replace(session_id) + } + + /// Unregister the registration identified by `token`, handing the ID back + /// to `replaced` if that session is still running. + pub(crate) fn restore_session_owned( + &self, + session_id: &SessionId, + token: crate::router::RegistrationToken, + replaced: Option, + ) { + self.inner.router.restore_owned(session_id, token, replaced); + } + /// Unregister a session only if `token` still identifies the live /// registration. /// diff --git a/rust/src/router.rs b/rust/src/router.rs index 02fd5cfde2..9b0d68464b 100644 --- a/rust/src/router.rs +++ b/rust/src/router.rs @@ -1,12 +1,12 @@ use std::collections::HashMap; -use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Weak}; use parking_lot::Mutex; use tokio::sync::{broadcast, mpsc}; use tracing::warn; -use crate::jsonrpc::JsonRpcRequest; +use crate::jsonrpc::{JsonRpcError, JsonRpcRequest, JsonRpcResponse, error_codes}; use crate::types::{SessionEventNotification, SessionId}; /// Identity of one specific registration of a session ID. @@ -41,6 +41,13 @@ struct SessionSenders { token: RegistrationToken, } +/// A registration displaced by [`SessionRouter::replace`]. +/// +/// Holding it keeps the displaced session's channels open, so a failed +/// resume can hand routing back with [`SessionRouter::restore_owned`]. +/// Dropping it closes them, which ends that session's event loop. +pub(crate) struct ReplacedRegistration(SessionSenders); + /// Routes notifications and requests by sessionId to per-session channels. /// /// Internal to the SDK — consumers interact via `Client::register_session()`. @@ -64,10 +71,20 @@ impl SessionRouter { /// Replaces any existing registration for the same ID and returns a /// fresh [`RegistrationToken`] identifying this registration. pub(crate) fn register(&self, session_id: &SessionId) -> SessionRegistration { + self.replace(session_id).0 + } + + /// Like [`register`](Self::register), but also returns the registration + /// it displaced so the caller can [`restore_owned`](Self::restore_owned) + /// it if the new one is abandoned. + pub(crate) fn replace( + &self, + session_id: &SessionId, + ) -> (SessionRegistration, Option) { let (notif_tx, notif_rx) = mpsc::unbounded_channel(); let (req_tx, req_rx) = mpsc::unbounded_channel(); let token = RegistrationToken(self.next_token.fetch_add(1, Ordering::Relaxed)); - self.sessions.lock().insert( + let replaced = self.sessions.lock().insert( session_id.clone(), SessionSenders { notifications: notif_tx, @@ -75,13 +92,14 @@ impl SessionRouter { token, }, ); - SessionRegistration { + let registration = SessionRegistration { channels: SessionChannels { notifications: notif_rx, requests: req_rx, }, token, - } + }; + (registration, replaced.map(ReplacedRegistration)) } /// Unregister a session, dropping its channels. @@ -115,17 +133,37 @@ impl SessionRouter { &self, session_id: &SessionId, token: RegistrationToken, + ) -> bool { + self.restore_owned(session_id, token, None) + } + + /// Unregister the registration identified by `token`, handing the ID + /// back to `replaced` when that session is still running. + /// + /// Returns `false`, leaving the router unchanged, when `token` no longer + /// identifies the live registration. + pub(crate) fn restore_owned( + &self, + session_id: &SessionId, + token: RegistrationToken, + replaced: Option, ) -> bool { let mut sessions = self.sessions.lock(); - if sessions + if !sessions .get(session_id.as_str()) .is_some_and(|senders| senders.token == token) { - sessions.remove(session_id.as_str()); - true - } else { - false + return false; } + match replaced.filter(|replaced| !replaced.0.requests.is_closed()) { + Some(ReplacedRegistration(senders)) => { + sessions.insert(session_id.clone(), senders); + } + None => { + sessions.remove(session_id.as_str()); + } + } + true } /// Snapshot every currently-registered session ID. @@ -156,12 +194,13 @@ impl SessionRouter { /// Takes the notification broadcast and request channel from the client. /// If its request receiver was already taken by `take_request_rx()`, /// only notification routing is available. - pub(crate) fn ensure_started(&self, client: &crate::ClientInner) { + pub(crate) fn ensure_started(&self, client: &Arc) { let mut started = self.started.lock(); if *started { return; } *started = true; + let weak_client = Arc::downgrade(client); let extension_launch_provider = client.extension_launch_provider.clone(); let llm_inference = client.llm_inference.get().cloned(); let github_telemetry = client.on_github_telemetry.clone(); @@ -298,27 +337,44 @@ impl SessionRouter { .params .as_ref() .and_then(|p| p.get("sessionId")) - .and_then(|v| v.as_str()); + .and_then(|v| v.as_str()) + .map(str::to_owned); + let Some(session_id) = session_id else { + warn!(method = %request.method, "request missing sessionId"); + send_error( + &weak_client, + request.id, + error_codes::INVALID_PARAMS, + "missing required field: sessionId".to_string(), + ) + .await; + continue; + }; - if let Some(sid) = session_id { - let sender = { - let guard = sessions.lock(); - guard.get(sid).map(|s| s.requests.clone()) - }; - if let Some(sender) = sender { - let _ = sender.send(request); - } else { - warn!( - session_id = sid, - method = %request.method, - "request for unregistered session" - ); - } - } else { + let sender = sessions + .lock() + .get(session_id.as_str()) + .map(|s| s.requests.clone()); + let unrouted = match sender { + Some(sender) => sender.send(request).err().map(|error| error.0), + None => Some(request), + }; + // Every request owes the peer a response. Answer unroutable + // ones (for example, after a `Session` was dropped without + // detaching) so the runtime does not wait for its timeout. + if let Some(request) = unrouted { warn!( + session_id = %session_id, method = %request.method, - "request missing sessionId" + "request for unregistered session" ); + send_error( + &weak_client, + request.id, + error_codes::INTERNAL_ERROR, + format!("Session not found: {session_id}"), + ) + .await; } } }); @@ -326,5 +382,30 @@ impl SessionRouter { } } +async fn send_error( + client: &Weak, + request_id: u64, + code: i32, + message: String, +) { + let Some(inner) = client.upgrade() else { + return; + }; + // Retire any cancellation registered for a request that never reaches a handler. + drop(inner.rpc.cancellable_requests.claim(request_id)); + let _ = crate::Client::from_inner(inner) + .send_response(&JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request_id, + result: None, + error: Some(JsonRpcError { + code, + message, + data: None, + }), + }) + .await; +} + #[cfg(test)] mod tests; diff --git a/rust/src/router/tests.rs b/rust/src/router/tests.rs index 4d89fd937f..f63ca005f7 100644 --- a/rust/src/router/tests.rs +++ b/rust/src/router/tests.rs @@ -34,3 +34,36 @@ fn unregister_owned_removes_only_the_matching_registration() { // Removing twice is a no-op rather than evicting a future tenant. assert!(!router.unregister_owned(&session_id(), live.token)); } + +#[test] +fn restore_owned_hands_the_id_back_to_a_running_replaced_registration() { + let router = SessionRouter::new(); + let resident = router.register(&session_id()); + let (attempt, replaced) = router.replace(&session_id()); + assert!(replaced.is_some()); + + assert!(router.restore_owned(&session_id(), attempt.token, replaced)); + assert!(router.is_registered_owner(&session_id(), resident.token)); +} + +#[test] +fn restore_owned_unregisters_when_the_replaced_session_has_stopped() { + let router = SessionRouter::new(); + let resident = router.register(&session_id()); + let (attempt, replaced) = router.replace(&session_id()); + drop(resident); + + assert!(router.restore_owned(&session_id(), attempt.token, replaced)); + assert!(router.session_ids().is_empty()); +} + +#[test] +fn restore_owned_leaves_a_newer_registration_alone() { + let router = SessionRouter::new(); + let _resident = router.register(&session_id()); + let (attempt, replaced) = router.replace(&session_id()); + let newer = router.register(&session_id()); + + assert!(!router.restore_owned(&session_id(), attempt.token, replaced)); + assert!(router.is_registered_owner(&session_id(), newer.token)); +} diff --git a/rust/src/session.rs b/rust/src/session.rs index 14864a3137..462d4ca5cc 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -30,6 +30,7 @@ use crate::handler::{ use crate::hooks::SessionHooks; use crate::provider_token::BearerTokenProvider; use crate::session_fs::SessionFsProvider; +use crate::skill_provider::SkillProvider; use crate::trace_context::inject_trace_context; use crate::transforms::SystemMessageTransform; use crate::types::{ @@ -92,11 +93,26 @@ pub(crate) struct SessionHandlers { pub user_input: Option>, pub exit_plan_mode: Option>, pub auto_mode_switch: Option>, + pub skill_provider: Option>, /// Shared with the owning [`Session`], which replaces the map when the /// runtime accepts a [`Session::set_tools`] call. pub tools: Arc>, } +struct ToolHandlerRetirement { + handlers: Arc>, + shutdown: CancellationToken, +} + +impl Drop for ToolHandlerRetirement { + fn drop(&mut self) { + self.shutdown.cancel(); + // Handler destructors may re-enter the SDK; release the map lock first. + let handlers = std::mem::take(&mut *self.handlers.write()); + drop(handlers); + } +} + type PendingExternalTools = Arc>>>; struct PendingExternalToolGuard { @@ -262,6 +278,10 @@ struct PendingSessionRegistration { shutdown: CancellationToken, external_tools_shutdown: CancellationToken, startup_tasks: Arc, + /// The resident session a resume displaced. The runtime keeps that + /// session's bindings when the resume fails, so cleanup routes its + /// callbacks back to it; success drops it. + replaced: Option, disarmed: bool, } @@ -295,10 +315,19 @@ impl PendingSessionRegistration { shutdown, external_tools_shutdown, startup_tasks: Arc::new(StartupTasks::default()), + replaced: None, disarmed: false, } } + fn restoring_on_failure( + mut self, + replaced: Option, + ) -> Self { + self.replaced = replaced; + self + } + /// Guard for a registration whose session ID is assigned by the server. fn deferred( client: Client, @@ -312,6 +341,7 @@ impl PendingSessionRegistration { shutdown: shutdown.clone(), external_tools_shutdown, startup_tasks: Arc::new(StartupTasks::default()), + replaced: None, disarmed: false, }; let callback: crate::jsonrpc::InlineResponseCallback = Box::new(move |response| { @@ -358,7 +388,8 @@ impl PendingSessionRegistration { let _ = event_loop.await; if let Some(id) = self.registered_id() { if let PendingSessionId::Known(_, token) = self.session_id { - self.client.unregister_session_owned(&id, token); + self.client + .restore_session_owned(&id, token, self.replaced.take()); } else if let PendingSessionId::Deferred(stash) = &self.session_id && let Some((id, registration)) = stash.lock().as_ref() { @@ -370,6 +401,7 @@ impl PendingSessionRegistration { fn disarm(&mut self) { self.startup_tasks.disarm(); + self.replaced = None; self.disarmed = true; } } @@ -380,7 +412,8 @@ impl Drop for PendingSessionRegistration { self.cancel(); if let Some(id) = self.registered_id() { if let PendingSessionId::Known(_, token) = self.session_id { - self.client.unregister_session_owned(&id, token); + self.client + .restore_session_owned(&id, token, self.replaced.take()); } else if let PendingSessionId::Deferred(stash) = &self.session_id && let Some((id, registration)) = stash.lock().as_ref() { @@ -539,8 +572,9 @@ pub struct Session { capabilities: Arc>, /// Canvas instances currently known to be open for this session. open_canvases: Arc>>, - /// Broadcast channel for runtime event subscribers — see [`Session::subscribe`]. - event_tx: tokio::sync::broadcast::Sender, + /// Only the event loop owns the producer, so retaining a retired session + /// cannot keep its subscriptions open. + event_tx: tokio::sync::broadcast::WeakSender, /// Resume-only queue retained until the first post-resume subscriber /// catches up and switches to bounded live delivery. resume_bootstrap: Option>, @@ -631,7 +665,7 @@ impl Session { /// Returns a [`CancellationToken`] that fires when this session shuts /// down (via [`Session::stop_event_loop`], [`Session::destroy`], or - /// [`Drop`]). + /// [`Drop`]), loses its router registration, or its owner connection closes. /// /// Use this to bind an external task's lifetime to the session — when /// the session shuts down, awaiting [`cancelled()`](CancellationToken::cancelled) @@ -679,6 +713,13 @@ impl Session { /// loop or any combinator from `tokio_stream::StreamExt` / /// `futures::StreamExt`. /// + /// After session shutdown or owner connection loss (including + /// [`Client::force_stop`]), returns an immediately closed subscription: + /// `recv` reports [`RecvErrorKind::Closed`](crate::subscription::RecvErrorKind::Closed) + /// and stream iteration ends. Ordinary shutdown lets existing subscribers + /// drain published events. Client force-stop instead closes all subscriptions + /// immediately and discards unread live and resume-bootstrap events. + /// /// If resume started with no active [`PreparedSession`] subscriber, the /// first subscription receives all retained routed startup events, durable /// and ephemeral, in order before live delivery. Ownership is assigned @@ -690,7 +731,8 @@ impl Session { /// that never subscribe or cannot catch up can retain arbitrarily many /// events, so subscribe and drain promptly. Dropping the owner discards /// its unread backlog; stopping the event loop releases an unclaimed - /// backlog. A claimed backlog can still be drained after shutdown. + /// backlog. A claimed backlog can still be drained after ordinary shutdown, + /// but client force-stop discards it. /// This guarantee covers events routed to this session, not events lost /// to overflow in the bounded client-global notification router. /// @@ -714,10 +756,18 @@ impl Session { /// # } /// ``` pub fn subscribe(&self) -> crate::subscription::EventSubscription { + let connection_closed = self.client.inner.rpc.connection_closed_token(); + if self.shutdown.is_cancelled() || connection_closed.is_cancelled() { + return crate::subscription::EventSubscription::closed(); + } + let Some(event_tx) = self.event_tx.upgrade() else { + return crate::subscription::EventSubscription::closed(); + }; match &self.resume_bootstrap { - Some(bootstrap) => bootstrap.subscribe(&self.event_tx), - None => crate::subscription::EventSubscription::new(self.event_tx.subscribe()), + Some(bootstrap) => bootstrap.subscribe(&event_tx), + None => crate::subscription::EventSubscription::new(event_tx.subscribe()), } + .with_force_stop(self.client.inner.force_stop_requested.clone()) } /// The underlying Client (for advanced use cases). @@ -976,7 +1026,16 @@ impl Session { async fn send_and_wait_structured(&self, opts: MessageOptions) -> Result { let duration = opts.wait_timeout.unwrap_or(Duration::from_secs(60)); // Internal request observers must not claim the caller's resume backlog. - let mut events = crate::subscription::EventSubscription::new(self.event_tx.subscribe()); + let mut events = self + .event_tx + .upgrade() + .map(|sender| { + crate::subscription::EventSubscription::new(sender.subscribe()) + .with_force_stop(self.client.inner.force_stop_requested.clone()) + }) + .ok_or_else(|| { + structured_output_error("session closed before structured output completed") + })?; let wait = async { let mut admission = Box::pin(self.send(opts)); let mut pending = Vec::new(); @@ -1207,18 +1266,34 @@ impl Session { pub async fn set_tools>(&self, tools: I) -> Result<(), Error> { let mut tools: Vec = tools.into_iter().collect(); let handlers = take_tool_handlers(&mut tools)?; + if self.shutdown.is_cancelled() || self.external_tools_shutdown.is_cancelled() { + return Err(ErrorKind::Session(SessionErrorKind::EventLoopClosed).into()); + } let mut params = serde_json::json!({ "sessionId": self.id }); params["tools"] = serde_json::to_value(&tools)?; let set_tools_lock = self.set_tools_lock.clone().lock_owned().await; + if self.shutdown.is_cancelled() || self.external_tools_shutdown.is_cancelled() { + return Err(ErrorKind::Session(SessionErrorKind::EventLoopClosed).into()); + } let installed = self.tool_handlers.clone(); + let shutdown = self.shutdown.clone(); + let connection_closed = self.external_tools_shutdown.clone(); let replaced = Arc::new(ParkingLotMutex::new(None)); // Swapping on the read task means no tool request read after the // runtime accepted is dispatched to the previous handlers. let accepted: crate::jsonrpc::InlineResponseCallback = Box::new({ let replaced = replaced.clone(); move |_| { - let previous = std::mem::replace(&mut *installed.write(), handlers); + let previous = { + let mut installed = installed.write(); + if shutdown.is_cancelled() || connection_closed.is_cancelled() { + // Retired replacements also drop captures on the replacement task. + *replaced.lock() = Some(handlers); + return Err(ErrorKind::Session(SessionErrorKind::EventLoopClosed).into()); + } + std::mem::replace(&mut *installed, handlers) + }; *replaced.lock() = Some(previous); Ok(()) } @@ -1658,6 +1733,12 @@ impl Client { shutdown: CancellationToken, ) -> Result { let total_start = Instant::now(); + if config.cloud.is_some() && config.skill_provider.is_some() { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + "Skill providers are not supported for cloud sessions.", + )); + } let ahp_creation_config = if self .inner .ahp_host_sessions @@ -1766,6 +1847,7 @@ impl Client { user_input: runtime.user_input_handler.take(), exit_plan_mode: runtime.exit_plan_mode_handler.take(), auto_mode_switch: runtime.auto_mode_switch_handler.take(), + skill_provider: runtime.skill_provider.take(), tools: tool_handlers.clone(), }; let hooks = runtime.hooks_handler.take(); @@ -1984,7 +2066,7 @@ impl Client { idle_waiter, capabilities, open_canvases, - event_tx, + event_tx: event_tx.downgrade(), resume_bootstrap: None, github_token_registration: ParkingLotMutex::new(github_token_registration), registration_token, @@ -2111,6 +2193,7 @@ impl Client { user_input: runtime.user_input_handler.take(), exit_plan_mode: runtime.exit_plan_mode_handler.take(), auto_mode_switch: runtime.auto_mode_switch_handler.take(), + skill_provider: runtime.skill_provider.take(), tools: tool_handlers.clone(), }; let hooks = runtime.hooks_handler.take(); @@ -2162,7 +2245,7 @@ impl Client { // Active prepared subscribers retain their existing bounded delivery. let resume_bootstrap = (event_tx.receiver_count() == 0) .then(|| crate::subscription::ResumeBootstrap::new(&event_tx)); - let registration = self.register_session(&session_id); + let (registration, replaced) = self.replace_session_registration(&session_id); let registration_token = registration.token; let channels = registration.channels; let idle_waiter = Arc::new(ParkingLotMutex::new(None)); @@ -2174,7 +2257,8 @@ impl Client { registration_token, shutdown.clone(), external_tools_shutdown.clone(), - ); + ) + .restoring_on_failure(replaced); let event_loop = spawn_event_loop( session_id.clone(), Client::from_inner(self.inner.clone()), @@ -2299,7 +2383,7 @@ impl Client { idle_waiter, capabilities, open_canvases, - event_tx, + event_tx: event_tx.downgrade(), resume_bootstrap, github_token_registration: ParkingLotMutex::new(github_token_registration), registration_token, @@ -2421,9 +2505,10 @@ impl PreparedSession { /// observes [`Lagged`](crate::subscription::Lagged) and skips the /// events it missed, rather than stalling the session's event loop. /// Subscriptions taken here close if the prepared session is dropped - /// without starting, or if startup fails. + /// without starting, if startup fails, or immediately on client force-stop. pub fn subscribe(&self) -> crate::subscription::EventSubscription { crate::subscription::EventSubscription::new(self.event_tx.subscribe()) + .with_force_stop(self.client.inner.force_stop_requested.clone()) } /// Create or resume the session on the CLI. @@ -2606,12 +2691,26 @@ fn spawn_event_loop( let bootstrap_cleanup = resume_bootstrap .as_ref() .map(|bootstrap| bootstrap.cleanup_guard()); + let tool_handler_retirement = ToolHandlerRetirement { + handlers: handlers.tools.clone(), + shutdown: shutdown.clone(), + }; + let connection_closed = client.inner.rpc.connection_closed_token(); let span = tracing::error_span!("session_event_loop", session_id = %session_id); tokio::spawn( async move { let _bootstrap_cleanup = bootstrap_cleanup; + let _tool_handler_retirement = tool_handler_retirement; + let mut notifications_open = true; + let mut requests_open = true; loop { + if shutdown.is_cancelled() + || connection_closed.is_cancelled() + || (!notifications_open && !requests_open) + { + break; + } // `mpsc::UnboundedReceiver::recv` and // `CancellationToken::cancelled` are both cancel-safe per // RFD 400. @@ -2637,12 +2736,21 @@ fn spawn_event_loop( // background tasks to perform cancel-unsafe operations" pattern. tokio::select! { _ = shutdown.cancelled() => break, - Some(notification) = notifications.recv() => { + _ = connection_closed.cancelled() => break, + notification = notifications.recv(), if notifications_open => { + let Some(notification) = notification else { + notifications_open = false; + continue; + }; handle_notification( &session_id, &client, &handlers, &command_handlers, notification, &idle_waiter, &capabilities, &open_canvases, &event_tx, resume_bootstrap.as_ref(), &shutdown, &external_tools_shutdown, &pending_external_tools, startup_tasks.as_ref(), ).await; } - Some(request) = requests.recv() => { + request = requests.recv(), if requests_open => { + let Some(request) = request else { + requests_open = false; + continue; + }; // Clone the Arc-backed dispatch context into the task so // the spawned `handle_request` future is `'static`. All // clones are cheap (Arc refcount bumps / small maps). @@ -2686,7 +2794,6 @@ fn spawn_event_loop( startup_tasks.as_ref(), ); } - else => break, } } // Channels closed or shutdown signaled — fail any pending @@ -3531,6 +3638,110 @@ struct RequestDispatchContext<'a> { bearer_token_providers: &'a HashMap>, } +/// Serve one `skillProvider.*` callback. Provider failures are reported +/// generically so their details never reach the runtime or the model. +/// +/// A `$/cancelRequest` for this request, or the connection closing, drops the +/// provider future. +async fn handle_skill_provider_request( + client: &Client, + session_id: &SessionId, + provider: Option<&Arc>, + request: &crate::JsonRpcRequest, +) { + let Some(pending) = client.inner.rpc.cancellable_requests.claim(request.id) else { + // The connection closed before dispatch, so nobody awaits a response. + return; + }; + let Some(provider) = provider else { + let _ = send_error_response( + client, + request.id, + error_codes::INTERNAL_ERROR, + &format!("No skill provider for session: {session_id}"), + ) + .await; + return; + }; + + let read_name = if request.method == rpc_methods::SKILLPROVIDER_READ { + let Some(name) = request + .params + .as_ref() + .and_then(|params| params.get("name")) + .and_then(Value::as_str) + else { + let _ = send_error_response( + client, + request.id, + error_codes::INVALID_PARAMS, + "missing required field: name", + ) + .await; + return; + }; + Some(name) + } else { + None + }; + let operation = if read_name.is_some() { + "readSkill" + } else { + "listSkills" + }; + let call = async { + match read_name { + Some(name) => provider + .read_skill(name) + .await + .map(|markdown| serde_json::json!({ "markdown": markdown })), + None => provider + .list_skills() + .await + .map(|skills| serde_json::json!({ "skills": skills })), + } + }; + + let connection_closed = client.inner.rpc.connection_closed_token(); + let result = tokio::select! { + biased; + _ = connection_closed.cancelled() => return, + _ = pending.cancellation().cancelled() => { + let _ = send_error_response( + client, + request.id, + error_codes::REQUEST_CANCELLED, + &format!("Skill provider {operation} cancelled"), + ) + .await; + return; + } + result = call => result, + }; + + match result { + Ok(result) => { + let response = JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request.id, + result: Some(result), + error: None, + }; + let _ = client.send_response(&response).await; + } + Err(error) => { + warn!(error = %error, operation, "skill provider callback failed"); + let _ = send_error_response( + client, + request.id, + error_codes::INTERNAL_ERROR, + &format!("Skill provider {operation} failed"), + ) + .await; + } + } +} + /// Process a JSON-RPC request from the CLI. async fn handle_request( session_id: &SessionId, @@ -3561,6 +3772,14 @@ async fn handle_request( return; } + if request.method == rpc_methods::SKILLPROVIDER_LIST + || request.method == rpc_methods::SKILLPROVIDER_READ + { + handle_skill_provider_request(client, &sid, handlers.skill_provider.as_ref(), &request) + .await; + return; + } + match request.method.as_str() { "hooks.invoke" => { let params = request.params.as_ref(); diff --git a/rust/src/session/startup_tasks_tests.rs b/rust/src/session/startup_tasks_tests.rs index bdec2551ad..5bc14f3287 100644 --- a/rust/src/session/startup_tasks_tests.rs +++ b/rust/src/session/startup_tasks_tests.rs @@ -68,6 +68,7 @@ async fn check_aborted_nested_dispatches(nested_first: bool) { user_input: None, exit_plan_mode: None, auto_mode_switch: None, + skill_provider: None, tools: Arc::new(Default::default()), }; let startup_tasks = Arc::new(StartupTasks::default()); diff --git a/rust/src/skill_provider.rs b/rust/src/skill_provider.rs new file mode 100644 index 0000000000..28d4fc8911 --- /dev/null +++ b/rust/src/skill_provider.rs @@ -0,0 +1,40 @@ +//! Session-scoped skill provider callbacks. +//! +//!
    +//! +//! **Experimental.** These types are part of an experimental wire-protocol +//! surface and may change or be removed in future SDK or CLI releases. +//! +//!
    + +use async_trait::async_trait; + +use crate::Error; +pub use crate::rpc::SkillProviderDescriptor; + +/// Supplies session-scoped skills to the runtime. +/// +///
    +/// +/// **Experimental.** This trait is part of an experimental wire-protocol +/// surface and may change or be removed in future SDK or CLI releases. +/// +///
    +/// +/// Each inbound `skillProvider.*` request is dispatched on its own spawned +/// task, so implementations must be safe for concurrent calls. When the +/// runtime cancels a call, for example after its time limit or when the +/// session disconnects, the SDK drops the provider future and the runtime +/// ignores any later result. Register a +/// provider with [`SessionConfig::with_skill_provider`](crate::SessionConfig::with_skill_provider) +/// or [`ResumeSessionConfig::with_skill_provider`](crate::ResumeSessionConfig::with_skill_provider). +#[async_trait] +pub trait SkillProvider: Send + Sync + 'static { + /// Return the catalog of skills this session exposes. + async fn list_skills(&self) -> std::result::Result, Error>; + + /// Return the markdown for one skill name. + /// + /// Return `Ok(None)` when the skill name is not found. + async fn read_skill(&self, name: &str) -> std::result::Result, Error>; +} diff --git a/rust/src/subscription.rs b/rust/src/subscription.rs index d4f989c107..f85ad9a5fc 100644 --- a/rust/src/subscription.rs +++ b/rust/src/subscription.rs @@ -34,6 +34,7 @@ use std::collections::VecDeque; use std::fmt; +use std::future::Future; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; @@ -43,6 +44,7 @@ use tokio::sync::broadcast::{Receiver, Sender, WeakSender}; use tokio_stream::wrappers::BroadcastStream; use tokio_stream::wrappers::errors::BroadcastStreamRecvError; use tokio_stream::{Stream, StreamExt as _}; +use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned}; use crate::types::{SessionEvent, SessionLifecycleEvent}; use crate::{Custom, Repr}; @@ -202,6 +204,7 @@ impl ResumeBootstrap { EventSubscription { inner: None, bootstrap: Some(self.clone()), + force_stop: None, } } ResumeBootstrapState::Claimed(_) | ResumeBootstrapState::Disabled => { @@ -255,16 +258,31 @@ impl ResumeBootstrap { pub struct EventSubscription { inner: Option>, bootstrap: Option>, + force_stop: Option>>, } impl EventSubscription { + pub(crate) fn closed() -> Self { + Self { + inner: None, + bootstrap: None, + force_stop: None, + } + } + pub(crate) fn new(rx: Receiver) -> Self { Self { inner: Some(BroadcastStream::new(rx)), bootstrap: None, + force_stop: None, } } + pub(crate) fn with_force_stop(mut self, force_stop: CancellationToken) -> Self { + self.force_stop = Some(Box::pin(force_stop.cancelled_owned())); + self + } + fn next_bootstrap_event(&mut self) -> Option { let event = self .bootstrap @@ -284,7 +302,8 @@ impl EventSubscription { /// - [`RecvErrorKind::Lagged`] if live delivery fell behind; call again /// to continue from the next available live event. /// - [`RecvErrorKind::Closed`] once the producer is gone and any retained - /// events have been drained. + /// events have been drained, or immediately after client force-stop. + /// Force-stop discards unread live and resume-bootstrap events. /// /// # Cancel safety /// @@ -304,6 +323,18 @@ impl Stream for EventSubscription { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + if self + .force_stop + .as_mut() + .is_some_and(|force_stop| force_stop.as_mut().poll(cx).is_ready()) + { + self.force_stop = None; + self.inner = None; + if let Some(bootstrap) = self.bootstrap.take() { + bootstrap.abandon(); + } + return Poll::Ready(None); + } if let Some(event) = self.next_bootstrap_event() { return Poll::Ready(Some(Ok(event))); } diff --git a/rust/src/subscription/tests.rs b/rust/src/subscription/tests.rs index be8a076f0b..adb4496352 100644 --- a/rust/src/subscription/tests.rs +++ b/rust/src/subscription/tests.rs @@ -41,6 +41,92 @@ fn make_event(id: &str) -> SessionEvent { } } +#[tokio::test] +async fn closed_subscription_ends_on_its_first_poll() { + let mut events = EventSubscription::closed(); + assert!(next_ready(&mut events).is_none()); + assert!(matches!( + recv_ready(&mut events).unwrap_err().kind(), + RecvErrorKind::Closed + )); + assert!(next_ready(&mut events).is_none()); +} + +#[tokio::test] +async fn force_stop_discards_buffered_live_events_with_a_retained_producer() { + let (tx, rx) = broadcast::channel(8); + let force_stop = CancellationToken::new(); + let mut events = EventSubscription::new(rx).with_force_stop(force_stop.clone()); + tx.send(make_event("unread")).unwrap(); + force_stop.cancel(); + + assert!(next_ready(&mut events).is_none()); + assert!(matches!( + recv_ready(&mut events).unwrap_err().kind(), + RecvErrorKind::Closed + )); + assert_eq!(tx.receiver_count(), 0); +} + +#[tokio::test] +async fn force_stop_discards_owned_resume_bootstrap() { + let (tx, _) = broadcast::channel(8); + let bootstrap = ResumeBootstrap::new(&tx); + bootstrap.publish(&tx, make_event("unread")); + let force_stop = CancellationToken::new(); + let mut events = bootstrap.subscribe(&tx).with_force_stop(force_stop.clone()); + force_stop.cancel(); + + assert!(next_ready(&mut events).is_none()); + assert!(matches!( + *bootstrap.state.lock(), + ResumeBootstrapState::Disabled + )); + assert!(matches!( + recv_ready(&mut events).unwrap_err().kind(), + RecvErrorKind::Closed + )); +} + +#[tokio::test] +async fn force_stop_wakes_a_pending_receive_without_producer_shutdown() { + use std::sync::atomic::{AtomicBool, Ordering}; + use std::task::{Wake, Waker}; + + struct WakeFlag(AtomicBool); + + impl Wake for WakeFlag { + fn wake(self: Arc) { + self.0.store(true, Ordering::SeqCst); + } + } + + let (_tx, rx) = broadcast::channel(8); + let force_stop = CancellationToken::new(); + let mut events = EventSubscription::new(rx).with_force_stop(force_stop.clone()); + let mut receive = Box::pin(events.recv()); + let woke = Arc::new(WakeFlag(AtomicBool::new(false))); + let waker = Waker::from(woke.clone()); + assert!( + receive + .as_mut() + .poll(&mut Context::from_waker(&waker)) + .is_pending() + ); + woke.0.store(false, Ordering::SeqCst); + force_stop.cancel(); + + assert!( + woke.0.load(Ordering::SeqCst), + "force-stop must notify the pending receiver's registered waker" + ); + let error = receive + .now_or_never() + .expect("the awakened receiver must close on its first poll") + .unwrap_err(); + assert!(matches!(error.kind(), RecvErrorKind::Closed), "{error:?}"); +} + #[tokio::test] async fn recv_yields_then_closes_on_drop_sender() { let (tx, rx) = broadcast::channel(8); diff --git a/rust/src/types.rs b/rust/src/types.rs index dbf40ce4f5..72399a2522 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -48,6 +48,7 @@ pub use crate::session_fs::{ SessionFsSqliteQueryResult, SessionFsSqliteQueryType, SessionFsSqliteTransactionError, SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionStatement, }; +use crate::skill_provider::SkillProvider; pub use crate::trace_context::{TraceContext, TraceContextProvider}; use crate::transforms::SystemMessageTransform; @@ -1871,6 +1872,9 @@ pub struct ManagedSettingsPermissions { /// are forwarded so newer runtime policies fail closed. #[serde(default, skip_serializing_if = "Option::is_none")] pub disable_bypass_permissions_mode: Option, + /// When true, prevents Assisted Permissions from being activated. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub disable_assisted_permissions_mode: Option, /// Tool-permission patterns that are always denied. #[serde(default, skip_serializing_if = "Option::is_none")] pub deny: Option>, @@ -1880,6 +1884,12 @@ pub struct ManagedSettingsPermissions { /// Tool-permission patterns that are allowed without prompting. #[serde(default, skip_serializing_if = "Option::is_none")] pub allow: Option>, + /// Closed-world host boundary expressed as `Domain(hostname)`, + /// `Domain(IP)`, or `Domain(*.example.com)` rules. Schemes, ports, paths, + /// queries, and fragments are rejected. Multiple managed layers intersect + /// their lists. A present empty list denies all hosts. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub limit_to: Option>, } impl ManagedSettingsPermissions { @@ -1889,6 +1899,12 @@ impl ManagedSettingsPermissions { self } + /// Sets whether this managed layer disables Assisted Permissions. + pub fn with_disable_assisted_permissions_mode(mut self, value: bool) -> Self { + self.disable_assisted_permissions_mode = Some(value); + self + } + /// Sets the rules that are always denied. pub fn with_deny(mut self, rules: Vec) -> Self { self.deny = Some(rules); @@ -1906,6 +1922,12 @@ impl ManagedSettingsPermissions { self.allow = Some(rules); self } + + /// Sets the closed-world domain boundary for this managed layer. + pub fn with_limit_to(mut self, rules: Vec) -> Self { + self.limit_to = Some(rules); + self + } } /// Managed-settings layer injected at session startup. Currently carries only a @@ -2115,6 +2137,13 @@ pub struct SessionConfig { pub enable_session_store: Option, /// When true, enables skills for this session. pub enable_skills: Option, + /// **Experimental.** Supplies session-scoped skills from the SDK host. + /// + /// The provider is runtime-only and is not serialized. Set + /// [`enable_skills`](Self::enable_skills) to `Some(true)` when using this + /// in [`ClientMode::Empty`](crate::ClientMode::Empty), where built-in + /// skill loading is otherwise disabled by default. + pub skill_provider: Option>, /// **Experimental.** This option is part of an experimental wire-protocol /// surface (SEP-1865) and may change or be removed in a future release. /// @@ -2287,6 +2316,11 @@ pub struct SessionConfig { /// session creation (fail-closed). When `None`, behaves exactly as before. Set via /// [`with_enable_managed_settings`](Self::with_enable_managed_settings). pub enable_managed_settings: Option, + /// When `Some(true)`, locked managed model controls are enforced for this + /// session. Conflicting model changes are rejected; overridable managed + /// defaults remain mutable. Set via + /// [`with_enforce_managed_model_defaults`](Self::with_enforce_managed_model_defaults). + pub enforce_managed_model_defaults: Option, /// Optional managed-settings layer injected at session bootstrap. Currently /// carries a [`permissions`](ManagedSettingsPermissions) object that composes /// restrictively with any server- or device-level managed settings. This @@ -2434,6 +2468,10 @@ impl std::fmt::Debug for SessionConfig { ) .field("enable_session_store", &self.enable_session_store) .field("enable_skills", &self.enable_skills) + .field( + "skill_provider", + &self.skill_provider.as_ref().map(|_| ""), + ) .field("enable_mcp_apps", &self.enable_mcp_apps) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) @@ -2479,6 +2517,10 @@ impl std::fmt::Debug for SessionConfig { .field("feature_flags", &self.feature_flags) .field("exp_assignments", &self.exp_assignments) .field("enable_managed_settings", &self.enable_managed_settings) + .field( + "enforce_managed_model_defaults", + &self.enforce_managed_model_defaults, + ) .field("enable_experimental_mode", &self.enable_experimental_mode) .field("managed_settings", &self.managed_settings) .field( @@ -2565,6 +2607,7 @@ impl Default for SessionConfig { enable_host_git_operations: None, enable_session_store: None, enable_skills: None, + skill_provider: None, embedding_cache_storage: None, enable_mcp_apps: None, github_mcp_tool_config: None, @@ -2602,6 +2645,7 @@ impl Default for SessionConfig { feature_flags: None, exp_assignments: None, enable_managed_settings: None, + enforce_managed_model_defaults: None, managed_settings: None, session_fs_provider: None, permission_handler: None, @@ -2641,6 +2685,7 @@ pub(crate) struct SessionConfigRuntime { pub tool_handlers: ToolHandlerMap, pub canvas_handler: Option>, pub session_fs_provider: Option>, + pub skill_provider: Option>, pub bearer_token_providers: HashMap>, pub github_token_provider: Option>, pub commands: Option>, @@ -2675,6 +2720,7 @@ impl SessionConfig { let request_auto_mode_switch = self.auto_mode_switch_handler.is_some(); let request_elicitation = self.elicitation_handler.is_some(); let hooks_flag = self.hooks_handler.is_some(); + let has_skill_provider = self.skill_provider.is_some(); let tool_handlers = take_tool_handlers(self.tools.as_deref_mut().unwrap_or_default())?; @@ -2728,6 +2774,7 @@ impl SessionConfig { enable_host_git_operations: self.enable_host_git_operations, enable_session_store: self.enable_session_store, enable_skills: self.enable_skills, + has_skill_provider: has_skill_provider.then_some(true), request_user_input, request_permission: permission_active, request_exit_plan_mode, @@ -2771,6 +2818,7 @@ impl SessionConfig { feature_flags: self.feature_flags, exp_assignments: self.exp_assignments, enable_managed_settings: self.enable_managed_settings, + enforce_managed_model_defaults: self.enforce_managed_model_defaults, is_experimental_mode: self.enable_experimental_mode, managed_settings: self.managed_settings, }; @@ -2788,6 +2836,7 @@ impl SessionConfig { tool_handlers, canvas_handler, session_fs_provider: self.session_fs_provider, + skill_provider: self.skill_provider, bearer_token_providers, github_token_provider: self.github_token_provider, commands: self.commands, @@ -3151,6 +3200,14 @@ impl SessionConfig { self } + /// Install an experimental session-scoped [`SkillProvider`]. + /// + /// The provider is not serialized and must be re-supplied on resume. + pub fn with_skill_provider(mut self, provider: Arc) -> Self { + self.skill_provider = Some(provider); + self + } + /// **Experimental.** This method is part of an experimental wire-protocol /// surface (SEP-1865) and may change or be removed in a future release. /// @@ -3481,6 +3538,12 @@ impl SessionConfig { self } + /// Enforce locked managed model controls for this session. + pub fn with_enforce_managed_model_defaults(mut self, enforce: bool) -> Self { + self.enforce_managed_model_defaults = Some(enforce); + self + } + /// Inject a managed-settings layer (currently permission rules) at session /// bootstrap. This layer is startup-only and is not persisted, so it must be /// re-supplied on resume to remain in effect. Can be combined with @@ -3599,6 +3662,11 @@ pub struct ResumeSessionConfig { pub enable_session_store: Option, /// When true, enables skills on resume. pub enable_skills: Option, + /// **Experimental.** Supplies session-scoped skills from the SDK host. + /// + /// The provider is runtime-only and is not serialized. Re-supply it on + /// each resume because providers are not persisted by the runtime. + pub skill_provider: Option>, /// **Experimental.** This option is part of an experimental wire-protocol /// surface (SEP-1865) and may change or be removed in a future release. /// @@ -3715,6 +3783,9 @@ pub struct ResumeSessionConfig { /// process restart. Set via /// [`with_enable_managed_settings`](Self::with_enable_managed_settings). pub enable_managed_settings: Option, + /// Re-applies managed model lock enforcement on resume. See + /// [`SessionConfig::enforce_managed_model_defaults`]. + pub enforce_managed_model_defaults: Option, /// Optional managed-settings layer injected on resume. See /// [`SessionConfig::managed_settings`]. This layer is not persisted, so it /// must be re-supplied on resume to remain in effect; omitting it clears the @@ -3840,6 +3911,10 @@ impl std::fmt::Debug for ResumeSessionConfig { ) .field("enable_session_store", &self.enable_session_store) .field("enable_skills", &self.enable_skills) + .field( + "skill_provider", + &self.skill_provider.as_ref().map(|_| ""), + ) .field("enable_mcp_apps", &self.enable_mcp_apps) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) @@ -3884,6 +3959,10 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("feature_flags", &self.feature_flags) .field("exp_assignments", &self.exp_assignments) .field("enable_managed_settings", &self.enable_managed_settings) + .field( + "enforce_managed_model_defaults", + &self.enforce_managed_model_defaults, + ) .field("enable_experimental_mode", &self.enable_experimental_mode) .field("managed_settings", &self.managed_settings) .field( @@ -3949,6 +4028,7 @@ impl ResumeSessionConfig { let request_auto_mode_switch = self.auto_mode_switch_handler.is_some(); let request_elicitation = self.elicitation_handler.is_some(); let hooks_flag = self.hooks_handler.is_some(); + let has_skill_provider = self.skill_provider.is_some(); let tool_handlers = take_tool_handlers(self.tools.as_deref_mut().unwrap_or_default())?; @@ -4002,6 +4082,7 @@ impl ResumeSessionConfig { enable_host_git_operations: self.enable_host_git_operations, enable_session_store: self.enable_session_store, enable_skills: self.enable_skills, + has_skill_provider: has_skill_provider.then_some(true), request_user_input, request_permission: permission_active, request_exit_plan_mode, @@ -4044,6 +4125,7 @@ impl ResumeSessionConfig { feature_flags: self.feature_flags, exp_assignments: self.exp_assignments, enable_managed_settings: self.enable_managed_settings, + enforce_managed_model_defaults: self.enforce_managed_model_defaults, is_experimental_mode: self.enable_experimental_mode, managed_settings: self.managed_settings, suppress_resume_event: self.suppress_resume_event, @@ -4064,6 +4146,7 @@ impl ResumeSessionConfig { tool_handlers, canvas_handler, session_fs_provider: self.session_fs_provider, + skill_provider: self.skill_provider, bearer_token_providers, github_token_provider: self.github_token_provider, commands: self.commands, @@ -4113,6 +4196,7 @@ impl ResumeSessionConfig { enable_host_git_operations: None, enable_session_store: None, enable_skills: None, + skill_provider: None, embedding_cache_storage: None, enable_mcp_apps: None, github_mcp_tool_config: None, @@ -4149,6 +4233,7 @@ impl ResumeSessionConfig { feature_flags: None, exp_assignments: None, enable_managed_settings: None, + enforce_managed_model_defaults: None, managed_settings: None, session_fs_provider: None, suppress_resume_event: None, @@ -4248,6 +4333,14 @@ impl ResumeSessionConfig { self } + /// Install an experimental session-scoped [`SkillProvider`]. + /// + /// The provider is not persisted and must be re-supplied on each resume. + pub fn with_skill_provider(mut self, provider: Arc) -> Self { + self.skill_provider = Some(provider); + self + } + /// Auto-approve every permission request on the resumed session. See /// [`SessionConfig::approve_all_permissions`]. pub fn approve_all_permissions(mut self) -> Self { @@ -4841,6 +4934,12 @@ impl ResumeSessionConfig { self } + /// Enforce locked managed model controls after resume. + pub fn with_enforce_managed_model_defaults(mut self, enforce: bool) -> Self { + self.enforce_managed_model_defaults = Some(enforce); + self + } + /// Inject a managed-settings layer (currently permission rules) on resume. /// See [`SessionConfig::with_managed_settings`]. Must be re-supplied on /// resume; omitting it clears the previously injected layer. diff --git a/rust/src/types/tests.rs b/rust/src/types/tests.rs index a61c083a4f..774feeda9f 100644 --- a/rust/src/types/tests.rs +++ b/rust/src/types/tests.rs @@ -888,6 +888,23 @@ fn resume_session_config_into_wire_serializes_plugin_directories_and_large_outpu assert!(empty_json.get("largeOutput").is_none()); } +#[test] +fn session_configs_serialize_managed_model_enforcement() { + let (create_wire, _) = SessionConfig::default() + .with_enforce_managed_model_defaults(true) + .into_wire(Some(SessionId::from("create"))) + .expect("no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!(create_json["enforceManagedModelDefaults"], true); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume")) + .with_enforce_managed_model_defaults(true) + .into_wire() + .expect("no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!(resume_json["enforceManagedModelDefaults"], true); +} + #[test] fn auth_client_id_metadata_url_reaches_create_and_resume_wire_payloads() { let url = "https://example.com/oauth/client-metadata.json"; @@ -1446,7 +1463,12 @@ fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() { for value in ["balanced", "Balance", "unknown"] { assert_eq!( serde_json::from_value::(json!(value)).unwrap(), - AutoTier::Unknown + AutoTier::Custom(value.to_owned()) + ); + assert_eq!( + serde_json::to_value(serde_json::from_value::(json!(value)).unwrap()) + .unwrap(), + json!(value) ); } let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap(); diff --git a/rust/src/wire.rs b/rust/src/wire.rs index 61e7d7c44f..2720d3bf2d 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -123,6 +123,8 @@ pub(crate) struct SessionCreateWire { pub enable_session_store: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enable_skills: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub has_skill_provider: Option, pub request_user_input: bool, pub request_permission: bool, pub request_exit_plan_mode: bool, @@ -209,6 +211,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub enable_managed_settings: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub enforce_managed_model_defaults: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub is_experimental_mode: Option, #[serde(skip_serializing_if = "Option::is_none")] pub managed_settings: Option, @@ -291,6 +295,8 @@ pub(crate) struct SessionResumeWire { pub enable_session_store: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enable_skills: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub has_skill_provider: Option, pub request_user_input: bool, pub request_permission: bool, pub request_exit_plan_mode: bool, @@ -382,6 +388,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub enable_managed_settings: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub enforce_managed_model_defaults: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub is_experimental_mode: Option, #[serde(skip_serializing_if = "Option::is_none")] pub managed_settings: Option, diff --git a/rust/tests/api_types_test.rs b/rust/tests/api_types_test.rs index fa714a7789..be00ef3847 100644 --- a/rust/tests/api_types_test.rs +++ b/rust/tests/api_types_test.rs @@ -11,18 +11,90 @@ use github_copilot_sdk::rpc::{ CustomizationReloadOutcome, CustomizationReloadStatus, CustomizationReloadSubsystem, EnqueueCommandResult, Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest, ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, - McpDisableRequest, McpEnableOptions, McpEnableRequest, McpInstallationOperationStatus, - McpOauthLoginOptions, McpOauthLoginRequest, McpServer, McpStopServerRequest, - ModelSetAllowedModelsRequest, ModelSetAllowedModelsResult, ModelSwitchAutoTierRequest, - ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus, QueuePendingItems, QueuePendingItemsKind, - SandboxConfig, SendAgentMode, TasksStartAgentRequest, UnsupportedEnqueueCommandResult, + McpConfiguredServer, McpConfiguredServerList, McpConfiguredServerState, McpDisableRequest, + McpEnableOptions, McpEnableRequest, McpInstallationOperationStatus, McpOauthLoginOptions, + McpOauthLoginRequest, McpServer, McpStopServerRequest, ModelSetAllowedModelsRequest, + ModelSetAllowedModelsResult, ModelSwitchAutoTierRequest, ModelSwitchAutoTierResult, + ModelSwitchAutoTierStatus, QueuePendingItems, QueuePendingItemsKind, SandboxConfig, + SendAgentMode, SessionMcpListConfiguredResult, TasksStartAgentRequest, + UnsupportedEnqueueCommandResult, }; use github_copilot_sdk::session_events::{ - McpServerStatus, PermissionRequest, PermissionRequestedData, SessionEventData, - TypedSessionEvent, + McpServerStatus, PermissionRequest, PermissionRequestedData, RecommendedAutoTier, + SessionEventData, TypedSessionEvent, }; use github_copilot_sdk::{AutoTier, AutoTierPreference, SetModelOptions}; +#[test] +fn extensible_tier_unknown_literal_retains_its_provider_identity() { + let wire = serde_json::json!("Unknown"); + let tier: AutoTier = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(tier, AutoTier::Custom("Unknown".to_owned())); + assert_eq!(serde_json::to_value(tier).unwrap(), wire); + let recommendation: RecommendedAutoTier = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!( + recommendation, + RecommendedAutoTier::Custom("Unknown".to_owned()) + ); + assert_eq!(serde_json::to_value(recommendation).unwrap(), wire); +} + +#[test] +fn configured_mcp_servers_preserve_enablement_without_live_state() { + for enabled in [false, true] { + let server = McpConfiguredServer { + name: "configured".to_string(), + enabled, + ..Default::default() + }; + let wire = serde_json::json!({ + "servers": [{ "name": "configured", "enabled": enabled }], + }); + assert_eq!( + serde_json::to_value(McpConfiguredServerList { + servers: vec![server], + }) + .unwrap(), + wire + ); + let result: SessionMcpListConfiguredResult = serde_json::from_value(wire.clone()).unwrap(); + assert!(result.servers[0].live.is_none()); + assert_eq!(serde_json::to_value(result).unwrap(), wire); + } +} + +#[test] +fn configured_mcp_servers_preserve_live_observations_independently_of_enablement() { + for enabled in [false, true] { + let server = McpConfiguredServer { + name: "configured".to_string(), + enabled, + live: Some(McpConfiguredServerState { + status: McpServerStatus::Stopped, + error: None, + }), + ..Default::default() + }; + assert_eq!( + serde_json::to_value(server).unwrap(), + serde_json::json!({ + "name": "configured", + "enabled": enabled, + "live": { "status": "stopped" }, + }) + ); + } + let wire = serde_json::json!({ + "servers": [{ + "name": "configured", + "enabled": true, + "live": { "status": "failed", "error": "connection failed" }, + }], + }); + let result: SessionMcpListConfiguredResult = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(result).unwrap(), wire); +} + #[test] fn customization_reload_outcome_preserves_future_wire_values() { let wire = serde_json::json!({ @@ -239,6 +311,10 @@ fn session_events_deserialize_auto_tier() { (Some(AutoTier::Balance), Some("balance")), (Some(AutoTier::Intelligence), Some("intelligence")), (Some(AutoTier::Fast), Some("fast")), + ( + Some(AutoTier::Custom("premium-v2".to_owned())), + Some("premium-v2"), + ), (None, None), ] { let mut wire = serde_json::json!({ @@ -257,6 +333,12 @@ fn session_events_deserialize_auto_tier() { wire["data"]["autoTier"] = serde_json::json!(wire_tier); } let event: TypedSessionEvent = serde_json::from_value(wire).unwrap(); + if let Some(wire_tier) = wire_tier { + assert_eq!( + serde_json::to_value(&event).unwrap()["data"]["autoTier"], + serde_json::json!(wire_tier), + ); + } let actual: Option = match event.payload { SessionEventData::SessionStart(data) if event_type == "session.start" => { data.auto_tier @@ -713,6 +795,7 @@ fn switch_auto_tier_request_serializes_each_tier() { (AutoTier::Balance, "balance"), (AutoTier::Intelligence, "intelligence"), (AutoTier::Fast, "fast"), + (AutoTier::Custom("premium-v2".to_owned()), "premium-v2"), ] { let request = ModelSwitchAutoTierRequest { auto_tier: Some(tier), diff --git a/rust/tests/e2e.rs b/rust/tests/e2e.rs index 7e24053d61..8c4e912bb5 100644 --- a/rust/tests/e2e.rs +++ b/rust/tests/e2e.rs @@ -80,6 +80,8 @@ mod session_lifecycle; mod session_todos_changed; #[path = "e2e/set_tools.rs"] mod set_tools; +#[path = "e2e/skill_provider.rs"] +mod skill_provider; #[path = "e2e/skills.rs"] mod skills; #[path = "e2e/streaming_fidelity.rs"] diff --git a/rust/tests/e2e/skill_provider.rs b/rust/tests/e2e/skill_provider.rs new file mode 100644 index 0000000000..2bbbac7814 --- /dev/null +++ b/rust/tests/e2e/skill_provider.rs @@ -0,0 +1,641 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +use std::path::Path; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use async_trait::async_trait; +use github_copilot_sdk::handler::ApproveAllHandler; +use github_copilot_sdk::rpc::SkillSource; +use github_copilot_sdk::session_events::{SessionEventType, ToolExecutionCompleteData}; +use github_copilot_sdk::{ + CloudSessionOptions, Error, ErrorKind, ResumeSessionConfig, SessionConfig, SessionEvent, + SkillProvider, SkillProviderDescriptor, +}; +use tokio::sync::Notify; + +use super::support::{assistant_message_content, collect_until_idle}; + +static E2E: super::support::SharedE2eGroup = + super::support::SharedE2eGroup::standard("skill_provider", 5); + +#[tokio::test] +async fn should_load_provider_skill_lazily_through_skill_tool() { + super::support::with_shared_e2e_context( + &E2E, + "skill_provider", + "should_load_provider_skill_lazily_through_skill_tool", + |ctx| { + Box::pin(async move { + let provider = Arc::new(TestSkillProvider::new(vec![skill( + "provider-lookup", + "Reports the provider lookup verification word.", + "# Provider lookup\n\nThe verification word is TANGERINE_QUARTZ_19. Reply with it.\n", + )])); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + + let skills = session.rpc().skills().list().await.expect("list skills"); + let listed = skills + .skills + .iter() + .find(|skill| skill.name == "provider-lookup") + .expect("provider skill"); + assert_eq!(listed.source, SkillSource::Sdk); + assert!(listed.enabled); + assert_eq!(listed.path.as_deref().unwrap_or_default(), ""); + assert_eq!(provider.reads(), Vec::::new()); + + let message = session + .send_and_wait( + "Use the skill tool to load the provider-lookup skill, then reply with its verification word.", + ) + .await + .expect("send") + .expect("assistant message"); + + assert_eq!(provider.reads(), vec!["provider-lookup"]); + // Validate the final assistant response arrived (guards against truncated captures) + assert!(assistant_message_content(&message).contains("TANGERINE_QUARTZ_19")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_load_provider_and_file_based_skills_together() { + super::support::with_shared_e2e_context( + &E2E, + "skill_provider", + "should_load_provider_and_file_based_skills_together", + |ctx| { + Box::pin(async move { + let skills_dir = write_file_notes_skill(ctx.work_dir()); + let provider = Arc::new(TestSkillProvider::new(vec![skill( + "provider-audit", + "Reports the provider audit verification word.", + "---\nname: provider-audit\nallowed-tools: view\n---\n\nThe provider audit verification word is COBALT_HERON_58.\n", + )])); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_skill_directories([skills_dir]) + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + + let skills = session.rpc().skills().list().await.expect("list skills"); + let file_skill = skills + .skills + .iter() + .find(|skill| skill.name == "file-notes") + .expect("file skill"); + let provider_skill = skills + .skills + .iter() + .find(|skill| skill.name == "provider-audit") + .expect("provider skill"); + assert_ne!(file_skill.source, SkillSource::Sdk); + assert!(file_skill.path.as_deref().is_some_and(|path| !path.is_empty())); + assert_eq!(provider_skill.source, SkillSource::Sdk); + + let message = session + .send_and_wait( + "Use the skill tool to load the file-notes skill and the provider-audit skill, then reply with both verification words.", + ) + .await + .expect("send") + .expect("assistant message"); + + assert_eq!(provider.reads(), vec!["provider-audit"]); + // Validate the final assistant response arrived (guards against truncated captures) + let content = assistant_message_content(&message); + assert!(content.contains("MAPLE_FALCON_27")); + assert!(content.contains("COBALT_HERON_58")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_rebind_skill_provider_on_resume() { + super::support::with_shared_e2e_context( + &E2E, + "skill_provider", + "should_rebind_skill_provider_on_resume", + |ctx| { + Box::pin(async move { + let original = Arc::new(TestSkillProvider::new(vec![skill( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is AMBER_ALPHA_11.\n", + )])); + let replacement = Arc::new(TestSkillProvider::new(vec![skill( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is BRONZE_BETA_22.\n", + )])); + let client = ctx.start_client().await; + let first = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(original.clone()), + ) + .await + .expect("create session"); + let session_id = first.id().clone(); + let ready = first + .send_and_wait( + "Without using any tools or skills, reply with exactly REBIND_READY.", + ) + .await + .expect("send readiness turn") + .expect("assistant message"); + assert!(assistant_message_content(&ready).contains("REBIND_READY")); + first.disconnect().await.expect("disconnect first session"); + assert_eq!(original.reads(), Vec::::new()); + let original_calls_before_resume = original.calls().len(); + + let session = client + .resume_session( + ResumeSessionConfig::new(session_id) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_github_token(super::support::DEFAULT_TEST_TOKEN) + .with_skill_provider(replacement.clone()), + ) + .await + .expect("resume session"); + + let message = session + .send_and_wait( + "Use the skill tool to load the rebind-check skill, then reply with its verification word.", + ) + .await + .expect("send") + .expect("assistant message"); + + assert_eq!(replacement.reads(), vec!["rebind-check"]); + assert_eq!(original.calls().len(), original_calls_before_resume); + // Validate the final assistant response arrived (guards against truncated captures) + let content = assistant_message_content(&message); + assert!(content.contains("BRONZE_BETA_22")); + assert!(!content.contains("AMBER_ALPHA_11")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_report_provider_read_failure_without_leaking_details() { + super::support::with_shared_e2e_context( + &E2E, + "skill_provider", + "should_report_provider_read_failure_without_leaking_details", + |ctx| { + Box::pin(async move { + let secret = "PROVIDER_SECRET_7F3A9C"; + let provider = Arc::new(TestSkillProvider::new(vec![ProvidedSkill::new( + "broken-lookup", + "Reports the broken lookup verification word.", + SkillRead::Error(format!("database unavailable: {secret}")), + )])); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + let events = session.subscribe(); + + let message = session + .send_and_wait( + "Use the skill tool to load the broken-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + ) + .await + .expect("send") + .expect("assistant message"); + let observed = collect_until_idle(events).await; + + assert!(provider.reads().contains(&"broken-lookup".to_string())); + let failures = failed_tool_executions(&observed); + assert_eq!(failures.len(), 1, "expected one failed tool execution"); + assert_no_secret(&observed, secret); + // Validate the final assistant response arrived (guards against truncated captures) + assert!(assistant_message_content(&message).contains("LOAD_FAILED")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_report_missing_provider_skill_as_not_found() { + super::support::with_shared_e2e_context( + &E2E, + "skill_provider", + "should_report_missing_provider_skill_as_not_found", + |ctx| { + Box::pin(async move { + let provider = Arc::new(TestSkillProvider::new(vec![ProvidedSkill::new( + "vanished-lookup", + "Reports the vanished lookup verification word.", + SkillRead::Missing, + )])); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + let events = session.subscribe(); + + let message = session + .send_and_wait( + "Use the skill tool to load the vanished-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + ) + .await + .expect("send") + .expect("assistant message"); + let observed = collect_until_idle(events).await; + + assert!(provider.reads().contains(&"vanished-lookup".to_string())); + let failures = failed_tool_executions(&observed); + assert_eq!(failures.len(), 1, "expected one failed tool execution"); + let failure = serde_json::to_string(&failures[0]).expect("serialize failure"); + assert!( + failure.to_ascii_lowercase().contains("not found"), + "expected not found failure, got {failure}" + ); + // Validate the final assistant response arrived (guards against truncated captures) + assert!(assistant_message_content(&message).contains("LOAD_FAILED")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_keep_provider_dormant_when_skills_disabled() { + super::support::with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + let provider = Arc::new(TestSkillProvider::new(vec![skill( + "dormant-lookup", + "Never listed.", + "Never read.\n", + )])); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_enable_skills(false) + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + + session + .rpc() + .skills() + .ensure_loaded() + .await + .expect("ensure skills loaded"); + let skills = session.rpc().skills().list().await.expect("list skills"); + + assert_no_sdk_skills(&skills.skills); + assert_eq!(provider.calls(), Vec::::new()); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }) + .await; +} + +#[tokio::test] +async fn should_unbind_provider_when_resumed_without_one() { + super::support::with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + let provider = Arc::new(TestSkillProvider::new(vec![skill( + "unbound-lookup", + "Reports the unbound lookup word.", + "Unbound.\n", + )])); + let client = ctx.start_client().await; + let first = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + let before = first.rpc().skills().list().await.expect("list skills"); + assert!( + before + .skills + .iter() + .any(|skill| skill.name == "unbound-lookup") + ); + let calls_before_resume = provider.calls().len(); + + let session = client + .resume_session( + ResumeSessionConfig::new(first.id().clone()) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_github_token(super::support::DEFAULT_TEST_TOKEN), + ) + .await + .expect("resume session"); + + session + .rpc() + .skills() + .reload() + .await + .expect("reload skills"); + let skills = session.rpc().skills().list().await.expect("list skills"); + + assert_no_sdk_skills(&skills.skills); + assert_eq!(provider.calls().len(), calls_before_resume); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + drop(first); + }) + }) + .await; +} + +#[tokio::test] +async fn should_cancel_a_blocked_provider_call_when_the_session_disconnects() { + super::support::with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + let entered = Arc::new(Notify::new()); + let dropped = Arc::new(Notify::new()); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(Arc::new(BlockingSkillProvider { + entered: entered.clone(), + dropped: dropped.clone(), + })), + ) + .await + .expect("create session"); + + // The list RPC fails once the binding is removed; only the provider's + // cancellation matters here. + let rpc = session.rpc(); + let skills = rpc.skills(); + let list = skills.list(); + tokio::pin!(list); + tokio::select! { + _ = entered.notified() => {} + _ = &mut list => panic!("skills.list finished before the provider was called"), + _ = tokio::time::sleep(Duration::from_secs(30)) => { + panic!("provider list_skills was not called"); + } + } + + session.disconnect().await.expect("disconnect session"); + tokio::time::timeout(Duration::from_secs(10), dropped.notified()) + .await + .expect("provider future was not dropped after disconnect"); + + client.stop().await.expect("stop client"); + }) + }) + .await; +} + +#[tokio::test] +async fn should_reject_skill_provider_for_cloud_sessions() { + super::support::with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + let provider = Arc::new(TestSkillProvider::new(vec![skill( + "cloud-lookup", + "Never listed.", + "Never read.\n", + )])); + let client = ctx.start_client().await; + + let result = client + .create_session( + SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_github_token(super::support::DEFAULT_TEST_TOKEN) + .with_cloud(CloudSessionOptions::default()) + .with_skill_provider(provider.clone()), + ) + .await; + + match result { + Ok(session) => { + let _ = session.disconnect().await; + panic!("cloud session with skill provider unexpectedly succeeded"); + } + Err(error) => { + assert!( + error + .to_string() + .contains("Skill providers are not supported for cloud sessions."), + "unexpected error: {error}" + ); + } + } + assert_eq!(provider.calls(), Vec::::new()); + + client.stop().await.expect("stop client"); + }) + }) + .await; +} + +struct ProvidedSkill { + descriptor: SkillProviderDescriptor, + read: SkillRead, +} + +impl ProvidedSkill { + fn new(name: &str, description: &str, read: SkillRead) -> Self { + Self { + descriptor: descriptor(name, description), + read, + } + } +} + +enum SkillRead { + Markdown(&'static str), + Missing, + Error(String), +} + +struct TestSkillProvider { + skills: Vec, + calls: Mutex>, +} + +impl TestSkillProvider { + fn new(skills: Vec) -> Self { + Self { + skills, + calls: Mutex::new(Vec::new()), + } + } + + fn calls(&self) -> Vec { + self.calls.lock().expect("provider call log").clone() + } + + fn reads(&self) -> Vec { + self.calls() + .into_iter() + .filter_map(|call| call.strip_prefix("read:").map(str::to_string)) + .collect() + } + + fn push_call(&self, call: impl Into) { + self.calls + .lock() + .expect("provider call log") + .push(call.into()); + } +} + +#[async_trait] +impl SkillProvider for TestSkillProvider { + async fn list_skills(&self) -> std::result::Result, Error> { + self.push_call("list"); + Ok(self + .skills + .iter() + .map(|skill| skill.descriptor.clone()) + .collect()) + } + + async fn read_skill(&self, name: &str) -> std::result::Result, Error> { + self.push_call(format!("read:{name}")); + let Some(skill) = self + .skills + .iter() + .find(|skill| skill.descriptor.name == name) + else { + return Ok(None); + }; + + match &skill.read { + SkillRead::Markdown(markdown) => Ok(Some((*markdown).to_string())), + SkillRead::Missing => Ok(None), + SkillRead::Error(message) => Err(Error::with_message( + ErrorKind::InvalidConfig, + message.clone(), + )), + } + } +} + +fn skill(name: &str, description: &str, markdown: &'static str) -> ProvidedSkill { + ProvidedSkill::new(name, description, SkillRead::Markdown(markdown)) +} + +struct NotifyOnDrop(Arc); + +impl Drop for NotifyOnDrop { + fn drop(&mut self) { + self.0.notify_one(); + } +} + +/// Blocks `list_skills` until the SDK drops its future. +struct BlockingSkillProvider { + entered: Arc, + dropped: Arc, +} + +#[async_trait] +impl SkillProvider for BlockingSkillProvider { + async fn list_skills(&self) -> std::result::Result, Error> { + let _dropped = NotifyOnDrop(self.dropped.clone()); + self.entered.notify_one(); + std::future::pending().await + } + + async fn read_skill(&self, _name: &str) -> std::result::Result, Error> { + Ok(None) + } +} + +fn descriptor(name: &str, description: &str) -> SkillProviderDescriptor { + SkillProviderDescriptor { + name: name.to_string(), + description: description.to_string(), + ..Default::default() + } +} + +fn write_file_notes_skill(work_dir: &Path) -> std::path::PathBuf { + let skills_dir = work_dir.join("file-skills"); + let skill_dir = skills_dir.join("file-notes"); + std::fs::create_dir_all(&skill_dir).expect("create skill directory"); + std::fs::write( + skill_dir.join("SKILL.md"), + "---\nname: file-notes\ndescription: Reports the file notes verification word.\n---\n\nThe file notes verification word is MAPLE_FALCON_27.\n", + ) + .expect("write file skill"); + skills_dir +} + +fn failed_tool_executions(events: &[SessionEvent]) -> Vec { + events + .iter() + .filter(|event| event.parsed_type() == SessionEventType::ToolExecutionComplete) + .filter_map(|event| event.typed_data::()) + .filter(|data| !data.success) + .collect() +} + +fn assert_no_secret(events: &[SessionEvent], secret: &str) { + let events_json = serde_json::to_string(events).expect("serialize events"); + assert!( + !events_json.contains(secret), + "provider secret leaked into events: {events_json}" + ); +} + +fn assert_no_sdk_skills(skills: &[github_copilot_sdk::rpc::Skill]) { + assert!( + skills.iter().all(|skill| skill.source != SkillSource::Sdk), + "expected no SDK skills, got {skills:?}" + ); +} diff --git a/rust/tests/e2e/support.rs b/rust/tests/e2e/support.rs index d352aa3131..fa945bf584 100644 --- a/rust/tests/e2e/support.rs +++ b/rust/tests/e2e/support.rs @@ -1227,8 +1227,7 @@ fn cli_path(repo_root: &Path) -> std::io::Result { } } - let npm = if cfg!(windows) { "npm.cmd" } else { "npm" }; - let output = std::process::Command::new(npm) + let output = std::process::Command::new(npm_program()) .args(["run", "--silent", "prepare:runtime", "--", "--print-path"]) .current_dir(repo_root.join("nodejs")) .output()?; @@ -1571,6 +1570,28 @@ fn node_program() -> &'static str { if cfg!(windows) { "node.exe" } else { "node" } } +fn npm_program() -> &'static str { + if cfg!(windows) { + windows_program(&["npm.cmd", "npm.exe", "npm"]) + } else { + "npm" + } +} + +fn windows_program(candidates: &[&'static str]) -> &'static str { + candidates + .iter() + .copied() + .find(|candidate| program_exists_on_path(candidate)) + .unwrap_or(candidates[0]) +} + +fn program_exists_on_path(program: &str) -> bool { + std::env::var_os("PATH").is_some_and(|path| { + std::env::split_paths(&path).any(|directory| directory.join(program).is_file()) + }) +} + #[test] fn e2e_context_isolates_copilot_cache() { let home_dir = tempfile::tempdir().expect("create test home"); diff --git a/rust/tests/prepared_session_test.rs b/rust/tests/prepared_session_test.rs index f3102d7f40..1ae60cbbe1 100644 --- a/rust/tests/prepared_session_test.rs +++ b/rust/tests/prepared_session_test.rs @@ -1425,9 +1425,13 @@ async fn check_stopping_resume_bootstrap(claim: bool) { expect_startup_burst(&mut events).await; expect_event_id(&mut events, "publication-fence").await; } else { + let result = events.recv().now_or_never(); assert!( - events.recv().now_or_never().is_none(), - "unclaimed backlog was replayed" + matches!( + &result, + Some(Err(error)) if matches!(error.kind(), RecvErrorKind::Closed) + ), + "subscription created after stop must close without replaying backlog, got {result:?}" ); drop(session); } diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 23a254d9e9..a3334d3f2c 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -215,6 +215,20 @@ impl Drop for NotifyOnDrop { } } +struct CapturingTool { + _on_drop: NotifyOnDrop, +} + +#[async_trait] +impl tool::ToolHandler for CapturingTool { + async fn call( + &self, + _invocation: ToolInvocation, + ) -> Result { + Ok(ToolResult::Text("captured handler".to_string())) + } +} + #[async_trait] impl PermissionHandler for PendingPermissionHandler { async fn handle( @@ -1784,6 +1798,13 @@ fn managed_bypass_permissions_modes_use_wire_values() { serde_json::to_value(future).unwrap()["disableBypassPermissionsMode"], "future-fail-closed-mode" ); + + let assisted_disabled = + ManagedSettingsPermissions::default().with_disable_assisted_permissions_mode(true); + assert_eq!( + serde_json::to_value(assisted_disabled).unwrap()["disableAssistedPermissionsMode"], + true + ); } #[tokio::test] @@ -1795,9 +1816,11 @@ async fn create_and_resume_send_managed_settings_permissions() { let managed = ManagedSettings::default().with_permissions( ManagedSettingsPermissions::default() .with_disable_bypass_permissions_mode(DisableBypassPermissionsModes::ALLOW_AUTO_ONLY) + .with_disable_assisted_permissions_mode(true) .with_deny(vec!["shell(rm*)".to_string()]) .with_ask(vec!["write".to_string()]) - .with_allow(vec![]), + .with_allow(vec![]) + .with_limit_to(vec![]), ); let create_handle = tokio::spawn({ @@ -1820,9 +1843,11 @@ async fn create_and_resume_send_managed_settings_permissions() { assert_eq!(request["params"]["enableManagedSettings"], true); let perms = &request["params"]["managedSettings"]["permissions"]; assert_eq!(perms["disableBypassPermissionsMode"], "allow-auto-only"); + assert_eq!(perms["disableAssistedPermissionsMode"], true); assert_eq!(perms["deny"][0], "shell(rm*)"); assert_eq!(perms["ask"][0], "write"); assert_eq!(perms["allow"], serde_json::json!([])); + assert_eq!(perms["limitTo"], serde_json::json!([])); let id = request["id"].as_u64().unwrap(); let session_id = requested_session_id(&request).to_string(); @@ -3024,6 +3049,204 @@ fn permission_request_data_extracts_typed_kind() { assert_eq!(unknown.kind, Some(PermissionRequestKind::Unknown)); } +#[tokio::test] +async fn session_retirement_releases_retained_tool_handler_captures() { + for action in ["force_stop", "stop_event_loop", "connection_loss"] { + let dropped = Arc::new(Notify::new()); + let handler = Arc::new(CapturingTool { + _on_drop: NotifyOnDrop(dropped.clone()), + }); + let retained = Arc::downgrade(&handler); + let (session, server) = create_session_pair_with_config(move |cfg| { + cfg.with_tools(vec![Tool::new("capture").with_handler(handler)]) + }) + .await; + assert!( + retained.upgrade().is_some(), + "{action}: handler not installed" + ); + + match action { + "force_stop" => session.client().force_stop(), + "stop_event_loop" => session.stop_event_loop().await, + "connection_loss" => drop(server), + _ => unreachable!(), + } + timeout(TIMEOUT, dropped.notified()) + .await + .unwrap_or_else(|_| panic!("{action}: retained session kept tool captures alive")); + assert!( + retained.upgrade().is_none(), + "{action}: handler still retained" + ); + assert!(session.cancellation_token().is_cancelled()); + } +} + +#[tokio::test] +async fn force_stop_closes_retained_session_subscriptions() { + use futures_util::FutureExt as _; + use github_copilot_sdk::subscription::RecvErrorKind; + use tokio_stream::StreamExt as _; + + let (session, mut server) = create_session_pair().await; + let mut existing = session.subscribe(); + let mut buffered = session.subscribe(); + server + .send_event("before-force-stop", serde_json::json!({})) + .await; + assert_eq!( + timeout(TIMEOUT, existing.recv()) + .await + .unwrap() + .unwrap() + .event_type, + "before-force-stop" + ); + + session.client().force_stop(); + let error = buffered + .recv() + .now_or_never() + .expect( + "force-stop must discard the already-published marker without awaiting the producer", + ) + .unwrap_err(); + assert!(matches!(error.kind(), RecvErrorKind::Closed), "{error:?}"); + assert!(buffered.next().now_or_never().unwrap().is_none()); + let mut late = session.subscribe(); + let error = late + .recv() + .now_or_never() + .expect("late subscription must close without waiting for event-loop scheduling") + .unwrap_err(); + assert!(matches!(error.kind(), RecvErrorKind::Closed), "{error:?}"); + assert!(late.next().now_or_never().unwrap().is_none()); + + let lifetime = session.cancellation_token(); + timeout(TIMEOUT, lifetime.cancelled()) + .await + .expect("force-stop must terminate the retained session's event loop"); + let error = timeout(TIMEOUT, existing.recv()) + .await + .unwrap() + .unwrap_err(); + assert!(matches!(error.kind(), RecvErrorKind::Closed), "{error:?}"); + assert!(existing.next().now_or_never().unwrap().is_none()); +} + +#[tokio::test] +async fn force_stop_closes_unstarted_prepared_subscriptions() { + use futures_util::FutureExt as _; + use github_copilot_sdk::subscription::RecvErrorKind; + + let (client, _server_read, _server_write) = make_client(); + let prepared = client.prepare_session(SessionConfig::default()).unwrap(); + let mut existing = prepared.subscribe(); + assert!(existing.recv().now_or_never().is_none()); + client.force_stop(); + + for mut events in [prepared.subscribe(), existing] { + let error = events + .recv() + .now_or_never() + .expect("force-stop must close observers even before an event loop exists") + .unwrap_err(); + assert!(matches!(error.kind(), RecvErrorKind::Closed), "{error:?}"); + } +} + +#[tokio::test] +async fn stopped_event_loop_closes_subscriptions_without_dropping_session() { + use futures_util::FutureExt as _; + use github_copilot_sdk::subscription::RecvErrorKind; + + let (session, _server) = create_session_pair().await; + let existing = session.subscribe(); + session.stop_event_loop().await; + + for mut events in [session.subscribe(), existing] { + let error = events + .recv() + .now_or_never() + .expect("a retained stopped session must not keep its producer alive") + .unwrap_err(); + assert!(matches!(error.kind(), RecvErrorKind::Closed), "{error:?}"); + } +} + +#[tokio::test] +async fn owner_connection_loss_retires_retained_session_subscriptions() { + use futures_util::FutureExt as _; + use github_copilot_sdk::subscription::RecvErrorKind; + + let (session, server) = create_session_pair().await; + let existing = session.subscribe(); + let lifetime = session.cancellation_token(); + drop(server); + timeout(TIMEOUT, lifetime.cancelled()) + .await + .expect("transport EOF must terminate the retained session's event loop"); + + for mut events in [session.subscribe(), existing] { + let error = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap_err(); + assert!(matches!(error.kind(), RecvErrorKind::Closed), "{error:?}"); + assert!(events.recv().now_or_never().unwrap().is_err()); + } +} + +#[tokio::test] +async fn replacing_router_registration_retires_only_previous_session() { + use futures_util::FutureExt as _; + use github_copilot_sdk::subscription::RecvErrorKind; + + let (session, mut server) = create_session_pair().await; + let lifetime = session.cancellation_token(); + let replacement = tokio::spawn({ + let client = session.client().clone(); + let session_id = session.id().clone(); + async move { + client + .create_session(SessionConfig::default().with_session_id(session_id)) + .await + .unwrap() + } + }); + let request = server.read_request().await; + assert_eq!(request["method"], "session.create"); + server + .respond( + &request, + serde_json::json!({ "sessionId": server.session_id }), + ) + .await; + let replacement = timeout(TIMEOUT, replacement).await.unwrap().unwrap(); + timeout(TIMEOUT, lifetime.cancelled()) + .await + .expect("closed routing channels must retire the previous event loop"); + let error = session + .subscribe() + .recv() + .now_or_never() + .unwrap() + .unwrap_err(); + assert!(matches!(error.kind(), RecvErrorKind::Closed), "{error:?}"); + + let mut events = replacement.subscribe(); + server + .send_event("replacement-live", serde_json::json!({})) + .await; + assert_eq!( + timeout(TIMEOUT, events.recv()) + .await + .unwrap() + .unwrap() + .event_type, + "replacement-live" + ); + assert!(!replacement.cancellation_token().is_cancelled()); +} + #[tokio::test] async fn force_stop_is_idempotent_with_no_child() { // Stream-based clients have no child process. force_stop should be a @@ -5809,6 +6032,93 @@ async fn assert_no_request(server: &mut FakeServer) { assert!(request.is_err(), "unexpected request: {:?}", request.ok()); } +#[tokio::test] +async fn set_tools_rejects_acknowledgment_after_event_loop_retirement() { + let (session, mut server) = create_session_pair().await; + let session = Arc::new(session); + let dropped = Arc::new(Notify::new()); + let handler = Arc::new(CapturingTool { + _on_drop: NotifyOnDrop(dropped.clone()), + }); + let retained = Arc::downgrade(&handler); + let replace = tokio::spawn({ + let session = session.clone(); + async move { + session + .set_tools([Tool::new("capture").with_handler(handler)]) + .await + } + }); + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(request["method"], "session.tools.set"); + + session.stop_event_loop().await; + server.respond(&request, serde_json::json!({})).await; + let error = timeout(TIMEOUT, replace) + .await + .unwrap() + .unwrap() + .unwrap_err(); + assert_eq!(error.kind(), &ErrorKind::Rpc { code: -32603 }); + assert_eq!( + error.to_string(), + format!( + "RPC error -32603: {}", + github_copilot_sdk::SessionErrorKind::EventLoopClosed + ) + ); + timeout(TIMEOUT, dropped.notified()).await.unwrap(); + assert!(retained.upgrade().is_none()); +} + +#[tokio::test] +async fn set_tools_rejects_queued_and_late_admission_after_retirement() { + let (session, mut server) = create_session_pair().await; + let session = Arc::new(session); + let first = tokio::spawn({ + let session = session.clone(); + async move { + session + .set_tools([label_tool("first", "first handler")]) + .await + } + }); + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(request["method"], "session.tools.set"); + + let queued = session.set_tools([label_tool("queued", "queued handler")]); + tokio::pin!(queued); + assert!(futures_util::poll!(queued.as_mut()).is_pending()); + + session.stop_event_loop().await; + server.respond(&request, serde_json::json!({})).await; + let error = timeout(TIMEOUT, first).await.unwrap().unwrap().unwrap_err(); + assert_eq!(error.kind(), &ErrorKind::Rpc { code: -32603 }); + assert_eq!( + error.to_string(), + format!( + "RPC error -32603: {}", + github_copilot_sdk::SessionErrorKind::EventLoopClosed + ) + ); + for result in [ + timeout(TIMEOUT, queued).await.unwrap(), + session + .set_tools([label_tool("late", "late handler")]) + .await, + ] { + let error = result.unwrap_err(); + assert!( + matches!( + error.kind(), + ErrorKind::Session(github_copilot_sdk::SessionErrorKind::EventLoopClosed) + ), + "{error}" + ); + } + assert_no_request(&mut server).await; +} + #[tokio::test] async fn set_tools_replaces_the_definitions_and_handlers_together() { let (session, mut server) = create_session_pair_with_config(|cfg| { diff --git a/rust/tests/skill_provider_test.rs b/rust/tests/skill_provider_test.rs new file mode 100644 index 0000000000..71494ee65c --- /dev/null +++ b/rust/tests/skill_provider_test.rs @@ -0,0 +1,965 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#![allow(clippy::unwrap_used)] + +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; + +use async_trait::async_trait; +use github_copilot_sdk::skill_provider::{SkillProvider, SkillProviderDescriptor}; +use github_copilot_sdk::types::{CloudSessionOptions, SessionConfig, SessionId, Tool}; +use github_copilot_sdk::{Client, ClientMode, Error, ErrorKind, ResumeSessionConfig}; +use serde_json::{Value, json}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, duplex}; +use tokio::sync::Notify; +use tokio::time::timeout; + +const TIMEOUT: Duration = Duration::from_secs(2); + +async fn write_framed(writer: &mut (impl AsyncWrite + Unpin), value: &Value) { + let body = serde_json::to_vec(value).unwrap(); + let header = format!("Content-Length: {}\r\n\r\n", body.len()); + writer.write_all(header.as_bytes()).await.unwrap(); + writer.write_all(&body).await.unwrap(); + writer.flush().await.unwrap(); +} + +async fn read_framed(reader: &mut (impl AsyncRead + Unpin)) -> Value { + timeout(TIMEOUT, read_framed_untimed(reader)).await.unwrap() +} + +async fn read_framed_untimed(reader: &mut (impl AsyncRead + Unpin)) -> Value { + let mut header = String::new(); + loop { + let mut byte = [0u8; 1]; + reader.read_exact(&mut byte).await.unwrap(); + header.push(byte[0] as char); + if header.ends_with("\r\n\r\n") { + break; + } + } + + let length: usize = header + .trim() + .strip_prefix("Content-Length: ") + .unwrap() + .parse() + .unwrap(); + let mut body = vec![0; length]; + reader.read_exact(&mut body).await.unwrap(); + serde_json::from_slice(&body).unwrap() +} + +fn make_client() -> (Client, FakeServer) { + make_client_with_mode(ClientMode::default()) +} + +fn make_client_with_mode(mode: ClientMode) -> (Client, FakeServer) { + let (client_write, server_read) = duplex(8192); + let (server_write, client_read) = duplex(8192); + let client = Client::from_streams_with_mode_for_test( + client_read, + client_write, + std::env::temp_dir(), + mode, + ) + .unwrap(); + ( + client, + FakeServer { + read: server_read, + write: server_write, + session_id: String::new(), + }, + ) +} + +struct FakeServer { + read: DuplexStream, + write: DuplexStream, + session_id: String, +} + +impl FakeServer { + async fn read_request(&mut self) -> Value { + read_framed(&mut self.read).await + } + + async fn respond(&mut self, request: &Value, result: Value) { + let response = json!({ + "jsonrpc": "2.0", + "id": request["id"].as_u64().unwrap(), + "result": result, + }); + write_framed(&mut self.write, &response).await; + } + + async fn respond_error(&mut self, request: &Value, code: i64, message: &str) { + let response = json!({ + "jsonrpc": "2.0", + "id": request["id"].as_u64().unwrap(), + "error": { "code": code, "message": message }, + }); + write_framed(&mut self.write, &response).await; + } + + async fn send_request(&mut self, id: u64, method: &str, params: Value) { + let request = json!({ + "jsonrpc": "2.0", + "id": id, + "method": method, + "params": params, + }); + write_framed(&mut self.write, &request).await; + } + + async fn send_notification(&mut self, method: &str, params: Value) { + let notification = json!({ + "jsonrpc": "2.0", + "method": method, + "params": params, + }); + write_framed(&mut self.write, ¬ification).await; + } + + async fn read_response(&mut self) -> Value { + read_framed(&mut self.read).await + } +} + +fn descriptor(name: &str, description: &str) -> SkillProviderDescriptor { + SkillProviderDescriptor { + name: name.to_string(), + description: description.to_string(), + ..Default::default() + } +} + +#[derive(Clone, Default)] +struct CallLog(Arc>>); + +impl CallLog { + fn push(&self, call: impl Into) { + self.0.lock().unwrap().push(call.into()); + } + + fn snapshot(&self) -> Vec { + self.0.lock().unwrap().clone() + } +} + +struct RecordingProvider { + skills: Vec, + markdown: HashMap, + calls: CallLog, +} + +impl RecordingProvider { + fn new() -> Self { + Self { + skills: vec![descriptor("review", "Reviews code")], + markdown: HashMap::from([("review".to_string(), "Review carefully.".to_string())]), + calls: CallLog::default(), + } + } + + fn empty() -> Self { + Self { + skills: Vec::new(), + markdown: HashMap::new(), + calls: CallLog::default(), + } + } +} + +#[async_trait] +impl SkillProvider for RecordingProvider { + async fn list_skills(&self) -> std::result::Result, Error> { + self.calls.push("list"); + Ok(self.skills.clone()) + } + + async fn read_skill(&self, name: &str) -> std::result::Result, Error> { + self.calls.push(format!("read:{name}")); + Ok(self.markdown.get(name).cloned()) + } +} + +struct FailingProvider { + fail_list: bool, + secret: &'static str, +} + +#[async_trait] +impl SkillProvider for FailingProvider { + async fn list_skills(&self) -> std::result::Result, Error> { + if self.fail_list { + Err(Error::with_message(ErrorKind::InvalidConfig, self.secret)) + } else { + Ok(vec![descriptor("review", "Reviews code")]) + } + } + + async fn read_skill(&self, _name: &str) -> std::result::Result, Error> { + Err(Error::with_message(ErrorKind::InvalidConfig, self.secret)) + } +} + +struct NotifyOnDrop(Arc); + +impl Drop for NotifyOnDrop { + fn drop(&mut self) { + self.0.notify_one(); + } +} + +/// Blocks every call until the SDK drops its future. +struct BlockingProvider { + entered: Arc, + dropped: Arc, +} + +#[async_trait] +impl SkillProvider for BlockingProvider { + async fn list_skills(&self) -> std::result::Result, Error> { + let _dropped = NotifyOnDrop(self.dropped.clone()); + self.entered.notify_one(); + std::future::pending().await + } + + async fn read_skill(&self, _name: &str) -> std::result::Result, Error> { + let _dropped = NotifyOnDrop(self.dropped.clone()); + self.entered.notify_one(); + std::future::pending().await + } +} + +fn requested_session_id(request: &Value) -> &str { + request["params"]["sessionId"].as_str().unwrap() +} + +async fn create_session( + client: &Client, + server: &mut FakeServer, + config: SessionConfig, +) -> github_copilot_sdk::session::Session { + let handle = tokio::spawn({ + let client = client.clone(); + async move { client.create_session(config).await.unwrap() } + }); + let request = server.read_request().await; + assert_eq!(request["method"], "session.create"); + server.session_id = requested_session_id(&request).to_string(); + server + .respond( + &request, + json!({ + "sessionId": server.session_id, + "workspacePath": "/tmp/workspace", + }), + ) + .await; + timeout(TIMEOUT, handle).await.unwrap().unwrap() +} + +async fn respond_to_resume(server: &mut FakeServer) { + let request = server.read_request().await; + assert_eq!(request["method"], "session.resume"); + server + .respond( + &request, + json!({ + "sessionId": requested_session_id(&request), + "workspacePath": "/tmp/workspace", + }), + ) + .await; + + let reload = server.read_request().await; + assert_eq!(reload["method"], "session.skills.reload"); + server.respond(&reload, json!({})).await; +} + +async fn disconnect_session( + session: &github_copilot_sdk::session::Session, + server: &mut FakeServer, +) { + let disconnect = session.disconnect(); + tokio::pin!(disconnect); + let detach = tokio::select! { + request = server.read_request() => request, + result = &mut disconnect => panic!("disconnect finished before detach request: {result:?}"), + }; + assert_eq!(detach["method"], "session.detach"); + server + .respond(&detach, json!({ "success": true, "error": null })) + .await; + timeout(TIMEOUT, disconnect).await.unwrap().unwrap(); +} + +fn assert_no_provider(response: &Value, session_id: &str) { + assert_eq!(response["error"]["code"], -32603); + assert_eq!( + response["error"]["message"], + format!("No skill provider for session: {session_id}") + ); + assert!(response["error"].get("data").is_none()); +} + +fn assert_session_not_found(response: &Value, session_id: &str) { + assert_eq!(response["error"]["code"], -32603); + assert_eq!( + response["error"]["message"], + format!("Session not found: {session_id}") + ); +} + +#[test] +fn descriptor_omits_unset_optional_fields_and_config_debug_redacts_provider() { + let value = serde_json::to_value(descriptor("review", "Reviews code")).unwrap(); + assert_eq!( + value, + json!({ "name": "review", "description": "Reviews code" }) + ); + + let provider: Arc = Arc::new(RecordingProvider::new()); + let config = SessionConfig::default().with_skill_provider(provider.clone()); + let cloned = config.clone(); + assert!(Arc::ptr_eq( + config.skill_provider.as_ref().unwrap(), + cloned.skill_provider.as_ref().unwrap() + )); + let debug = format!("{config:?}"); + assert!(debug.contains("skill_provider")); + assert!(debug.contains("")); + + let resume = + ResumeSessionConfig::new(SessionId::new("resume-id")).with_skill_provider(provider.clone()); + let cloned_resume = resume.clone(); + assert!(Arc::ptr_eq( + resume.skill_provider.as_ref().unwrap(), + cloned_resume.skill_provider.as_ref().unwrap() + )); + assert!(format!("{resume:?}").contains("skill_provider")); +} + +#[tokio::test] +async fn create_and_resume_payloads_flag_only_when_provider_is_supplied() { + let (client, mut server) = make_client(); + + let session = create_session(&client, &mut server, SessionConfig::default()).await; + let resume = tokio::spawn({ + let client = client.clone(); + let session_id = session.id().clone(); + async move { + client + .resume_session(ResumeSessionConfig::new(session_id)) + .await + .unwrap() + } + }); + respond_to_resume(&mut server).await; + let _resumed = timeout(TIMEOUT, resume).await.unwrap().unwrap(); + + let provider: Arc = Arc::new(RecordingProvider::new()); + let create = tokio::spawn({ + let client = client.clone(); + let provider = provider.clone(); + async move { + client + .create_session(SessionConfig::default().with_skill_provider(provider)) + .await + .unwrap() + } + }); + let create_request = server.read_request().await; + assert_eq!(create_request["params"]["hasSkillProvider"], true); + assert!(create_request["params"].get("skillProvider").is_none()); + server.session_id = requested_session_id(&create_request).to_string(); + server + .respond( + &create_request, + json!({ "sessionId": server.session_id, "workspacePath": "/tmp/workspace" }), + ) + .await; + let session_with_provider = timeout(TIMEOUT, create).await.unwrap().unwrap(); + + let resume_with_provider = tokio::spawn({ + let client = client.clone(); + let provider = Arc::new(RecordingProvider::new()); + let session_id = session_with_provider.id().clone(); + async move { + client + .resume_session(ResumeSessionConfig::new(session_id).with_skill_provider(provider)) + .await + .unwrap() + } + }); + let resume_request = server.read_request().await; + assert_eq!(resume_request["method"], "session.resume"); + assert_eq!(resume_request["params"]["hasSkillProvider"], true); + assert!(resume_request["params"].get("skillProvider").is_none()); + server + .respond( + &resume_request, + json!({ "sessionId": requested_session_id(&resume_request), "workspacePath": "/tmp/workspace" }), + ) + .await; + let reload = server.read_request().await; + server.respond(&reload, json!({})).await; + let _ = timeout(TIMEOUT, resume_with_provider) + .await + .unwrap() + .unwrap(); +} + +#[tokio::test] +async fn empty_mode_keeps_enable_skills_default_with_provider() { + let (client, mut server) = make_client_with_mode(ClientMode::Empty); + let provider: Arc = Arc::new(RecordingProvider::new()); + let create = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session( + SessionConfig::default() + .with_available_tools(Vec::::new()) + .with_tools(Vec::::new()) + .with_skill_provider(provider), + ) + .await + .unwrap() + } + }); + let request = server.read_request().await; + assert_eq!(request["params"]["enableSkills"], false); + assert_eq!(request["params"]["hasSkillProvider"], true); + server.session_id = requested_session_id(&request).to_string(); + server + .respond( + &request, + json!({ "sessionId": server.session_id, "workspacePath": "/tmp/workspace" }), + ) + .await; + let update = server.read_request().await; + assert_eq!(update["method"], "session.options.update"); + server.respond(&update, json!({ "success": true })).await; + let _ = timeout(TIMEOUT, create).await.unwrap().unwrap(); +} + +#[tokio::test] +async fn serves_early_create_and_resume_callbacks() { + let (client, mut server) = make_client(); + let provider: Arc = Arc::new(RecordingProvider::new()); + let create = tokio::spawn({ + let client = client.clone(); + let provider = provider.clone(); + async move { + client + .create_session( + SessionConfig::default() + .with_session_id("early-create") + .with_skill_provider(provider), + ) + .await + .unwrap() + } + }); + let create_request = server.read_request().await; + assert_eq!(create_request["method"], "session.create"); + server + .send_request( + 10, + "skillProvider.list", + json!({ "sessionId": "early-create" }), + ) + .await; + let list_response = server.read_response().await; + assert_eq!(list_response["result"]["skills"][0]["name"], "review"); + server + .respond( + &create_request, + json!({ "sessionId": "early-create", "workspacePath": "/tmp/workspace" }), + ) + .await; + let _ = timeout(TIMEOUT, create).await.unwrap().unwrap(); + + let resume_provider: Arc = Arc::new(RecordingProvider::new()); + let resume = tokio::spawn({ + let client = client.clone(); + async move { + client + .resume_session( + ResumeSessionConfig::new(SessionId::new("early-resume")) + .with_skill_provider(resume_provider), + ) + .await + .unwrap() + } + }); + let resume_request = server.read_request().await; + assert_eq!(resume_request["method"], "session.resume"); + server + .send_request( + 11, + "skillProvider.list", + json!({ "sessionId": "early-resume" }), + ) + .await; + let list_response = server.read_response().await; + assert_eq!(list_response["result"]["skills"][0]["name"], "review"); + server + .respond( + &resume_request, + json!({ "sessionId": "early-resume", "workspacePath": "/tmp/workspace" }), + ) + .await; + let reload = server.read_request().await; + server.respond(&reload, json!({})).await; + let _ = timeout(TIMEOUT, resume).await.unwrap().unwrap(); +} + +#[tokio::test] +async fn cloud_sessions_reject_provider_before_rpc() { + let (client, mut server) = make_client(); + let provider = Arc::new(RecordingProvider::new()); + let calls = provider.calls.clone(); + let error = match client + .create_session( + SessionConfig::default() + .with_skill_provider(provider) + .with_cloud(CloudSessionOptions::default()), + ) + .await + { + Ok(_) => panic!("cloud session unexpectedly accepted a skill provider"), + Err(error) => error, + }; + + assert_eq!( + error.to_string(), + "Skill providers are not supported for cloud sessions." + ); + assert_eq!(calls.snapshot(), Vec::::new()); + assert!( + timeout(Duration::from_millis(100), server.read_request()) + .await + .is_err() + ); +} + +#[tokio::test] +async fn dispatches_list_read_empty_and_not_found() { + let (client, mut server) = make_client(); + let provider = Arc::new(RecordingProvider { + skills: vec![ + descriptor("review", "Reviews code"), + SkillProviderDescriptor { + name: "deploy".to_string(), + description: "Deploys service".to_string(), + argument_hint: Some("[environment]".to_string()), + disable_model_invocation: Some(true), + user_invocable: Some(false), + }, + ], + markdown: HashMap::from([("deploy".to_string(), "# deploy".to_string())]), + calls: CallLog::default(), + }); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(provider), + ) + .await; + let session_id = session.id().to_string(); + + server + .send_request(20, "skillProvider.list", json!({ "sessionId": session_id })) + .await; + let response = server.read_response().await; + assert_eq!( + response["result"]["skills"][0], + json!({ + "name": "review", + "description": "Reviews code", + }) + ); + assert_eq!( + response["result"]["skills"][1]["argumentHint"], + "[environment]" + ); + assert_eq!( + response["result"]["skills"][1]["disableModelInvocation"], + true + ); + assert_eq!(response["result"]["skills"][1]["userInvocable"], false); + + server + .send_request( + 21, + "skillProvider.read", + json!({ "sessionId": session_id, "name": "deploy" }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["result"], json!({ "markdown": "# deploy" })); + + server + .send_request( + 22, + "skillProvider.read", + json!({ "sessionId": session_id, "name": "missing" }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["result"], json!({ "markdown": null })); + + let empty = Arc::new(RecordingProvider::empty()); + let empty_session = create_session( + &client, + &mut server, + SessionConfig::default() + .with_session_id("empty-catalog") + .with_skill_provider(empty), + ) + .await; + server + .send_request( + 23, + "skillProvider.list", + json!({ "sessionId": empty_session.id() }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["result"], json!({ "skills": [] })); +} + +#[tokio::test] +async fn provider_errors_are_generic_and_invalid_params_are_rejected() { + let (client, mut server) = make_client(); + let secret = "db-password-in-error"; + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(Arc::new(FailingProvider { + fail_list: true, + secret, + })), + ) + .await; + let session_id = session.id().to_string(); + + server + .send_request(30, "skillProvider.list", json!({ "sessionId": session_id })) + .await; + let response = server.read_response().await; + assert_eq!(response["error"]["code"], -32603); + assert_eq!( + response["error"]["message"], + "Skill provider listSkills failed" + ); + assert!(response["error"].get("data").is_none()); + assert!(!response.to_string().contains(secret)); + + let read_session = create_session( + &client, + &mut server, + SessionConfig::default() + .with_session_id("failing-read") + .with_skill_provider(Arc::new(FailingProvider { + fail_list: false, + secret, + })), + ) + .await; + server + .send_request( + 31, + "skillProvider.read", + json!({ "sessionId": read_session.id(), "name": "review" }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["error"]["code"], -32603); + assert_eq!( + response["error"]["message"], + "Skill provider readSkill failed" + ); + assert!(response["error"].get("data").is_none()); + assert!(!response.to_string().contains(secret)); + + server + .send_request( + 32, + "skillProvider.read", + json!({ "sessionId": read_session.id() }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["error"]["code"], -32602); +} + +#[tokio::test] +async fn cancel_request_drops_provider_future_and_reports_cancellation() { + let (client, mut server) = make_client(); + let entered = Arc::new(Notify::new()); + let dropped = Arc::new(Notify::new()); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(Arc::new(BlockingProvider { + entered: entered.clone(), + dropped: dropped.clone(), + })), + ) + .await; + + for (id, method, params) in [ + ( + 60, + "skillProvider.list", + json!({ "sessionId": session.id() }), + ), + ( + 61, + "skillProvider.read", + json!({ "sessionId": session.id(), "name": "review" }), + ), + ] { + server.send_request(id, method, params).await; + timeout(TIMEOUT, entered.notified()).await.unwrap(); + server + .send_notification("$/cancelRequest", json!({ "id": id })) + .await; + + let response = server.read_response().await; + assert_eq!(response["id"], id); + assert_eq!(response["error"]["code"], -32800); + timeout(TIMEOUT, dropped.notified()).await.unwrap(); + } +} + +#[tokio::test] +async fn connection_close_drops_provider_future() { + let (client, mut server) = make_client(); + let entered = Arc::new(Notify::new()); + let dropped = Arc::new(Notify::new()); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(Arc::new(BlockingProvider { + entered: entered.clone(), + dropped: dropped.clone(), + })), + ) + .await; + + server + .send_request( + 62, + "skillProvider.list", + json!({ "sessionId": session.id() }), + ) + .await; + timeout(TIMEOUT, entered.notified()).await.unwrap(); + drop(server); + + timeout(TIMEOUT, dropped.notified()).await.unwrap(); +} + +#[tokio::test] +async fn missing_provider_reports_generic_error() { + let (client, mut server) = make_client(); + let session = create_session(&client, &mut server, SessionConfig::default()).await; + let session_id = session.id().to_string(); + server + .send_request( + 41, + "skillProvider.read", + json!({ "sessionId": session_id, "name": "review" }), + ) + .await; + let response = server.read_response().await; + assert_no_provider(&response, &session_id); +} + +#[tokio::test] +async fn teardown_and_failed_open_stop_serving_provider() { + let (client, mut server) = make_client(); + let provider = Arc::new(RecordingProvider::new()); + let calls = provider.calls.clone(); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(provider), + ) + .await; + let session_id = session.id().to_string(); + disconnect_session(&session, &mut server).await; + + let failed_provider = Arc::new(RecordingProvider::new()); + let failed_calls = failed_provider.calls.clone(); + let failed_create = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session( + SessionConfig::default() + .with_session_id("failed-open") + .with_skill_provider(failed_provider), + ) + .await + } + }); + let request = server.read_request().await; + assert_eq!(request["method"], "session.create"); + server + .respond_error(&request, -32603, "create failed") + .await; + let error = match timeout(TIMEOUT, failed_create).await.unwrap().unwrap() { + Ok(_) => panic!("failed-open session unexpectedly succeeded"), + Err(error) => error, + }; + assert!(error.to_string().contains("create failed")); + server + .send_request(50, "skillProvider.list", json!({ "sessionId": session_id })) + .await; + let response = server.read_response().await; + assert_eq!(response["id"], 50); + assert_session_not_found(&response, &session_id); + server + .send_request( + 51, + "skillProvider.list", + json!({ "sessionId": "failed-open" }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["id"], 51); + assert_session_not_found(&response, "failed-open"); + assert_eq!(calls.snapshot(), Vec::::new()); + assert_eq!(failed_calls.snapshot(), Vec::::new()); +} + +#[tokio::test] +async fn dropped_session_answers_provider_callbacks_with_an_error() { + let (client, mut server) = make_client(); + let provider = Arc::new(RecordingProvider::new()); + let calls = provider.calls.clone(); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(provider), + ) + .await; + let session_id = session.id().to_string(); + drop(session); + + server + .send_request(60, "skillProvider.list", json!({ "sessionId": session_id })) + .await; + let response = server.read_response().await; + assert_eq!(response["id"], 60); + assert_session_not_found(&response, &session_id); + + server + .send_request(61, "skillProvider.read", json!({ "name": "review" })) + .await; + let response = server.read_response().await; + assert_eq!(response["id"], 61); + assert_eq!(response["error"]["code"], -32602); + assert_eq!(calls.snapshot(), Vec::::new()); +} + +#[tokio::test] +async fn resume_rebinds_provider() { + let (client, mut server) = make_client(); + let original = Arc::new(RecordingProvider::new()); + let original_calls = original.calls.clone(); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(original), + ) + .await; + let session_id = session.id().clone(); + disconnect_session(&session, &mut server).await; + + let replacement = Arc::new(RecordingProvider::new()); + let replacement_calls = replacement.calls.clone(); + let resume = tokio::spawn({ + let client = client.clone(); + let session_id = session_id.clone(); + async move { + client + .resume_session( + ResumeSessionConfig::new(session_id).with_skill_provider(replacement), + ) + .await + .unwrap() + } + }); + respond_to_resume(&mut server).await; + let resumed = timeout(TIMEOUT, resume).await.unwrap().unwrap(); + server + .send_request( + 70, + "skillProvider.list", + json!({ "sessionId": resumed.id() }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["result"]["skills"][0]["name"], "review"); + assert_eq!(original_calls.snapshot(), Vec::::new()); + assert_eq!(replacement_calls.snapshot(), vec!["list"]); +} + +#[tokio::test] +async fn failed_resume_restores_the_resident_provider() { + let (client, mut server) = make_client(); + let resident = Arc::new(RecordingProvider::new()); + let resident_calls = resident.calls.clone(); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(resident), + ) + .await; + let session_id = session.id().clone(); + + let replacement = Arc::new(RecordingProvider::new()); + let replacement_calls = replacement.calls.clone(); + let resume = tokio::spawn({ + let client = client.clone(); + let session_id = session_id.clone(); + async move { + client + .resume_session( + ResumeSessionConfig::new(session_id).with_skill_provider(replacement), + ) + .await + } + }); + let request = server.read_request().await; + assert_eq!(request["method"], "session.resume"); + server + .respond_error(&request, -32603, "resume failed") + .await; + let error = match timeout(TIMEOUT, resume).await.unwrap().unwrap() { + Ok(_) => panic!("resume unexpectedly succeeded"), + Err(error) => error, + }; + assert!(error.to_string().contains("resume failed")); + + // The runtime keeps the resident binding when a resume fails. + server + .send_request(80, "skillProvider.list", json!({ "sessionId": session_id })) + .await; + let response = server.read_response().await; + assert_eq!(response["id"], 80); + assert_eq!(response["result"]["skills"][0]["name"], "review"); + assert_eq!(resident_calls.snapshot(), vec!["list"]); + assert_eq!(replacement_calls.snapshot(), Vec::::new()); + drop(session); +} diff --git a/scripts/ci/run-tasks.test.mjs b/scripts/ci/run-tasks.test.mjs index 99c456e3c5..7c205a2255 100644 --- a/scripts/ci/run-tasks.test.mjs +++ b/scripts/ci/run-tasks.test.mjs @@ -95,6 +95,25 @@ for (const language of ["nodejs", "rust"]) { }); } +for (const checkout of [true, false]) { + test(`Java docs install once through the Maven wrapper before validation (${checkout ? "checkout" : "standalone"})`, async (t) => { + const { runTasks, preparations, calls, sdkRoot } = await taskFixture(t, checkout); + + runTasks("docs", "java", { environment: {}, runtimeSource: checkout ? "checkout" : "published" }); + + assert.deepEqual(preparations, []); + assert.deepEqual(calls, [ + { executable: "npm", args: ["run", "extract"], cwd: path.join(sdkRoot, "scripts/docs-validation") }, + { + executable: process.platform === "win32" ? "mvnw.cmd" : "./mvnw", + args: ["install", "-Dmaven.test.skip=true", "-Dskip.test.harness=true", "-Dcopilot.native.skip.download=true"], + cwd: path.join(sdkRoot, "java"), + }, + { executable: "npm", args: ["run", "validate:java"], cwd: path.join(sdkRoot, "scripts/docs-validation") }, + ]); + }); +} + async function taskFixture(t, checkout) { const root = fs.mkdtempSync(path.join(import.meta.dirname, ".run-tasks-")); t.after(() => fs.rmSync(root, { recursive: true, force: true })); diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 1c59d64f18..faef5dd771 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -59,6 +59,7 @@ import { type SessionEventEnvelopeProperty, } from "./utils.js"; import { isOmittableRequest, readLegacyParameters, validateLegacyDefinitions } from "./legacy-parameters.js"; +import { extensibleEnumValues, normalizeExtensibleEnums } from "./extensible-enums.js"; const execFileAsync = promisify(execFile); @@ -659,7 +660,8 @@ function getOrCreateEnum( enumValueDescriptions?: EnumValueDescriptions, explicitName?: string, deprecated?: boolean, - experimental?: boolean + experimental?: boolean, + caseSensitive = false ): string { const enumName = explicitName ?? `${parentClassName}${propName}`; const existing = generatedEnums.get(enumName); @@ -698,9 +700,10 @@ function getOrCreateEnum( lines.push(` /// `); lines.push(` public override bool Equals(object? obj) => obj is ${enumName} other && Equals(other);`, ""); lines.push(` /// `); - lines.push(` public bool Equals(${enumName} other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase);`, ""); + const comparison = caseSensitive ? "Ordinal" : "OrdinalIgnoreCase"; + lines.push(` public bool Equals(${enumName} other) => string.Equals(Value, other.Value, StringComparison.${comparison});`, ""); lines.push(` /// `); - lines.push(` public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);`, ""); + lines.push(` public override int GetHashCode() => StringComparer.${comparison}.GetHashCode(Value);`, ""); lines.push(` /// `); lines.push(` public override string ToString() => Value;`, ""); lines.push(` /// Provides a for serializing instances.`); @@ -1310,7 +1313,7 @@ function resolveSessionPropertyType( } if (refSchema.enum && Array.isArray(refSchema.enum)) { - const enumName = getOrCreateEnum(className, "", refSchema.enum as string[], enumOutput, refSchema.description, getEnumValueDescriptions(refSchema), undefined, isSchemaDeprecated(refSchema), isSchemaExperimental(refSchema)); + const enumName = getOrCreateEnum(className, "", refSchema.enum as string[], enumOutput, refSchema.description, getEnumValueDescriptions(refSchema), undefined, isSchemaDeprecated(refSchema), isSchemaExperimental(refSchema), extensibleEnumValues(refSchema) !== undefined); return isRequired ? enumName : `${enumName}?`; } @@ -1359,7 +1362,7 @@ function resolveSessionPropertyType( failUnmappable(`oneOf without discriminator (${parentClassName}.${propName})`, propSchema); } if (propSchema.enum && Array.isArray(propSchema.enum)) { - const enumName = getOrCreateEnum(parentClassName, propName, propSchema.enum as string[], enumOutput, propSchema.description, getEnumValueDescriptions(propSchema), propSchema.title as string | undefined, isSchemaDeprecated(propSchema), isSchemaExperimental(propSchema)); + const enumName = getOrCreateEnum(parentClassName, propName, propSchema.enum as string[], enumOutput, propSchema.description, getEnumValueDescriptions(propSchema), propSchema.title as string | undefined, isSchemaDeprecated(propSchema), isSchemaExperimental(propSchema), extensibleEnumValues(propSchema) !== undefined); return isRequired ? enumName : `${enumName}?`; } if (propSchema.type === "object" && propSchema.properties) { @@ -1692,6 +1695,10 @@ export async function generateSessionEvents(schemaPath?: string): Promise // ══════════════════════════════════════════════════════════════════════════════ let emittedRpcClassSchemas = new Map(); +// Class names reachable from a public method's params or result. A type shared +// with an internal method must stay public regardless of which method emits it +// first; see the upgrade in `emitRpcClass`. +const publicReachableRpcClasses = new Set(); const nonSessionRequestTypeNames = new Set(); let emittedRpcEnumResultTypes = new Set(); let experimentalRpcTypes = new Set(); @@ -1900,7 +1907,7 @@ function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassNam } if (refSchema.enum && Array.isArray(refSchema.enum)) { - const enumName = getOrCreateEnum(typeName, "", refSchema.enum as string[], rpcEnumOutput, refSchema.description, getEnumValueDescriptions(refSchema), undefined, isSchemaDeprecated(refSchema), isSchemaExperimental(refSchema) || experimentalRpcTypes.has(typeName)); + const enumName = getOrCreateEnum(typeName, "", refSchema.enum as string[], rpcEnumOutput, refSchema.description, getEnumValueDescriptions(refSchema), undefined, isSchemaDeprecated(refSchema), isSchemaExperimental(refSchema) || experimentalRpcTypes.has(typeName), extensibleEnumValues(refSchema) !== undefined); return isRequired ? enumName : `${enumName}?`; } @@ -1987,6 +1994,7 @@ function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassNam explicitName, isSchemaDeprecated(schema), isSchemaExperimental(schema) || experimentalRpcTypes.has(generatedEnumName), + extensibleEnumValues(schema) !== undefined, ); return isRequired ? enumName : `${enumName}?`; } @@ -2035,6 +2043,13 @@ function emitRpcClass( (effectiveSchema as Record).visibility === "internal" ) { visibility = "internal"; + } else if (publicReachableRpcClasses.has(className)) { + // The caller's default follows the *method* being emitted, and only the + // first emission of a class name wins. When an internal method shares a + // type with a public one and is emitted first, that would freeze the + // type as internal and leave the public method referencing it + // (CS0050/CS0051). Reachability from a public method decides instead. + visibility = "public"; } const schemaKey = stableStringify(effectiveSchema); const existingSchema = emittedRpcClassSchemas.get(className); @@ -2362,6 +2377,24 @@ function emitServerInstanceMethod( } } lines.push(`${indent}}`); + if ( + method.rpcMethod === "managedSettings.resolve" && + paramEntries.some(([name]) => name === "workingDirectory") + ) { + lines.push(""); + lines.push( + ...xmlDocComment( + "Compatibility overload preserving the positional CancellationToken parameter from before workingDirectory was added.", + indent + ) + ); + lines.push( + `${indent}${methodVisibility} ${taskType} ${methodName}Async(string? selectionId, string? gitHubToken, string? clientName, CancellationToken cancellationToken)` + ); + lines.push( + `${indent} => ${methodName}Async(selectionId, gitHubToken, clientName, workingDirectory: null, cancellationToken);` + ); + } if (legacyEntries && requestClassName) { emitRequestObjectMethod(methodName, method, requestClassName, requestClassName, undefined, resultClassName, lines, indent, groupExperimental, groupDeprecated, requiredMemberNames); } @@ -2903,9 +2936,10 @@ export function generateRpcCode( externalJsonSerializableRefs: Map> = new Map(), externalValueTypes: Set = new Set() ): string { - schema = cloneSchemaForCodegen(schema); + schema = normalizeExtensibleEnums(cloneSchemaForCodegen(schema)); omitUnrepresentableInternalProperties(schema); emittedRpcClassSchemas.clear(); + publicReachableRpcClasses.clear(); nonSessionRequestTypeNames.clear(); emittedRpcEnumResultTypes.clear(); experimentalRpcTypes.clear(); @@ -2947,6 +2981,18 @@ export function generateRpcCode( } } } + for (const name of collectRpcMethodReferencedDefinitionNames( + allMethods.filter((method) => method.visibility !== "internal"), + rpcDefinitions + )) { + publicReachableRpcClasses.add(typeToClassName(name)); + } + // A method's own request wrapper is deliberately emitted `internal` at its + // call site; public methods take the individual parameters instead. Only + // types shared *inside* a signature need the upgrade, so drop the wrappers. + for (const method of allMethods) { + publicReachableRpcClasses.delete(paramsTypeName(method)); + } const classes: string[] = []; let serverRpcParts: string[] = []; diff --git a/scripts/codegen/extensible-enums.ts b/scripts/codegen/extensible-enums.ts new file mode 100644 index 0000000000..8ab92cf774 --- /dev/null +++ b/scripts/codegen/extensible-enums.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** Known values of an open string contract; this annotation does not constrain the wire schema. */ +export function extensibleEnumValues(schema: object): string[] | undefined { + const values = (schema as Record)["x-extensible-enum"]; + if (values === undefined) return undefined; + if ((schema as Record).type !== "string" || + !Array.isArray(values) || values.length === 0 || !values.every((value) => typeof value === "string")) { + throw new Error("x-extensible-enum requires a string schema with known string values"); + } + return values; +} + +/** Supply known enum values to generators without rewriting other schema constraints. */ +export function normalizeExtensibleEnums(schema: T): T { + if (Array.isArray(schema)) { + for (const item of schema) normalizeExtensibleEnums(item); + } else if (schema !== null && typeof schema === "object") { + const knownValues = extensibleEnumValues(schema); + if (knownValues) (schema as Record).enum = knownValues; + for (const value of Object.values(schema)) normalizeExtensibleEnums(value); + } + return schema; +} diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index 5e01ee7bf2..378d36678a 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -60,6 +60,7 @@ import { type SessionEventEnvelopeProperty, } from "./utils.js"; import { validateLegacyRequests, validateLegacyDefinitions } from "./legacy-parameters.js"; +import { normalizeExtensibleEnums } from "./extensible-enums.js"; const execFileAsync = promisify(execFile); @@ -3924,7 +3925,7 @@ async function generateRpc(schemaPath?: string): Promise { console.log("Go: generating RPC types..."); const resolvedPath = schemaPath ?? (await getApiSchemaPath()); - const schema = propagateInternalVisibility(fixNullableRequiredRefsInApiSchema(cloneSchemaForCodegen((await loadSchemaJson(resolvedPath)) as ApiSchema)) as JSONSchema7) as unknown as ApiSchema; + const schema = propagateInternalVisibility(normalizeExtensibleEnums(fixNullableRequiredRefsInApiSchema(cloneSchemaForCodegen((await loadSchemaJson(resolvedPath)) as ApiSchema))) as JSONSchema7) as unknown as ApiSchema; const allMethods = [ ...collectRpcMethods(schema.server || {}), diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 3ca936f461..237ebf216f 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -9,6 +9,7 @@ import fs from "fs/promises"; import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import { fileURLToPath } from "url"; +import { extensibleEnumValues } from "./extensible-enums.js"; import { addManagedApprovalRequiredToPermissionRequests, cloneSchemaForCodegen, @@ -114,16 +115,7 @@ function rewriteExternalRefsForPython(schema: JSONSchema7 & { definitions?: Reco if (!schema.definitions) schema.definitions = {}; for (const placeholder of placeholderNames.keys()) { if (!schema.definitions[placeholder]) { - const markerProperty = `__externalRefMarker_${placeholder}`; - schema.definitions[placeholder] = { - type: "object", - additionalProperties: false, - title: placeholder, - properties: { - [markerProperty]: { type: "string" }, - }, - required: [markerProperty], - }; + schema.definitions[placeholder] = pythonReferencePlaceholder(placeholder); } } } @@ -131,6 +123,17 @@ function rewriteExternalRefsForPython(schema: JSONSchema7 & { definitions?: Reco return { placeholderNames, imports }; } +function pythonReferencePlaceholder(placeholder: string): JSONSchema7 { + const markerProperty = `__externalRefMarker_${placeholder}`; + return { + type: "object", + additionalProperties: false, + title: placeholder, + properties: { [markerProperty]: { type: "string" } }, + required: [markerProperty], + }; +} + function placeholderToQuicktypeIdentifier(placeholder: string): string { return placeholder .replace(/^_+/, "") @@ -297,6 +300,7 @@ function preservePythonSessionEventConstructorOrder(schema: JSONSchema7): void { */ const PY_RPC_APPEND_LAST_FIELDS: ReadonlyArray = [ ["ConnectorReconcileRequest", "forceConnectorName"], + ["SessionModelList", "auto"], ]; /** @@ -308,10 +312,32 @@ export function pythonAppendLastFieldsPresentIn( definitions: Record, fields: ReadonlyArray = PY_RPC_APPEND_LAST_FIELDS ): ReadonlyArray { - return fields.filter(([className, propertyName]) => { + const result = fields.filter(([className, propertyName]) => { const definition = definitions[className]; return typeof definition === "object" && Object.hasOwn(definition.properties ?? {}, propertyName); }); + const seen = new Set(result.map(([className, propertyName]) => `${className}.${propertyName}`)); + + for (const [className, definition] of Object.entries(definitions)) { + if (typeof definition !== "object") continue; + // Quicktype merges inline union variants into the definition's dataclass. + const shapes = [definition, ...(definition.anyOf ?? definition.oneOf ?? [])]; + for (const [propertyName, property] of shapes.flatMap((shape) => + typeof shape === "object" ? Object.entries(shape.properties ?? {}) : [] + )) { + if ( + typeof property !== "object" || + property["x-copilot-sdk-append-last"] !== true || + seen.has(`${className}.${propertyName}`) + ) { + continue; + } + result.push([className, propertyName]); + seen.add(`${className}.${propertyName}`); + } + } + + return result; } function preservePythonRpcStringDateFields(definitions: Record): void { @@ -2260,10 +2286,17 @@ function resolvePyNamedUnion( }; } +/** + * Enums whose contract defines `unknown` as "outside the supported vocabulary". The runtime + * contract decodes later values as `unknown` (`#[serde(other)]`); Python does the same instead + * of failing the whole event. + */ +const COLLAPSE_UNKNOWN_PYTHON_ENUMS = new Set(["PermissionApprovalEvaluationReasonCode"]); + function getOrCreatePyEnum( enumName: string, values: string[], - ctx: PyCodegenCtx, + ctx: Pick, description?: string, enumValueDescriptions?: EnumValueDescriptions, deprecated?: boolean, @@ -2296,6 +2329,7 @@ function getOrCreatePyEnum( } lines.push(` ${toEnumMemberName(value)} = ${JSON.stringify(value)}`); } + const open = extensibleEnumValues(resolveSchema({ $ref: `#/$defs/${enumName}` }, ctx.definitions) ?? {}) !== undefined; if (enumName === "ToolExecutionCompleteFileEditKind") { lines.push(``); lines.push(` @classmethod`); @@ -2306,6 +2340,22 @@ function getOrCreatePyEnum( lines.push(` member._name_ = "UNKNOWN"`); lines.push(` member._value_ = value`); lines.push(` return cls._value2member_map_.setdefault(value, member)`); + } else if (COLLAPSE_UNKNOWN_PYTHON_ENUMS.has(enumName)) { + if (!values.includes("unknown")) throw new Error(`${enumName} has no "unknown" value to collapse into`); + lines.push(``); + lines.push(` @classmethod`); + lines.push(` def _missing_(cls, value: object) -> "${enumName} | None":`); + lines.push(` return cls.UNKNOWN if isinstance(value, str) else None`); + } else if (open) { + lines.push(""); + lines.push(" @classmethod"); + lines.push(" def _missing_(cls, value: object):"); + lines.push(" if not isinstance(value, str):"); + lines.push(" return None"); + lines.push(" member = object.__new__(cls)"); + lines.push(" member._name_ = None"); + lines.push(" member._value_ = value"); + lines.push(" return cls._value2member_map_.setdefault(value, member)"); } ctx.enumsByName.set(enumName, enumName); ctx.enums.push(lines.join("\n")); @@ -3315,7 +3365,8 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema const { FetchingJSONSchemaStore, InputData, JSONSchemaInput, quicktype } = await import("quicktype-core"); const resolvedPath = schemaPath ?? (await getApiSchemaPath()); - let schema = fixNullableRequiredRefsInApiSchema(cloneSchemaForCodegen((await loadSchemaJson(resolvedPath)) as ApiSchema)); + const sourceSchema = fixNullableRequiredRefsInApiSchema(cloneSchemaForCodegen((await loadSchemaJson(resolvedPath)) as ApiSchema)); + let schema = sourceSchema; if (sessionEventsSchema) { const sharedDefinitions = findSharedSchemaDefinitions( schema as unknown as Record, @@ -3404,6 +3455,30 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema }; const externalRefs = rewriteExternalRefsForPython(singleSchema as JSONSchema7 & { definitions?: Record }); const externalEnumNames = collectPythonExternalEnumNames(sessionEventsSchema, externalRefs.placeholderNames); + // Discovery and session availability are independently versioned despite identical values. + // Hide the discovery enum from quicktype's enum merging, then restore it with our enum emitter. + const distinctEnums: Pick = { + enumsByName: new Map(), + enums: [], + }; + for (const name of ["ConnectorDiscoveryAvailability"]) { + const definition = allDefinitions[name]; + if (!definition) continue; + if (!definition.enum?.every((value): value is string => typeof value === "string")) { + throw new Error(`Expected a string enum for ${name}`); + } + getOrCreatePyEnum( + name, + definition.enum, + distinctEnums, + definition.description, + getEnumValueDescriptions(definition) + ); + const placeholder = `__ExternalRef_${name}`; + (singleSchema.definitions as Record)[name] = pythonReferencePlaceholder(placeholder); + externalRefs.placeholderNames.set(placeholder, name); + externalEnumNames.add(name); + } const externalDiscriminatedUnionNames = collectPythonExternalDiscriminatedUnionNames( sessionEventsSchema, externalRefs.placeholderNames @@ -3469,6 +3544,7 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema externalEnumNames, externalDiscriminatedUnionNames ); + typesCode += `\n\n${distinctEnums.enums.join("\n\n")}\n`; typesCode = removeShadowedSessionEventEnumsForPython( typesCode, externalRefs.imports.get(".session_events") ?? new Set(), @@ -3725,25 +3801,28 @@ def _patch_model_capabilities(data: dict) -> dict: `); - // Emit RPC wrapper classes - if (schema.server) { - const publicNode = filterNodeByVisibility(schema.server, "public"); + // Imports change where types live, not how their wire values are decoded. + const projectionDefinitions = rpcDefinitions; + rpcDefinitions = collectDefinitionCollections(sourceSchema as Record); + if (sourceSchema.server) { + const publicNode = filterNodeByVisibility(sourceSchema.server, "public"); if (publicNode) emitRpcWrapper(lines, publicNode, false, resolveType, ""); - const internalNode = filterNodeByVisibility(schema.server, "internal"); + const internalNode = filterNodeByVisibility(sourceSchema.server, "internal"); if (internalNode) emitRpcWrapper(lines, internalNode, false, resolveType, "_Internal"); } - if (schema.session) { - const publicNode = filterNodeByVisibility(schema.session, "public"); + if (sourceSchema.session) { + const publicNode = filterNodeByVisibility(sourceSchema.session, "public"); if (publicNode) emitRpcWrapper(lines, publicNode, true, resolveType, ""); - const internalNode = filterNodeByVisibility(schema.session, "internal"); + const internalNode = filterNodeByVisibility(sourceSchema.session, "internal"); if (internalNode) emitRpcWrapper(lines, internalNode, true, resolveType, "_Internal"); } - if (schema.clientSession) { - emitClientSessionApiRegistration(lines, schema.clientSession, resolveType); + if (sourceSchema.clientSession) { + emitClientSessionApiRegistration(lines, sourceSchema.clientSession, resolveType); } - if (schema.clientGlobal) { - emitClientGlobalApiRegistration(lines, schema.clientGlobal, resolveType); + if (sourceSchema.clientGlobal) { + emitClientGlobalApiRegistration(lines, sourceSchema.clientGlobal, resolveType); } + rpcDefinitions = projectionDefinitions; // Patch models.list to normalize capabilities before deserialization let finalCode = lines.join("\n"); @@ -4054,6 +4133,14 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio lines.push(``); } +/** Preserve published dictionary-array results while typing new object-array APIs. */ +const PYTHON_LEGACY_RAW_ARRAY_RESULTS = new Set([ + "account.getAllUsers", + "sessions.getClientMetadata", + "session.gitHubAuth.getAllAuthAvailable", + "session.gitHubAuth.lastAuthErrors", +]); + export function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: boolean, resolveType: (name: string) => string, groupExperimental = false, groupDeprecated = false): void { const isInternal = method.visibility === "internal"; const methodName = (isInternal ? "_" : "") + toSnakeCase(name); @@ -4064,13 +4151,22 @@ export function emitMethod(lines: string[], name: string, method: RpcMethod, isS const hasNullableResult = !!nullableInner; const resultIsOpaque = isOpaqueJson(effectiveResultSchema); const resultIsObject = !resultIsOpaque && isPythonObjectResultSchema(effectiveResultSchema); + const resultItems = effectiveResultSchema?.type === "array" && + typeof effectiveResultSchema.items === "object" && !Array.isArray(effectiveResultSchema.items) + ? effectiveResultSchema.items + : undefined; + const resultItemType = !PYTHON_LEGACY_RAW_ARRAY_RESULTS.has(method.rpcMethod) && + resultItems?.$ref && isPythonObjectResultSchema(resultItems) + ? resolveType(toPascalCase(refTypeName(resultItems.$ref, rpcDefinitions))) + : undefined; + const arrayResultType = resultItemType ? `list[${resultItemType}]` : undefined; let resultType: string; if (hasNullableResult) { - const innerTypeName = resolveType(pythonResultTypeName(method, nullableInner)); + const innerTypeName = arrayResultType ?? resolveType(pythonResultTypeName(method, nullableInner)); resultType = `${innerTypeName} | None`; } else if (hasResult) { - resultType = resolveType(pythonResultTypeName(method)); + resultType = arrayResultType ?? resolveType(pythonResultTypeName(method)); } else { resultType = "None"; } @@ -4107,6 +4203,10 @@ export function emitMethod(lines: string[], name: string, method: RpcMethod, isS if (resultIsOpaque || isAnyType) { return expr; } + if (resultItemType) { + const arrayResult = `from_list(${resultItemType}.from_dict, ${expr})`; + return hasNullableResult ? `${arrayResult} if ${expr} is not None else None` : arrayResult; + } if (hasNullableResult) { return resultIsObject ? `${innerTypeName}.from_dict(${expr}) if ${expr} is not None else None` diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 0048ceb53a..9ce9f54511 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -16,6 +16,7 @@ import path from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; +import { extensibleEnumValues } from "./extensible-enums.js"; import { isCodegenEntrypoint, addManagedApprovalRequiredToPermissionRequests, @@ -1266,6 +1267,7 @@ function emitRustStringEnum( if (!claimRustStringEnum(enumName, values, false, ctx)) return; const preserveUnknownValue = enumName === "ToolExecutionCompleteFileEditKind"; + const open = extensibleEnumValues(resolveRef(`#/$defs/${enumName}`, ctx.definitions) ?? {}) !== undefined; const lines: string[] = []; const preserveUnknown = preserveUnknownValue || preservesUnknownStringValue(enumName); if (description) { @@ -1277,6 +1279,8 @@ function emitRustStringEnum( lines.push( preserveUnknown ? "#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]" + : open + ? "#[derive(Debug, Clone, Default, PartialEq, Eq)]" : "#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]", ); if (preserveUnknown) { @@ -1285,8 +1289,8 @@ function emitRustStringEnum( lines.push(`pub enum ${enumName} {`); const usedVariantNames = new Set(); - const reservedVariantNames = new Set(["Unknown"]); - const variants: { value: string; name: string }[] = []; + const reservedVariantNames = new Set(["Unknown", ...(open ? ["Custom"] : [])]); + const variants: Array<{ name: string; value: string }> = []; for (const value of values) { // Keep the protocol's explicit "unknown" distinct from the serde fallback, // including anonymous enums whose names depend on their containing type. @@ -1299,7 +1303,7 @@ function emitRustStringEnum( ); variants.push({ value, name: variantName }); pushRustDoc(lines, enumValueDescriptions?.[value], " "); - if (!preserveUnknown && variantName !== value) { + if (!open && !preserveUnknown && variantName !== value) { lines.push(` #[serde(rename = "${value}")]`); } lines.push(` ${variantName},`); @@ -1315,9 +1319,13 @@ function emitRustStringEnum( } else { // For wire-protocol enums an unknown/sentinel value is the only safe default. lines.push(" #[default]"); - lines.push(" #[serde(other)]"); + if (!open) lines.push(" #[serde(other)]"); lines.push(" Unknown,"); } + if (open) { + lines.push(" /// Provider-advertised identifier, retained verbatim."); + lines.push(" Custom(String),"); + } lines.push("}"); if (preserveUnknown) { @@ -1347,6 +1355,31 @@ function emitRustStringEnum( lines.push(" }"); lines.push(" }"); lines.push("}"); + } else if (open) { + lines.push(`impl ${enumName} {`); + lines.push(" /// Returns the routing identifier without losing unknown values."); + lines.push(" pub fn as_str(&self) -> &str {"); + lines.push(" match self {"); + for (const variant of variants) lines.push(` Self::${variant.name} => ${JSON.stringify(variant.value)},`); + lines.push(' Self::Unknown => "Unknown",'); + lines.push(" Self::Custom(value) => value,"); + lines.push(" }"); + lines.push(" }"); + lines.push("}"); + lines.push(`impl Serialize for ${enumName} {`); + lines.push(" fn serialize(&self, serializer: S) -> Result {"); + lines.push(" serializer.serialize_str(self.as_str())"); + lines.push(" }"); + lines.push("}"); + lines.push(`impl<'de> Deserialize<'de> for ${enumName} {`); + lines.push(" fn deserialize>(deserializer: D) -> Result {"); + lines.push(" let value = String::deserialize(deserializer)?;"); + lines.push(" Ok(match value.as_str() {"); + for (const variant of variants) lines.push(` ${JSON.stringify(variant.value)} => Self::${variant.name},`); + lines.push(" _ => Self::Custom(value),"); + lines.push(" })"); + lines.push(" }"); + lines.push("}"); } ctx.enums.push(lines.join("\n")); } diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index 9cce8743ee..51abbd460c 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -442,7 +442,8 @@ export function normalizeSchemaForTypeScript( } const enumValueDescriptions = getEnumValueDescriptions(rewritten as JSONSchema7); - if ((enumValueDescriptions || openReloadEnums.has(source)) && Array.isArray(rewritten.enum) && rewritten.enum.every((entry) => typeof entry === "string")) { + const openEnum = Array.isArray(rewritten["x-extensible-enum"]); + if ((enumValueDescriptions || openEnum || openReloadEnums.has(source)) && Array.isArray(rewritten.enum) && rewritten.enum.every((entry) => typeof entry === "string")) { const documentedValues = (rewritten.enum as string[]) .map((entry) => { const comment = enumValueDescriptions?.[entry]; @@ -452,12 +453,13 @@ export function normalizeSchemaForTypeScript( .join("\n"); // Preserve future wire values without losing completion for the known edit kinds. rewritten.tsType = - rewritten.title === "ToolExecutionCompleteFileEditKind" || openReloadEnums.has(source) + openEnum || rewritten.title === "ToolExecutionCompleteFileEditKind" || openReloadEnums.has(source) ? `${documentedValues}\n| (string & {})` : documentedValues; delete rewritten.type; delete rewritten.enum; delete rewritten["x-enumDescriptions"]; + delete rewritten["x-extensible-enum"]; } if (typeof rewritten.$ref === "string") { diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index 5bb3c9f995..eca2a34182 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -16,6 +16,7 @@ import { promisify } from "util"; import { COPILOT_CLI_VERSION } from "../../nodejs/src/cliVersion.js"; import { ensureCopilotPackage } from "../../nodejs/scripts/releaseArtifacts.js"; import { findRuntimeRoot } from "../runtime-layout.mjs"; +import { extensibleEnumValues } from "./extensible-enums.js"; export const execFileAsync = promisify(execFile); @@ -245,6 +246,11 @@ export function postProcessSchema(schema: JSONSchema7): JSONSchema7 { if (typeof schema !== "object" || schema === null) return schema; const processed = { ...schema } as JSONSchema7WithDefs; + const knownValues = extensibleEnumValues(processed); + if (knownValues) { + // Enum emitters retain the familiar constants; open-value emitters must also retain unknown strings. + processed.enum = knownValues; + } if (processed.title === "ProviderModelConfig" && processed.properties) { const tokenFields = new Set([ @@ -1718,8 +1724,13 @@ function normalizeDefinitionForComparison(definition: JSONSchema7Definition): un } const result: Record = {}; + const knownValues = extensibleEnumValues(definition); for (const [key, value] of Object.entries(definition as Record)) { - if (key === "description" || key === "markdownDescription" || key === "x-enumDescriptions") { + if (key === "enum" && knownValues && Array.isArray(value) && + value.length === knownValues.length && value.every((item) => knownValues.includes(item))) { + // The processed event schema adds known values for codegen, not a wire restriction. + continue; + } else if (key === "description" || key === "markdownDescription" || key === "x-enumDescriptions") { continue; } else if (key === "$ref" && typeof value === "string") { const localRef = parseLocalDefinitionRef(value); diff --git a/scripts/docs-validation/extract.ts b/scripts/docs-validation/extract.ts index 7ddd2c136c..d869196ba7 100644 --- a/scripts/docs-validation/extract.ts +++ b/scripts/docs-validation/extract.ts @@ -457,6 +457,7 @@ async function main() { cwd: DOCS_DIR, ignore: [".validation/**", "node_modules/**", "IMPROVEMENT_PLAN.md"], }); + mdFiles.push("../java/README.md"); console.log(`Found ${mdFiles.length} markdown files\n`); @@ -472,7 +473,15 @@ async function main() { for (const mdFile of mdFiles) { const fullPath = path.join(DOCS_DIR, mdFile); - const content = fs.readFileSync(fullPath, "utf-8"); + let content = fs.readFileSync(fullPath, "utf-8"); + if (mdFile === "../java/README.md") { + // Replace the legacy README smoke with validation of its exact Quick Start. + const quickStart = /^## Quick Start\r?\n[\s\S]*?^```java\r?\n[\s\S]*?^```/m.exec(content); + if (!quickStart) { + throw new Error(`Could not find the Java Quick Start in ${mdFile}`); + } + content = "\n".repeat(content.slice(0, quickStart.index).split("\n").length - 1) + quickStart[0]; + } const blocks = parseMarkdownCodeBlocks(content, mdFile); for (const block of blocks) { diff --git a/scripts/docs-validation/validate.ts b/scripts/docs-validation/validate.ts index cb7b2ae2f6..cfa14b9251 100644 --- a/scripts/docs-validation/validate.ts +++ b/scripts/docs-validation/validate.ts @@ -385,12 +385,10 @@ async function validateJava(): Promise { fs.copyFileSync(path.join(javaDir, file), path.join(srcDir, file)); } - // Read the inherited SDK version from java/sdk/pom.xml - const sdkPomPath = path.join(ROOT_DIR, "java", "sdk", "pom.xml"); + // The required SDK install writes a flattened POM with ${revision} resolved. + const sdkPomPath = path.join(ROOT_DIR, "java", "sdk", ".flattened-pom.xml"); const sdkPomContent = fs.readFileSync(sdkPomPath, "utf-8"); - const versionMatch = sdkPomContent.match( - /[\s\S]*?([^<]+)<\/version>[\s\S]*?<\/parent>/, - ); + const versionMatch = sdkPomContent.match(/([^<]+)<\/version>/); if (!versionMatch) { throw new Error(`Could not read the Java SDK version from ${sdkPomPath}`); } @@ -421,37 +419,13 @@ async function validateJava(): Promise { fs.writeFileSync(path.join(javaDir, "pom.xml"), pomXml); - // First, install the local SDK into the local Maven repo - const pomPath = path.join(ROOT_DIR, "java", "pom.xml"); - try { - execSync(`mvn install -f "${pomPath}" -Dmaven.test.skip=true -q`, { - encoding: "utf-8", - cwd: path.join(ROOT_DIR, "java"), - }); - } catch (err: any) { - // If SDK install fails, all Java snippets fail - const errorMsg = `SDK install failed: ${(err.stderr || err.message || "").slice(0, 200)}`; - for (const file of files) { - const block = manifest.blocks.find( - (b) => b.outputFile === `java/${file}`, - ); - results.push({ - file: `java/${file}`, - sourceFile: block?.sourceFile || "unknown", - sourceLine: block?.sourceLine || 0, - success: false, - errors: [errorMsg], - }); - } - return results; - } - - // Compile the validation project + // The docs task or CI installs the SDK before validation. try { const validationPom = path.join(javaDir, "pom.xml"); - execSync(`mvn compile -f "${validationPom}" -q`, { + const maven = process.platform === "win32" ? "mvnw.cmd" : "./mvnw"; + execSync(`${maven} compile -f "${validationPom}" -q`, { encoding: "utf-8", - cwd: javaDir, + cwd: path.join(ROOT_DIR, "java"), }); // All files passed @@ -487,6 +461,10 @@ async function validateJava(): Promise { } } + if (fileErrors.size === 0) { + throw new Error(`Java documentation compilation failed:\n${output}`); + } + for (const file of files) { const block = manifest.blocks.find( (b) => b.outputFile === `java/${file}`, diff --git a/scripts/run-tasks.mjs b/scripts/run-tasks.mjs index 63ca1b77e2..8b0e4592b7 100644 --- a/scripts/run-tasks.mjs +++ b/scripts/run-tasks.mjs @@ -143,7 +143,15 @@ const tasks = { python: [command("scripts/docs-validation", "npm", ["run", "validate:py"])], go: [command("scripts/docs-validation", "npm", ["run", "validate:go"])], dotnet: [command("scripts/docs-validation", "npm", ["run", "validate:cs"])], - java: [command("scripts/docs-validation", "npm", ["run", "validate:java"])], + java: [ + command("java", maven, [ + "install", + "-Dmaven.test.skip=true", + "-Dskip.test.harness=true", + "-Dcopilot.native.skip.download=true", + ]), + command("scripts/docs-validation", "npm", ["run", "validate:java"]), + ], }, }; diff --git a/test/harness/legacyExitPlanNotice.ts b/test/harness/legacyExitPlanNotice.ts new file mode 100644 index 0000000000..7622026f25 --- /dev/null +++ b/test/harness/legacyExitPlanNotice.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +export function isLegacyExitPlanNotice(body: string): boolean { + const parts = body + .trim() + .split(/\r?\n\s*\r?\n/) + .map((part) => part.trim()); + const delta = + /^(?:New tools available|Tools no longer available): exit_plan_mode$/; + return ( + parts.some((part) => delta.test(part)) && + parts.every( + (part) => + delta.test(part) || + part === + "Important: Do not attempt to call tools that are no longer available unless you've been notified that they're available again.", + ) + ); +} diff --git a/test/harness/mockHandlers.ts b/test/harness/mockHandlers.ts index 9f75d6819e..8c520a6ff2 100644 --- a/test/harness/mockHandlers.ts +++ b/test/harness/mockHandlers.ts @@ -19,6 +19,9 @@ export function createE2eRequestHandler(options: { } if (targetHost === "github.com") { + if (req.method === "POST" && req.url === "/login/oauth/access_token") { + return forwardToCapiProxy(req, res, options.capiProxyUrl); + } respondJson(res, 404, { message: "Not Found (e2e mock)" }); return true; } diff --git a/test/harness/replayingCapiProxy.test.ts b/test/harness/replayingCapiProxy.test.ts index 1092d19f10..cbd51c1b09 100644 --- a/test/harness/replayingCapiProxy.test.ts +++ b/test/harness/replayingCapiProxy.test.ts @@ -964,69 +964,118 @@ describe("ReplayingCapiProxy", () => { expect(result.conversations[0].messages[0].content).toBe("What is 2+2?"); }); - test("strips mode_changed_notice from user messages", async () => { - const requestBody = JSON.stringify({ - messages: [ - { - role: "user", - content: - "Context before.\n\n\nWrite a plan only.\n\n\nCreate a brief implementation plan.", - }, - ], - }); - const responseBody = JSON.stringify({ - choices: [{ message: { role: "assistant", content: "Plan ready" } }], - }); + test.each([ + ["mode_changed_notice", "Write a plan only."], + ["tools_changed_notice", "Tools no longer available: exit_plan_mode"], + ])( + "strips %s from user messages without losing surrounding text", + async (tag, body) => { + const requestBody = JSON.stringify({ + messages: [ + { + role: "user", + content: + `Context before.\n\n<${tag}>\n${body}\n\n\nCreate a brief implementation plan.`, + }, + ], + }); + const responseBody = JSON.stringify({ + choices: [{ message: { role: "assistant", content: "Plan ready" } }], + }); - const outputPath = await createProxy([ - { url: "/chat/completions", requestBody, responseBody }, - ]); + const outputPath = await createProxy([ + { url: "/chat/completions", requestBody, responseBody }, + ]); - const result = await readYamlOutput(outputPath); - expect(result.conversations[0].messages[0].content).toBe( - "Context before.\n\nCreate a brief implementation plan.", - ); - }); + const result = await readYamlOutput(outputPath); + expect(result.conversations[0].messages[0].content).toBe( + "Context before.\n\nCreate a brief implementation plan.", + ); + }, + ); - test("drops notice-only user turns while preserving genuinely empty input", async () => { - const responseBody = JSON.stringify({ - choices: [{ message: { role: "assistant", content: "Ready" } }], - }); - const noticeOutputPath = await createProxy([ - { - url: "/chat/completions", - requestBody: JSON.stringify({ - messages: [ - { - role: "user", - content: - "\nPlan mode is no longer active.\n", - }, - ], - }), - responseBody, - }, - ]); - const noticeResult = await readYamlOutput(noticeOutputPath); - expect(noticeResult.conversations[0].messages).toEqual([ - { role: "assistant", content: "Ready" }, - ]); + test.each([1, 2])( + "strips %i adjacent mode notices without duplicating the retained model separator", + async (count) => { + const modelNotice = + "\nThe model has been switched from claude-sonnet-5 to claude-haiku-4.5.\n"; + const modeNotice = + "\nPlan mode is no longer active.\n"; + const instructions = "You are now in fleet mode. Implement the plan."; + const outputPath = await createProxy([ + { + url: "/chat/completions", + requestBody: JSON.stringify({ + messages: [ + { + role: "user", + content: [ + modelNotice, + ...Array.from({ length: count }, () => modeNotice), + instructions, + ].join(count === 2 ? "\n\n\n\n" : "\n\n"), + }, + ], + }), + responseBody: JSON.stringify({ + choices: [{ message: { role: "assistant", content: "Done" } }], + }), + }, + ]); + const result = await readYamlOutput(outputPath); + expect(result.conversations[0].messages[0].content).toBe( + `${modelNotice}\n\n${instructions}`, + ); + }, + ); - const emptyOutputPath = await createProxy([ - { - url: "/chat/completions", - requestBody: JSON.stringify({ - messages: [{ role: "user", content: "" }], - }), - responseBody, - }, - ]); - const emptyResult = await readYamlOutput(emptyOutputPath); - expect(emptyResult.conversations[0].messages).toEqual([ - { role: "user" }, - { role: "assistant", content: "Ready" }, - ]); - }); + test.each([ + ["mode_changed_notice", "Plan mode is no longer active."], + [ + "tools_changed_notice", + "Tools no longer available: exit_plan_mode\n\nImportant: Do not attempt to call tools that are no longer available unless you've been notified that they're available again.", + ], + ])( + "drops %s-only user turns while preserving genuinely empty input", + async (tag, body) => { + const responseBody = JSON.stringify({ + choices: [{ message: { role: "assistant", content: "Ready" } }], + }); + const noticeOutputPath = await createProxy([ + { + url: "/chat/completions", + requestBody: JSON.stringify({ + messages: [ + { + role: "user", + content: `<${tag}>\n${body}\n`, + }, + ], + }), + responseBody, + }, + ]); + const noticeResult = await readYamlOutput(noticeOutputPath); + expect(noticeResult.conversations[0].messages).toEqual([ + { role: "assistant", content: "Ready" }, + ]); + + const emptyOutputPath = await createProxy([ + { + url: "/chat/completions", + requestBody: JSON.stringify({ + messages: [{ role: "user", content: "" }], + }), + responseBody, + }, + ]); + const emptyResult = await readYamlOutput(emptyOutputPath); + expect(emptyResult.conversations[0].messages).toEqual([ + { role: "user" }, + { role: "assistant", content: "Ready" }, + ]); + }, + ); test("strips plan mode prefix from user messages", async () => { const requestBody = JSON.stringify({ @@ -1718,6 +1767,11 @@ Related files (use view tool to read): content: "\nPlan mode is no longer active.\n", }, + { + role: "user", + content: + "\nTools no longer available: exit_plan_mode\n", + }, { role: "assistant", content: "Notice ready" }, ], }, @@ -1776,15 +1830,18 @@ Related files (use view tool to read): }; try { - const noticeRequest = request( + for (const notice of [ "\nPlan mode is no longer active.\n", - ); - const response = await makeRequest(proxyUrl, noticeRequest.endpoint, { - body: noticeRequest.body, - }); + "\nNew tools available: exit_plan_mode\n", + ]) { + const noticeRequest = request(notice); + const response = await makeRequest(proxyUrl, noticeRequest.endpoint, { + body: noticeRequest.body, + }); - expect(response.status).toBe(200); - expect(response.body).toContain("Notice ready"); + expect(response.status).toBe(200); + expect(response.body).toContain("Notice ready"); + } const emptyRequest = request(""); const emptyResponse = await makeRequest(proxyUrl, emptyRequest.endpoint, { @@ -1995,6 +2052,150 @@ Related files (use view tool to read): } }); + test.each([ + [1, 0], + [0, 1], + [2, 1], + [1, 2], + [0, 0], + ])( + "replays plan approval with %i stored and %i live catalog notices", + async (storedCount, liveCount) => { + const notice = + "\nTools no longer available: exit_plan_mode\n"; + const notices = (count: number) => + Array.from({ length: count }, () => ({ + role: "user", + content: notice, + })); + const prefix = [ + { role: "system", content: "${system}" }, + { role: "user", content: "Create fleet-alpha.txt." }, + { + role: "assistant", + tool_calls: [ + { + id: "toolcall_0", + type: "function", + function: { name: "exit_plan_mode", arguments: "{}" }, + }, + ], + }, + { + role: "tool", + tool_call_id: "toolcall_0", + content: + "Plan approved!\nImplement the plan.", + }, + ]; + const createMessage = { + role: "assistant", + tool_calls: [ + { + id: "toolcall_1", + type: "function", + function: { + name: "create", + arguments: '{"file_text":"alpha","path":"fleet-alpha.txt"}', + }, + }, + ], + }; + const created = [ + createMessage, + { + role: "tool", + tool_call_id: "toolcall_1", + content: "Created fleet-alpha.txt.", + }, + ]; + const finished = { role: "assistant", content: "Created the file." }; + const continuation = (count: number) => ({ + role: "user", + content: `${count ? `${notice}\n\n` : ""}Finish the task.`, + }); + const cachePath = path.join(tempDir, "plan-catalog-notices.yaml"); + await writeFile( + cachePath, + yaml.stringify({ + models: ["test-model"], + conversations: [ + { + messages: [ + ...prefix, + ...notices(storedCount), + ...created, + finished, + continuation(storedCount), + { role: "assistant", content: "Fleet complete." }, + ], + }, + ], + } satisfies NormalizedData), + ); + const proxy = new ReplayingCapiProxy("http://localhost:9999"); + await proxy.updateConfig({ + filePath: cachePath, + workDir, + replayOnly: true, + }); + const proxyUrl = await proxy.start(); + try { + for (const [messages, expected] of [ + [[...prefix, ...notices(liveCount)], createMessage], + [[...prefix, ...created], finished], + [ + [...prefix, ...created, finished, continuation(liveCount)], + { role: "assistant", content: "Fleet complete." }, + ], + ] as const) { + const response = await makeRequest(proxyUrl, "/chat/completions", { + body: { model: "test-model", messages }, + }); + expect(response.status).toBe(200); + expect( + (JSON.parse(response.body) as ChatCompletion).choices[0].message, + ).toMatchObject(expected); + } + for (const messages of [ + [ + ...prefix.slice(0, -1), + { + ...prefix.at(-1), + content: "Approval without fleet instructions.", + }, + ], + [ + ...prefix, + ...created, + finished, + { role: "user", content: "Do a different task." }, + ], + ...[ + "New tools available: read_file", + "Tools no longer available: bash", + "New tools available: exit_plan_mode, read_file", + "New tools available: read_file\n\nTools no longer available: exit_plan_mode", + "The model has been switched to another model.\n\nTools no longer available: exit_plan_mode", + ].map((body) => [ + ...prefix, + { + role: "user", + content: `\n${body}\n`, + }, + ]), + ]) { + const response = await makeRequest(proxyUrl, "/chat/completions", { + body: { model: "test-model", messages }, + }); + expect(response.status).toBe(500); + } + } finally { + await proxy.stop(true); + } + }, + ); + test("replays a CAPI view-image capture for BYOK tool-result and image-turn continuations", async () => { const cachePath = path.join(tempDir, "view-image.yaml"); await writeFile( diff --git a/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index dd5627df3a..ad21f0d317 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -27,6 +27,7 @@ import { chatCompletionResponseToAnthropicSseChunks, } from "./anthropicMessagesAdapter"; import { canonicalUserMessageSeparator } from "./modelProtocolAdapterShared"; +import { isLegacyExitPlanNotice } from "./legacyExitPlanNotice"; import { chatCompletionResponseToResponsesApiMessage, chatCompletionResponseToResponsesApiSseChunks, @@ -124,7 +125,15 @@ const defaultModel = "claude-sonnet-5"; export class ReplayingCapiProxy extends CapturingHttpProxy { private state: ReplayingCapiProxyState | null = null; private memoryApiStub: MemoryApiStub | undefined; + private entraLogin: { subjectToken: string; githubToken: string } | undefined; private startPromise: Promise | null = null; + private metaResponse: { body: unknown; statusCode: number } | undefined; + private metaResponseGate: + | { + reached: PromiseWithResolvers; + release: PromiseWithResolvers; + } + | undefined; private defaultToolResultNormalizers: ToolResultNormalizer[] = [ { toolName: "*", normalizer: normalizeLargeOutputFilepaths }, { toolName: "*", normalizer: normalizeInterruptedToolResult }, @@ -184,6 +193,7 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { if (!config.filePath || !config.workDir) { throw new Error("filePath and workDir must be provided in config"); } + this.metaResponse = undefined; // Since we're about to switch to a new file, write out any captured exchanges // Note that the final call to stop() will also write out any remaining exchanges. @@ -237,6 +247,8 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { } async stop(skipWritingCache?: boolean): Promise { + this.metaResponseGate?.release.resolve(); + this.metaResponseGate = undefined; await super.stop(); // CAPI is the authoritative capture path. BYOK modes only verify that the @@ -279,6 +291,52 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { }; try { + if (options.requestOptions.path === "/meta-response-gate") { + if (options.requestOptions.method === "POST") { + const { hold } = JSON.parse(options.body!) as { hold: boolean }; + this.metaResponseGate?.release.resolve(); + this.metaResponseGate = hold + ? { + reached: Promise.withResolvers(), + release: Promise.withResolvers(), + } + : undefined; + } else if (this.metaResponseGate) { + await this.metaResponseGate.reached.promise; + } else { + throw new Error("No metadata response gate is installed"); + } + options.onResponseStart(200, {}); + options.onResponseEnd(); + return; + } + if ( + options.requestOptions.path === "/meta-response-config" && + options.requestOptions.method === "POST" + ) { + this.metaResponse = JSON.parse(options.body!) as { body: unknown; statusCode: number }; + options.onResponseStart(200, {}); + options.onResponseEnd(); + return; + } + if (options.requestOptions.path === "/meta") { + const gate = this.metaResponseGate; + if (gate) { + gate.reached.resolve(); + await gate.release.promise; + } + const fixture = this.metaResponse ?? { + statusCode: 501, + body: { message: "No explicit /meta fixture configured" }, + }; + options.onResponseStart(fixture.statusCode, { + "content-type": "application/json", + ...commonResponseHeaders, + }); + options.onData(Buffer.from(JSON.stringify(fixture.body))); + options.onResponseEnd(); + return; + } // Handle /copilot-user-config endpoint for configuring per-token user responses if ( options.requestOptions.path === "/copilot-user-config" && @@ -304,6 +362,65 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { return; } + if ( + options.requestOptions.path === "/entra-login-config" && + options.requestOptions.method === "POST" + ) { + this.entraLogin = JSON.parse(options.body!) as { + subjectToken: string; + githubToken: string; + }; + options.onResponseStart(200, {}); + options.onResponseEnd(); + return; + } + if ( + this.entraLogin && + options.requestOptions.path === "/entra-broker" + ) { + options.onResponseStart(200, { "content-type": "application/json" }); + options.onData( + Buffer.from( + JSON.stringify({ + accessToken: this.entraLogin.subjectToken, + expiresOnTimestamp: Date.now() + 3600000, + username: "entra-sdk@example.test", + accountId: "entra-sdk-account", + }), + ), + ); + options.onResponseEnd(); + return; + } + if ( + this.entraLogin && + options.requestOptions.path === "/login/oauth/access_token" + ) { + const form = new URLSearchParams(options.body ?? ""); + const valid = + form.get("grant_type") === + "urn:ietf:params:oauth:grant-type:token-exchange" && + form.get("subject_token") === this.entraLogin.subjectToken; + options.onResponseStart(valid ? 200 : 400, { + "content-type": "application/json", + }); + options.onData( + Buffer.from( + JSON.stringify( + valid + ? { + access_token: this.entraLogin.githubToken, + expires_in: 3600, + token_type: "bearer", + } + : { error: "invalid_request" }, + ), + ), + ); + options.onResponseEnd(); + return; + } + // Handle /config endpoint for updating proxy configuration if ( options.requestOptions.path === "/config" && @@ -1064,7 +1181,7 @@ function coalesceAdjacentUserMessages(requestBody: string): string { if ( message.role === "user" && typeof message.content === "string" && - containsModeChangedNotice(message.content) && + containsRuntimeNotice(message.content) && normalizeUserMessage(message.content) === "" ) { continue; @@ -1505,19 +1622,19 @@ function transformOpenAIRequestMessage( m: ChatCompletionMessageParam, ): NormalizedMessage | undefined { let content: string | undefined; - let strippedModeNoticeOnly = false; + let strippedRuntimeNoticeOnly = false; if (m.role === "system") { // System message changes too often to include in snapshots - just store placeholder content = "${system}"; } else if (m.role === "user" && typeof m.content === "string") { content = normalizeUserMessage(m.content); - strippedModeNoticeOnly = - content === "" && containsModeChangedNotice(m.content); + strippedRuntimeNoticeOnly = + content === "" && containsRuntimeNotice(m.content); } else if (m.role === "user" && Array.isArray(m.content)) { // Multimodal user messages have array content with text and image_url parts. // Extract and normalize text parts; represent image_url parts as a stable marker. const parts: string[] = []; - let sawModeNotice = false; + let sawRuntimeNotice = false; let sawVisibleContent = false; for (const part of m.content) { if ( @@ -1525,7 +1642,7 @@ function transformOpenAIRequestMessage( part.type === "text" && typeof part.text === "string" ) { - sawModeNotice ||= containsModeChangedNotice(part.text); + sawRuntimeNotice ||= containsRuntimeNotice(part.text); const normalized = normalizeUserMessage(part.text); if (normalized) { parts.push(normalized); @@ -1537,7 +1654,7 @@ function transformOpenAIRequestMessage( } } content = parts.join("\n") || undefined; - strippedModeNoticeOnly = sawModeNotice && !sawVisibleContent; + strippedRuntimeNoticeOnly = sawRuntimeNotice && !sawVisibleContent; } else if (m.role === "tool" && typeof m.content === "string") { // If it's a JSON tool call result, normalize the whitespace and property ordering. // For successful tool results wrapped in {resultType, textResultForLlm}, unwrap to @@ -1564,7 +1681,7 @@ function transformOpenAIRequestMessage( content = m.content; } - if (strippedModeNoticeOnly) { + if (strippedRuntimeNoticeOnly) { return undefined; } const msg: NormalizedMessage = { role: m.role }; @@ -1579,7 +1696,7 @@ function transformOpenAIRequestMessage( } function normalizeUserMessage(content: string): string { - return stripModeChangedNotice(normalizeSkillContext(content)) + return stripRuntimeNotice(normalizeSkillContext(content)) .replace( taskCompletionNotificationPattern, taskCompletionNotificationReplacement, @@ -1601,41 +1718,44 @@ const taskCompletionNotificationPattern = const taskCompletionNotificationReplacement = 'Agent "$1" ($2) has completed successfully. Use read_agent with agent_id "$1" to retrieve the full results.'; -const modeChangedNoticePattern = - /(\s*)[\s\S]*?<\/mode_changed_notice>(\s*)/g; - -function containsModeChangedNotice(content: string): boolean { - return /[\s\S]*?<\/mode_changed_notice>/.test(content); -} - -function stripModeChangedNotice(content: string): string { - let removed = false; - const stripped = content.replace( - modeChangedNoticePattern, - ( - match, - leading: string, - trailing: string, - offset: number, - source: string, - ) => { - removed = true; - const before = source.slice(0, offset); - const after = source.slice(offset + match.length); - if (!before.trim() || !after.trim()) { - return ""; - } - return /[\r\n]/.test(leading + trailing) ? "\n\n" : " "; - }, +// Only the obsolete exit_plan_mode withdrawal is optional; other catalog deltas stay strict. +const runtimeNoticePattern = + /<(mode_changed_notice|tools_changed_notice)>([\s\S]*?)<\/\1>/g; + +function containsRuntimeNotice(content: string): boolean { + return /<(mode_changed_notice|tools_changed_notice)>[\s\S]*?<\/\1>/.test( + content, ); - return removed ? stripped.trim() : content; +} + +function stripRuntimeNotice(content: string): string { + let stripped = content; + // Work backward to preserve offsets and include separators held by retained notices. + for (const match of [...content.matchAll(runtimeNoticePattern)].reverse()) { + const [, tag, body] = match; + if (tag === "tools_changed_notice" && !isLegacyExitPlanNotice(body)) { + continue; + } + const before = stripped.slice(0, match.index); + const after = stripped.slice(match.index + match[0].length); + const whitespace = [ + before.match(/\s*$/)?.[0], + after.match(/^\s*/)?.[0], + ].join(""); + let separator = ""; + if (before.trim() && after.trim()) { + separator = /[\r\n]/.test(whitespace) ? "\n\n" : " "; + } + stripped = before.trimEnd() + separator + after.trimStart(); + } + return stripped === content ? content : stripped.trim(); } function normalizeStoredUserMessages(conversations: NormalizedConversation[]) { for (const conversation of conversations) { for (const message of conversation.messages) { if (message.role === "user" && typeof message.content === "string") { - message.content = stripModeChangedNotice( + message.content = stripRuntimeNotice( normalizeSkillContext(message.content).replace( taskCompletionNotificationPattern, taskCompletionNotificationReplacement, @@ -2381,7 +2501,7 @@ function convertToStreamingResponseChunks( return chunks; } -function createGetModelsResponse( +export function createGetModelsResponse( modelIds: string[], modelNames?: Record, ) { diff --git a/test/harness/sessionExpiryMcpServer.test.mjs b/test/harness/sessionExpiryMcpServer.test.mjs new file mode 100644 index 0000000000..3a24a708e3 --- /dev/null +++ b/test/harness/sessionExpiryMcpServer.test.mjs @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { test } from "vitest"; +import { startSessionExpiryMcpServer } from "./test-mcp-session-expiry-server.mjs"; + +test("negotiates legacy session semantics even when the client requests modern MCP", async ({ + expect, + onTestFinished, +}) => { + const server = await startSessionExpiryMcpServer(); + onTestFinished(() => server.close()); + + const initialize = () => + fetch(`${server.url}/mcp`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2026-07-28", + capabilities: {}, + clientInfo: { name: "session-expiry-test", version: "1" }, + }, + }), + }); + const initialized = await initialize(); + expect(initialized.status).toBe(200); + expect((await initialized.json()).result.protocolVersion).toBe("2025-03-26"); + const sessionId = initialized.headers.get("mcp-session-id"); + expect(sessionId).toBeTruthy(); + + const ping = () => + fetch(`${server.url}/mcp`, { + method: "POST", + headers: { + "content-type": "application/json", + "mcp-session-id": sessionId, + }, + body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "ping" }), + }); + const alive = await ping(); + expect(alive.status).toBe(200); + expect(await alive.json()).toEqual({ jsonrpc: "2.0", id: 2, result: {} }); + + const expired = await fetch(`${server.url}/__expire`, { method: "POST" }); + expect(expired.status).toBe(200); + expect(await expired.json()).toEqual({ expired: 1 }); + const afterExpiry = await ping(); + expect(afterExpiry.status).toBe(404); + expect(await afterExpiry.json()).toEqual({ error: "session_expired" }); + + const reinitialized = await initialize(); + expect(reinitialized.status).toBe(200); + expect((await reinitialized.json()).result.protocolVersion).toBe("2025-03-26"); + expect(reinitialized.headers.get("mcp-session-id")).not.toBe(sessionId); + expect(server.stats.initializations).toBe(2); +}); + +test("distinguishes answered expiry probes from unrelated traffic", async ({ + expect, + onTestFinished, +}) => { + const startingServer = startSessionExpiryMcpServer(); + onTestFinished(async () => (await startingServer).close()); + const server = await startingServer; + + const initialized = await fetch(`${server.url}/mcp`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize" }), + }); + const sessionId = initialized.headers.get("mcp-session-id"); + expect(initialized.status).toBe(200); + expect(sessionId).toBeTruthy(); + await initialized.json(); + + const request = (method) => + fetch(`${server.url}/mcp`, { + method: "POST", + headers: { "content-type": "application/json", "mcp-session-id": sessionId }, + body: JSON.stringify({ jsonrpc: "2.0", id: 2, method }), + }); + const alive = await request("ping"); + expect(alive.status).toBe(200); + expect(await alive.json()).toEqual({ jsonrpc: "2.0", id: 2, result: {} }); + expect(server.stats.expiredSessionProbes).toBe(0); + + const expired = await fetch(`${server.url}/__expire`, { method: "POST" }); + expect(expired.status).toBe(200); + expect(await expired.json()).toEqual({ expired: 1 }); + expect(server.stats.expiredSessionProbes).toBe(0); + + const ordinary = await request("tools/list"); + expect(ordinary.status).toBe(404); + expect(await ordinary.json()).toEqual({ error: "session_expired" }); + expect(server.stats.expiredSessionProbes).toBe(0); + + const probe = await request("ping"); + expect(probe.status).toBe(404); + expect(await probe.json()).toEqual({ error: "session_expired" }); + expect(server.stats.expiredSessionProbes).toBe(1); + const observed = await fetch(`${server.url}/__stats`); + expect(observed.status).toBe(200); + expect(await observed.json()).toMatchObject({ activeSessions: 0, expiredSessionProbes: 1 }); +}); diff --git a/test/harness/sessionExpiryServer.test.ts b/test/harness/sessionExpiryServer.test.ts new file mode 100644 index 0000000000..5a97f3cdfa --- /dev/null +++ b/test/harness/sessionExpiryServer.test.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { onTestFinished, test } from "vitest"; +import { startSessionExpiryMcpServer } from "./test-mcp-session-expiry-server.mjs"; + +test.for(["2025-03-26", "2026-07-28", "2099-01-01"])( + "negotiates a stateful session when the client proposes %s", + async (protocolVersion, { expect }) => { + const startingServer = startSessionExpiryMcpServer(); + onTestFinished(async () => { + const [result] = await Promise.allSettled([startingServer]); + if (result.status === "fulfilled") { + await result.value.close(); + } + }); + const server = await startingServer; + const initialized = await fetch(`${server.url}/mcp`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion, + capabilities: {}, + clientInfo: { name: "test", version: "1" }, + }, + }), + }); + expect(initialized.status).toBe(200); + expect((await initialized.json()).result.protocolVersion).toBe("2025-03-26"); + const sessionId = initialized.headers.get("mcp-session-id"); + expect(sessionId).not.toBeNull(); + if (sessionId === null) { + throw new Error("The stateful fixture did not issue an MCP session ID"); + } + + const ping = () => + fetch(`${server.url}/mcp`, { + method: "POST", + headers: { "content-type": "application/json", "mcp-session-id": sessionId }, + body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "ping" }), + }); + const alive = await ping(); + expect(alive.status).toBe(200); + expect(await alive.json()).toEqual({ jsonrpc: "2.0", id: 2, result: {} }); + + const expired = await fetch(`${server.url}/__expire`, { method: "POST" }); + expect(expired.status).toBe(200); + expect(await expired.json()).toEqual({ expired: 1 }); + const lost = await ping(); + expect(lost.status).toBe(404); + expect(await lost.json()).toEqual({ error: "session_expired" }); + const lostStream = await fetch(`${server.url}/mcp`, { + headers: { "mcp-session-id": sessionId }, + }); + expect(lostStream.status).toBe(404); + expect(await lostStream.json()).toEqual({ error: "session_expired" }); + }, +); diff --git a/test/harness/stdio-shutdown-runtime.cjs b/test/harness/stdio-shutdown-runtime.cjs index 99d1c8cec2..154537a3c0 100644 --- a/test/harness/stdio-shutdown-runtime.cjs +++ b/test/harness/stdio-shutdown-runtime.cjs @@ -4,10 +4,12 @@ // Shared SDK shutdown fixture: node